travelynx/lib/Travelynx/Controller/Traveling.pm
Daniel Friesel 89e709d8d5 Allow linking a Träwelling account, auto-sync Träwelling→travelynx
travelynx→Träwelling is still work-in-progress

Squashed commit of the following:

commit 97faa6e2e6c8d20fba30f2d0f6e78187ceeb72e6
Author: Daniel Friesel <derf@finalrewind.org>
Date:   Wed Sep 30 18:50:05 2020 +0200

    improve traewelling log and tx handling

commit 487d7dd728b9d45b731bdc7098cf3358ea2e206e
Author: Daniel Friesel <derf@finalrewind.org>
Date:   Wed Sep 30 18:02:41 2020 +0200

    add missing traewelling template

commit 0148da2f48d9a52dcddc0ab81f83d8f8ac3062ab
Author: Daniel Friesel <derf@finalrewind.org>
Date:   Wed Sep 30 18:02:35 2020 +0200

    improve traewelling pull sync

commit 4861a9750f9f2d7621043361d0af6b0a8869a0df
Author: Daniel Friesel <derf@finalrewind.org>
Date:   Tue Sep 29 22:14:24 2020 +0200

    wip checkin from traewelling

commit f6aeb6f06998a2a7a80f63a7b1b688b1a26b66bd
Author: Daniel Friesel <derf@finalrewind.org>
Date:   Tue Sep 29 18:37:53 2020 +0200

    refactor traewelling integration. login and logout are less of a hack now.

    checkin and checkout are not supported at the moment.
2020-09-30 19:12:29 +02:00

1357 lines
30 KiB
Perl
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package Travelynx::Controller::Traveling;
use Mojo::Base 'Mojolicious::Controller';
use DateTime;
use DateTime::Format::Strptime;
use JSON;
use List::Util qw(uniq min max);
use List::UtilsBy qw(max_by uniq_by);
use List::MoreUtils qw(first_index);
use Text::CSV;
use Travel::Status::DE::IRIS::Stations;
sub homepage {
my ($self) = @_;
if ( $self->is_user_authenticated ) {
$self->render(
'landingpage',
version => $self->app->config->{version} // 'UNKNOWN',
with_autocomplete => 1,
with_geolocation => 1
);
$self->users->mark_seen( uid => $self->current_user->{id} );
}
else {
$self->render(
'landingpage',
version => $self->app->config->{version} // 'UNKNOWN',
intro => 1
);
}
}
sub user_status {
my ($self) = @_;
my $name = $self->stash('name');
my $ts = $self->stash('ts') // 0;
my $user = $self->users->get_privacy_by_name( name => $name );
if ( not $user or not $user->{public_level} & 0x03 ) {
$self->render('not_found');
return;
}
if ( $user->{public_level} & 0x01 and not $self->is_user_authenticated ) {
$self->render( 'login', redirect_to => $self->req->url );
return;
}
my $status = $self->get_user_status( $user->{id} );
my $journey;
if (
$ts
and ( not $status->{checked_in}
or $status->{sched_departure}->epoch != $ts )
and ( $user->{public_level} & 0x20
or
( $user->{public_level} & 0x10 and $self->is_user_authenticated ) )
)
{
for my $candidate (
$self->journeys->get(
uid => $user->{id},
limit => 10,
)
)
{
if ( $candidate->{sched_dep_ts} eq $ts ) {
$journey = $self->journeys->get_single(
uid => $user->{id},
journey_id => $candidate->{id},
verbose => 1,
with_datetime => 1,
with_polyline => 1,
);
}
}
}
my %tw_data = (
card => 'summary',
site => '@derfnull',
image => $self->url_for('/static/icons/icon-512x512.png')
->to_abs->scheme('https'),
);
my %og_data = (
type => 'article',
image => $tw_data{image},
url => $self->url_for("/status/${name}")->to_abs->scheme('https'),
site_name => 'travelynx',
);
if ($journey) {
$og_data{title} = $tw_data{title} = sprintf( 'Fahrt von %s nach %s',
$journey->{from_name}, $journey->{to_name} );
$og_data{description} = $tw_data{description}
= $journey->{rt_arrival}->strftime('Ankunft am %d.%m.%Y um %H:%M');
$og_data{url} .= "/${ts}";
}
elsif (
$ts
and ( not $status->{checked_in}
or $status->{sched_departure}->epoch != $ts )
)
{
$og_data{title} = $tw_data{title} = "Bahnfahrt beendet";
$og_data{description} = $tw_data{description}
= "${name} hat das Ziel erreicht";
}
elsif ( $status->{checked_in} ) {
$og_data{url} .= '/' . $status->{sched_departure}->epoch;
$og_data{title} = $tw_data{title} = "${name} ist unterwegs";
$og_data{description} = $tw_data{description} = sprintf(
'%s %s von %s nach %s',
$status->{train_type}, $status->{train_line} // $status->{train_no},
$status->{dep_name}, $status->{arr_name} // 'irgendwo'
);
if ( $status->{real_arrival}->epoch ) {
$tw_data{description} .= $status->{real_arrival}
->strftime(' Ankunft gegen %H:%M Uhr');
$og_data{description} .= $status->{real_arrival}
->strftime(' Ankunft gegen %H:%M Uhr');
}
}
else {
$og_data{title} = $tw_data{title}
= "${name} ist gerade nicht eingecheckt";
$og_data{description} = $tw_data{description}
= "Letztes Fahrtziel: $status->{arr_name}";
}
if ($journey) {
if ( not $user->{public_level} & 0x04 ) {
delete $journey->{user_data}{comment};
}
my $map_data = $self->journeys_to_map_data(
journeys => [$journey],
include_manual => 1,
);
$self->render(
'journey',
error => undef,
with_map => 1,
readonly => 1,
journey => $journey,
twitter => \%tw_data,
opengraph => \%og_data,
%{$map_data},
);
}
else {
$self->render(
'user_status',
name => $name,
public_level => $user->{public_level},
journey => $status,
twitter => \%tw_data,
opengraph => \%og_data,
);
}
}
sub public_profile {
my ($self) = @_;
my $name = $self->stash('name');
my $user = $self->users->get_privacy_by_name( name => $name );
if (
$user
and ( $user->{public_level} & 0x22
or
( $user->{public_level} & 0x11 and $self->is_user_authenticated ) )
)
{
my $status = $self->get_user_status( $user->{id} );
my @journeys;
if ( $user->{public_level} & 0x40 ) {
@journeys = $self->journeys->get(
uid => $user->{id},
limit => 10,
with_datetime => 1
);
}
else {
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
my $month_ago = $now->clone->subtract( weeks => 4 );
@journeys = $self->journeys->get(
uid => $user->{id},
limit => 10,
with_datetime => 1,
after => $month_ago,
before => $now
);
}
$self->render(
'profile',
name => $name,
uid => $user->{id},
public_level => $user->{public_level},
journey => $status,
journeys => [@journeys],
version => $self->app->config->{version} // 'UNKNOWN',
);
}
else {
$self->render('not_found');
}
}
sub public_journey_details {
my ($self) = @_;
my $name = $self->stash('name');
my $journey_id = $self->stash('id');
my $user = $self->users->get_privacy_by_name( name => $name );
$self->param( journey_id => $journey_id );
if ( not( $journey_id and $journey_id =~ m{ ^ \d+ $ }x ) ) {
$self->render(
'journey',
status => 404,
error => 'notfound',
journey => {}
);
return;
}
if (
$user
and ( $user->{public_level} & 0x20
or
( $user->{public_level} & 0x10 and $self->is_user_authenticated ) )
)
{
my $journey = $self->journeys->get_single(
uid => $user->{id},
journey_id => $journey_id,
verbose => 1,
with_datetime => 1,
with_polyline => 1,
);
if ( not( $user->{public_level} & 0x40 ) ) {
my $month_ago = DateTime->now( time_zone => 'Europe/Berlin' )
->subtract( weeks => 4 )->epoch;
if ( $journey and $journey->{rt_dep_ts} < $month_ago ) {
$journey = undef;
}
}
if ($journey) {
my $title = sprintf( 'Fahrt von %s nach %s am %s',
$journey->{from_name}, $journey->{to_name},
$journey->{rt_arrival}->strftime('%d.%m.%Y') );
my $description = sprintf( 'Ankunft mit %s %s %s',
$journey->{type}, $journey->{no},
$journey->{rt_arrival}->strftime('um %H:%M') );
my %tw_data = (
card => 'summary',
site => '@derfnull',
image => $self->url_for('/static/icons/icon-512x512.png')
->to_abs->scheme('https'),
title => $title,
description => $description,
);
my %og_data = (
type => 'article',
image => $tw_data{image},
url => $self->url_for->to_abs,
site_name => 'travelynx',
title => $title,
description => $description,
);
my $map_data = $self->journeys_to_map_data(
journeys => [$journey],
include_manual => 1,
);
if ( $journey->{user_data}{comment}
and not $user->{public_level} & 0x04 )
{
delete $journey->{user_data}{comment};
}
$self->render(
'journey',
error => undef,
journey => $journey,
with_map => 1,
username => $name,
readonly => 1,
twitter => \%tw_data,
opengraph => \%og_data,
%{$map_data},
);
}
else {
$self->render('not_found');
}
}
else {
$self->render('not_found');
}
}
sub public_status_card {
my ($self) = @_;
my $name = $self->stash('name');
my $user = $self->users->get_privacy_by_name( name => $name );
delete $self->stash->{layout};
if (
$user
and ( $user->{public_level} & 0x02
or
( $user->{public_level} & 0x01 and $self->is_user_authenticated ) )
)
{
my $status = $self->get_user_status( $user->{id} );
$self->render(
'_public_status_card',
name => $name,
public_level => $user->{public_level},
journey => $status
);
}
else {
$self->render('not_found');
}
}
sub status_card {
my ($self) = @_;
my $status = $self->get_user_status;
delete $self->stash->{layout};
if ( $status->{checked_in} ) {
$self->render( '_checked_in', journey => $status );
}
elsif ( $status->{cancellation} ) {
$self->render( '_cancelled_departure',
journey => $status->{cancellation} );
}
else {
$self->render( '_checked_out', journey => $status );
}
}
sub geolocation {
my ($self) = @_;
my $lon = $self->param('lon');
my $lat = $self->param('lat');
if ( not $lon or not $lat ) {
$self->render( json => { error => 'Invalid lon/lat received' } );
}
else {
my @candidates = map {
{
ds100 => $_->[0][0],
name => $_->[0][1],
eva => $_->[0][2],
lon => $_->[0][3],
lat => $_->[0][4],
distance => $_->[1],
}
} Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon,
$lat, 10 );
@candidates = uniq_by { $_->{name} } @candidates;
if ( @candidates > 5 ) {
$self->render(
json => {
candidates => [ @candidates[ 0 .. 4 ] ],
}
);
}
else {
$self->render(
json => {
candidates => [@candidates],
}
);
}
}
}
sub log_action {
my ($self) = @_;
my $params = $self->req->json;
if ( not exists $params->{action} ) {
$params = $self->req->params->to_hash;
}
if ( not $self->is_user_authenticated ) {
# We deliberately do not set the HTTP status for these replies, as it
# confuses jquery.
$self->render(
json => {
success => 0,
error => 'Session error, please login again',
},
);
return;
}
if ( not $params->{action} ) {
$self->render(
json => {
success => 0,
error => 'Missing action value',
},
);
return;
}
my $station = $params->{station};
if ( $params->{action} eq 'checkin' ) {
my ( $train, $error ) = $self->checkin(
station => $params->{station},
train_id => $params->{train}
);
my $destination = $params->{dest};
if ($error) {
$self->render(
json => {
success => 0,
error => $error,
},
);
}
elsif ( not $destination ) {
$self->render(
json => {
success => 1,
redirect_to => '/',
},
);
}
else {
# Silently ignore errors -- if they are permanent, the user will see
# them when selecting the destination manually.
my ( $still_checked_in, undef ) = $self->checkout(
station => $destination,
force => 0
);
my $station_link = '/s/' . $destination;
$self->render(
json => {
success => 1,
redirect_to => $still_checked_in ? '/' : $station_link,
},
);
}
}
elsif ( $params->{action} eq 'checkout' ) {
my ( $still_checked_in, $error ) = $self->checkout(
station => $params->{station},
force => $params->{force}
);
my $station_link = '/s/' . $params->{station};
if ($error) {
$self->render(
json => {
success => 0,
error => $error,
},
);
}
else {
$self->render(
json => {
success => 1,
redirect_to => $still_checked_in ? '/' : $station_link,
},
);
}
}
elsif ( $params->{action} eq 'undo' ) {
my $status = $self->get_user_status;
my $error = $self->undo( $params->{undo_id} );
if ($error) {
$self->render(
json => {
success => 0,
error => $error,
},
);
}
else {
my $redir = '/';
if ( $status->{checked_in} or $status->{cancelled} ) {
$redir = '/s/' . $status->{dep_ds100};
}
$self->render(
json => {
success => 1,
redirect_to => $redir,
},
);
}
}
elsif ( $params->{action} eq 'cancelled_from' ) {
my ( undef, $error ) = $self->checkin(
station => $params->{station},
train_id => $params->{train}
);
if ($error) {
$self->render(
json => {
success => 0,
error => $error,
},
);
}
else {
$self->render(
json => {
success => 1,
redirect_to => '/',
},
);
}
}
elsif ( $params->{action} eq 'cancelled_to' ) {
my ( undef, $error ) = $self->checkout(
station => $params->{station},
force => 1
);
if ($error) {
$self->render(
json => {
success => 0,
error => $error,
},
);
}
else {
$self->render(
json => {
success => 1,
redirect_to => '/',
},
);
}
}
elsif ( $params->{action} eq 'delete' ) {
my $error = $self->journeys->delete(
uid => $self->current_user->{id},
id => $params->{id},
checkin => $params->{checkin},
checkout => $params->{checkout}
);
if ($error) {
$self->render(
json => {
success => 0,
error => $error,
},
);
}
else {
$self->render(
json => {
success => 1,
redirect_to => '/history',
},
);
}
}
else {
$self->render(
json => {
success => 0,
error => 'invalid action value',
},
);
}
}
sub station {
my ($self) = @_;
my $station = $self->stash('station');
my $train = $self->param('train');
my $status = $self->iris->get_departures(
station => $station,
lookbehind => 120,
lookahead => 30,
with_related => 1
);
if ( $status->{errstr} ) {
$self->render(
'landingpage',
version => $self->app->config->{version} // 'UNKNOWN',
with_autocomplete => 1,
with_geolocation => 1,
error => $status->{errstr}
);
}
else {
# You can't check into a train which terminates here
my @results = grep { $_->departure } @{ $status->{results} };
@results = map { $_->[0] }
sort { $b->[1] <=> $a->[1] }
map { [ $_, $_->departure->epoch // $_->sched_departure->epoch ] }
@results;
if ($train) {
@results
= grep { $_->type . ' ' . $_->train_no eq $train } @results;
}
$self->render(
'departures',
eva => $status->{station_eva},
results => \@results,
station => $status->{station_name},
related_stations => $status->{related_stations},
title => "travelynx: $status->{station_name}",
);
}
$self->users->mark_seen( uid => $self->current_user->{id} );
}
sub redirect_to_station {
my ($self) = @_;
my $station = $self->param('station');
$self->redirect_to("/s/${station}");
}
sub cancelled {
my ($self) = @_;
my @journeys = $self->journeys->get(
uid => $self->current_user->{id},
cancelled => 1,
with_datetime => 1
);
$self->respond_to(
json => { json => [@journeys] },
any => {
template => 'cancelled',
journeys => [@journeys]
}
);
}
sub history {
my ($self) = @_;
$self->render( template => 'history' );
}
sub commute {
my ($self) = @_;
my $year = $self->param('year');
my $filter_type = $self->param('filter_type') || 'exact';
my $station = $self->param('station');
# DateTime is very slow when looking far into the future due to DST changes
# -> Limit time range to avoid accidental DoS.
if (
not( $year
and $year =~ m{ ^ [0-9]{4} $ }x
and $year > 1990
and $year < 2100 )
)
{
$year = DateTime->now( time_zone => 'Europe/Berlin' )->year - 1;
}
my $interval_start = DateTime->new(
time_zone => 'Europe/Berlin',
year => $year,
month => 1,
day => 1,
hour => 0,
minute => 0,
second => 0,
);
my $interval_end = $interval_start->clone->add( years => 1 );
my @journeys = $self->journeys->get(
uid => $self->current_user->{id},
after => $interval_start,
before => $interval_end,
with_datetime => 1,
);
if ( not $station ) {
my %candidate_count;
for my $journey (@journeys) {
my $dep = $journey->{rt_departure};
my $arr = $journey->{rt_arrival};
if ( $arr->dow <= 5 and $arr->hour <= 12 ) {
$candidate_count{ $journey->{to_name} }++;
}
elsif ( $dep->dow <= 5 and $dep->hour > 12 ) {
$candidate_count{ $journey->{from_name} }++;
}
else {
# Avoid selecting an intermediate station for multi-leg commutes.
# Assumption: The intermediate station is also used for private
# travels -> penalize stations which are used on weekends or at
# unexpected times.
$candidate_count{ $journey->{from_name} }--;
$candidate_count{ $journey->{to_name} }--;
}
}
$station = max_by { $candidate_count{$_} } keys %candidate_count;
}
my %journeys_by_month;
my %count_by_month;
my $total = 0;
my $prev_doy = 0;
for my $journey ( reverse @journeys ) {
my $month = $journey->{rt_departure}->month;
if (
(
$filter_type eq 'exact' and ( $journey->{to_name} eq $station
or $journey->{from_name} eq $station )
)
or (
$filter_type eq 'substring'
and ( $journey->{to_name} =~ m{\Q$station\E}
or $journey->{from_name} =~ m{\Q$station\E} )
)
or (
$filter_type eq 'regex'
and ( $journey->{to_name} =~ m{$station}
or $journey->{from_name} =~ m{$station} )
)
)
{
push( @{ $journeys_by_month{$month} }, $journey );
my $doy = $journey->{rt_departure}->day_of_year;
if ( $doy != $prev_doy ) {
$count_by_month{$month}++;
$total++;
}
$prev_doy = $doy;
}
}
$self->param( year => $year );
$self->param( filter_type => $filter_type );
$self->param( station => $station );
$self->render(
template => 'commute',
with_autocomplete => 1,
journeys_by_month => \%journeys_by_month,
count_by_month => \%count_by_month,
total_journeys => $total,
months => [
qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember)
],
);
}
sub map_history {
my ($self) = @_;
my $location = $self->app->coordinates_by_station;
if ( not $self->param('route_type') ) {
$self->param( route_type => 'polybee' );
}
my $route_type = $self->param('route_type');
my $with_polyline = $route_type eq 'beeline' ? 0 : 1;
my @journeys = $self->journeys->get(
uid => $self->current_user->{id},
with_polyline => $with_polyline
);
if ( not @journeys ) {
$self->render(
template => 'history_map',
with_map => 1,
skipped_journeys => [],
station_coordinates => [],
polyline_groups => [],
);
return;
}
my $include_manual = $self->param('include_manual') ? 1 : 0;
my $res = $self->journeys_to_map_data(
journeys => \@journeys,
route_type => $route_type,
include_manual => $include_manual
);
$self->render(
template => 'history_map',
with_map => 1,
%{$res}
);
}
sub json_history {
my ($self) = @_;
$self->render(
json => [ $self->journeys->get( uid => $self->current_user->{id} ) ] );
}
sub csv_history {
my ($self) = @_;
my $csv = Text::CSV->new( { eol => "\r\n" } );
my $buf = q{};
$csv->combine(
qw(Zugtyp Linie Nummer Start Ziel),
'Start (DS100)',
'Ziel (DS100)',
'Abfahrt (soll)',
'Abfahrt (ist)',
'Ankunft (soll)',
'Ankunft (ist)',
'Kommentar',
'ID'
);
$buf .= $csv->string;
for my $journey (
$self->journeys->get(
uid => $self->current_user->{id},
with_datetime => 1
)
)
{
if (
$csv->combine(
$journey->{type},
$journey->{line},
$journey->{no},
$journey->{from_name},
$journey->{to_name},
$journey->{from_ds100},
$journey->{to_ds100},
$journey->{sched_departure}->strftime('%Y-%m-%d %H:%M'),
$journey->{rt_departure}->strftime('%Y-%m-%d %H:%M'),
$journey->{sched_arrival}->strftime('%Y-%m-%d %H:%M'),
$journey->{rt_arrival}->strftime('%Y-%m-%d %H:%M'),
$journey->{user_data}{comment} // q{},
$journey->{id}
)
)
{
$buf .= $csv->string;
}
}
$self->render(
text => $buf,
format => 'csv'
);
}
sub yearly_history {
my ($self) = @_;
my $year = $self->stash('year');
my @journeys;
my $stats;
# DateTime is very slow when looking far into the future due to DST changes
# -> Limit time range to avoid accidental DoS.
if ( not( $year =~ m{ ^ [0-9]{4} $ }x and $year > 1990 and $year < 2100 ) )
{
@journeys = $self->journeys->get(
uid => $self->current_user->{id},
with_datetime => 1
);
}
else {
my $interval_start = DateTime->new(
time_zone => 'Europe/Berlin',
year => $year,
month => 1,
day => 1,
hour => 0,
minute => 0,
second => 0,
);
my $interval_end = $interval_start->clone->add( years => 1 );
@journeys = $self->journeys->get(
uid => $self->current_user->{id},
after => $interval_start,
before => $interval_end,
with_datetime => 1
);
$stats = $self->get_journey_stats( year => $year );
}
$self->respond_to(
json => {
json => {
journeys => [@journeys],
statistics => $stats
}
},
any => {
template => 'history_by_year',
journeys => [@journeys],
year => $year,
statistics => $stats
}
);
}
sub monthly_history {
my ($self) = @_;
my $year = $self->stash('year');
my $month = $self->stash('month');
my @journeys;
my $stats;
my @months
= (
qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember)
);
if (
not( $year =~ m{ ^ [0-9]{4} $ }x
and $year > 1990
and $year < 2100
and $month =~ m{ ^ [0-9]{1,2} $ }x
and $month > 0
and $month < 13 )
)
{
@journeys = $self->journeys->get(
uid => $self->current_user->{id},
with_datetime => 1
);
}
else {
my $interval_start = DateTime->new(
time_zone => 'Europe/Berlin',
year => $year,
month => $month,
day => 1,
hour => 0,
minute => 0,
second => 0,
);
my $interval_end = $interval_start->clone->add( months => 1 );
@journeys = $self->journeys->get(
uid => $self->current_user->{id},
after => $interval_start,
before => $interval_end,
with_datetime => 1
);
$stats = $self->get_journey_stats(
year => $year,
month => $month
);
}
$self->respond_to(
json => {
json => {
journeys => [@journeys],
statistics => $stats
}
},
any => {
template => 'history_by_month',
journeys => [@journeys],
year => $year,
month => $month,
month_name => $months[ $month - 1 ],
statistics => $stats
}
);
}
sub journey_details {
my ($self) = @_;
my $journey_id = $self->stash('id');
my $uid = $self->current_user->{id};
$self->param( journey_id => $journey_id );
if ( not( $journey_id and $journey_id =~ m{ ^ \d+ $ }x ) ) {
$self->render(
'journey',
status => 404,
error => 'notfound',
journey => {}
);
return;
}
my $journey = $self->journeys->get_single(
uid => $uid,
journey_id => $journey_id,
verbose => 1,
with_datetime => 1,
with_polyline => 1,
);
if ($journey) {
my $map_data = $self->journeys_to_map_data(
journeys => [$journey],
include_manual => 1,
);
$self->render(
'journey',
error => undef,
journey => $journey,
with_map => 1,
%{$map_data},
);
}
else {
$self->render(
'journey',
status => 404,
error => 'notfound',
journey => {}
);
}
}
sub comment_form {
my ($self) = @_;
my $dep_ts = $self->param('dep_ts');
my $status = $self->get_user_status;
if ( not $status->{checked_in} ) {
$self->render(
'edit_comment',
error => 'notfound',
journey => {}
);
}
elsif ( not $dep_ts ) {
$self->param( dep_ts => $status->{sched_departure}->epoch );
$self->param( comment => $status->{comment} );
$self->render(
'edit_comment',
error => undef,
journey => $status
);
}
elsif ( $self->validation->csrf_protect->has_error('csrf_token') ) {
$self->render(
'edit_comment',
error => undef,
journey => $status
);
}
elsif ( $dep_ts != $status->{sched_departure}->epoch ) {
# TODO find and update appropriate past journey (if it exists)
$self->param( comment => $status->{comment} );
$self->render(
'edit_comment',
error => undef,
journey => $status
);
}
else {
$self->app->log->debug("set comment");
$self->update_in_transit_comment( $self->param('comment') );
$self->redirect_to('/');
}
}
sub edit_journey {
my ($self) = @_;
my $journey_id = $self->param('journey_id');
my $uid = $self->current_user->{id};
if ( not( $journey_id =~ m{ ^ \d+ $ }x ) ) {
$self->render(
'edit_journey',
status => 404,
error => 'notfound',
journey => {}
);
return;
}
my $journey = $self->journeys->get_single(
uid => $uid,
journey_id => $journey_id,
verbose => 1,
with_datetime => 1,
);
if ( not $journey ) {
$self->render(
'edit_journey',
status => 404,
error => 'notfound',
journey => {}
);
return;
}
my $error = undef;
if ( $self->param('action') and $self->param('action') eq 'save' ) {
my $parser = DateTime::Format::Strptime->new(
pattern => '%d.%m.%Y %H:%M',
locale => 'de_DE',
time_zone => 'Europe/Berlin'
);
my $db = $self->pg->db;
my $tx = $db->begin;
for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival))
{
my $datetime = $parser->parse_datetime( $self->param($key) );
if ( $datetime and $datetime->epoch ne $journey->{$key}->epoch ) {
$error = $self->journeys->update(
uid => $uid,
db => $db,
id => $journey->{id},
$key => $datetime
);
if ($error) {
last;
}
}
}
for my $key (qw(from_name to_name)) {
if ( defined $self->param($key)
and $self->param($key) ne $journey->{$key} )
{
$error = $self->journeys->update(
uid => $uid,
db => $db,
id => $journey->{id},
$key => $self->param($key)
);
if ($error) {
last;
}
}
}
for my $key (qw(comment)) {
if (
defined $self->param($key)
and ( not $journey->{user_data}
or $journey->{user_data}{$key} ne $self->param($key) )
)
{
$error = $self->journeys->update(
uid => $uid,
db => $db,
id => $journey->{id},
$key => $self->param($key)
);
if ($error) {
last;
}
}
}
if ( defined $self->param('route') ) {
my @route_old = map { $_->[0] } @{ $journey->{route} };
my @route_new = split( qr{\r?\n\r?}, $self->param('route') );
@route_new = grep { $_ ne '' } @route_new;
if ( join( '|', @route_old ) ne join( '|', @route_new ) ) {
$error = $self->journeys->update(
uid => $uid,
db => $db,
id => $journey->{id},
route => [@route_new]
);
}
}
{
my $cancelled_old = $journey->{cancelled} // 0;
my $cancelled_new = $self->param('cancelled') // 0;
if ( $cancelled_old != $cancelled_new ) {
$error = $self->journeys->update(
uid => $uid,
db => $db,
id => $journey->{id},
cancelled => $cancelled_new
);
}
}
if ( not $error ) {
$journey = $self->journeys->get_single(
uid => $uid,
db => $db,
journey_id => $journey_id,
verbose => 1,
with_datetime => 1,
);
$error = $self->journeys->sanity_check($journey);
}
if ( not $error ) {
$tx->commit;
$self->redirect_to("/journey/${journey_id}");
return;
}
}
for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival)) {
if ( $journey->{$key} and $journey->{$key}->epoch ) {
$self->param(
$key => $journey->{$key}->strftime('%d.%m.%Y %H:%M') );
}
}
$self->param(
route => join( "\n", map { $_->[0] } @{ $journey->{route} } ) );
$self->param( cancelled => $journey->{cancelled} ? 1 : 0 );
$self->param( from_name => $journey->{from_name} );
$self->param( to_name => $journey->{to_name} );
for my $key (qw(comment)) {
if ( $journey->{user_data} and $journey->{user_data}{$key} ) {
$self->param( $key => $journey->{user_data}{$key} );
}
}
$self->render(
'edit_journey',
with_autocomplete => 1,
error => $error,
journey => $journey
);
}
sub add_journey_form {
my ($self) = @_;
if ( $self->param('action') and $self->param('action') eq 'save' ) {
my $parser = DateTime::Format::Strptime->new(
pattern => '%d.%m.%Y %H:%M',
locale => 'de_DE',
time_zone => 'Europe/Berlin'
);
my %opt;
my @parts = split( qr{\s+}, $self->param('train') );
if ( @parts == 2 ) {
@opt{ 'train_type', 'train_no' } = @parts;
}
elsif ( @parts == 3 ) {
@opt{ 'train_type', 'train_line', 'train_no' } = @parts;
}
else {
$self->render(
'add_journey',
with_autocomplete => 1,
error =>
'Zug muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.'
);
return;
}
for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival))
{
if ( $self->param($key) ) {
my $datetime = $parser->parse_datetime( $self->param($key) );
if ( not $datetime ) {
$self->render(
'add_journey',
with_autocomplete => 1,
error => "${key}: Ungültiges Datums-/Zeitformat"
);
return;
}
$opt{$key} = $datetime;
}
}
$opt{rt_departure} //= $opt{sched_departure};
$opt{rt_arrival} //= $opt{sched_arrival};
for my $key (qw(dep_station arr_station route cancelled comment)) {
$opt{$key} = $self->param($key);
}
if ( $opt{route} ) {
$opt{route} = [ split( qr{\r?\n\r?}, $opt{route} ) ];
}
my $db = $self->pg->db;
my $tx = $db->begin;
$opt{db} = $db;
$opt{uid} = $self->current_user->{id};
my ( $journey_id, $error ) = $self->journeys->add(%opt);
if ( not $error ) {
my $journey = $self->journeys->get_single(
uid => $self->current_user->{id},
db => $db,
journey_id => $journey_id,
verbose => 1
);
$error = $self->journeys->sanity_check($journey);
}
if ($error) {
$self->render(
'add_journey',
with_autocomplete => 1,
error => $error,
);
}
else {
$tx->commit;
$self->redirect_to("/journey/${journey_id}");
}
}
else {
$self->render(
'add_journey',
with_autocomplete => 1,
error => undef
);
}
}
1;