#!/usr/bin/perl -w # index.pl: # 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.39 2006-10-07 17:55:10 matthew Exp $ # TODO # Nothing is done about the update checkboxes - not stored anywhere on anything! # Nothing is done with fixed checkbox either 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 LWP::Simple; use RABX; use POSIX qw(strftime); use CGI::Carp; use Page; use mySociety::AuthToken; use mySociety::Config; use mySociety::DBHandle qw(dbh select_all); use mySociety::Email; 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('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')) { $title = 'Map'; $out = display($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 . 'Please try again.

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

Reports are sent directly to the local council – at the moment, we only cover Newham, Lewisham, and Islington councils. The rest of the UK is coming soon!

Reporting a problem is hopefully very simple:

  1. Enter a postcode;
  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 updates); 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}; push(@errors, 'Please enter your email') unless $input{email}; return display_problem($q, @errors) if (@errors); my $template = File::Slurp::read_file("$FindBin::Bin/../templates/emails/update-confirm"); my $id = dbh()->selectrow_array("select nextval('comment_id_seq');"); dbh()->do("insert into comment (id, problem_id, name, email, website, text, state) values (?, ?, ?, ?, ?, ?, 'unconfirmed')", {}, $id, $input{id}, $input{name}, $input{email}, '', $input{update}); 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 $email = mySociety::Email::construct_email({ _template_ => $template, _parameters_ => \%h, From => [mySociety::Config::get('CONTACT_EMAIL'), 'Heighbourhood Fix-It'], To => [[$input{email}, $input{name}]], }); my $result = mySociety::Util::send_email($email, mySociety::Config::get('CONTACT_EMAIL'), $input{email}); my $out; if ($result == mySociety::Util::EMAIL_SUCCESS) { $out = <Nearly Done! Now check your email...

The confirmation email may take a few minutes to arrive — please be patient.

If you use web-based email or have 'junk mail' filters, you may wish to check your bulk/spam mail folders: sometimes, our messages are marked that way.

You must now click on the link within the email we've just sent you -
if you do not, your update will not be posted.

(Don't worry - we'll hang on to your update while you're checking your email.)

EOF } else { $out = <I'm afraid something went wrong when we tried to send your email. Please click Back, check your details, and try again.

EOF } return $out; } sub submit_problem { my $q = shift; my @vars = qw(title detail name email pc easting northing updates); my %input = map { $_ => $q->param($_) || '' } @vars; my @errors; 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}; push(@errors, 'Please enter your email') unless $input{email}; return display_form($q, @errors) if (@errors); my $template = File::Slurp::read_file("$FindBin::Bin/../templates/emails/problem-confirm"); my $id = dbh()->selectrow_array("select nextval('problem_id_seq');"); dbh()->do("insert into problem (id, postcode, easting, northing, title, detail, name, email, state) values (?, ?, ?, ?, ?, ?, ?, ?, 'unconfirmed')", {}, $id, $input{pc}, $input{easting}, $input{northing}, $input{title}, $input{detail}, $input{name}, $input{email} ); 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 $email = mySociety::Email::construct_email({ _template_ => $template, _parameters_ => \%h, From => [mySociety::Config::get('CONTACT_EMAIL'), 'Heighbourhood Fix-It'], To => [[$input{email}, $input{name}]], }); my $result = mySociety::Util::send_email($email, mySociety::Config::get('CONTACT_EMAIL'), $input{email}); my $out; if ($result == mySociety::Util::EMAIL_SUCCESS) { $out = <Nearly Done! Now check your email...

The confirmation email may take a few minutes to arrive — please be patient.

If you use web-based email or have 'junk mail' filters, you may wish to check your bulk/spam mail folders: sometimes, our messages are marked that way.

You must now click on the link within the email we've just sent you -
if you do not, your problem will not be posted on the site.

(Don't worry - we'll hang on to your information while you're checking your email.)

EOF } else { $out = <I'm afraid something went wrong when we tried to send your email. Please click Back, check your details, and try again.

EOF } 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 updates pc easting northing x y skipped); 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($q) unless $input{skipped} || ($pin_x && $pin_y) || ($input{easting} && $input{northing}); my $out = ''; if ($input{skipped}) { $out .= <

Reporting a problem

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

EOF } else { my ($px, $py, $easting, $northing); if ($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}; } # XXX: How to do this for not London? # Needs to return all council types, so passing in an array of types would be good # And then display choice to user my $council = mySociety::MaPit::get_voting_area_by_location_en($easting, $northing, 'polygon', 'LBO'); my $areas_info = mySociety::MaPit::get_voting_areas_info($council); $council = join(', ', map { $areas_info->{$_}->{name} } @$council); my $pins = display_pin($q, $px, $py, 'yellow'); $out .= display_map($q, $input{x}, $input{y}, 1, 0, $pins); $out .= '

Reporting a problem

'; $out .= '

You have located the problem at the location marked with a yellow pin on the map, which is within ' . $council . '. If this is not the correct location, simply click on the map again.

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 $updates = (!defined($q->param('updates')) || $input{updates}) ? ' checked' : ''; my $back = NewURL($q, map => undef, "tile_$pin_tile_x.$pin_tile_y.x" => undef, "tile_$pin_tile_x.$pin_tile_y.y" => undef); $out .= <
(work out from details?)

Back to listings

EOF $out .= display_map_end(1); return $out; } sub display { 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, $x, $y, $name); try { ($name, $x, $y) = postcode_check($input{pc}, $input{x}, $input{y}); } 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; } } catch Error::Simple with { my $e = shift; $error = $e; }; return front_page($q, $error) if ($error); 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); $pins .= display_pin($q, $px, $py, 'red', 1); } my $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 created desc limit 3", $mid_e, $mid_n); foreach (@$current) { my $px = os_to_px($_->{easting}, $x); my $py = os_to_px($_->{northing}, $y); $pins .= display_pin($q, $px, $py, 'red', 1); } my $out = ''; $out .= display_map($q, $x, $y, 1, 1, $pins); $out .= '

Click on the map to report a problem

'; if (@errors) { $out .= '
  • ' . join('
  • ', @errors) . '
'; } $out .= <

Recent problems reported on this map

    EOF foreach (@$current_map) { $out .= '
  • '; $out .= $_->{title}; $out .= '
  • '; } unless (@$current_map) { $out .= '
  • No problems have been reported yet.
  • '; } $out .= <

    Recent problems reported within 10km

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

      Recent updates to problems?

      Recently fixed problems

        EOF my $fixed = select_all( "select id,title from problem where state='fixed' order by created desc limit 3"); foreach (@$fixed) { $out .= '
      • '; $out .= $_->{title}; $out .= '
      • '; } unless (@$fixed) { $out .= '
      • No problems have been fixed yet
      • '; } my $skipurl = NewURL($q, 'map'=>1, skipped=>1); $out .= '
      '; $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 .= display_map_end(1); return $out; } sub display_pin { my ($q, $px, $py, $col, $id) = @_; $id = 0 unless $id; # return '' if ($px<0 || $px>508 || $py<0 || $py>508); my $out = 'Problem'; return $out unless $id; my $url = NewURL($q, id=>$_->{id}, x=>undef, y=>undef); $out = '' . $out . ''; return $out; } sub display_problem { my ($q, @errors) = @_; my @vars = qw(id name email update updates 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 easting, northing, title, detail, name, extract(epoch from created) from problem where id=? and state='confirmed'", {}, $input{id}); return display($q, 'Unknown problem ID') unless $problem; my ($easting, $northing, $title, $desc, $name, $time) = @$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 $created = time(); my $px = os_to_px($easting, $x_tile); my $py = os_to_px($northing, $y_tile); my $out = ''; my $pins = display_pin($q, $px, $py, 'red'); $out .= display_map($q, $x_tile, $y_tile, 0, 1, $pins); $out .= "

      $title

      "; # Display information about problem $out .= '

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

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

      '; my $back = NewURL($q, id=>undef); $out .= '

      Back to listings

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

      Updates

      '; foreach my $row (@$updates) { $out .= "
      Posted by $row->{name} at " . prettify_epoch($row->{whenposted}) . ''; $out .= '
      ' . $row->{text} . '
      '; } $out .= '
      '; } $out .= '

      Provide an update

      '; if (@errors) { $out .= '
      • ' . join('
      • ', @errors) . '
      '; } my $updates = (!defined($q->param('updates')) || $input{updates}) ? ' checked' : ''; # XXX: Should we have website too? $out .= <
      EOF $out .= display_map_end(0); 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 $pc_enc = ent($q->param('pc')); $out .= < x = $x - 2; y = $y - 2;
      EOF $img_type = '
      $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; } # Checks the postcode is in one of the two London boroughs # and sets default X/Y co-ordinates if not provided in the URI sub postcode_check { my ($pc, $x, $y) = @_; my $areas; $areas = mySociety::MaPit::get_voting_areas($pc); my @councils_allowed = (2510, 2492, 2507); 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,2563,2652,2607,2582,14287,14317,14328,2223,2225,2242,2222,2248,2246,2235,2224,2244,2236); my ($valid_councils, $invalid_councils); grep (vec($valid_councils, $_, 1) = 1, @councils_allowed); grep (vec($invalid_councils, $_, 1) = 1, @councils_no_email); # Cheltenham example: CTY=2226 DIS=2326 # Check for covered council my @councils; my $types = $mySociety::VotingArea::council_parent_types; foreach my $type (@$types) { push(@councils, $type) if ($areas->{$type} && !vec($invalid_councils, $areas->{$type}, 1)); } throw Error::Simple("I'm afraid that postcode isn't yet covered by us.\n") unless $areas && @councils; # XXX: Pick first council, hmm my $council = $areas->{$councils[0]}; throw Error::Simple("I'm afraid that postcode isn't in our covered London boroughs.\n") if (@councils_allowed && !vec($valid_councils, $council, 1)); my $area_info = mySociety::MaPit::get_voting_area_info($council); my $name = $area_info->{name}; $x ||= 0; $x += 0; $y ||= 0; $y += 0; if (!$x && !$y) { my $location = mySociety::MaPit::get_location($pc); my $northing = $location->{northing}; my $easting = $location->{easting}; $x = int(os_to_tile($easting)); $y = int(os_to_tile($northing)); } return ($name, $x, $y); } # 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; }