travelynx/lib/Travelynx/Controller/Traveling.pm
2021-01-29 18:32:13 +01:00

1368 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;
# Copyright (C) 2020 Daniel Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
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->journeys->get_stats(
uid => $self->current_user->{id},
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->journeys->get_stats(
uid => $self->current_user->{id},
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->in_transit->update_user_data(
uid => $self->current_user->{id},
user_data => { 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;