diff options
author | Daniel Friesel <derf@finalrewind.org> | 2019-04-23 18:08:07 +0200 |
---|---|---|
committer | Daniel Friesel <derf@finalrewind.org> | 2019-04-23 18:08:07 +0200 |
commit | e168d9cd39c38b8e5a1994c8bf11376d26e9ea77 (patch) | |
tree | dee321d330a8fc9c106c18863db179972c9abac6 | |
parent | 812be4f0cbeb74db379a2c931ceb2c88bd9fabf7 (diff) |
Use one row per journey instead of split checkin/checkout entries
Whether a user is in transit or not is now determined by an entry in the
in_transit table instead of a dangling checkin.
All completed journeys are stored in the "journeys" table.
This does most of the work needed for automatic checkout. However, note that
the corresponding worker process is not implemented yet.
-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 | ||||
-rw-r--r-- | public/static/js/travelynx-actions.js | 18 | ||||
-rw-r--r-- | public/static/js/travelynx-actions.min.js | 2 | ||||
l--------- | public/static/v6 (renamed from public/static/v4) | 0 | ||||
-rw-r--r-- | templates/_history_trains.html.ep | 72 | ||||
-rw-r--r-- | templates/departures.html.ep | 12 | ||||
-rw-r--r-- | templates/edit_journey.html.ep | 2 | ||||
-rw-r--r-- | templates/journey.html.ep | 2 | ||||
-rw-r--r-- | templates/landingpage.html.ep | 86 | ||||
-rw-r--r-- | templates/layouts/default.html.ep | 2 |
15 files changed, 783 insertions, 589 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; } } diff --git a/public/static/js/travelynx-actions.js b/public/static/js/travelynx-actions.js index 3f1eb89..583c806 100644 --- a/public/static/js/travelynx-actions.js +++ b/public/static/js/travelynx-actions.js @@ -1,11 +1,11 @@ -function tvly_run(link, req, redir, err_callback) { +function tvly_run(link, req, err_callback) { var error_icon = '<i class="material-icons">error</i>'; var progressbar = $('<div class="progress"><div class="indeterminate"></div></div>'); link.hide(); link.after(progressbar); $.post('/action', req, function(data) { if (data.success) { - $(location).attr('href', redir); + $(location).attr('href', data.redirect_to); } else { M.toast({html: error_icon + ' ' + data.error}); progressbar.remove(); @@ -25,7 +25,7 @@ $(document).ready(function() { station: link.data('station'), train: link.data('train'), }; - tvly_run(link, req, '/'); + tvly_run(link, req); }); $('.action-checkout').click(function() { var link = $(this); @@ -34,7 +34,7 @@ $(document).ready(function() { station: link.data('station'), force: link.data('force'), }; - tvly_run(link, req, '/s/' + req.station, function() { + tvly_run(link, req, function() { link.append(' – Ohne Echtzeitdaten auschecken?') link.data('force', true); }); @@ -45,7 +45,7 @@ $(document).ready(function() { action: 'undo', undo_id: link.data('id'), }; - tvly_run(link, req, '/'); + tvly_run(link, req); }); $('.action-cancelled-from').click(function() { var link = $(this); @@ -54,7 +54,7 @@ $(document).ready(function() { station: link.data('station'), train: link.data('train'), }; - tvly_run(link, req, '/'); + tvly_run(link, req); }); $('.action-cancelled-to').click(function() { var link = $(this); @@ -63,19 +63,19 @@ $(document).ready(function() { station: link.data('station'), force: true, }; - tvly_run(link, req, '/'); + tvly_run(link, req); }); $('.action-delete').click(function() { var link = $(this); var req = { action: 'delete', - ids: link.data('id'), + id: link.data('id'), checkin: link.data('checkin'), checkout: link.data('checkout'), }; really_delete = confirm("Diese Zugfahrt wirklich löschen? Der Eintrag wird sofort aus der Datenbank entfernt und kann nicht wiederhergestellt werden."); if (really_delete) { - tvly_run(link, req, '/history'); + tvly_run(link, req); } }); }); diff --git a/public/static/js/travelynx-actions.min.js b/public/static/js/travelynx-actions.min.js index 57b6284..db9fa1b 100644 --- a/public/static/js/travelynx-actions.min.js +++ b/public/static/js/travelynx-actions.min.js @@ -1 +1 @@ -function tvly_run(n,t,a,c){var i='<i class="material-icons">error</i>',e=$('<div class="progress"><div class="indeterminate"></div></div>');n.hide(),n.after(e),$.post("/action",t,function(t){t.success?$(location).attr("href",a):(M.toast({html:i+" "+t.error}),e.remove(),c&&c(),n.append(" "+i),n.show())})}$(document).ready(function(){$(".action-checkin").click(function(){var t=$(this);tvly_run(t,{action:"checkin",station:t.data("station"),train:t.data("train")},"/")}),$(".action-checkout").click(function(){var t=$(this),n={action:"checkout",station:t.data("station"),force:t.data("force")};tvly_run(t,n,"/s/"+n.station,function(){t.append(" – Ohne Echtzeitdaten auschecken?"),t.data("force",!0)})}),$(".action-undo").click(function(){var t=$(this);tvly_run(t,{action:"undo",undo_id:t.data("id")},"/")}),$(".action-cancelled-from").click(function(){var t=$(this);tvly_run(t,{action:"cancelled_from",station:t.data("station"),train:t.data("train")},"/")}),$(".action-cancelled-to").click(function(){var t=$(this);tvly_run(t,{action:"cancelled_to",station:t.data("station"),force:!0},"/")}),$(".action-delete").click(function(){var t=$(this),n={action:"delete",ids:t.data("id"),checkin:t.data("checkin"),checkout:t.data("checkout")};really_delete=confirm("Diese Zugfahrt wirklich löschen? Der Eintrag wird sofort aus der Datenbank entfernt und kann nicht wiederhergestellt werden."),really_delete&&tvly_run(t,n,"/history")})}); +function tvly_run(t,n,a){var c='<i class="material-icons">error</i>',i=$('<div class="progress"><div class="indeterminate"></div></div>');t.hide(),t.after(i),$.post("/action",n,function(n){n.success?$(location).attr("href",n.redirect_to):(M.toast({html:c+" "+n.error}),i.remove(),a&&a(),t.append(" "+c),t.show())})}$(document).ready(function(){$(".action-checkin").click(function(){var t=$(this);tvly_run(t,{action:"checkin",station:t.data("station"),train:t.data("train")})}),$(".action-checkout").click(function(){var t=$(this),n={action:"checkout",station:t.data("station"),force:t.data("force")};tvly_run(t,n,function(){t.append(" – Ohne Echtzeitdaten auschecken?"),t.data("force",!0)})}),$(".action-undo").click(function(){var t=$(this);tvly_run(t,{action:"undo",undo_id:t.data("id")})}),$(".action-cancelled-from").click(function(){var t=$(this);tvly_run(t,{action:"cancelled_from",station:t.data("station"),train:t.data("train")})}),$(".action-cancelled-to").click(function(){var t=$(this);tvly_run(t,{action:"cancelled_to",station:t.data("station"),force:!0})}),$(".action-delete").click(function(){var t=$(this),n={action:"delete",id:t.data("id"),checkin:t.data("checkin"),checkout:t.data("checkout")};really_delete=confirm("Diese Zugfahrt wirklich löschen? Der Eintrag wird sofort aus der Datenbank entfernt und kann nicht wiederhergestellt werden."),really_delete&&tvly_run(t,n)})}); diff --git a/public/static/v4 b/public/static/v6 index 945c9b4..945c9b4 120000 --- a/public/static/v4 +++ b/public/static/v6 diff --git a/templates/_history_trains.html.ep b/templates/_history_trains.html.ep index def83d6..2328285 100644 --- a/templates/_history_trains.html.ep +++ b/templates/_history_trains.html.ep @@ -11,46 +11,44 @@ </thead> <tbody> % for my $travel (@{$journeys}) { - % if ($travel->{completed}) { - % my $detail_link = '/journey/' . current_user()->{id} . '-' . $travel->{ids}->[1]; - <tr> - <td><%= $travel->{sched_departure}->strftime('%d.%m.') %></td> - <td><a href="<%= $detail_link %>"><%= $travel->{type} %> <%= $travel->{line} // $travel->{no} %></a></td> - <td> - <a href="<%= $detail_link %>" class="unmarked"> - % if (param('cancelled')) { - %= $travel->{sched_departure}->strftime('%H:%M') + % my $detail_link = '/journey/' . $travel->{id}; + <tr> + <td><%= $travel->{sched_departure}->strftime('%d.%m.') %></td> + <td><a href="<%= $detail_link %>"><%= $travel->{type} %> <%= $travel->{line} // $travel->{no} %></a></td> + <td> + <a href="<%= $detail_link %>" class="unmarked"> + % if (param('cancelled')) { + %= $travel->{sched_departure}->strftime('%H:%M') + % } + % else { + <%= $travel->{rt_departure}->strftime('%H:%M') %> + % if ($travel->{sched_departure} != $travel->{rt_departure}) { + (<%= sprintf('%+d', ($travel->{rt_departure}->epoch - $travel->{sched_departure}->epoch) / 60) %>) % } - % else { - <%= $travel->{rt_departure}->strftime('%H:%M') %> - % if ($travel->{sched_departure} != $travel->{rt_departure}) { - (<%= sprintf('%+d', ($travel->{rt_departure}->epoch - $travel->{sched_departure}->epoch) / 60) %>) + % } + <br/> + <%= $travel->{from_name} %> + </a> + </td> + <td> + <a href="<%= $detail_link %>" class="unmarked"> + % if (param('cancelled') and $travel->{sched_arrival}->epoch != 0) { + %= $travel->{sched_arrival}->strftime('%H:%M') + % } + % else { + % if ($travel->{rt_arrival}->epoch == 0 and $travel->{sched_arrival}->epoch == 0) { + <i class="material-icons">timer_off</i> + % } else { + %= $travel->{rt_arrival}->strftime('%H:%M'); + % if ($travel->{sched_arrival} != $travel->{rt_arrival}) { + (<%= sprintf('%+d', ($travel->{rt_arrival}->epoch - $travel->{sched_arrival}->epoch) / 60) %>) % } % } - <br/> - <%= $travel->{from_name} %> - </a> - </td> - <td> - <a href="<%= $detail_link %>" class="unmarked"> - % if (param('cancelled') and $travel->{sched_arrival}->epoch != 0) { - %= $travel->{sched_arrival}->strftime('%H:%M') - % } - % else { - % if ($travel->{rt_arrival}->epoch == 0 and $travel->{sched_arrival}->epoch == 0) { - <i class="material-icons">timer_off</i> - % } else { - %= $travel->{rt_arrival}->strftime('%H:%M'); - % if ($travel->{sched_arrival} != $travel->{rt_arrival}) { - (<%= sprintf('%+d', ($travel->{rt_arrival}->epoch - $travel->{sched_arrival}->epoch) / 60) %>) - % } - % } - % } - <br/> - <%= $travel->{to_name} %> - </a></td> - </tr> - % } + % } + <br/> + <%= $travel->{to_name} %> + </a></td> + </tr> % } </tbody> </table> diff --git a/templates/departures.html.ep b/templates/departures.html.ep index 1240d1d..7d33417 100644 --- a/templates/departures.html.ep +++ b/templates/departures.html.ep @@ -6,10 +6,10 @@ <div class="card-content white-text"> <span class="card-title">Aktuell eingecheckt</span> <p>In <%= $status->{train_type} %> <%= $status->{train_no} %> - ab <%= $status->{station_name} %></p> + ab <%= $status->{dep_name} %></p> </div> <div class="card-action"> - <a class="action-checkout" data-station="<%= $ds100 %>"> + <a class="action-checkout" data-station="<%= $ds100 %>" data-force="1"> Hier auschecken </a> </div> @@ -20,10 +20,10 @@ <div class="card-content white-text"> <span class="card-title">Ausgecheckt</span> <p>Aus <%= $status->{train_type} %> <%= $status->{train_no} %> - bis <%= $status->{station_name} %></p> + bis <%= $status->{arr_name} %></p> </div> <div class="card-action"> - <a class="action-undo" data-id="<%= $status->{action_id} %>"> + <a class="action-undo" data-id="<%= $status->{journey_id} %>"> <i class="material-icons">undo</i> Rückgängig? </a> </div> @@ -38,8 +38,8 @@ – Zug auswählen zum Einchecken. % } % else { - – Keine Abfahrten gefunden. Ein Checkin ist frühestens 10 Minuten vor - und maximal 180 Minuten nach Abfahrt möglich. + – Keine Abfahrten gefunden. Ein Checkin ist frühestens 30 Minuten vor + und maximal 120 Minuten nach Abfahrt möglich. % } <br/> <table class="striped"> diff --git a/templates/edit_journey.html.ep b/templates/edit_journey.html.ep index 0418d52..c37adba 100644 --- a/templates/edit_journey.html.ep +++ b/templates/edit_journey.html.ep @@ -80,7 +80,7 @@ </div> <div class="row"> <div class="col s6 m6 l6 center-align"> - <a href="/journey/<%= current_user()->{id} %>-<%= param('journey_id') %>" class="waves-effect waves-light btn"> + <a href="/journey/<%= param('journey_id') %>" class="waves-effect waves-light btn"> Abbrechen </a> </div> diff --git a/templates/journey.html.ep b/templates/journey.html.ep index 1e0ccc7..4af9694 100644 --- a/templates/journey.html.ep +++ b/templates/journey.html.ep @@ -161,7 +161,7 @@ <div class="row"> <div class="col s6 m6 l6 center-align"> <a class="waves-effect waves-light red btn action-delete" - data-id="<%= join(q{,}, @{$journey->{ids}}) %>" + data-id="<%= $journey->{id} %>" data-checkin="<%= $journey->{checkin}->epoch %>" data-checkout="<%= $journey->{checkout}->epoch %>"> <i class="material-icons left">delete_forever</i> diff --git a/templates/landingpage.html.ep b/templates/landingpage.html.ep index 5f69fd1..cd14f5c 100644 --- a/templates/landingpage.html.ep +++ b/templates/landingpage.html.ep @@ -14,26 +14,84 @@ <div class="row"> <div class="col s12"> % my $status = get_user_status(); + % my $now = DateTime->now(time_zone => 'Europe/Berlin'); + % my $dep_wait = ($status->{real_departure}->epoch - $now->epoch)/60; + % my $arr_wait = undef; + % if ($status->{real_arrival}->epoch) { + % $arr_wait = ($status->{real_arrival}->epoch - $now->epoch)/60; + % } % if ($status->{checked_in}) { <div class="card green darken-4"> <div class="card-content white-text"> - <span class="card-title">Hallo, <%= current_user()->{name} %>!</span> - <p>Du bist gerade eingecheckt in - <%= $status->{train_type} %> <%= $status->{train_no} %> - ab <%= $status->{station_name} %>. - % if ($status->{timestamp_delta} < 180) { - <a class="action-undo" data-id="<%= $status->{action_id} %>"><i class="material-icons">undo</i> Rückgängig</a> + <span class="card-title">Eingecheckt</span> + <p> + In <b><%= $status->{train_type} %> <%= $status->{train_no} %></b> + % if ($status->{arr_name}) { + von <b><%= $status->{dep_name} %></b> nach <b><%= $status->{arr_name} %></b>. % } - </p> - <p>Bei Ankunft: Station auswählen zum Auschecken.</p> + % else { + ab <b><%= $status->{dep_name} %></b>. + % } + </p> + <p> + Abfahrt + % if ($dep_wait > 0) { + in <%= int(($status->{real_departure}->epoch - $now->epoch)/60) %> Minute<%= $dep_wait >= 2 ? 'n' : '' %> + % } + um <b><%= $status->{real_departure}->strftime('%H:%M') %></b> + % if ($status->{real_departure}->epoch != $status->{sched_departure}->epoch) { + (+<%= int(($status->{real_departure}->epoch - $status->{sched_departure}->epoch)/60) %>) + % } + </p> + <p> + % if ($status->{real_arrival}->epoch) { + Voraussichtliche Ankunft um <b><%= $status->{real_arrival}->strftime('%H:%M') %></b> + % if ($status->{real_arrival}->epoch != $status->{sched_arrival}->epoch) { + (+<%= int(($status->{real_arrival}->epoch - $status->{sched_arrival}->epoch)/60) %>) + % } + % } + % else { + Ankunft: noch nicht bekannt + % } + </p> + <p> + <b>Achtung:</b> Automatischer Checkout ist noch nicht + implementiert. Bitte spätestens eine Stunde nach Ankunft + am Ziel manuell auschecken. + </p> + % if ($status->{arr_name}) { + <p>Zielstation ändern?</p> + % } + % else { + <p>Zielstation wählen:</p> + % } <table> <tbody> % my $is_after = 0; % for my $station (@{$status->{route_after}}) { - <tr><td><a class="action-checkout" data-station="<%= $station %>"><%= $station %></a></td></tr> + % if ($status->{arr_name} and $station eq $status->{arr_name}) { + <tr><td><b><a class="action-checkout" data-station="<%= $station %>"><%= $station %></a></b></td></tr> + % } + % else { + <tr><td><a class="action-checkout" data-station="<%= $station %>"><%= $station %></a></td></tr> + % } % } </tbody> </table> + % if ($status->{arr_name}) { + <p> + Falls das Backend ausgefallen ist oder der Zug aus anderen + Gründen verloren ging: <a class="action-checkout" + data-force="1" data-station="<%= $status->{arr_name} + %>">Ohne Echtzeitdaten in <%= $status->{arr_name} %> + auschecken</a>. + </p> + % } + </div> + <div class="card-action"> + <a class="action-undo" data-id="in_transit"> + <i class="material-icons">undo</i> Checkin Rückgängig? + </a> </div> </div> % } @@ -44,9 +102,6 @@ <p>Prinzipiell wärest du nun eingecheckt in <%= $status->{train_type} %> <%= $status->{train_no} %> ab <%= $status->{station_name} %>, doch dieser Zug fällt aus. - % if ($status->{timestamp_delta} < 180) { - <a class="action-undo" data-id="<%= $status->{action_id} %>"><i class="material-icons">undo</i> Checkinversuch rückgängig</a> - % } </p> <p>Falls du den Zugausfall z.B. für ein Fahrgastrechteformular dokumentieren möchtest, wähle bitte jetzt deine geplante @@ -62,6 +117,11 @@ </tbody> </table> </div> + <div class="card-action"> + <a class="action-undo" data-id="in_transit"> + <i class="material-icons">undo</i> Checkinversuch Rückgängig? + </a> + </div> </div> % } % else { @@ -91,7 +151,7 @@ </div> </div> <h1>Letzte Fahrten</h1> - %= include '_history_trains', journeys => [get_user_travels(limit => 1)]; + %= include '_history_trains', journeys => [get_user_travels(limit => 5)]; % } % else { <div class="row"> diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep index 9ab9269..cdd624a 100644 --- a/templates/layouts/default.html.ep +++ b/templates/layouts/default.html.ep @@ -5,7 +5,7 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="theme-color" content="#673ab7"> - % my $av = 'v5'; # asset version + % my $av = 'v6'; # asset version %= stylesheet "/static/${av}/css/materialize.min.css" %= stylesheet "/static/${av}/css/material-icons.css" %= stylesheet "/static/${av}/css/local.css" |