#!/usr/bin/perl -w # index.cgi: # Main code for Neighbourhood Fix-It # # Copyright (c) 2006 UK Citizens Online Democracy. All rights reserved. # Email: matthew@mysociety.org. WWW: http://www.mysociety.org # # $Id: index.cgi,v 1.59 2007-01-26 22:48:31 matthew Exp $ # TODO # Nothing is done about the update checkboxes - not stored anywhere on anything! use strict; require 5.8.0; # Horrible boilerplate to set up appropriate library paths. use FindBin; use lib "$FindBin::Bin/../perllib"; use lib "$FindBin::Bin/../../perllib"; use Error qw(:try); use File::Slurp; use Image::Magick; use LWP::Simple; use RABX; use POSIX qw(strftime); use CGI::Carp; use Digest::MD5 qw(md5_hex); use URI::Escape; use Page; use mySociety::AuthToken; use mySociety::Config; use mySociety::DBHandle qw(dbh select_all); use mySociety::GeoUtil; use mySociety::Util; use mySociety::MaPit; use mySociety::VotingArea; use mySociety::Web qw(ent NewURL); BEGIN { mySociety::Config::set_file("$FindBin::Bin/../conf/general"); mySociety::DBHandle::configure( Name => mySociety::Config::get('BCI_DB_NAME'), User => mySociety::Config::get('BCI_DB_USER'), Password => mySociety::Config::get('BCI_DB_PASS'), Host => mySociety::Config::get('BCI_DB_HOST', undef), Port => mySociety::Config::get('BCI_DB_PORT', undef) ); if (!dbh()->selectrow_array('select secret from secret for update of secret')) { local dbh()->{HandleError}; dbh()->do('insert into secret (secret) values (?)', {}, unpack('h*', mySociety::Util::random_bytes(32))); } dbh()->commit(); mySociety::MaPit::configure(); } # Main code for index.cgi sub main { my $q = shift; my $out = ''; my $title = ''; if ($q->param('submit_problem')) { $title = 'Submitting your problem'; $out = submit_problem($q); } elsif ($q->param('submit_update')) { $title = 'Submitting your update'; $out = submit_update($q); } elsif ($q->param('submit_map')) { $title = 'Reporting a problem'; $out = display_form($q); } elsif ($q->param('id')) { $title = 'Viewing a problem'; $out = display_problem($q); } elsif ($q->param('pc') || ($q->param('x') && $q->param('y'))) { $title = 'Viewing a location'; $out = display_location($q); } else { $out = front_page($q); } print Page::header($q, $title); print $out; print Page::footer(); } Page::do_fastcgi(\&main); # Display front page sub front_page { my ($q, $error) = @_; my $pc_h = ent($q->param('pc') || ''); my $out = <Report, view, or discuss local problems like graffiti, fly tipping, broken paving slabs, or street lighting

EOF $out .= '

' . $error . '

' if ($error); $out .= <    

Reports are sent directly to the local council, apart from a few councils where we’re missing details.

Reporting a problem is very simple:

  1. Enter a postcode or street name and area;
  2. Locate the problem on a high-scale map;
  3. Enter details of the problem;
  4. Submit to your council.
EOF return $out; } sub submit_update { my $q = shift; my @vars = qw(id name email update fixed reopen); my %input = map { $_ => $q->param($_) || '' } @vars; my @errors; push(@errors, 'Please enter a message') unless $input{update}; push(@errors, 'Please enter your name') unless $input{name}; if (!$input{email}) { push(@errors, 'Please enter your email'); } elsif (!mySociety::Util::is_valid_email($input{email})) { push(@errors, 'Please enter a valid email'); } return display_problem($q, @errors) if (@errors); my $id = dbh()->selectrow_array("select nextval('comment_id_seq');"); dbh()->do("insert into comment (id, problem_id, name, email, website, text, state, mark_fixed, mark_open) values (?, ?, ?, ?, ?, ?, 'unconfirmed', ?, ?)", {}, $id, $input{id}, $input{name}, $input{email}, '', $input{update}, $input{fixed}?'t':'f', $input{reopen}?'t':'f'); my %h = (); $h{update} = $input{update}; $h{name} = $input{name}; $h{url} = mySociety::Config::get('BASE_URL') . '/C/' . mySociety::AuthToken::store('update', $id); dbh()->commit(); my $out = Page::send_email($input{email}, $input{name}, 'update-confirm', %h); return $out; } sub submit_problem { my $q = shift; my @vars = qw(council title detail name email phone pc easting northing); my %input = map { $_ => scalar $q->param($_) } @vars; my @errors; my $fh = $q->upload('photo'); if ($fh) { my $ct = $q->uploadInfo($fh)->{'Content-Type'}; my $cd = $q->uploadInfo($fh)->{'Content-Disposition'}; # Must delete photo param, otherwise display functions get confused $q->delete('photo'); push (@errors, 'Please upload a JPEG image only') unless ($ct eq 'image/jpeg' || $ct eq 'image/pjpeg'); } push(@errors, 'No council selected') unless $input{council} && $input{council} =~ /^(?:-1|\d+)$/; push(@errors, 'Please enter a title') unless $input{title}; push(@errors, 'Please enter some details') unless $input{detail}; push(@errors, 'Please enter your name') unless $input{name}; if (!$input{email}) { push(@errors, 'Please enter your email'); } elsif (!mySociety::Util::is_valid_email($input{email})) { push(@errors, 'Please enter a valid email'); } if ($input{easting} && $input{northing}) { if ($input{council} != -1) { my $councils = mySociety::MaPit::get_voting_area_by_location_en($input{easting}, $input{northing}, 'polygon', $mySociety::VotingArea::council_parent_types); my %councils = map { $_ => 1 } @$councils; push(@errors, 'That location is not part of that council') unless $councils{$input{council}}; push(@errors, 'We do not yet have details for the council that covers that location') unless is_valid_council($input{council}); } } elsif ($input{easting} || $input{northing}) { push(@errors, 'Somehow, you only have one co-ordinate. Please try again.'); } return display_form($q, @errors) if (@errors); my $id = dbh()->selectrow_array("select nextval('problem_id_seq');"); my $image; if ($fh) { $image = Image::Magick->new; $image->Read(file=>$fh); close $fh; $image->Scale(geometry=>"250x250>"); my @blobs = $image->ImageToBlob(); undef $image; $image = $blobs[0]; } delete $input{council} if $input{council} == -1; # This is horrid my $s = dbh()->prepare("insert into problem (id, postcode, easting, northing, title, detail, name, email, phone, photo, state, council) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'unconfirmed', ?)"); $s->bind_param(1, $id); $s->bind_param(2, $input{pc}); $s->bind_param(3, $input{easting}); $s->bind_param(4, $input{northing}); $s->bind_param(5, $input{title}); $s->bind_param(6, $input{detail}); $s->bind_param(7, $input{name}); $s->bind_param(8, $input{email}); $s->bind_param(9, $input{phone}); $s->bind_param(10, $image, { pg_type => DBD::Pg::PG_BYTEA }); $s->bind_param(11, $input{council}); $s->execute(); my %h = (); $h{title} = $input{title}; $h{detail} = $input{detail}; $h{name} = $input{name}; $h{url} = mySociety::Config::get('BASE_URL') . '/P/' . mySociety::AuthToken::store('problem', $id); dbh()->commit(); my $out = Page::send_email($input{email}, $input{name}, 'problem-confirm', %h); return $out; } sub display_form { my ($q, @errors) = @_; my ($pin_x, $pin_y, $pin_tile_x, $pin_tile_y); my @vars = qw(title detail name email phone pc easting northing x y skipped council); my %input = map { $_ => $q->param($_) || '' } @vars; my %input_h = map { $_ => $q->param($_) ? ent($q->param($_)) : '' } @vars; my @ps = $q->param; foreach (@ps) { ($pin_tile_x, $pin_tile_y, $pin_x) = ($1, $2, $q->param($_)) if /^tile_(\d+)\.(\d+)\.x$/; $pin_y = $q->param($_) if /\.y$/; } return display_location($q) unless $input{skipped} || ($pin_x && $pin_y) || ($input{easting} && $input{northing}); my $out = ''; my ($px, $py, $easting, $northing); if ($input{skipped}) { my ($x, $y, $e, $n, $error) = geocode($input{pc}); $easting = $e; $northing = $n; } elsif ($pin_x && $pin_y) { # Map was clicked on $pin_x = click_to_tile($pin_tile_x, $pin_x); $pin_y = click_to_tile($pin_tile_y, $pin_y, 1); $px = tile_to_px($pin_x, $input{x}); $py = tile_to_px($pin_y, $input{y}); $easting = tile_to_os($pin_x); $northing = tile_to_os($pin_y); } else { # Normal form submission $px = os_to_px($input{easting}, $input{x}); $py = os_to_px($input{northing}, $input{y}); $easting = $input_h{easting}; $northing = $input_h{northing}; } my $all_councils = mySociety::MaPit::get_voting_area_by_location_en($easting, $northing, 'polygon', $mySociety::VotingArea::council_parent_types); my $areas_info = mySociety::MaPit::get_voting_areas_info($all_councils); my @councils = is_valid_council($all_councils); if ($input{skipped}) { $out .= <

Reporting a problem

EOF } else { my $pins = display_pin($q, $px, $py, 'purple'); $out .= display_map($q, $input{x}, $input{y}, 2, 1, $pins); if ($px && $py) { $out .= < drag_x = $px - 254; drag_y = 254 - $py; EOF } $out .= '

Reporting a problem

'; $out .= '

You have located the problem at the point marked with a purple pin on the map. If this is not the correct location, simply click on the map again.

'; } if (@councils > 1) { $out .= '

This spot lies in more than one council area; if you want, please choose which council you wish to send the report to below:

'; $out .= '
    '; my $c = 0; # XXX: We don't know the order of display here! foreach my $council (@councils) { $out .= '
  • '; } $out .= '
'; } elsif (@councils == 1) { $out .= '

This problem will be reported to ' . $areas_info->{$councils[0]}->{name} . '.

'; $out .= ''; } else { my $e = mySociety::Config::get('CONTACT_EMAIL'); my $list = join(', ', map { $areas_info->{$_}->{name} } @$all_councils); my $n = @$all_councils; $out .= '

We do not yet have details for the council'; $out .= ($n>1) ? 's that cover' : ' that covers'; $out .= " this location. If you submit a problem here it will be left on the site, but not reported to your council. You can help us by finding a contact email address for local problems for $list and emailing it to us at $e.

"; $out .= ''; } if ($input{skipped}) { $out .= '

Please fill in the form below with details of the problem, and describe the location as precisely as possible in the details box.

'; } else { $out .= '

Please fill in details of the problem below. Your council won\'t be able to help unless you leave as much detail as you can, so please describe the exact location of the problem (ie. on a wall or the floor), and so on.

'; } $out .= ' '; if (@errors) { $out .= '
  • ' . join('
  • ', @errors) . '
'; } my $back = NewURL($q, submit_map => undef, "tile_$pin_tile_x.$pin_tile_y.x" => undef, "tile_$pin_tile_x.$pin_tile_y.y" => undef, skipped => undef); $out .= <
(optional, so the council can get in touch)

Back to listings

EOF $out .= display_map_end(1); return $out; } sub display_location { my ($q, @errors) = @_; my @vars = qw(pc x y); my %input = map { $_ => $q->param($_) || '' } @vars; my %input_h = map { $_ => $q->param($_) ? ent($q->param($_)) : '' } @vars; my($error, $easting, $northing); my $x = $input{x}; my $y = $input{y}; $x ||= 0; $x += 0; $y ||= 0; $y += 0; if (!$x && !$y) { try { ($x, $y, $easting, $northing, $error) = geocode($input{pc}); } catch Error::Simple with { $error = shift; }; } return geocode_choice($error) if (ref($error) eq 'ARRAY'); return front_page($q, $error) if ($error); my ($pins, $current_map, $current, $fixed) = map_pins($q, $x, $y); my $out = display_map($q, $x, $y, 1, 1, $pins); $out .= '

Click on the map to report a problem

'; if (@errors) { $out .= '
  • ' . join('
  • ', @errors) . '
'; } my $skipurl = NewURL($q, 'submit_map'=>1, skipped=>1); $out .= <If you cannot see a map – if you have images turned off, or are using a text only browser, for example – and you wish to report a problem, please skip this step and we will ask you to describe the location of your problem instead.

EOF $out .= <

Recent problems reported on this map

    EOF foreach (@$current_map) { $out .= '
  1. '; $out .= $_->{title}; $out .= '
  2. '; } unless (@$current_map) { $out .= '
  3. No problems have been reported yet.
  4. '; } my $list_start = @$current_map + 1; $out .= <

    Recent problems reported within 10km

    RSS feed

      EOF foreach (@$current) { $out .= '
    1. '; $out .= $_->{title} . ' (c. ' . int($_->{distance}/100+.5)/10 . 'km)'; $out .= '
    2. '; } unless (@$current) { $out .= '
    3. No problems have been reported yet.
    4. '; } $out .= <

      Recent updates to problems?

      Recently fixed problems within 10km

        EOF foreach (@$fixed) { $out .= '
      1. '; $out .= $_->{title} . ' (c. ' . int($_->{distance}/100+.5)/10 . 'km)'; $out .= '
      2. '; } unless (@$fixed) { $out .= '
      3. No problems have been fixed yet
      4. '; } $out .= '
      '; $out .= display_map_end(1); return $out; } sub display_problem { my ($q, @errors) = @_; my @vars = qw(id name email update fixed reopen x y); my %input = map { $_ => $q->param($_) || '' } @vars; my %input_h = map { $_ => $q->param($_) ? ent($q->param($_)) : '' } @vars; $input{x} += 0; $input{y} += 0; # Get all information from database my $problem = dbh()->selectrow_arrayref( "select state, easting, northing, title, detail, name, extract(epoch from created), photo from problem where id=? and state in ('confirmed','fixed')", {}, $input{id}); return display_location($q, 'Unknown problem ID') unless $problem; my ($state, $easting, $northing, $title, $desc, $name, $time, $photo) = @$problem; my $x = os_to_tile($easting); my $y = os_to_tile($northing); my $x_tile = $input{x} || int($x); my $y_tile = $input{y} || int($y); my $px = os_to_px($easting, $x_tile); my $py = os_to_px($northing, $y_tile); my ($pins) = map_pins($q, $x_tile, $y_tile, $input{id}); my $out = display_map($q, $x_tile, $y_tile, 0, 1, $pins); $out .= "

      $title

      "; $out .= < drag_x = $px - 254; drag_y = 254 - $py; EOF # Display information about problem $out .= '

      Reported by ' . $name . ' at ' . prettify_epoch($time); $out .= '

      '; $out .= ent($desc); $out .= '

      '; if ($photo) { $out .= '

      '; } my $back = NewURL($q, id=>undef, x=>$x_tile, y=>$y_tile); $out .= '

      Back to listings

      '; $out .= 'RSS feed of updates to this problem '; $out .= 'Email alerts of updates to this problem'; $out .= <

      Receive email when updates are left on this problem

      EOF # Display updates my $updates = select_all( "select id, name, extract(epoch from created) as created, text, mark_fixed, mark_open from comment where problem_id = ? and state='confirmed' order by created desc", $input{id}); if (@$updates) { $out .= '
      '; $out .= '

      Updates

      '; foreach my $row (@$updates) { $out .= "
      {id}\">Posted by $row->{name} at " . prettify_epoch($row->{created}); $out .= ', marked fixed' if ($row->{mark_fixed}); $out .= ', reopened' if ($row->{mark_open}); $out .= ''; $out .= '
      ' . $row->{text} . '
      '; } $out .= '
      '; } $out .= '

      Provide an update

      '; if (@errors) { $out .= '
      • ' . join('
      • ', @errors) . '
      '; } my $fixed = ($input{fixed}) ? ' checked' : ''; my $reopen = ($input{reopen}) ? ' checked' : ''; my $fixedline = $state eq 'fixed' ? qq{
      } : qq{
      }; $out .= <
      $fixedline
      EOF $out .= display_map_end(0); return $out; } sub map_pins { my ($q, $x, $y, $id) = @_; my $pins = ''; my $min_e = tile_to_os($x); my $min_n = tile_to_os($y); my $mid_e = tile_to_os($x+1); my $mid_n = tile_to_os($y+1); my $max_e = tile_to_os($x+2); my $max_n = tile_to_os($y+2); my $current_map = select_all( "select id,title,easting,northing from problem where state='confirmed' and easting>=? and easting=? and northing{id}); my $px = os_to_px($_->{easting}, $x); my $py = os_to_px($_->{northing}, $y); if ($_->{id}==$id) { $pins .= display_pin($q, $px, $py, 'blue'); } else { $pins .= display_pin($q, $px, $py, 'red', $count_prob++); } } my $current = []; if (@$current_map < 9) { my $limit = 9 - @$current_map; $current = select_all( "select id, title, easting, northing, distance from problem_find_nearby(?, ?, 10) as nearby, problem where nearby.problem_id = problem.id and state = 'confirmed'" . (@ids ? ' and id not in (' . join(',' , @ids) . ')' : '') . " order by distance limit $limit", $mid_e, $mid_n); foreach (@$current) { my $px = os_to_px($_->{easting}, $x); my $py = os_to_px($_->{northing}, $y); if ($_->{id}==$id) { $pins .= display_pin($q, $px, $py, 'blue'); } else { $pins .= display_pin($q, $px, $py, 'red', $count_prob++); } } } my $fixed = select_all( "select id, title, easting, northing, distance from problem_find_nearby(?, ?, 10) as nearby, problem where nearby.problem_id = problem.id and state='fixed' order by created desc limit 9", $mid_e, $mid_n); foreach (@$fixed) { my $px = os_to_px($_->{easting}, $x); my $py = os_to_px($_->{northing}, $y); if ($_->{id}==$id) { $pins .= display_pin($q, $px, $py, 'blue'); } else { $pins .= display_pin($q, $px, $py, 'green', $count_fixed++); } } return ($pins, $current_map, $current, $fixed); } sub display_pin { my ($q, $px, $py, $col, $num) = @_; $num = '' unless $num; my %cols = (red=>'R', green=>'G', blue=>'B', purple=>'P'); my $out = 'Problem'; return $out unless $_->{id} && $col ne 'blue'; my $url = NewURL($q, id=>$_->{id}, x=>undef, y=>undef); $out = '' . $out . ''; return $out; } # display_map Q X Y TYPE COMPASS PINS # X,Y is bottom left tile of 2x2 grid # TYPE is 1 if the map is clickable, 0 if not # COMPASS is 1 to show the compass, 0 to not # PINS is HTML of pins to show sub display_map { my ($q, $x, $y, $type, $compass, $pins) = @_; $pins ||= ''; my $url = mySociety::Config::get('TILES_URL'); my $tiles_url = $url . $x . '-' . ($x+1) . ',' . $y . '-' . ($y+1) . '/RABX'; my $tiles = LWP::Simple::get($tiles_url); throw Error::Simple("Unable to get tiles from URL $tiles_url\n") if !$tiles; my $tileids = RABX::unserialise($tiles); my $tl = $x . '.' . ($y+1); my $tr = ($x+1) . '.' . ($y+1); my $bl = $x . '.' . $y; my $br = ($x+1) . '.' . $y; my $tl_src = $url . $tileids->[0][0]; my $tr_src = $url . $tileids->[0][1]; my $bl_src = $url . $tileids->[1][0]; my $br_src = $url . $tileids->[1][1]; my $out = ''; my $img_type; if ($type) { my $encoding = ''; $encoding = ' enctype="multipart/form-data"' if ($type==2); my $pc_enc = ent($q->param('pc')); $out .= < EOF $img_type = ' var x = $x - 2; var y = $y - 2; var drag_x = 0; var drag_y = 0;
      $img_type id="t2.2" name="tile_$tl" src="$tl_src" style="top:0px; left:0px;">$img_type id="t2.3" name="tile_$tr" src="$tr_src" style="top:0px; left:$imgw;">
      $img_type id="t3.2" name="tile_$bl" src="$bl_src" style="top:$imgh; left:0px;">$img_type id="t3.3" name="tile_$br" src="$br_src" style="top:$imgh; left:$imgw;"> $pins
      EOF $out .= Page::compass($q, $x, $y) if $compass; $out .= '
      '; return $out; } sub display_map_end { my ($type) = @_; my $out = '
      '; $out .= '' if ($type); return $out; } sub geocode_choice { my $choices = shift; my $out = '

      We found more than one match for that location:

        '; foreach my $choice (@$choices) { my $qs = $choice->[0]; my $text = $choice->[1]; $text =~ s/<\/?(?:b|i)>//g; $text =~ s/, United Kingdom//; $qs =~ s/,\+United\+Kingdom//; $out .= '
      • ' . $text . "
      • \n"; } $out .= '
      '; return $out; } sub geocode { my ($s) = @_; my ($x, $y, $easting, $northing, $error); if (mySociety::Util::is_valid_postcode($s)) { try { my $location = mySociety::MaPit::get_location($s); $easting = $location->{easting}; $northing = $location->{northing}; my $xx = os_to_tile($easting); my $yy = os_to_tile($northing); $x = int($xx); $y = int($yy); $x -= 1 if ($xx - $x < 0.5); $y -= 1 if ($yy - $y < 0.5); } catch RABX::Error with { my $e = shift; if ($e->value() == mySociety::MaPit::BAD_POSTCODE || $e->value() == mySociety::MaPit::POSTCODE_NOT_FOUND) { $error = 'That postcode was not recognised, sorry.'; } else { $error = $e; } } } else { ($x, $y, $easting, $northing, $error) = geocode_string($s); } return ($x, $y, $easting, $northing, $error); } sub geocode_string { my $s = shift; $s = lc($s); $s =~ s/[^-&0-9a-z ']/ /g; $s = uri_escape($s); $s =~ s/%20/+/g; my $url = 'http://maps.google.co.uk/maps?output=js&q=' . $s; my $cache_dir = mySociety::Config::get('GEO_CACHE'); my $cache_file = $cache_dir . md5_hex($url); my ($js, $error, $x, $y, $easting, $northing); if (-s $cache_file) { $js = File::Slurp::read_file($cache_file); } else { $js = LWP::Simple::get($url); File::Slurp::write_file($cache_file, $js); } if ($js =~ /panel: '(.*?)'/ && $js =~ /We could not understand/) { $error = $1; } elsif ($js =~ /panel: '(.*?)'/) { my $refine = $1; while ($refine =~ /
      (.*?)<\/a><\/div>/g) { push (@$error, [ $1, $2 ]); } $error = 'We could not understand that location.' unless $error; } else { $js =~ /center: {lat: (.*?),lng: (.*?)}/; my $lat = $1; my $lon = $2; ($easting,$northing) = mySociety::GeoUtil::wgs84_to_national_grid($lat, $lon, 'G'); $x = int(os_to_tile($easting))-1; $y = int(os_to_tile($northing))-1; } return ($x, $y, $easting, $northing, $error); } sub is_valid_council { my $councils = shift; $councils = [ $councils ] unless ref($councils) eq 'ARRAY'; my @councils_no_email = (2288,2402,2390,2252,2351,2430,2375,2285,2377,2374,2330,2454,2284,2378,2294,2312,2419,2386,2363,2353,2296,2300,2291,2268,2512,2504,2495,2510, 2530,2516,2531,2545,2586,2554,2574,2580,2615,2596,2599,2601,2648,2652,2607,2582,14287,14317,14328,2225,2242,2222,2248,2246,2235,2224,2244,2236); my $invalid_councils; grep (vec($invalid_councils, $_, 1) = 1, @councils_no_email); # Cheltenham example: CTY=2226 DIS=2326 # Check for covered council my @return; foreach my $council (@$councils) { push @return, $council unless vec($invalid_councils, $council, 1); } return @return; } # P is easting or northing # BL is bottom left tile reference of displayed map sub os_to_px { my ($p, $bl) = @_; return tile_to_px(os_to_tile($p), $bl); } # Convert tile co-ordinates to pixel co-ordinates from top right of map # BL is bottom left tile reference of displayed map sub tile_to_px { my ($p, $bl) = @_; $p = 508 - 254 * ($p - $bl); $p = int($p + .5 * ($p <=> 0)); return $p; } # Tile co-ordinates are linear scale of OS E/N # Will need more generalising when more zooms appear sub os_to_tile { return $_[0] / (5000/31); } sub tile_to_os { return $_[0] * (5000/31); } sub click_to_tile { my ($pin_tile, $pin, $invert) = @_; $pin -= 254 while $pin > 254; $pin += 254 while $pin < 0; $pin = 254 - $pin if $invert; # image submits measured from top down return $pin_tile + $pin / 254; } sub prettify_epoch { my $s = shift; my @s = localtime($s); my $tt = strftime('%H:%M', @s); my @t = localtime(); if (strftime('%Y%m%d', @s) eq strftime('%Y%m%d', @t)) { $tt = "$tt " . 'today'; } elsif (strftime('%U', @s) eq strftime('%U', @t)) { $tt = "$tt, " . strftime('%A', @s); } elsif (strftime('%Y', @s) eq strftime('%Y', @t)) { $tt = "$tt, " . strftime('%A %e %B', @s); } else { $tt = "$tt, " . strftime('%a %e %B %Y', @s); } return $tt; }