diff options
Diffstat (limited to 'lib')
-rwxr-xr-x | lib/Travelynx.pm | 801 | ||||
-rw-r--r-- | lib/Travelynx/Command/database.pm | 208 | ||||
-rw-r--r-- | lib/Travelynx/Controller/Account.pm | 47 | ||||
-rwxr-xr-x | lib/Travelynx/Controller/Api.pm | 16 | ||||
-rw-r--r-- | lib/Travelynx/Controller/Static.pm | 3 | ||||
-rwxr-xr-x | lib/Travelynx/Controller/Traveling.pm | 101 |
6 files changed, 656 insertions, 520 deletions
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index fd59cc0..da10a9a 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -191,9 +191,10 @@ sub startup { $self->helper( 'get_departures' => sub { - my ( $self, $station, $lookbehind ) = @_; + my ( $self, $station, $lookbehind, $lookahead ) = @_; $lookbehind //= 180; + $lookahead //= 30; my @station_matches = Travel::Status::DE::IRIS::Stations::get_station($station); @@ -207,7 +208,7 @@ sub startup { lookbehind => 20, datetime => DateTime->now( time_zone => 'Europe/Berlin' ) ->subtract( minutes => $lookbehind ), - lookahead => $lookbehind + 10, + lookahead => $lookbehind + $lookahead, ); return { results => [ $status->results ], @@ -241,6 +242,10 @@ sub startup { 'add_journey' => sub { my ( $self, %opt ) = @_; + $self->app->log->error( + "add_journey is not implemented at the moment"); + return ( undef, undef, 'not implemented' ); + my $user_status = $self->get_user_status; if ( $user_status->{checked_in} or $user_status->{cancelled} ) { @@ -326,11 +331,9 @@ sub startup { $self->helper( 'checkin' => sub { - my ( $self, $station, $train_id, $action_id ) = @_; - - $action_id //= $self->app->action_type->{checkin}; + my ( $self, $station, $train_id ) = @_; - my $status = $self->get_departures($station); + my $status = $self->get_departures( $station, 140, 30 ); if ( $status->{errstr} ) { return ( undef, $status->{errstr} ); } @@ -343,40 +346,35 @@ sub startup { else { my $user = $self->get_user_status; - if ( $user->{checked_in} ) { + if ( $user->{checked_in} or $user->{cancelled} ) { # If a user is already checked in, we assume that they forgot to # check out and do it for them. $self->checkout( $station, 1 ); } - elsif ( $user->{cancelled} ) { - - # Same - $self->checkout( $station, 1, - $self->app->action_type->{cancelled_to} ); - } eval { $self->pg->db->insert( - 'user_actions', + 'in_transit', { - user_id => $self->current_user->{id}, - action_id => $action_id, - station_id => $self->get_station_id( + user_id => $self->current_user->{id}, + cancelled => $train->departure_is_cancelled + ? 1 + : 0, + checkin_station_id => $self->get_station_id( ds100 => $status->{station_ds100}, name => $status->{station_name} ), - action_time => + checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ), - edited => 0, - train_type => $train->type, - train_line => $train->line_no, - train_no => $train->train_no, - train_id => $train->train_id, - sched_time => $train->sched_departure, - real_time => $train->departure, - route => join( '|', $train->route ), - messages => join( + train_type => $train->type, + train_line => $train->line_no, + train_no => $train->train_no, + train_id => $train->train_id, + sched_departure => $train->sched_departure, + real_departure => $train->departure, + route => join( '|', $train->route ), + messages => join( '|', map { ( $_->[0] ? $_->[0]->epoch : q{} ) . ':' @@ -389,7 +387,7 @@ sub startup { if ($@) { my $uid = $self->current_user->{id}; $self->app->log->error( - "Checkin($uid, $action_id): INSERT failed: $@"); + "Checkin($uid): INSERT failed: $@"); return ( undef, 'INSERT failed: ' . $@ ); } return ( $train, undef ); @@ -400,43 +398,86 @@ sub startup { $self->helper( 'undo' => sub { - my ( $self, $action_id ) = @_; - - my $status = $self->get_user_status; + my ( $self, $journey_id ) = @_; + my $uid = $self->current_user->{id}; - if ( $action_id < 1 or $status->{action_id} != $action_id ) { - return -"Invalid action ID: $action_id != $status->{action_id}. Note that you can only undo your latest action."; + if ( $journey_id eq 'in_transit' ) { + eval { + $self->pg->db->delete( 'in_transit', { user_id => $uid } ); + }; + if ($@) { + $self->app->log->error("Undo($uid, $journey_id): $@"); + return "Undo($journey_id): $@"; + } + return undef; + } + if ( $journey_id !~ m{ ^ \d+ $ }x ) { + return 'Invalid Journey ID'; } eval { - $self->pg->db->delete( 'user_actions', { id => $action_id } ); + my $db = $self->pg->db; + my $tx = $db->begin; + + my $journey = $db->select( + 'journeys', + '*', + { + user_id => $uid, + id => $journey_id + } + )->hash; + $db->delete( + 'journeys', + { + user_id => $uid, + id => $journey_id + } + ); + + if ( $journey->{edited} ) { + die( +"Cannot undo a journey which has already been edited. Please delete manually.\n" + ); + } + + delete $journey->{edited}; + delete $journey->{id}; + + $db->insert( 'in_transit', $journey ); + + my $cache_ts = DateTime->now( time_zone => 'Europe/Berlin' ); + if ( $journey->{real_departure} + =~ m{ ^ (?<year> \d{4} ) - (?<month> \d{2} ) }x ) + { + $cache_ts->set( + year => $+{year}, + month => $+{month} + ); + } + + $self->invalidate_stats_cache( $cache_ts, $db ); + + $tx->commit; }; if ($@) { - my $uid = $self->current_user->{id}; - $self->app->log->error( - "Undo($uid, $action_id): DELETE failed: $@"); - return 'DELETE failed: ' . $@; + $self->app->log->error("Undo($uid, $journey_id): $@"); + return "Undo($journey_id): $@"; } - return; + return undef; } ); + # Statistics are partitioned by real_departure, which must be provided + # when calling this function e.g. after journey deletion or editing. + # If a joureny's real_departure has been edited, this function must be + # called twice: once with the old and once with the new value. $self->helper( 'invalidate_stats_cache' => sub { - my ( $self, $ts ) = @_; + my ( $self, $ts, $db ) = @_; my $uid = $self->current_user->{id}; - $ts //= DateTime->now( time_zone => 'Europe/Berlin' ); - - # ts is the checkout timestamp or (for manual entries) the - # time of arrival. As the journey may span a month or year boundary, - # there is a total of five cache entries we need to invalidate: - # * year, month - # * year - # * (year, month) - 1 month (with wraparound) - # * (year) - 1 year - # * total stats + $db //= $self->pg->db; $self->pg->db->delete( 'journey_stats', @@ -454,196 +495,207 @@ sub startup { month => 0, } ); - $ts->subtract( months => 1 ); - $self->pg->db->delete( - 'journey_stats', - { - user_id => $uid, - year => $ts->year, - month => $ts->month, - } - ); - $ts->subtract( months => 11 ); - $self->pg->db->delete( - 'journey_stats', - { - user_id => $uid, - year => $ts->year, - month => 0, - } - ); - $self->pg->db->delete( - 'journey_stats', - { - user_id => $uid, - year => 0, - month => 0, - } - ); } ); $self->helper( 'checkout' => sub { - my ( $self, $station, $force, $action_id ) = @_; - - $action_id //= $self->app->action_type->{checkout}; + my ( $self, $station, $force ) = @_; - my $status = $self->get_departures( $station, 180 ); + my $db = $self->pg->db; + my $uid = $self->current_user->{id}; + my $status = $self->get_departures( $station, 120, 120 ); my $user = $self->get_user_status; my $train_id = $user->{train_id}; if ( not $user->{checked_in} and not $user->{cancelled} ) { - return 'You are not checked into any train'; + return ( 0, 'You are not checked into any train' ); } if ( $status->{errstr} and not $force ) { - return $status->{errstr}; + return ( 1, $status->{errstr} ); } my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + my $journey + = $db->select( 'in_transit', '*', { user_id => $uid } )->hash; my ($train) = first { $_->train_id eq $train_id } @{ $status->{results} }; - if ( not defined $train ) { - if ($force) { - eval { - $self->pg->db->insert( - 'user_actions', - { - user_id => $self->current_user->{id}, - action_id => $action_id, - station_id => $self->get_station_id( - ds100 => $status->{station_ds100}, - name => $status->{station_name} - ), - action_time => $now, - edited => 0 - } - ); - }; - if ($@) { - my $uid = $self->current_user->{id}; - $self->app->log->error( -"Force checkout($uid, $action_id): INSERT failed: $@" - ); - return 'INSERT failed: ' . $@; - } - $self->invalidate_stats_cache; - return; - } - else { - return "Train ${train_id} not found"; - } + + # Store the intended checkout station regardless of this operation's + # success. + my $new_checkout_station_id = $self->get_station_id( + ds100 => $status->{station_ds100}, + name => $status->{station_name} + ); + $db->update( + 'in_transit', + { + checkout_station_id => $new_checkout_station_id, + }, + { user_id => $uid } + ); + + # If in_transit already contains arrival data for another estimated + # destination, we must invalidate it. + if ( defined $journey->{checkout_station_id} + and $journey->{checkout_station_id} + != $new_checkout_station_id ) + { + $db->update( + 'in_transit', + { + checkout_time => undef, + sched_arrival => undef, + real_arrival => undef, + }, + { user_id => $uid } + ); } - else { - eval { - $self->pg->db->insert( - 'user_actions', + + if ( not( defined $train or $force ) ) { + return ( 1, undef ); + } + + my $has_arrived = 0; + + eval { + + my $tx = $db->begin; + + if ( defined $train ) { + $has_arrived = $train->arrival->epoch < $now->epoch ? 1 : 0; + $db->update( + 'in_transit', { - user_id => $self->current_user->{id}, - action_id => $action_id, - station_id => $self->get_station_id( - ds100 => $status->{station_ds100}, - name => $status->{station_name} - ), - action_time => $now, - edited => 0, - train_type => $train->type, - train_line => $train->line_no, - train_no => $train->train_no, - train_id => $train->train_id, - sched_time => $train->sched_arrival, - real_time => $train->arrival, - route => join( '|', $train->route ), - messages => join( + checkout_time => $now, + sched_arrival => $train->sched_arrival, + real_arrival => $train->arrival, + cancelled => $train->arrival_is_cancelled ? 1 : 0, + route => join( '|', $train->route ), + messages => join( '|', map { ( $_->[0] ? $_->[0]->epoch : q{} ) . ':' . $_->[1] } $train->messages - ) - } + ), + }, + { user_id => $uid } ); - }; - if ($@) { - my $uid = $self->current_user->{id}; - $self->app->log->error( - "Checkout($uid, $action_id): INSERT failed: $@"); - return 'INSERT failed: ' . $@; } - $self->invalidate_stats_cache; - return; + + $journey + = $db->select( 'in_transit', '*', { user_id => $uid } )->hash; + + if ( $has_arrived or $force ) { + $journey->{edited} = 0; + $journey->{checkout_time} = $now; + $db->insert( 'journeys', $journey ); + $db->delete( 'in_transit', { user_id => $uid } ); + + my $cache_ts = $now->clone; + if ( $journey->{real_departure} + =~ m{ ^ (?<year> \d{4} ) - (?<month> \d{2} ) }x ) + { + $cache_ts->set( + year => $+{year}, + month => $+{month} + ); + } + $self->invalidate_stats_cache( $cache_ts, $db ); + } + + $tx->commit; + }; + + if ($@) { + $self->app->log->error("Checkout($uid): $@"); + return ( 1, 'Checkout error: ' . $@ ); } + + if ( $has_arrived or $force ) { + return ( 0, undef ); + } + return ( 1, undef ); } ); $self->helper( 'update_journey_part' => sub { - my ( $self, $db, $checkin_id, $checkout_id, $key, $value ) = @_; + my ( $self, $db, $journey_id, $key, $value ) = @_; my $rows; + my $journey = $self->get_journey( + db => $db, + journey_id => $journey_id, + ); + eval { if ( $key eq 'sched_departure' ) { $rows = $db->update( - 'user_actions', + 'journeys', { - sched_time => $value, + sched_departure => $value, + edited => $journey->{edited} | 0x0001, }, { - id => $checkin_id, - action_id => $self->app->action_type->{checkin}, + id => $journey_id, } )->rows; } elsif ( $key eq 'rt_departure' ) { $rows = $db->update( - 'user_actions', + 'journeys', { - real_time => $value, + real_departure => $value, + edited => $journey->{edited} | 0x0002, }, { - id => $checkin_id, - action_id => $self->app->action_type->{checkin}, + id => $journey_id, } )->rows; + + # stats are partitioned by rt_departure -> both the cache for + # the old value (see bottom of this function) and the new value + # (here) must be invalidated. + $self->invalidate_stats_cache( $value, $db ); } elsif ( $key eq 'sched_arrival' ) { $rows = $db->update( - 'user_actions', + 'journeys', { - sched_time => $value, + sched_arrival => $value, + edited => $journey->{edited} | 0x0100, }, { - id => $checkout_id, - action_id => $self->app->action_type->{checkout}, + id => $journey_id, } )->rows; } elsif ( $key eq 'rt_arrival' ) { $rows = $db->update( - 'user_actions', + 'journeys', { - real_time => $value, + real_arrival => $value, + edited => $journey->{edited} | 0x0200, }, { - id => $checkout_id, - action_id => $self->app->action_type->{checkout}, + id => $journey_id, } )->rows; } else { - $self->app->log->error( -"update_journey_part($checkin_id, $checkout_id): Invalid key $key" - ); + die("Invalid key $key\n"); } }; if ($@) { $self->app->log->error( -"update_journey_part($checkin_id, $checkout_id): UPDATE failed: $@" - ); - return 'UPDATE failed: ' . $@; + "update_journey_part($journey_id, $key): $@"); + return "update_journey_part($key): $@"; } if ( $rows == 1 ) { + $self->invalidate_stats_cache( $journey->{rt_departure}, $db ); return undef; } return 'UPDATE failed: did not match any journey part'; @@ -930,14 +982,12 @@ sub startup { $self->helper( 'delete_journey' => sub { - my ( $self, $checkin_id, $checkout_id, $checkin_epoch, - $checkout_epoch ) - = @_; + my ( $self, $journey_id, $checkin_epoch, $checkout_epoch ) = @_; my $uid = $self->current_user->{id}; my @journeys = $self->get_user_travels( - uid => $uid, - checkout_id => $checkout_id + uid => $uid, + journey_id => $journey_id ); if ( @journeys == 0 ) { return 'Journey not found'; @@ -947,8 +997,7 @@ sub startup { # Double-check (comparing both ID and action epoch) to make sure we # are really deleting the right journey and the user isn't just # playing around with POST requests. - if ( $journey->{ids}[0] != $checkin_id - or $journey->{ids}[1] != $checkout_id + if ( $journey->{id} != $journey_id or $journey->{checkin}->epoch != $checkin_epoch or $journey->{checkout}->epoch != $checkout_epoch ) { @@ -958,26 +1007,24 @@ sub startup { my $rows; eval { $rows = $self->pg->db->delete( - 'user_actions', + 'journeys', { user_id => $uid, - id => [ $checkin_id, $checkout_id ] + id => $journey_id, } )->rows; }; if ($@) { - $self->app->log->error( - "Delete($uid, $checkin_id, $checkout_id): DELETE failed: $@" - ); + $self->app->log->error("Delete($uid, $journey_id): $@"); return 'DELETE failed: ' . $@; } - if ( $rows == 2 ) { - $self->invalidate_stats_cache( $journey->{checkout} ); + if ( $rows == 1 ) { + $self->invalidate_stats_cache( $journey->{rt_departure} ); return undef; } - return sprintf( 'Deleted %d rows, expected 2', $rows ); + return sprintf( 'Deleted %d rows, expected 1', $rows ); } ); @@ -1075,203 +1122,103 @@ sub startup { # Otherwise, we grab a fresh one. my $db = $opt{db} // $self->pg->db; - my $selection = qq{ - user_actions.id as action_log_id, action_id, - extract(epoch from action_time) as action_time_ts, - stations.ds100 as ds100, stations.name as name, - train_type, train_line, train_no, train_id, - extract(epoch from sched_time) as sched_time_ts, - extract(epoch from real_time) as real_time_ts, - route, messages, edited - }; - $selection =~ tr{\n}{}d; - my %where = ( user_id => $uid ); + my %where = ( + user_id => $uid, + cancelled => 0 + ); my %order = ( order_by => { - -desc => 'action_time', + -desc => 'real_dep_ts', } ); - if ( $opt{limit} ) { - $order{limit} = 10; + if ( $opt{cancelled} ) { + $where{cancelled} = 1; } - if ( $opt{checkout_id} ) { - $where{'user_actions.id'} = { '<=', $opt{checkout_id} }; - $order{limit} = 2; + if ( $opt{limit} ) { + $order{limit} = $opt{limit}; } - elsif ( $opt{after} and $opt{before} ) { - # Each journey consists of exactly two database entries: one for - # checkin, one for checkout. A simple query using e.g. - # after = YYYY-01-01T00:00:00 and before YYYY-02-01T00:00:00 - # will miss journeys where checkin and checkout take place in - # different months. - # We therefore add one day to the before timestamp and filter out - # journeys whose checkin lies outside the originally requested - # time range afterwards. - # For an additional twist, get_interval_actions_query filters based - # on the action time, not actual departure, as force - # checkout actions lack sched_time and real_time data. By - # subtracting one day from "after" (i.e., moving it one day into - # the past), we make sure not to miss journeys where the real departure - # time falls into the interval, but the checkin time does not. - # Again, this is addressed in postprocessing at the bottom of this - # helper. - # This works under the assumption that there are no DB trains whose - # journey takes more than 24 hours. If this no longer holds, - # please adjust the intervals accordingly. - $where{action_time} = { - -between => [ - $opt{after}->clone->subtract( days => 1 ), - $opt{before}->clone->add( days => 1 ) - ] - }; + if ( $opt{journey_id} ) { + $where{journey_id} = $opt{journey_id}; + delete $where{cancelled}; } - - my @match_actions = ( - $self->app->action_type->{checkout}, - $self->app->action_type->{checkin} - ); - if ( $opt{cancelled} ) { - @match_actions = ( - $self->app->action_type->{cancelled_to}, - $self->app->action_type->{cancelled_from} - ); + elsif ( $opt{after} and $opt{before} ) { + $where{real_dep_ts} = { + -between => [ $opt{after}->epoch, $opt{before}->epoch, ] }; } my @travels; - my $prev_action = 0; - my $res = $db->select( - [ - 'user_actions', - [ - -left => 'stations', - id => 'station_id' - ] - ], - $selection, - \%where, - \%order - ); + my $res = $db->select( 'journeys_str', '*', \%where, \%order ); for my $entry ( $res->hashes->each ) { - if ( $entry->{action_id} == $match_actions[0] - or ( $opt{checkout_id} and not @travels ) ) - { - push( - @travels, - { - ids => [ undef, $entry->{action_log_id} ], - to_name => $entry->{name}, - sched_arrival => - epoch_to_dt( $entry->{sched_time_ts} ), - rt_arrival => epoch_to_dt( $entry->{real_time_ts} ), - checkout => epoch_to_dt( $entry->{action_time_ts} ), - type => $entry->{train_type}, - line => $entry->{train_line}, - no => $entry->{train_no}, - messages => $entry->{messages} - ? [ split( qr{[|]}, $entry->{messages} ) ] - : undef, - route => $entry->{route} - ? [ split( qr{[|]}, $entry->{route} ) ] - : undef, - completed => 0, - edited => $entry->{edited} << 8, - } - ); - } - elsif ( - ( - $entry->{action_id} == $match_actions[1] - and $prev_action == $match_actions[0] - ) - or $opt{checkout_id} - ) - { - my $ref = $travels[-1]; - $ref->{ids}->[0] = $entry->{action_log_id}; - $ref->{from_name} = $entry->{name}; - $ref->{completed} = 1; - $ref->{sched_departure} - = epoch_to_dt( $entry->{sched_time_ts} ); - $ref->{rt_departure} - = epoch_to_dt( $entry->{real_time_ts} ); - $ref->{checkin} = epoch_to_dt( $entry->{action_time_ts} ); - $ref->{type} //= $entry->{train_type}; - $ref->{line} //= $entry->{train_line}; - $ref->{no} //= $entry->{train_no}; - $ref->{messages} - //= [ split( qr{[|]}, $entry->{messages} ) ]; - $ref->{route} //= [ split( qr{[|]}, $entry->{route} ) ]; - $ref->{edited} |= $entry->{edited}; - - if ( $opt{verbose} ) { - my @parsed_messages; - for my $message ( @{ $ref->{messages} // [] } ) { - my ( $ts, $msg ) = split( qr{:}, $message ); - push( @parsed_messages, - [ epoch_to_dt($ts), $msg ] ); - } - $ref->{messages} = [ reverse @parsed_messages ]; - $ref->{sched_duration} - = $ref->{sched_arrival} - ? $ref->{sched_arrival}->epoch - - $ref->{sched_departure}->epoch - : undef; - $ref->{rt_duration} - = $ref->{rt_arrival} - ? $ref->{rt_arrival}->epoch - - $ref->{rt_departure}->epoch - : undef; - my ( $km, $skip ) - = $self->get_travel_distance( $ref->{from_name}, - $ref->{to_name}, $ref->{route} ); - $ref->{km_route} = $km; - $ref->{skip_route} = $skip; - ( $km, $skip ) - = $self->get_travel_distance( $ref->{from_name}, - $ref->{to_name}, - [ $ref->{from_name}, $ref->{to_name} ] ); - $ref->{km_beeline} = $km; - $ref->{skip_beeline} = $skip; - my $kmh_divisor - = ( $ref->{rt_duration} // $ref->{sched_duration} - // 999999 ) / 3600; - $ref->{kmh_route} - = $kmh_divisor ? $ref->{km_route} / $kmh_divisor : -1; - $ref->{kmh_beeline} - = $kmh_divisor - ? $ref->{km_beeline} / $kmh_divisor - : -1; - } - if ( $opt{checkout_id} - and $entry->{action_id} - == $self->app->action_type->{cancelled_from} ) - { - $ref->{cancelled} = 1; + my $ref = { + id => $entry->{journey_id}, + type => $entry->{train_type}, + line => $entry->{train_line}, + no => $entry->{train_no}, + from_name => $entry->{dep_name}, + checkin => epoch_to_dt( $entry->{checkin_ts} ), + sched_departure => epoch_to_dt( $entry->{sched_dep_ts} ), + rt_departure => epoch_to_dt( $entry->{real_dep_ts} ), + to_name => $entry->{arr_name}, + checkout => epoch_to_dt( $entry->{checkout_ts} ), + sched_arrival => epoch_to_dt( $entry->{sched_arr_ts} ), + rt_arrival => epoch_to_dt( $entry->{real_arr_ts} ), + messages => $entry->{messages} + ? [ split( qr{[|]}, $entry->{messages} ) ] + : undef, + route => $entry->{route} + ? [ split( qr{[|]}, $entry->{route} ) ] + : undef, + edited => $entry->{edited}, + }; + + if ( $opt{verbose} ) { + $ref->{cancelled} = $entry->{cancelled}; + my @parsed_messages; + for my $message ( @{ $ref->{messages} // [] } ) { + my ( $ts, $msg ) = split( qr{:}, $message ); + push( @parsed_messages, [ epoch_to_dt($ts), $msg ] ); } + $ref->{messages} = [ reverse @parsed_messages ]; + $ref->{sched_duration} + = $ref->{sched_arrival} + ? $ref->{sched_arrival}->epoch + - $ref->{sched_departure}->epoch + : undef; + $ref->{rt_duration} + = $ref->{rt_arrival} + ? $ref->{rt_arrival}->epoch - $ref->{rt_departure}->epoch + : undef; + my ( $km, $skip ) + = $self->get_travel_distance( $ref->{from_name}, + $ref->{to_name}, $ref->{route} ); + $ref->{km_route} = $km; + $ref->{skip_route} = $skip; + ( $km, $skip ) + = $self->get_travel_distance( $ref->{from_name}, + $ref->{to_name}, + [ $ref->{from_name}, $ref->{to_name} ] ); + $ref->{km_beeline} = $km; + $ref->{skip_beeline} = $skip; + my $kmh_divisor + = ( $ref->{rt_duration} // $ref->{sched_duration} + // 999999 ) / 3600; + $ref->{kmh_route} + = $kmh_divisor ? $ref->{km_route} / $kmh_divisor : -1; + $ref->{kmh_beeline} + = $kmh_divisor + ? $ref->{km_beeline} / $kmh_divisor + : -1; } - $prev_action = $entry->{action_id}; - } - if ( $opt{before} and $opt{after} ) { - @travels = grep { - $_->{rt_departure} >= $opt{after} - and $_->{rt_departure} < $opt{before} - } @travels; + push( @travels, $ref ); } - # user_actions are sorted by action_time. As users are allowed to check - # into trains in arbitrary order, action_time does not always - # correspond to departure/arrival time, so we ensure a proper sort - # order here. - @travels - = sort { $b->{rt_departure} <=> $a->{rt_departure} } @travels; - return @travels; } ); @@ -1280,11 +1227,9 @@ sub startup { 'get_journey' => sub { my ( $self, %opt ) = @_; + $opt{cancelled} = 'any'; my @journeys = $self->get_user_travels(%opt); - if ( @journeys == 0 - or not $journeys[0]{completed} - or $journeys[0]{ids}[1] != $opt{checkout_id} ) - { + if ( @journeys == 0 ) { return undef; } @@ -1298,86 +1243,96 @@ sub startup { $uid //= $self->current_user->{id}; - my $selection = qq{ - user_actions.id as action_log_id, action_id, - extract(epoch from action_time) as action_time_ts, - stations.ds100 as ds100, stations.name as name, - train_type, train_line, train_no, train_id, - extract(epoch from sched_time) as sched_time_ts, - extract(epoch from real_time) as real_time_ts, - route - }; - $selection =~ tr{\n}{}d; + my $db = $self->pg->db; + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); - my $res = $self->pg->db->select( - [ - 'user_actions', - [ - -left => 'stations', - id => 'station_id' - ] - ], - $selection, - { - user_id => $uid, - }, - { - order_by => { - -desc => 'action_time', - }, - limit => 1, - } - ); - my $status = $res->hash; + my $in_transit + = $db->select( 'in_transit_str', '*', { user_id => $uid } )->hash; - if ($status) { - my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + if ($in_transit) { - my $action_ts = epoch_to_dt( $status->{action_time_ts} ); - my $sched_ts = epoch_to_dt( $status->{sched_time_ts} ); - my $real_ts = epoch_to_dt( $status->{real_time_ts} ); - my $checkin_station_name = $status->{name}; - my @route = split( qr{[|]}, $status->{route} // q{} ); + my @route = split( qr{[|]}, $in_transit->{route} // q{} ); my @route_after; my $is_after = 0; for my $station (@route) { - if ( $station eq $checkin_station_name ) { + if ( $station eq $in_transit->{dep_name} ) { $is_after = 1; } if ($is_after) { push( @route_after, $station ); } } + + my $ts = $in_transit->{checkout_ts} + // $in_transit->{checkin_ts}; + my $action_time = epoch_to_dt($ts); + return { - checked_in => ( - $status->{action_id} - == $self->app->action_type->{checkin} - ), - cancelled => ( - $status->{action_id} - == $self->app->action_type->{cancelled_from} - ), - timestamp => $action_ts, - timestamp_delta => $now->epoch - $action_ts->epoch, - action_id => $status->{action_log_id}, - sched_ts => $sched_ts, - real_ts => $real_ts, - station_ds100 => $status->{ds100}, - station_name => $checkin_station_name, - train_type => $status->{train_type}, - train_line => $status->{train_line}, - train_no => $status->{train_no}, - train_id => $status->{train_id}, - route => \@route, - route_after => \@route_after, + checked_in => !$in_transit->{cancelled}, + cancelled => $in_transit->{cancelled}, + timestamp => $action_time, + timestamp_delta => $now->epoch - $action_time->epoch, + train_type => $in_transit->{train_type}, + train_line => $in_transit->{train_line}, + train_no => $in_transit->{train_no}, + train_id => $in_transit->{train_id}, + sched_departure => + epoch_to_dt( $in_transit->{sched_dep_ts} ), + real_departure => epoch_to_dt( $in_transit->{real_dep_ts} ), + dep_ds100 => $in_transit->{dep_ds100}, + dep_name => $in_transit->{dep_name}, + sched_arrival => epoch_to_dt( $in_transit->{sched_arr_ts} ), + real_arrival => epoch_to_dt( $in_transit->{real_arr_ts} ), + arr_ds100 => $in_transit->{arr_ds100}, + arr_name => $in_transit->{arr_name}, + route_after => \@route_after, }; } + + my $latest = $db->select( + 'journeys_str', + '*', + { + user_id => $uid, + cancelled => 0 + }, + { + order_by => { -desc => 'journey_id' }, + limit => 1 + } + )->hash; + + if ($latest) { + my $ts = $latest->{checkout_ts}; + my $action_time = epoch_to_dt($ts); + return { + checked_in => 0, + cancelled => 0, + journey_id => $latest->{journey_id}, + timestamp => $action_time, + timestamp_delta => $now->epoch - $action_time->epoch, + train_type => $latest->{train_type}, + train_line => $latest->{train_line}, + train_no => $latest->{train_no}, + train_id => $latest->{train_id}, + sched_departure => epoch_to_dt( $latest->{sched_dep_ts} ), + real_departure => epoch_to_dt( $latest->{real_dep_ts} ), + dep_ds100 => $latest->{dep_ds100}, + dep_name => $latest->{dep_name}, + sched_arrival => epoch_to_dt( $latest->{sched_arr_ts} ), + real_arrival => epoch_to_dt( $latest->{real_arr_ts} ), + arr_ds100 => $latest->{arr_ds100}, + arr_name => $latest->{arr_name}, + }; + } + return { - checked_in => 0, - timestamp => epoch_to_dt(0), - sched_ts => epoch_to_dt(0), - real_ts => epoch_to_dt(0), + checked_in => 0, + cancelled => 0, + no_journeys_yet => 1, + timestamp => epoch_to_dt(0), + timestamp_delta => $now->epoch, }; } ); diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index b270262..b5e8cf5 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -12,9 +12,11 @@ sub get_schema_version { my $version; eval { - $version = $db->select( 'schema_version', ['version'] )->hash->{version}; + $version + = $db->select( 'schema_version', ['version'] )->hash->{version}; }; if ($@) { + # If it failed, the version table does not exist -> run setup first. return undef; } @@ -124,6 +126,210 @@ my @migrations = ( } ); }, + + # v3 -> v4 + # Introduces "journeys", containing one row for each complete + # journey, and "in_transit", containing the journey which is currently + # in progress (if any). "user_actions" is no longer used, but still kept + # as a backup for now. + sub { + my ($db) = @_; + + $db->query( + qq{ + create table journeys ( + id serial not null primary key, + user_id integer not null references users (id), + train_type varchar(16) not null, + train_line varchar(16), + train_no varchar(16) not null, + train_id varchar(128) not null, + checkin_station_id integer not null references stations (id), + checkin_time timestamptz not null, + sched_departure timestamptz not null, + real_departure timestamptz not null, + checkout_station_id integer not null references stations (id), + checkout_time timestamptz not null, + sched_arrival timestamptz, + real_arrival timestamptz, + cancelled boolean not null, + edited smallint not null, + route text, + messages text + ); + create table in_transit ( + user_id integer not null references users (id) primary key, + train_type varchar(16) not null, + train_line varchar(16), + train_no varchar(16) not null, + train_id varchar(128) not null, + checkin_station_id integer not null references stations (id), + checkin_time timestamptz not null, + sched_departure timestamptz not null, + real_departure timestamptz not null, + checkout_station_id int references stations (id), + checkout_time timestamptz, + sched_arrival timestamptz, + real_arrival timestamptz, + cancelled boolean not null, + route text, + messages text + ); + create view journeys_str as select + journeys.id as journey_id, user_id, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + dep_stations.ds100 as dep_ds100, + dep_stations.name as dep_name, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + arr_stations.ds100 as arr_ds100, + arr_stations.name as arr_name, + cancelled, edited, route, messages + from journeys + join stations as dep_stations on dep_stations.id = checkin_station_id + join stations as arr_stations on arr_stations.id = checkout_station_id + ; + create view in_transit_str as select + user_id, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + dep_stations.ds100 as dep_ds100, + dep_stations.name as dep_name, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + arr_stations.ds100 as arr_ds100, + arr_stations.name as arr_name, + cancelled, route, messages + from in_transit + join stations as dep_stations on dep_stations.id = checkin_station_id + left join stations as arr_stations on arr_stations.id = checkout_station_id + ; + } + ); + + my @uids + = $db->select( 'users', ['id'] )->hashes->map( sub { shift->{id} } ) + ->each; + my $count = 0; + + for my $uid (@uids) { + my %cache; + my $prev_action_type = 0; + my $actions = $db->select( + 'user_actions', '*', + { user_id => $uid }, + { order_by => { -asc => 'id' } } + ); + for my $action ( $actions->hashes->each ) { + my $action_type = $action->{action_id}; + my $id = $action->{id}; + + if ( $action_type == 2 and $prev_action_type != 1 ) { + die( +"Inconsistent data at uid ${uid} action ${id}: Illegal transition $prev_action_type -> $action_type.\n" + ); + } + + if ( $action_type == 5 and $prev_action_type != 4 ) { + die( +"Inconsistent data at uid ${uid} action ${id}: Illegal transition $prev_action_type -> $action_type.\n" + ); + } + + if ( $action_type == 1 or $action_type == 4 ) { + %cache = ( + train_type => $action->{train_type}, + train_line => $action->{train_line}, + train_no => $action->{train_no}, + train_id => $action->{train_id}, + checkin_station_id => $action->{station_id}, + checkin_time => $action->{action_time}, + sched_departure => $action->{sched_time}, + real_departure => $action->{real_time}, + route => $action->{route}, + messages => $action->{messages}, + cancelled => $action->{action_id} == 4 ? 1 : 0, + edited => $action->{edited}, + ); + } + elsif ( $action_type == 2 or $action_type == 5 ) { + $cache{checkout_station_id} = $action->{station_id}; + $cache{checkout_time} = $action->{action_time}; + $cache{sched_arrival} = $action->{sched_time}; + $cache{real_arrival} = $action->{real_time}; + $cache{edited} |= $action->{edited} << 8; + if ( $action->{route} ) { + $cache{route} = $action->{route}; + } + if ( $action->{messages} ) { + $cache{messages} = $action->{messages}; + } + + $db->insert( + 'journeys', + { + user_id => $uid, + train_type => $cache{train_type}, + train_line => $cache{train_line}, + train_no => $cache{train_no}, + train_id => $cache{train_id}, + checkin_station_id => $cache{checkin_station_id}, + checkin_time => $cache{checkin_time}, + sched_departure => $cache{sched_departure}, + real_departure => $cache{real_departure}, + checkout_station_id => $cache{checkout_station_id}, + checkout_time => $cache{checkout_time}, + sched_arrival => $cache{sched_arrival}, + real_arrival => $cache{real_arrival}, + cancelled => $cache{cancelled}, + edited => $cache{edited}, + route => $cache{route}, + messages => $cache{messages} + } + ); + + %cache = (); + + } + + $prev_action_type = $action_type; + } + + if (%cache) { + + # user is currently in transit + $db->insert( + 'in_transit', + { + user_id => $uid, + train_type => $cache{train_type}, + train_line => $cache{train_line}, + train_no => $cache{train_no}, + train_id => $cache{train_id}, + checkin_station_id => $cache{checkin_station_id}, + checkin_time => $cache{checkin_time}, + sched_departure => $cache{sched_departure}, + real_departure => $cache{real_departure}, + cancelled => $cache{cancelled}, + route => $cache{route}, + messages => $cache{messages} + } + ); + } + + $count++; + printf( " journey storage migration: %3.0f%% complete\n", + $count * 100 / @uids ); + } + $db->update( 'schema_version', { version => 4 } ); + }, ); sub setup_db { diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index 0037e16..e60f1d3 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -286,45 +286,18 @@ sub account { sub json_export { my ($self) = @_; - my $uid = $self->current_user->{id}; - my $query = $self->app->get_all_actions_query; - - $query->execute($uid); - - my @entries; - - while ( my @row = $query->fetchrow_array ) { - my ( - $action_id, $action, $raw_ts, $ds100, - $name, $train_type, $train_line, $train_no, - $train_id, $raw_sched_ts, $raw_real_ts, $raw_route, - $raw_messages - ) = @row; - - push( - @entries, - { - action => $self->app->action_types->[ $action - 1 ], - action_ts => $raw_ts, - station_ds100 => $ds100, - station_name => $name, - train_type => $train_type, - train_line => $train_line, - train_no => $train_no, - train_id => $train_id, - scheduled_ts => $raw_sched_ts, - realtime_ts => $raw_real_ts, - messages => $raw_messages - ? [ map { [ split(qr{:}) ] } split( qr{[|]}, $raw_messages ) ] - : undef, - route => $raw_route ? [ split( qr{[|]}, $raw_route ) ] - : undef, - } - ); - } + my $uid = $self->current_user->{id}; + + my $db = $self->pg->db; $self->render( - json => [@entries], + json => { + account => $db->select( 'users', '*', { id => $uid } )->hash, + journeys => [ + $db->select( 'journeys', '*', { user_id => $uid } ) + ->hashes->each + ], + } ); } diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm index a9500f1..8e72374 100755 --- a/lib/Travelynx/Controller/Api.pm +++ b/lib/Travelynx/Controller/Api.pm @@ -48,10 +48,10 @@ sub get_v0 { my $station_lon = undef; my $station_lat = undef; - if ( $status->{station_ds100} ) { + if ( $status->{arr_ds100} // $status->{dep_ds100} ) { @station_descriptions = Travel::Status::DE::IRIS::Stations::get_station( - $status->{station_ds100} ); + $status->{arr_ds100} // $status->{dep_ds100} ); } if ( @station_descriptions == 1 ) { ( undef, undef, $station_eva, $station_lon, $station_lat ) @@ -59,14 +59,14 @@ sub get_v0 { } $self->render( json => { - deprecated => \0, + deprecated => \1, checked_in => ( $status->{checked_in} or $status->{cancelled} ) ? \1 : \0, station => { - ds100 => $status->{station_ds100}, - name => $status->{station_name}, + ds100 => $status->{arr_ds100} // $status->{dep_ds100}, + name => $status->{arr_ds100} // $status->{dep_ds100}, uic => $station_eva, longitude => $station_lon, latitude => $station_lat, @@ -77,8 +77,10 @@ sub get_v0 { no => $status->{train_no}, }, actionTime => $status->{timestamp}->epoch, - scheduledTime => $status->{sched_ts}->epoch, - realTime => $status->{real_ts}->epoch, + scheduledTime => $status->{sched_arrival}->epoch + || $status->{sched_departure}->epoch, + realTime => $status->{real_arrival}->epoch + || $status->{real_departure}->epoch, }, ); } diff --git a/lib/Travelynx/Controller/Static.pm b/lib/Travelynx/Controller/Static.pm index 0144d83..09d7f51 100644 --- a/lib/Travelynx/Controller/Static.pm +++ b/lib/Travelynx/Controller/Static.pm @@ -6,7 +6,8 @@ my $travelynx_version = qx{git describe --dirty} || 'experimental'; sub about { my ($self) = @_; - $self->render( 'about', version => $self->app->config->{version} // 'UNKNOWN' ); + $self->render( 'about', + version => $self->app->config->{version} // 'UNKNOWN' ); } sub imprint { diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index b43c891..d8e5e03 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -97,13 +97,16 @@ sub log_action { else { $self->render( json => { - success => 1, + success => 1, + redirect_to => '/', }, ); } } elsif ( $params->{action} eq 'checkout' ) { - my $error = $self->checkout( $params->{station}, $params->{force} ); + my ( $still_checked_in, $error ) + = $self->checkout( $params->{station}, $params->{force} ); + my $station_link = '/s/' . $params->{station}; if ($error) { $self->render( @@ -116,7 +119,8 @@ sub log_action { else { $self->render( json => { - success => 1, + success => 1, + redirect_to => $still_checked_in ? '/' : $station_link, }, ); } @@ -134,7 +138,8 @@ sub log_action { else { $self->render( json => { - success => 1, + success => 1, + redirect_to => '/', }, ); } @@ -155,13 +160,15 @@ sub log_action { else { $self->render( json => { - success => 1, + success => 1, + redirect_to => '/', }, ); } } elsif ( $params->{action} eq 'cancelled_to' ) { - my $error = $self->checkout( $params->{station}, 1, + my ( undef, $error ) + = $self->checkout( $params->{station}, 1, $self->app->action_type->{cancelled_to} ); if ($error) { @@ -175,14 +182,14 @@ sub log_action { else { $self->render( json => { - success => 1, + success => 1, + redirect_to => '/', }, ); } } elsif ( $params->{action} eq 'delete' ) { - my ( $from, $to ) = split( qr{,}, $params->{ids} ); - my $error = $self->delete_journey( $from, $to, $params->{checkin}, + my $error = $self->delete_journey( $params->{id}, $params->{checkin}, $params->{checkout} ); if ($error) { $self->render( @@ -195,7 +202,8 @@ sub log_action { else { $self->render( json => { - success => 1, + success => 1, + redirect_to => '/history', }, ); } @@ -215,7 +223,7 @@ sub station { my $station = $self->stash('station'); my $train = $self->param('train'); - my $status = $self->get_departures($station); + my $status = $self->get_departures( $station, 120, 30 ); if ( $status->{errstr} ) { $self->render( @@ -382,11 +390,13 @@ sub monthly_history { sub journey_details { my ($self) = @_; - my ( $uid, $checkout_id ) = split( qr{-}, $self->stash('id') ); + my $journey_id = $self->stash('id'); + + my $uid = $self->current_user->{id}; - $self->param( journey_id => $checkout_id ); + $self->param( journey_id => $journey_id ); - if ( not( $uid == $self->current_user->{id} and $checkout_id ) ) { + if ( not($journey_id) ) { $self->render( 'journey', error => 'notfound', @@ -395,36 +405,35 @@ sub journey_details { return; } - my @journeys = $self->get_user_travels( - uid => $uid, - checkout_id => $checkout_id, - verbose => 1, + my $journey = $self->get_journey( + uid => $uid, + journey_id => $journey_id, + verbose => 1, ); - if ( @journeys == 0 - or not $journeys[0]{completed} - or $journeys[0]{ids}[1] != $checkout_id ) - { + + if ($journey) { + $self->render( + 'journey', + error => undef, + journey => $journey, + ); + } + else { $self->render( 'journey', error => 'notfound', journey => {} ); - return; } - $self->render( - 'journey', - error => undef, - journey => $journeys[0] - ); } sub edit_journey { - my ($self) = @_; - my $checkout_id = $self->param('journey_id'); - my $uid = $self->current_user->{id}; + my ($self) = @_; + my $journey_id = $self->param('journey_id'); + my $uid = $self->current_user->{id}; - if ( not( $uid == $self->current_user->{id} and $checkout_id ) ) { + if ( not( $journey_id =~ m{ ^ \d+ $ }x ) ) { $self->render( 'edit_journey', error => 'notfound', @@ -434,8 +443,8 @@ sub edit_journey { } my $journey = $self->get_journey( - uid => $uid, - checkout_id => $checkout_id + uid => $uid, + journey_id => $journey_id ); if ( not $journey ) { @@ -449,11 +458,6 @@ sub edit_journey { my $error = undef; - if ( $self->param('action') and $self->param('action') eq 'cancel' ) { - $self->redirect_to("/journey/${uid}-${checkout_id}"); - return; - } - if ( $self->param('action') and $self->param('action') eq 'save' ) { my $parser = DateTime::Format::Strptime->new( pattern => '%d.%m.%Y %H:%M', @@ -468,12 +472,8 @@ sub edit_journey { { my $datetime = $parser->parse_datetime( $self->param($key) ); if ( $datetime and $datetime->epoch ne $journey->{$key}->epoch ) { - $error = $self->update_journey_part( - $db, - $journey->{ids}[0], - $journey->{ids}[1], - $key, $datetime - ); + $error = $self->update_journey_part( $db, $journey->{id}, + $key, $datetime ); if ($error) { last; } @@ -482,17 +482,16 @@ sub edit_journey { if ( not $error ) { $journey = $self->get_journey( - uid => $uid, - db => $db, - checkout_id => $checkout_id, - verbose => 1 + uid => $uid, + db => $db, + journey_id => $journey_id, + verbose => 1 ); $error = $self->journey_sanity_check($journey); } if ( not $error ) { $tx->commit; - $self->redirect_to("/journey/${uid}-${checkout_id}"); - $self->invalidate_stats_cache( $journey->{checkout} ); + $self->redirect_to("/journey/${journey_id}"); return; } } |