From 001474fd7fae89ba7edd4b3d1f6e7902589c557d Mon Sep 17 00:00:00 2001 From: Gerhard Jungwirth Date: Tue, 6 Mar 2018 15:50:36 +0100 Subject: [PATCH] TT#34800 inflate/deflate DateTime for timestamps inflate/deflate DateTime for simple (complete) timestamps considering the correct timezone at the latest possible point in the action chains: on form-level as well as in the DataTables json output. Change-Id: Icfe94d6d5a9ac02d9fca0f4b8d048d86cf66cffa --- lib/NGCP/Panel/Controller/API/BannedUsers.pm | 2 +- lib/NGCP/Panel/Controller/Lnp.pm | 12 +++- lib/NGCP/Panel/Controller/Login.pm | 2 + lib/NGCP/Panel/Controller/Root.pm | 58 +++++++++++---- lib/NGCP/Panel/Field/DateTime.pm | 40 +++++++++-- lib/NGCP/Panel/Utils/Datatables.pm | 22 +++--- lib/NGCP/Panel/Utils/DateTime.pm | 74 +++++++++++++++----- lib/NGCP/Panel/Utils/Security.pm | 3 +- share/layout/body.tt | 3 + 9 files changed, 164 insertions(+), 52 deletions(-) diff --git a/lib/NGCP/Panel/Controller/API/BannedUsers.pm b/lib/NGCP/Panel/Controller/API/BannedUsers.pm index f416083bc6..0e5c77a68d 100644 --- a/lib/NGCP/Panel/Controller/API/BannedUsers.pm +++ b/lib/NGCP/Panel/Controller/API/BannedUsers.pm @@ -21,7 +21,7 @@ sub api_description { sub get_list{ my ($self, $c) = @_; - return NGCP::Panel::Utils::Security::list_banned_users($c, data_for_json => 1); + return NGCP::Panel::Utils::Security::list_banned_users($c); } 1; diff --git a/lib/NGCP/Panel/Controller/Lnp.pm b/lib/NGCP/Panel/Controller/Lnp.pm index 75e5eaed6f..14a44d18f3 100644 --- a/lib/NGCP/Panel/Controller/Lnp.pm +++ b/lib/NGCP/Panel/Controller/Lnp.pm @@ -313,12 +313,20 @@ sub number_edit :Chained('number_base') :PathPart('edit') { my $schema = $c->model('DB'); $schema->txn_do(sub { if(length $form->values->{start}) { - $form->values->{start} .= 'T00:00:00'; + $form->values->{start} .= ' 00:00:00'; + my $start_dt = NGCP::Panel::Utils::DateTime::from_forminput_string( + $form->values->{start}, + $c->session->{user_tz}); + $form->values->{start} = NGCP::Panel::Utils::DateTime::to_local_string($start_dt); } else { $form->values->{start} = undef; } if(length $form->values->{end}) { - $form->values->{end} .= 'T23:59:59'; + $form->values->{end} .= ' 23:59:59'; + my $end_dt = NGCP::Panel::Utils::DateTime::from_forminput_string( + $form->values->{end}, + $c->session->{user_tz}); + $form->values->{end} = NGCP::Panel::Utils::DateTime::to_local_string($end_dt); } else { $form->values->{end} = undef; } diff --git a/lib/NGCP/Panel/Controller/Login.pm b/lib/NGCP/Panel/Controller/Login.pm index 276b3b21f0..76ad7de208 100644 --- a/lib/NGCP/Panel/Controller/Login.pm +++ b/lib/NGCP/Panel/Controller/Login.pm @@ -63,6 +63,8 @@ sub index :Path Form { if($res) { # auth ok + $c->session->{user_tz} = undef; # reset to reload from db + $c->session->{user_tz_name} = undef; # reset to reload from db my $target = $c->session->{'target'} || '/'; delete $c->session->{target}; $target =~ s!^https?://[^/]+/!/!; diff --git a/lib/NGCP/Panel/Controller/Root.pm b/lib/NGCP/Panel/Controller/Root.pm index 5346470489..392fbd51d5 100644 --- a/lib/NGCP/Panel/Controller/Root.pm +++ b/lib/NGCP/Panel/Controller/Root.pm @@ -15,6 +15,8 @@ use Time::HiRes qw(); use DateTime::Format::RFC3339 qw(); use HTTP::Status qw(:constants); +use NGCP::Schema qw//; + # # Sets the actions in this controller to be registered with no prefix # so they function identically to actions created in MyApp.pm @@ -53,6 +55,36 @@ sub auto :Private { $c->log->debug("lang set by browser or config: " . $c->language); } + ################################################### timezone retrieval + if ($c->user_exists) { + if ($c->session->{user_tz}) { + # nothing to do + } elsif ($c->user->roles eq 'admin') { + my $reseller_id = $c->user->reseller_id; + my $tz_row = $c->model('DB')->resultset('reseller_timezone')->find({reseller_id => $reseller_id}); + _set_session_tz_from_row($c, $tz_row, 'admin', $reseller_id); + } elsif($c->user->roles eq 'reseller') { + my $reseller_id = $c->user->reseller_id; + my $tz_row = $c->model('DB')->resultset('reseller_timezone')->find({reseller_id => $reseller_id}); + _set_session_tz_from_row($c, $tz_row, 'reseller', $reseller_id); + } elsif($c->user->roles eq 'subscriberadmin') { + my $contract_id = $c->user->account_id; + my $tz_row = $c->model('DB')->resultset('contract_timezone')->find({contract_id => $contract_id}); + _set_session_tz_from_row($c, $tz_row, 'subscriberadmin', $contract_id); + } elsif($c->user->roles eq 'subscriber') { + my $uuid = $c->user->uuid; + my $tz_row = $c->model('DB')->resultset('voip_subscriber_timezone')->find({uuid => $uuid}); + _set_session_tz_from_row($c, $tz_row, 'subscriber', $uuid); + } else { + # this shouldnt happen + } + $NGCP::Schema::CURRENT_USER_TZ = $c->session->{user_tz}; + } else { + $NGCP::Schema::CURRENT_USER_TZ = undef; + } + + ################################################### + if ( __PACKAGE__ eq $c->controller->catalyst_component_name or 'NGCP::Panel::Controller::Login' eq $c->controller->catalyst_component_name @@ -314,18 +346,6 @@ sub end :Private { } } -sub _prune_row { - my ($columns, %row) = @_; - while (my ($k,$v) = each %row) { - unless (grep { $k eq $_ } @$columns) { - delete $row{$k}; - next; - } - $row{$k} = $v->datetime if blessed($v) && $v->isa('DateTime'); - } - return { %row }; -} - sub error_page :Private { my ($self,$c) = @_; $c->log->error( 'Failed to find path ' . $c->request->path ); @@ -472,6 +492,20 @@ sub api_apply_fake_time :Private { } } +sub _set_session_tz_from_row { + my ($c, $tz_row, $role, $identifier) = @_; + + my $tz_name = $tz_row ? $tz_row->name : undef; + $tz_name =~ s/^localtime$/local/ if $tz_name; + eval { $c->session->{user_tz} = DateTime::TimeZone->new( name => $tz_name ); }; + if ($@) { + $c->log->warning("couldnt set timezone. error in creation probably caused by invalid timezone name. role $role ($identifier) to $tz_name"); + } else { + $c->session->{user_tz_name} = $tz_name; + $c->log->debug("timezone set for $role ($identifier) to $tz_name"); + } +} + 1; diff --git a/lib/NGCP/Panel/Field/DateTime.pm b/lib/NGCP/Panel/Field/DateTime.pm index 5832413f82..6133139a9d 100644 --- a/lib/NGCP/Panel/Field/DateTime.pm +++ b/lib/NGCP/Panel/Field/DateTime.pm @@ -1,17 +1,43 @@ package NGCP::Panel::Field::DateTime; use HTML::FormHandler::Moose; + use Sipwise::Base; +use NGCP::Panel::Utils::DateTime qw//; extends 'HTML::FormHandler::Field::Text'; has '+deflate_method' => ( default => sub { \&datetime_deflate } ); +has '+inflate_method' => ( default => sub { \&datetime_inflate } ); + +sub datetime_deflate { # deflate: DateTime (in any tz) -> User representation (with correct tz) + my ( $self, $value ) = @_; + + my $c = $self->form->ctx; + + if(blessed($value) && $value->isa('DateTime')) { + if($c && $c->session->{user_tz}) { + $value->set_time_zone('local'); # starting point for conversion + $value->set_time_zone($c->session->{user_tz}); # desired time zone + } + return $value->ymd('-') . ' ' . $value->hms(':'); + } else { + return $value; + } +} + +sub datetime_inflate { # inflate: User entry -> DateTime -> Plaintext but converted + my ( $self, $value ) = @_; + + my $c = $self->form->ctx; + + my $tz; + if($c && $c->session->{user_tz}) { + $tz = $c->session->{user_tz}; + } + + my $date = NGCP::Panel::Utils::DateTime::from_forminput_string($value, $tz); + $date->set_time_zone('local'); # convert to local -sub datetime_deflate { - my ( $self, $value ) = @_; - if(blessed($value) && $value->isa('DateTime')) { - return $value->ymd('-') . ' ' . $value->hms(':'); - } else { - return $value; - } + return $date->ymd('-') . ' ' . $date->hms(':'); } no Moose; diff --git a/lib/NGCP/Panel/Utils/Datatables.pm b/lib/NGCP/Panel/Utils/Datatables.pm index 08a95887ee..f759c39337 100644 --- a/lib/NGCP/Panel/Utils/Datatables.pm +++ b/lib/NGCP/Panel/Utils/Datatables.pm @@ -3,10 +3,10 @@ use strict; use warnings; use Sipwise::Base; +use NGCP::Panel::Utils::DateTime qw(); use NGCP::Panel::Utils::Generic qw(:all); use List::Util qw/first/; use Scalar::Util qw/blessed/; -use DateTime::Format::Strptime; sub process { my ($c, $rs, $cols, $row_func, $params) = @_; @@ -20,6 +20,8 @@ sub process { my $aggregate_cols = []; my $aggregations = {}; + my $user_tz = $c->session->{user_tz}; + # check if we need to join more tables # TODO: can we nest it deeper than once level? @@ -116,15 +118,11 @@ sub process { my $from_date_in = $c->request->params->{sSearch_0} // ""; my $to_date_in = $c->request->params->{sSearch_1} // ""; my($from_date,$to_date); - my $parser = DateTime::Format::Strptime->new( - #pattern => '%Y-%m-%d %H:%M', - pattern => '%Y-%m-%d', - ); if($from_date_in) { - $from_date = $parser->parse_datetime($from_date_in); + $from_date = NGCP::Panel::Utils::DateTime::from_forminput_string($from_date_in, $c->session->{user_tz}); } if($to_date_in) { - $to_date = $parser->parse_datetime($to_date_in); + $to_date = NGCP::Panel::Utils::DateTime::from_forminput_string($to_date_in, $c->session->{user_tz}); } foreach my $col(@{ $cols }) { # avoid amigious column names if we have the same column in different joined tables @@ -192,7 +190,7 @@ sub process { my $topId = $c->request->params->{iIdOnTop}; if(defined $topId) { if(defined(my $row = $rs->find($topId))) { - push @{ $aaData }, _prune_row($cols, $row->get_inflated_columns); + push @{ $aaData }, _prune_row($user_tz, $cols, $row->get_inflated_columns); if (defined $row_func) { $aaData->[-1] = {%{$aaData->[-1]}, $row_func->($row)}; } @@ -247,7 +245,7 @@ sub process { } for my $row ($rs->all) { - push @{ $aaData }, _prune_row($cols, $row->get_inflated_columns); + push @{ $aaData }, _prune_row($user_tz, $cols, $row->get_inflated_columns); if (defined $row_func) { $aaData->[-1] = {%{$aaData->[-1]}, $row_func->($row)} ; } @@ -277,13 +275,17 @@ sub set_columns { } sub _prune_row { - my ($columns, %row) = @_; + my ($user_tz, $columns, %row) = @_; while (my ($k,$v) = each %row) { unless (first { $_->{accessor} eq $k && $_->{title} } @{ $columns }) { delete $row{$k}; next; } if(blessed($v) && $v->isa('DateTime')) { + if($user_tz) { + $v->set_time_zone('local'); # starting point for conversion + $v->set_time_zone($user_tz); # desired time zone + } $row{$k} = $v->ymd('-') . ' ' . $v->hms(':'); $row{$k} .= '.'.$v->millisecond if $v->millisecond > 0.0; } diff --git a/lib/NGCP/Panel/Utils/DateTime.pm b/lib/NGCP/Panel/Utils/DateTime.pm index ba097c80d5..dcd442ad2a 100644 --- a/lib/NGCP/Panel/Utils/DateTime.pm +++ b/lib/NGCP/Panel/Utils/DateTime.pm @@ -1,18 +1,19 @@ package NGCP::Panel::Utils::DateTime; -#use Sipwise::Base; seg fault when creating threads in test scripts use strict; use warnings; -use Time::HiRes; #prevent warning from Time::Warp -use Time::Warp qw(); -#use Time::Fake; #load this before any use DateTime -use DateTime; + use DateTime::Format::ISO8601; use DateTime::Format::Strptime; +use DateTime; use POSIX qw(floor fmod); +use Readonly qw(); +use Time::HiRes; #prevent warning from Time::Warp +use Time::Warp qw(); -use constant RFC_1123_FORMAT_PATTERN => '%a, %d %b %Y %T %Z'; -use constant TIMEZONE_MAP => { map { $_ => 1; } DateTime::TimeZone->all_names }; + my $RFC_1123_FORMAT_PATTERN = '%a, %d %b %Y %T %Z'; + my $TIMEZONE_MAP = { map { $_ => 1; } DateTime::TimeZone->all_names }; + my $LOCAL_TZ = DateTime::TimeZone->new(name => 'local'); my $is_fake_time = 0; @@ -22,7 +23,7 @@ sub is_valid_timezone_name { if ($all) { return DateTime::TimeZone->is_valid_name($tz); } else { - return 0 unless exists TIMEZONE_MAP->{$tz}; + return 0 unless exists $TIMEZONE_MAP->{$tz}; return 1; } } @@ -30,11 +31,11 @@ sub is_valid_timezone_name { sub current_local { if ($is_fake_time) { return DateTime->from_epoch(epoch => Time::Warp::time, - time_zone => DateTime::TimeZone->new(name => 'local') + time_zone => $LOCAL_TZ, ); } else { return DateTime->now( - time_zone => DateTime::TimeZone->new(name => 'local') + time_zone => $LOCAL_TZ, ); } } @@ -43,7 +44,7 @@ sub current_local_hires { #If the epoch value is a floating-point value, it will be rounded to nearest microsecond. return DateTime->from_epoch( epoch => Time::HiRes::time, - time_zone => DateTime::TimeZone->new(name => 'local') + time_zone => $LOCAL_TZ, ); } @@ -51,7 +52,7 @@ sub current_local_hires { sub set_local_tz { my $dt = shift; if (defined $dt && ref $dt eq 'DateTime' && !is_infinite($dt)) { - $dt->set_time_zone('local'); + $dt->set_time_zone($LOCAL_TZ); } return $dt; } @@ -150,17 +151,16 @@ sub last_day_of_month { sub epoch_local { my $epoch = shift; return DateTime->from_epoch( - time_zone => DateTime::TimeZone->new(name => 'local'), + time_zone => $LOCAL_TZ, epoch => $epoch, ); } sub epoch_tz { - my $epoch = shift; - my $tz = shift; + my ($epoch, $tz) = @_; #if(!$tz || !DateTime::TimeZone->is_valid_name($tz)) { if(not is_valid_timezone_name($tz,1)) { - $tz = DateTime::TimeZone->new(name => 'local'); + $tz = $LOCAL_TZ; } return DateTime->from_epoch( time_zone => $tz, @@ -186,12 +186,37 @@ sub from_string { sub from_rfc1123_string { my $s = shift; - my $strp = DateTime::Format::Strptime->new(pattern => RFC_1123_FORMAT_PATTERN, + my $strp = DateTime::Format::Strptime->new(pattern => $RFC_1123_FORMAT_PATTERN, locale => 'en_US', on_error => 'undef'); return $strp->parse_datetime($s); } +# this shall give a little freedom in how datetime is entered +# this shall be allowed: Y-m-d H:M:S, Y-m-d H:M, Y-m-d +# it returns a DateTime object in floating (meaning local) timezone or the specified timezone +sub from_forminput_string { + my($string, $tz) = @_; + my $parser1 = DateTime::Format::Strptime->new( + pattern => '%Y-%m-%d %H:%M:%S', $tz ? (time_zone => $tz) : (), + ); + my $parser2 = DateTime::Format::Strptime->new( + pattern => '%Y-%m-%d %H:%M', $tz ? (time_zone => $tz) : (), + ); + my $parser3 = DateTime::Format::Strptime->new( + pattern => '%Y-%m-%d', $tz ? (time_zone => $tz) : (), + ); + $string =~ s/^\s*(.*)\s*$/$1/; # remove whitespace around + my $dt = $parser1->parse_datetime($string); + unless ($dt) { + $dt = $parser2->parse_datetime($string); + } + unless ($dt) { + $dt = $parser3->parse_datetime($string); + } + return $dt; +} + sub new_local { my %params; @params{qw/year month day hour minute second nanosecond/} = @_; @@ -199,7 +224,7 @@ sub new_local { !defined $params{$_} and delete $params{$_}; } return DateTime->new( - time_zone => DateTime::TimeZone->new(name => 'local'), + time_zone => $LOCAL_TZ, %params, ); } @@ -227,12 +252,23 @@ sub to_string { sub to_rfc1123_string { my $dt = shift; - my $strp = DateTime::Format::Strptime->new(pattern => RFC_1123_FORMAT_PATTERN, + my $strp = DateTime::Format::Strptime->new(pattern => $RFC_1123_FORMAT_PATTERN, locale => 'en_US', on_error => 'undef'); return $strp->format_datetime($dt); } +sub to_local_string { + my ($dt) = @_; + + unless ('DateTime' eq ref $dt) { + die 'needs a DateTime object to be converted'; + } + + $dt->set_time_zone($LOCAL_TZ); + return to_string($dt); +} + sub get_weekday_names { my $c = shift; return [ diff --git a/lib/NGCP/Panel/Utils/Security.pm b/lib/NGCP/Panel/Utils/Security.pm index 46fe4ce666..285daa261c 100644 --- a/lib/NGCP/Panel/Utils/Security.pm +++ b/lib/NGCP/Panel/Utils/Security.pm @@ -76,7 +76,8 @@ EOF my $config_failed_auth_attempts = $c->config->{security}->{failed_auth_attempts} // 3; for my $key (keys %{ $usr }) { my $last_auth = $usr->{$key}->{last_auth} ? NGCP::Panel::Utils::DateTime::epoch_local($usr->{$key}->{last_auth}) : undef; - if($last_auth && $params{data_for_json}){ + if ($last_auth) { + $last_auth->set_time_zone($c->session->{user_tz}) if $c->session->{user_tz}; $last_auth = $last_auth->ymd.' '. $last_auth->hms; } if( defined $usr->{$key}->{auth_count} diff --git a/share/layout/body.tt b/share/layout/body.tt index f1cfe02f71..95aac2e0a4 100644 --- a/share/layout/body.tt +++ b/share/layout/body.tt @@ -22,6 +22,9 @@ [% c.loc("Not logged in") %] [%- END -%] +
  • + [% IF c.user && c.session.user_tz_name; '(' _ c.session.user_tz_name _ ')'; END; %] +