I am having trouble comparing DateTime objects in my Catalylst. I have an end_date column which is being inflated by DBIx::Class::InflateColumn::DateTime, and I am inflating it with my timezone:
__PACKAGE__->add_columns(
end_date => { data_type => 'datetime', time_zone => 'America/Chicago' },
);
I have a function that is supposed to tell me if my event has closed or not, this is defined in my Schema for this class:
sub closed {
my ($self) = #_;
my $now = DateTime->now(time_zone => 'America/Chicago');
warn DateTime->compare($now, $self->end_date);
warn $now;
warn $self->end_date;
return DateTime->compare($now, $self->end_date) == 1;
}
However, it is not working properly. It is telling me that events have closed before they actually have. Here is an example output from the warns:
1
2014-06-29T12:20:48
2014-06-29T12:20:50
As you can see, it is saying that the first date is greater than end_date, even though it is not. I haven't been able to figure out why this is. However, whenever I convert them and create new DateTime objects:
sub closed {
my ($self) = #_;
my $now = DateTime::Format::ISO8601->parse_datetime(DateTime->now(time_zone => 'America/Chicago'));
my $end_date = DateTime::Format::ISO8601->parse_datetime($self->end_date);
return DateTime->compare($now, $end_date) == 1;
}
Then they compare correctly, and compare returns -1. Does anyone know why this could be?
Your debugging information is useless since you didn't include the time zone offsets (e.g. by using ->strftime('%FT%T%z')). If you did, I bet you'll find the first date is indeed greater than the end date, and I bet it's using UTC for your inflated column.
Looking at the docs, the time zone is to be provided by the timezone attribute, but you used time_zone.
{ data_type => 'datetime', timezone => "America/Chicago", locale => "de_DE" }
(That was a poor, confusing choice on D::C::IC::DT's behalf.)
Related
What should I do if I have to pass two values for same variable? Does following syntax work ?
sub get_db { return "database_name", "new_database"};
It does not pass both the value
The simplest way to return several values from a sub is a list: (NOTE - none of the following code has been tested)
return ($db_name, $new_db, $table, $rows)
...
my ($database_name, $database, $table_name, $entry_count) = get_db();
but that's easy to mess up - if you have a missmatch between the number of things returned and received something silently becomes undef. Likewise if the order of returned values is wrong, you are going to introduce a subtle bug.
Slightly better is to return a hashref;
....
my $return_values = { NAME => $db_name , DB => $new_db , TABLE => $table } ;
# add the number of rows and return it
$return_values->{ COUNT } = $rows ;
return $return_values ;
...
...
my $db_stuff = get_db();
for my $i (1 .. $db_stuff->{COUNT}) {
...
Better again is to learn a little OO and return an object. Moo is one of many options - it would look something like this:
Package DBstuff;
has name, is => ro ;
has db, is => ro ;
has table, is => ro ;
has count, is => rw ;
1;
... in another file ...
my $db_stuff = DBstuff->new(
name => $name ,
db => $db ,
table => $table,
);
# Add rows and return
$db_stuff->count( $rows );
return $db_stuff ;
...
...
my $db_data = get_db();
for my $i (1 .. $db_data->count) {
...
There is also a module called Object::Result which would almost certainly be overkill but whether you use it or not, I'd like to recommend the RATIONAL section of that module's documentation which covers the issue of returning several things from a sub in more depth.
Perl allows to return an array with multiple values, e.g.
sub get_db { return ["database_name", "new_database"] };
(Similar to, but with more concrete details that, #11526999)
My Result Classes have been built using dbicdump, however I wish to overload the default accessor for a date field.
Works, but a bodge
To hackytest my idea, I simply added an accessor attribute to the created date key of the add_columns call:
__PACKAGE__->add_columns(
"stamp_id",
{
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
sequence => "timestamp_stamp_id_seq",
},
"date",
{ data_type => "date", is_nullable => 0, accessor => '_date' },
);
... and created my accessor routine below the Schema::Loader checksum line:
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:nB5koMYAhBwz4ET77Q8qlA
sub date {
my $self = shift;
warn "overloaded date\n"; # Added for debugging
my $date;
# The date needs to be just the date, not the time
if ( #_ ) {
$date = shift;
if ( $date =~ /^([\d\-]+)/ ) {
$date = $1
}
return $self->_date($date)
}
# Fetch the column value & remove the time part.
$date = $self->_date;
if ( $date =~ /^([\d\-]+)/ ) {
$date = $1
}
return $date;
}
This works, as it returns an expected 2014-10-04, but is a bodge.
Do it the right way
The problem is that I've hacked the checksum'd code, so I can't neatly re-generate my Class objects.
Reading ResultSource and the CookBook the correct approach appears to be:
Have the ResultSource built by dbicdump as standard:
__PACKAGE__->add_columns(
"stamp_id",
{
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
sequence => "timestamp_stamp_id_seq",
},
"date",
{ data_type => "date", is_nullable => 0 },
);
.... add a change the accessor below the line, using the + to indicate it's an alteration to an existing definition:
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:nB5koMYAhBwz4ET77Q8qlA
__PACKAGE__->add_columns(
"+date", { accessor => '_date' },
);
.... use the overload method as before
Not working.
I've double-checked my spelling, I've tried add_column rather than add_columns, and I've tried putting the second add_columns to directly below the first - all to now avail.... the code uses the default accessor, and returns 2014-10-04T00:00:00
How do I over-ride the default accessor, so I can use my own method?
Thankee...
What you need here is a col_accessor_map passed in as a loader option.
col_accessor_map => {
table_name => {
date => _date,
}
}
You can pass loader options to dbicdump with -o.
$ dbicdump -o col_accessor_map="{ table_name => { date => _date } }" ... other options ...
(Replace table_name above with the name of your table - that's obvious, right?)
Update: This was posted untested, and when I finally got round to testing it, I found it didn't work. After a conversation with the author on IRC I was told that the col_accessor_map option doesn't support this nested hash approach, so if you wanted to use this approach you would need to use a coderef.
However, the author also agreed that adding this support would be a good idea and I've just got back from lunch to find this Github commit which adds the feature. I don't know how soon it will get to CPAN though.
This may be the first time that CPAN has been updated to make a SO answer correct :-)
At a different level of abstraction I believe you could use a method modifier
use Class::Method::Modifier; # or Moose/Moo
around date => sub {...};
I'm trying to generate an ICal feed using Data:ICal but some events are printed without a time. I've read that the time is not required if it's 000000 but Google Calendar does not handle those events without a time properly.
Here is an example script and output. I need the output to to be in the UTC timezone.
#!/usr/bin/perl -w
use strict;
use Date::ICal;
use Data::ICal;
use Data::ICal::Entry::Event;
use DateTime;
use Data::Dumper;
sub get_utc_offset($) {
my ($orig_tz_str) = #_;
# Using a set winter date to avoid problems with daylight savings time
my $utc_compare_datetime = DateTime->new(
year => 2012,
month => 1,
day => 1,
hour => 1,
minute => 1,
time_zone => 'UTC'
);
my $tz = DateTime::TimeZone->new(name => $orig_tz_str);
my $utc_offset = $tz->offset_for_datetime($utc_compare_datetime);
my $utc_offset_str = DateTime::TimeZone->offset_as_string($utc_offset);
return $utc_offset_str;
}
sub add_ical_event($$$$$$) {
my ($calendar, $start, $end, $summary, $description, $timezone) = #_;
my $offset = get_utc_offset($timezone);
$description = 'none' if (!$description);
my $event = Data::ICal::Entry::Event->new();
$event->add_properties(
summary => $summary,
description => $description,
dtstart => Date::ICal->new( ical => $start, offset => $offset )->ical,
dtend => Date::ICal->new( ical => $end, offset => $offset )->ical,
dtstamp => Date::ICal->new( epoch => time )->ical
);
$calendar->add_entry($event);
}
# Tests
# ----------------------------------------------------------------------------
my $timezone = 'America/New_York';
my $calendar = Data::ICal->new();
$calendar->add_properties(
method => "PUBLISH",
prodid => "-//Test Cal//NONSGML Calendar//EN",
'X-WR-CALNAME' => 'Test Cal'
);
my (%events) = (
1 => {
summary => 'Test Shift Tool - Testing Shift',
description => '',
start => '20130828T160000',
end => '20130828T190000',
timezone => $timezone
},
2 => {
summary => 'New Member Meeting',
description => '',
start => '20130722T190000',
end => '20130722T210000',
timezone => $timezone
},
3 => {
summary => 'public',
description => '',
start => '20130630T130000',
end => '20130630T140000',
timezone => $timezone
}
);
foreach my $key (sort keys %events) {
my $e = $events{$key};
add_ical_event(
$calendar,
$e->{start},
$e->{end},
$e->{summary},
$e->{description},
$e->{timezone}
);
}
print $calendar->as_string;
Notice that some events have start or end dates without a time. When I manually add T000000Z, those events are imported properly to Google Calendar. Any suggestions on how to force all events to have a time?
BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
PRODID:-//Digital Cheetah//NONSGML Calendar//EN
X-WR-CALNAME:Digital Cheetah
BEGIN:VEVENT
DESCRIPTION:none
DTEND:20130829Z
DTSTAMP:20130823T214317Z
DTSTART:20130828T210000Z
SUMMARY:Test Shift Tool - Testing Shift
END:VEVENT
BEGIN:VEVENT
DESCRIPTION:none
DTEND:20130723T020000Z
DTSTAMP:20130823T214317Z
DTSTART:20130723Z
SUMMARY:New Member Meeting
END:VEVENT
BEGIN:VEVENT
DESCRIPTION:none
DTEND:20130630T190000Z
DTSTAMP:20130823T214317Z
DTSTART:20130630T180000Z
SUMMARY:public
END:VEVENT
END:VCALENDAR
I've read that the time is not required if it's 000000
That's not what the RFC says. Let's refer to the following sections:
4.3.4 Date
4.3.5 Date-Time
4.8.7.2 Date/Time
I'll quote the relevant format specifications, here:
date = date-value
date-value = date-fullyear date-month date-mday
date-fullyear = 4DIGIT
date-time = date "T" time ;As specified in the date and time
;value definitions
dtstamp = "DTSTAMP" stmparam ":" date-time CRLF
When your output includes a DTSTAMP, the ICal specification expects a date-time following it.
Which brings us to Date::ICal and its ical method. Does it return an iCal date or a date-time? As it turns out, it tries to guess which format you want by checking whether your timestamp has a time of 000000. See for yourself at line 286 of ICal.pm.
It could be that we expect Data::ICal::Entry to handle this scenario. I could be missing validation code on that end, but at the moment I'm not seeing anything that's obviously relevant. It looks like it accepts the property values without checking them.
Depending on your perspective, this sounds like a bug or a limitation of the libraries.
So... how should you fix this? Ideally, one of these libraries should probably check for and handle this scenario. In the meantime, though, you need to get back on your feet:
Quick and dirty fix: if your time is zero, bump it by one second; ical will now return a valid but slightly inaccurate date-time string.
A little better: check the return value from ical; if it's a date, reformat it as a date-time.
Test this before using it, but maybe something like this:
dtstart => $ical =~ s/(\d{8})Z/$1T000000Z/r;
apologises and thanks in advance for what, even as I type, seems likely silly question, but here goes anyway.
I have basic Catalyst application using DBIx::Class with an 'Author' and associated 'Book' table. In addition I also use DBIx::Class::Cursor::Cached to cache data as appropriate.
The issue is that, following an edit, I need to clear cached data BEFORE it has actually expired.
1.) Author->show_author_and_books which fetchs and caches resultset.
2.) the Book->edit_do which needs to clear the cached data from the Author->show_author_and_books request.
See basic/appropriate setup below.
-- MyApp.pm definition including backend 'Cache::FileCache' cache.
__PACKAGE__->config(
name => 'MyApp',
...
'Plugin::Cache' => { 'backend' => { class => 'Cache::FileCache',
cache_root => "./cache",
namespace => "dbix",
default_expires_in => '8 hours',
auto_remove_stale => 1
}
},
...
-- MyApp::Model::DB definition with 'Caching' traits set using 'DBIx::Class::Cursor::Cached'.
...
__PACKAGE__->config(
schema_class => 'MyApp::Schema',
traits => [ 'Caching' ],
connect_info => { dsn => '<dsn>',
user => '<user>',
password => '<password>',
cursor_class => 'DBIx::Class::Cursor::Cached'
}
);
...
-- MyApp::Controller::Author.pm definition with 'show_author_and_books' method - resultset is cached.
...
sub show_author_and_books :Chained('base') :PathPart('') :Args(0)
{
my ( $self, $c ) = #_;
my $author_id = $c->request->params->{author_id};
my $author_and_books_rs = $c->stash->{'DB::Author'}->search({ author_id => $author_id },
{ prefetch => 'book' },
cache_for => 600 } ); # Cache results for 10 minutes.
# More interesting stuff, but no point calling $author_and_books_rs->clear_cache here, it would make no sense:s
...
}
...
-- MyApp::Controller::Book.pm definition with 'edit_do' method which updates book entry and so invalidates the cached data in show_author_and_books.
...
sub edit_do :Chained('base') :PathPart('') :Args(0)
{
my ( $self, $c ) = #_;
# Assume stash contains a book for some author, and that we want to update the description.
my $book = $c->stash->{'book'}->update({ desc => $c->request->params->{desc} });
# How do I now clear the cached DB::Author data to ensure the new desc is displayed on next request to 'Author->show_author_and_books'?
# HOW DO I CLEAR CACHED DB::Author DATA?
...
}
Naturally I'm aware that $author_and_books_rs, as defined in Author->show_author_and_books, contains a method 'clear_cache', but obviously this is out of scope in Book->edit_do ( not to mention another problem there might be).
So, is the correct approach to make the DBIx request again , as per ...show_author_and_books and then call the 'clear_cache' again that or is there a more direct way where I can just say something like this $c->cache->('DB::Author')->clear_cache?
Thank you again.
PS. I'm sure when I look at this tomorrow, the full silliness of the question will hit me:s
Try
$c->model( 'DB::Author' )->clear_cache() ;
The solution I went for in the end was to NOT use 'DBIx::Class::Cursor::Cached', but instead directly use the Catalyst Cache plugin defining multiple
backend caches to handle the different namespaces I trying to manage in the real-world scenario.
I backed away from D::C::Cursor::Cached as all data was/is held in the same namespace plus there doesn't appear to be a method to expire data in advance of
time already set.
So for completeness, from the code above, the MyApp::Model::DB.pm definition would lose the 'traits' and 'cursor_class' key/values.
Then...
The MyApp.pm Plugin::Cache' would expand to contain multiple cache namespaces...
-- MyApp.pm definition including backend 'Cache::FileCache' cache.
...
'Plugin::Cache' => { 'backends' => { Authors => { class => 'Cache::FileCache',
cache_root => "./cache",
namespace => "Authors",
default_expires_in => '8 hours',
auto_remove_stale => 1
},
CDs => { class => 'Cache::FileCache',
cache_root => "./cache",
namespace => "CDs",
default_expires_in => '8 hours',
auto_remove_stale => 1
},
...
}
...
-- MyApp::Controller::Author.pm definition with 'show_author_and_books' method - resultset is cached.
...
sub show_author_and_books :Chained('base') :PathPart('') :Args(0)
{
my ( $self, $c ) = #_;
my $author_id = $c->request->params->{author_id};
my $author = $c->get_cache_backend('Authors')->get( $author_id );
if( !defined($author) )
{
$author = $c->stash->{'DB::Author'}->search({ author_id => $author_id },
{ prefetch => 'book', rows => 1 } )->single;
$c->get_cache_backend('Authors')->set( $author_id, $author, "10 minutes" );
}
# More interesting stuff, ...
...
}
...
-- MyApp::Controller::Book.pm definition with 'edit_do' method which updates book entry and so invalidates the cached data in show_author_and_books.
...
sub edit_do :Chained('base') :PathPart('') :Args(0)
{
my ( $self, $c ) = #_;
# Assume stash contains a book for some author, and that we want to update the description.
my $book = $c->stash->{'book'}->update({ desc => $c->request->params->{desc} });
# How do I now clear the cached DB::Author data to ensure the new desc is displayed on next request to 'Author->show_author_and_books'?
# HOW DO I CLEAR CACHED DB::Author DATA? THIS IS HOW, EITHER...
$c->get_cache_backend('Authors')->set( $c->stash->{'book'}->author_id, {}, "now" ); # Expire now.
# ... OR ... THE WHOLE Authors namespace...
$c->get_cache_backend('Authors')->clear;
...
}
NOTE : as you'll expect from the use of Author and CDs, this isn't the real world scenario I'm working, but should serve to show my intent.
As I'm relatively new to the wonder of DBIx and indeed Catalyst, I'd be interested to hear if there a better approach to this (I very much expect there is), but it will serve for the moment as I'm attempting to update a legacy application.
The plugin could probably be patched to make per result set caches easy to namespace and clear independently, and alternatively it would probably not be so hard to add a namespace to the attributes. If you want to work on that hit #dbix-class and I'd be willing to mentor you - jnap
I've got the following code:
package MyPackage::ResultSet::Case;
use base 'DBIx::Class::ResultSet';
sub cases_last_fourteen_days {
my ($self, $username) = #_;
return $self->search({
username => $username,
date => { '>=' => 'DATE_SUB(CURDATE(),INTERVAL 14 DAY)' },
});
};
But when I try to use it this way:
$schema->resultset('Case')->cases_last_fourteen_days($username)
I always get zero results, can anyone tell what I'm doing wrong?
Thanks!
The way you use the SQL::Abstract condition would result in this where condition:
WHERE username = ? AND date >= 'DATE_SUB(CURDATE(),INTERVAL 14 DAY)'
When you wish to use database functions in a where clause you need to use a reference to a scalar, like this:
date => { '>=' => \'DATE_SUB(CURDATE(),INTERVAL 14 DAY)' },
ProTip: if you set the environment variable DBIC_TRACE to 1, DBIx::Class will print the queries it generates to STDERR ... this way you can check if it really does what you wish.