diff options
author | matthew <matthew> | 2007-05-04 14:36:55 +0000 |
---|---|---|
committer | matthew <matthew> | 2007-05-04 14:36:55 +0000 |
commit | e5f199be14d2ba82ca03524f7d1fb2fc79f7677e (patch) | |
tree | ac29685642b15e76a54d3b3c262fcea452f52f20 | |
parent | 43a7480776473f2122df9e8a3194f0b6eac91164 (diff) |
NFI questionnaire code. Deals with sending out questionnaires, taking in
responses,updating problems, etc. What's left is front-end display issues
such as timing out old problems, moving them to different lists, etc.
-rwxr-xr-x | bin/send-questionnaires | 97 | ||||
-rw-r--r-- | conf/httpd.conf | 3 | ||||
-rw-r--r-- | db/schema.sql | 13 | ||||
-rw-r--r-- | perllib/Page.pm | 223 | ||||
-rw-r--r-- | templates/emails/questionnaire | 20 | ||||
-rw-r--r-- | web/css.css | 2 | ||||
-rwxr-xr-x | web/index.cgi | 261 | ||||
-rw-r--r-- | web/js.js | 15 | ||||
-rwxr-xr-x | web/questionnaire.cgi | 228 |
9 files changed, 636 insertions, 226 deletions
diff --git a/bin/send-questionnaires b/bin/send-questionnaires new file mode 100755 index 000000000..cc36934c0 --- /dev/null +++ b/bin/send-questionnaires @@ -0,0 +1,97 @@ +#!/usr/bin/perl -w + +# send-questionnaires: +# Send out creator questionnaires +# +# Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved. +# Email: matthew@mysociety.org. WWW: http://www.mysociety.org +# +# $Id: send-questionnaires,v 1.1 2007-05-04 14:36:55 matthew Exp $ + +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 File::Slurp; + +use Page; +use mySociety::AuthToken; +use mySociety::Config; +use mySociety::DBHandle qw(dbh select_all); +use mySociety::Email; +use mySociety::MaPit; +use mySociety::EmailUtil; + +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) + ); +} + +die "Either no arguments, --nomail or --verbose" if (@ARGV>1); +my $nomail = 0; +my $verbose = 0; +$nomail = 1 if (@ARGV==1 && $ARGV[0] eq '--nomail'); +$verbose = 1 if (@ARGV==1 && $ARGV[0] eq '--verbose'); +$verbose = 1 if $nomail; + +# Select all problems that need a questionnaire email sending +my $unsent = select_all( + "select id, council, category, title, detail, name, email, + extract(epoch from ms_current_timestamp()-created) as created + from problem + where state in ('confirmed','fixed') + and whensent is not null + and whensent < ms_current_timestamp() - '4 weeks'::interval + and send_questionnaire = 't' + and ( (select max(whenanswered) from questionnaire where problem.id=problem_id) is null + or (select max(whenanswered) from questionnaire where problem.id=problem_id) < ms_current_timestamp() - '4 weeks'::interval) +"); + +foreach my $row (@$unsent) { + my @all_councils = split /,|\|/, $row->{council}; + my ($councils, $missing) = $row->{council} =~ /^([\d,]+)(?:\|([\d,]+))?/; + my @councils = split /,/, $councils; + my $areas_info = mySociety::MaPit::get_voting_areas_info(\@all_councils); + my $template = File::Slurp::read_file("$FindBin::Bin/../templates/emails/questionnaire"); + + my %h = map { $_ => $row->{$_} } qw/name title detail category/; + $h{created} = Page::prettify_duration($row->{created}, 'day'); + $h{councils} = join(' and ', map { $areas_info->{$_}->{name} } @councils); + + my $id = dbh()->selectrow_array("select nextval('questionnaire_id_seq');"); + dbh()->do('insert into questionnaire (id, problem_id, whensent) + values (?, ?, ms_current_timestamp())', {}, $id, $row->{id}); + dbh()->do("update problem set send_questionnaire = 'f' where id=?", {}, $row->{id}); + + $h{url} = mySociety::Config::get('BASE_URL') . '/Q/' . mySociety::AuthToken::store('questionnaire', $id); + + my $email = mySociety::Email::construct_email({ + _template_ => $template, + _parameters_ => \%h, + To => [ [ $row->{email}, $row->{name} ] ], + From => [ mySociety::Config::get('CONTACT_EMAIL'), 'Neighbourhood Fix-It' ], + }); + + my $result; + if (mySociety::Config::get('STAGING_SITE') || $nomail) { + $result = -1; + print $email; + } else { + $result = mySociety::EmailUtil::send_email($email, mySociety::Config::get('CONTACT_EMAIL'), $row->{email}); + } + if ($result == mySociety::EmailUtil::EMAIL_SUCCESS) { + dbh()->commit(); + } else { + dbh()->rollback(); + } +} + diff --git a/conf/httpd.conf b/conf/httpd.conf index 53653f3e8..91b832b8e 100644 --- a/conf/httpd.conf +++ b/conf/httpd.conf @@ -20,7 +20,7 @@ # Copyright (c) 2006 UK Citizens Online Democracy. All rights reserved. # Email: francis@mysociety.org; WWW: http://www.mysociety.org # -# $Id: httpd.conf,v 1.9 2007-04-20 08:59:03 matthew Exp $ +# $Id: httpd.conf,v 1.10 2007-05-04 14:36:55 matthew Exp $ DirectoryIndex index.cgi @@ -31,6 +31,7 @@ RewriteEngine on RewriteRule ^/[Aa]/([0-9A-Za-z]{16}).*$ /alert.cgi?token=$1 RewriteRule ^/[Cc]/([0-9A-Za-z]{16}).*$ /confirm.cgi?type=update;token=$1 RewriteRule ^/[Pp]/([0-9A-Za-z]{16}).*$ /confirm.cgi?type=problem;token=$1 +RewriteRule ^/[Qq]/([0-9A-Za-z]{16}).*$ /questionnaire.cgi?token=$1 RewriteRule ^/rss/([0-9]+)$ /rss.cgi?type=new_updates;id=$1 [QSA] RewriteRule ^/rss/([0-9]+),([0-9]+)$ /rss.cgi?type=local_problems;x=$1;y=$2 [QSA] diff --git a/db/schema.sql b/db/schema.sql index 47c71c73f..f000482f8 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -4,7 +4,7 @@ -- Copyright (c) 2006 UK Citizens Online Democracy. All rights reserved. -- Email: matthew@mysociety.org; WWW: http://www.mysociety.org/ -- --- $Id: schema.sql,v 1.26 2007-05-03 09:34:20 matthew Exp $ +-- $Id: schema.sql,v 1.27 2007-05-04 14:36:55 matthew Exp $ -- -- secret @@ -145,10 +145,19 @@ create table problem ( or state = 'fixed' or state = 'hidden' ), - whensent timestamp + whensent timestamp, + send_questionnaire boolean not null default 't' ); create index problem_state_easting_northing_idx on problem(state, easting, northing); +create table questionnaire ( + id serial not null primary key, + problem_id integer not null references problem(id), + whensent timestamp not null, + whenanswered timestamp, + ever_reported boolean +); + -- angle_between A1 A2 -- Given two angles A1 and A2 on a circle expressed in radians, return the -- smallest angle between them. diff --git a/perllib/Page.pm b/perllib/Page.pm index fb6a1a90b..e2d1f9f57 100644 --- a/perllib/Page.pm +++ b/perllib/Page.pm @@ -6,7 +6,7 @@ # Copyright (c) 2006 UK Citizens Online Democracy. All rights reserved. # Email: matthew@mysociety.org; WWW: http://www.mysociety.org/ # -# $Id: Page.pm,v 1.42 2007-05-03 09:21:31 matthew Exp $ +# $Id: Page.pm,v 1.43 2007-05-04 14:36:56 matthew Exp $ # package Page; @@ -16,8 +16,10 @@ use Carp; use CGI::Fast qw(-no_xhtml); use Error qw(:try); use File::Slurp; +use LWP::Simple; use POSIX qw(strftime); use mySociety::Config; +use mySociety::DBHandle qw/select_all/; use mySociety::EvEl; use mySociety::WatchUpdate; use mySociety::Web qw(ent NewURL); @@ -129,6 +131,99 @@ sub error_page ($$) { print $q->header(-content_length => length($html)), $html; } +# display_map Q PARAMS +# PARAMS include: +# X,Y is bottom left tile of 2x2 grid +# TYPE is 1 if the map is clickable, 2 if clickable and has a form upload, +# 0 if not clickable +# PINS is HTML of pins to show +# PX,PY are coordinates of pin +# PRE/POST are HTML to show above/below map +sub display_map { + my ($q, %params) = @_; + $params{pins} ||= ''; + $params{pre} ||= ''; + $params{post} ||= ''; + my $px = defined($params{px}) ? $params{px}-254 : 0; + my $py = defined($params{py}) ? 254-$params{py} : 0; + my $x = $params{x}<=0 ? 0 : $params{x}; + my $y = $params{y}<=0 ? 0 : $params{y}; + 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; + return '<div id="side">' if (!$tileids->[0][0] || !$tileids->[0][1] || !$tileids->[1][0] || !$tileids->[1][1]); + 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 ($params{type}) { + my $encoding = ''; + $encoding = ' enctype="multipart/form-data"' if ($params{type}==2); + my $pc = $q->param('pc') || ''; + my $pc_enc = ent($pc); + $out .= <<EOF; +<form action="./" method="post" id="mapForm"$encoding> +<input type="hidden" name="submit_map" value="1"> +<input type="hidden" name="x" value="$x"> +<input type="hidden" name="y" value="$y"> +<input type="hidden" name="pc" value="$pc_enc"> +EOF + $img_type = '<input type="image"'; + } else { + $img_type = '<img'; + } + my $imgw = '254px'; + my $imgh = '254px'; + $out .= <<EOF; +<script type="text/javascript"> +var x = $x - 2; var y = $y - 2; +var drag_x = $px; var drag_y = $py; +</script> +<div id="map_box"> +$params{pre} + <div id="map"><div id="drag"> + $img_type alt="NW map tile" id="t2.2" name="tile_$tl" src="$tl_src" style="top:0px; left:0px;">$img_type alt="NE map tile" id="t2.3" name="tile_$tr" src="$tr_src" style="top:0px; left:$imgw;"><br>$img_type alt="SW map tile" id="t3.2" name="tile_$bl" src="$bl_src" style="top:$imgh; left:0px;">$img_type alt="SE map tile" id="t3.3" name="tile_$br" src="$br_src" style="top:$imgh; left:$imgw;"> + $params{pins} + </div></div> + <p id="copyright">© Crown copyright. All rights reserved. + Department for Constitutional Affairs 100037819 2007</p> +$params{post} + </div> +EOF + $out .= Page::compass($q, $x, $y); + $out .= '<div id="side">'; + return $out; +} + +sub display_map_end { + my ($type) = @_; + my $out = '</div>'; + $out .= '</form>' if ($type); + return $out; +} + +sub display_pin { + my ($q, $px, $py, $col, $num) = @_; + $num = '' unless $num; + my %cols = (red=>'R', green=>'G', blue=>'B', purple=>'P'); + my $out = '<img class="pin" src="/i/pin' . $cols{$col} + . $num . '.gif" alt="Problem" style="top:' . ($py-59) + . 'px; right:' . ($px-31) . 'px; position: absolute;">'; + return $out unless $_ && $_->{id} && $col ne 'blue'; + my $url = NewURL($q, id=>$_->{id}, x=>undef, y=>undef); + $out = '<a title="' . $_->{title} . '" href="' . $url . '">' . $out . '</a>'; + return $out; +} + sub compass ($$$) { my ($q, $x, $y) = @_; my @compass; @@ -140,24 +235,57 @@ sub compass ($$$) { return <<EOF; <table cellpadding="0" cellspacing="0" border="0" id="compass"> <tr valign="bottom"> -<td align="right"><a href="${compass[$x-1][$y+1]}"><img src="i/arrow-northwest.gif" alt="NW"></a></td> -<td align="center"><a href="${compass[$x][$y+1]}"><img src="i/arrow-north.gif" vspace="3" alt="N"></a></td> -<td><a href="${compass[$x+1][$y+1]}"><img src="i/arrow-northeast.gif" alt="NE"></a></td> +<td align="right"><a href="${compass[$x-1][$y+1]}"><img src="/i/arrow-northwest.gif" alt="NW"></a></td> +<td align="center"><a href="${compass[$x][$y+1]}"><img src="/i/arrow-north.gif" vspace="3" alt="N"></a></td> +<td><a href="${compass[$x+1][$y+1]}"><img src="/i/arrow-northeast.gif" alt="NE"></a></td> </tr> <tr> -<td><a href="${compass[$x-1][$y]}"><img src="i/arrow-west.gif" hspace="3" alt="W"></a></td> -<td align="center"><img src="i/rose.gif" alt=""></td> -<td><a href="${compass[$x+1][$y]}"><img src="i/arrow-east.gif" hspace="3" alt="E"></a></td> +<td><a href="${compass[$x-1][$y]}"><img src="/i/arrow-west.gif" hspace="3" alt="W"></a></td> +<td align="center"><img src="/i/rose.gif" alt=""></td> +<td><a href="${compass[$x+1][$y]}"><img src="/i/arrow-east.gif" hspace="3" alt="E"></a></td> </tr> <tr valign="top"> -<td align="right"><a href="${compass[$x-1][$y-1]}"><img src="i/arrow-southwest.gif" alt="SW"></a></td> -<td align="center"><a href="${compass[$x][$y-1]}"><img src="i/arrow-south.gif" vspace="3" alt="S"></a></td> -<td><a href="${compass[$x+1][$y-1]}"><img src="i/arrow-southeast.gif" alt="SE"></a></td> +<td align="right"><a href="${compass[$x-1][$y-1]}"><img src="/i/arrow-southwest.gif" alt="SW"></a></td> +<td align="center"><a href="${compass[$x][$y-1]}"><img src="/i/arrow-south.gif" vspace="3" alt="S"></a></td> +<td><a href="${compass[$x+1][$y-1]}"><img src="/i/arrow-southeast.gif" alt="SE"></a></td> </tr> </table> EOF } +# 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; +} + # send_email TO (NAME) TEMPLATE-NAME PARAMETERS sub send_email { my ($email, $name, $thing, %h) = @_; @@ -202,9 +330,19 @@ sub prettify_epoch { # argument is duration in seconds, rounds to the nearest minute sub prettify_duration { - my $s = shift; - $s = int(($s+30)/60)*60; + my ($s, $nearest) = @_; + if ($nearest eq 'week') { + $s = int(($s+60*60*24*3.5)/60/60/24/7)*60*60*24*7; + } elsif ($nearest eq 'day') { + $s = int(($s+60*60*12)/60/60/24)*60*60*24; + } elsif ($nearest eq 'hour') { + $s = int(($s+60*30)/60/60)*60*60; + } elsif ($nearest eq 'minute') { + $s = int(($s+30)/60)*60; + return 'less than a minute' if $s == 0; + } my @out = (); + _part(\$s, 60*60*24*7, 'week', \@out); _part(\$s, 60*60*24, 'day', \@out); _part(\$s, 60*60, 'hour', \@out); _part(\$s, 60, 'minute', \@out); @@ -223,4 +361,65 @@ sub _part { sub _ { return $_[0]; } + +sub display_problem_text { + my ($q, $problem) = @_; + my $out = "<h1>$problem->{title}</h1>"; + + # Display information about problem + $out .= '<p><em>Reported '; + $out .= ($problem->{anonymous}) ? 'anonymously' : "by " . ent($problem->{name}); + $out .= ' at ' . Page::prettify_epoch($problem->{time}); + if ($problem->{council}) { + if ($problem->{whensent}) { + $problem->{council} =~ s/\|.*//g; + my @councils = split /,/, $problem->{council}; + my $areas_info = mySociety::MaPit::get_voting_areas_info(\@councils); + my $council = join(' and ', map { $areas_info->{$_}->{name} } @councils); + $out .= $q->br() . $q->small('Sent to ' . $council . ' ' . + Page::prettify_duration($problem->{whensent}, 'minute') . ' later'); + } + } else { + $out .= $q->br() . $q->small('Not reported to council'); + } + $out .= '</em></p> <p>'; + $out .= ent($problem->{detail}); + $out .= '</p>'; + + if ($problem->{photo}) { + $out .= '<p align="center"><img src="/photo?id=' . $problem->{id} . '"></p>'; + } + + return $out; +} + +# Display updates +sub display_problem_updates { + my $id = shift; + 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", $id); + my $out = ''; + if (@$updates) { + $out .= '<div id="updates">'; + $out .= '<h2>Updates</h2>'; + foreach my $row (@$updates) { + $out .= "<div><a name=\"update_$row->{id}\"></a><em>"; + if ($row->{name}) { + $out .= "Posted by $row->{name}"; + } else { + $out .= "Posted anonymously"; + } + $out .= " at " . Page::prettify_epoch($row->{created}); + $out .= ', marked fixed' if ($row->{mark_fixed}); + $out .= ', reopened' if ($row->{mark_open}); + $out .= '</em>'; + $out .= '<br>' . $row->{text} . '</div>'; + } + $out .= '</div>'; + } + return $out; +} + 1; diff --git a/templates/emails/questionnaire b/templates/emails/questionnaire new file mode 100644 index 000000000..c73f2552a --- /dev/null +++ b/templates/emails/questionnaire @@ -0,0 +1,20 @@ +Subject: Questionnaire about your problem on Neighbourhood Fix-It + +Hi <?=$values['name']?>, + +<?=$values['created']?> ago, you left a problem on Neighbourhood Fix-It +with the details provided at the end of this email. To keep our +site up to date and relevant, we'd appreciate it if you could fill in +this short questionnaire updating the status of your problem: + + <?=$values['url']?> + +Yours, +The Neighbourhood Fix-It team + +Your problem was as follows: + +<?=$values['title']?> + +<?=$values['detail']?> + diff --git a/web/css.css b/web/css.css index 3318ae679..e2bbe2070 100644 --- a/web/css.css +++ b/web/css.css @@ -200,7 +200,7 @@ fieldset div.checkbox label, label.n { width: 510px; } -#map_box p { +p#copyright { float: right; margin: 0 0 1em 0; font-size: 78%; diff --git a/web/index.cgi b/web/index.cgi index 87c9cb8a8..bed7cfae7 100755 --- a/web/index.cgi +++ b/web/index.cgi @@ -6,10 +6,7 @@ # Copyright (c) 2006 UK Citizens Online Democracy. All rights reserved. # Email: matthew@mysociety.org. WWW: http://www.mysociety.org # -# $Id: index.cgi,v 1.122 2007-05-04 00:19:59 matthew Exp $ - -# TODO -# Nothing is done about the update checkboxes - not stored anywhere on anything! +# $Id: index.cgi,v 1.123 2007-05-04 14:36:56 matthew Exp $ use strict; require 5.8.0; @@ -296,24 +293,24 @@ sub display_form { if ($input{skipped}) { # Map is being skipped if ($input{x} && $input{y}) { - $easting = tile_to_os($input{x}); - $northing = tile_to_os($input{y}); + $easting = Page::tile_to_os($input{x}); + $northing = Page::tile_to_os($input{y}); } else { my ($x, $y, $e, $n, $i, $error) = geocode($input{pc}); $easting = $e; $northing = $n; $island = $i; } } 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); + $pin_x = Page::click_to_tile($pin_tile_x, $pin_x); + $pin_y = Page::click_to_tile($pin_tile_y, $pin_y, 1); + $px = Page::tile_to_px($pin_x, $input{x}); + $py = Page::tile_to_px($pin_y, $input{y}); + $easting = Page::tile_to_os($pin_x); + $northing = Page::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}); + $px = Page::os_to_px($input{easting}, $input{x}); + $py = Page::os_to_px($input{northing}, $input{y}); $easting = $input_h{easting}; $northing = $input_h{northing}; } @@ -360,15 +357,9 @@ sub display_form { <h1>Reporting a problem</h1> 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 .= <<EOF; -<script type="text/javascript"> -drag_x = $px - 254; drag_y = 254 - $py; -</script> -EOF - } + my $pins = Page::display_pin($q, $px, $py, 'purple'); + $out .= Page::display_map($q, x => $input{x}, y => $input{y}, type => 2, + pins => $pins, px => $px, py => $py ); $out .= '<h1>Reporting a problem</h1>'; $out .= '<p>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.</p>'; @@ -454,7 +445,7 @@ $category <p align="right"><a href="$back">Back to listings</a></p> EOF - $out .= display_map_end(1); + $out .= Page::display_map_end(1); return $out; } @@ -480,7 +471,7 @@ sub display_location { 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); + my $out = Page::display_map($q, x => $x, y => $y, type => 1, pins => $pins ); $out .= '<h1>Click on the map to report a problem</h1>'; if (@errors) { $out .= '<ul id="error"><li>' . join('</li><li>', @errors) . '</li></ul>'; @@ -535,7 +526,7 @@ EOF $out .= '<li>No problems have been fixed yet</li>'; } $out .= '</ol></div>'; - $out .= display_map_end(1); + $out .= Page::display_map_end(1); return $out; } @@ -549,58 +540,26 @@ sub display_problem { $input{y} ||= 0; $input{y} += 0; # Get all information from database - my $problem = dbh()->selectrow_arrayref( - "select state, easting, northing, title, detail, name, extract(epoch from confirmed), photo, anonymous, - extract(epoch from whensent-confirmed), council + my $problem = dbh()->selectrow_hashref( + "select state, easting, northing, title, detail, name, extract(epoch from confirmed) as time, photo, anonymous, + extract(epoch from whensent-confirmed) as whensent, council, id from problem where id=? and state in ('confirmed','fixed', 'hidden')", {}, $input{id}); return display_location($q, 'Unknown problem ID') unless $problem; - my ($state, $easting, $northing, $title, $desc, $name, $time, - $photo, $anonymous, $whensent, $council) = @$problem; - return front_page($q, 'That problem has been removed') if $state eq 'hidden'; - my $x = os_to_tile($easting); - my $y = os_to_tile($northing); + return front_page($q, 'That problem has been removed') if $problem->{state} eq 'hidden'; + my $x = Page::os_to_tile($problem->{easting}); + my $y = Page::os_to_tile($problem->{northing}); my $x_tile = $input{x} || int($x); my $y_tile = $input{y} || int($y); + my $px = Page::os_to_px($problem->{easting}, $x_tile); + my $py = Page::os_to_px($problem->{northing}, $y_tile); - my $px = os_to_px($easting, $x_tile); - my $py = os_to_px($northing, $y_tile); - - my $pins = display_pin($q, $px, $py, 'blue'); - my $out = display_map($q, $x_tile, $y_tile, 0, 1, $pins); - - $out .= "<h1>$title</h1>"; - $out .= <<EOF; -<script type="text/javascript"> -drag_x = $px - 254; drag_y = 254 - $py; -</script> -EOF - - # Display information about problem - $out .= '<p><em>Reported '; - $out .= ($anonymous) ? 'anonymously' : "by " . ent($name); - $out .= ' at ' . Page::prettify_epoch($time); - if ($council) { - if ($whensent) { - $council =~ s/\|.*//g; - my @councils = split /,/, $council; - my $areas_info = mySociety::MaPit::get_voting_areas_info(\@councils); - $council = join(' and ', map { $areas_info->{$_}->{name} } @councils); - $out .= $q->br() . $q->small('Sent to ' . $council . ' ' . - Page::prettify_duration($whensent) . ' later'); - } - } else { - $out .= $q->br() . $q->small('Not reported to council'); - } - $out .= '</em></p> <p>'; - $out .= ent($desc); - $out .= '</p>'; - - if ($photo) { - $out .= '<p align="center"><img src="/photo?id=' . $input{id} . '"></p>'; - } + my $pins = Page::display_pin($q, $px, $py, 'blue'); + my $out = Page::display_map($q, x => $x_tile, y => $y_tile, type => 0, + pins => $pins, px => $px, py => $py ); + $out .= Page::display_problem_text($q, $problem); $out .= $q->p({align=>'right'}, - $q->a({href => '/contact?id=' . $input{id}}, $q->small('Offensive? Unsuitable? Tell us')) + $q->small($q->a({href => '/contact?id=' . $input{id}}, 'Offensive? Unsuitable? Tell us')) ); my $back = NewURL($q, id=>undef, x=>$x_tile, y=>$y_tile); $out .= '<p style="padding-bottom: 0.5em; border-bottom: dotted 1px #999999;" align="right"><a href="' . $back . '">Back to listings</a></p>'; @@ -647,7 +606,7 @@ EOF } my $fixed = ($input{fixed}) ? ' checked' : ''; - my $fixedline = $state eq 'fixed' ? '' : qq{ + my $fixedline = $problem->{state} eq 'fixed' ? '' : qq{ <div class="checkbox"><input type="checkbox" name="fixed" id="form_fixed" value="1"$fixed> <label for="form_fixed">This problem has been fixed</label></div> }; @@ -667,7 +626,7 @@ $fixedline </fieldset> </form> EOF - $out .= display_map_end(0); + $out .= Page::display_map_end(0); return $out; } @@ -675,12 +634,12 @@ sub map_pins { my ($q, $x, $y) = @_; 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 $min_e = Page::tile_to_os($x); + my $min_n = Page::tile_to_os($y); + my $mid_e = Page::tile_to_os($x+1); + my $mid_n = Page::tile_to_os($y+1); + my $max_e = Page::tile_to_os($x+2); + my $max_n = Page::tile_to_os($y+2); my $current_map = select_all( "select id,title,easting,northing from problem where state='confirmed' @@ -691,9 +650,9 @@ sub map_pins { my $count_fixed = 1; foreach (@$current_map) { push(@ids, $_->{id}); - my $px = os_to_px($_->{easting}, $x); - my $py = os_to_px($_->{northing}, $y); - $pins .= display_pin($q, $px, $py, 'red', $count_prob++); + my $px = Page::os_to_px($_->{easting}, $x); + my $py = Page::os_to_px($_->{northing}, $y); + $pins .= Page::display_pin($q, $px, $py, 'red', $count_prob++); } my $current = []; @@ -706,9 +665,9 @@ sub map_pins { and state = 'confirmed'" . (@ids ? ' and id not in (' . join(',' , @ids) . ')' : '') . " order by distance, created desc limit $limit", $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', $count_prob++); + my $px = Page::os_to_px($_->{easting}, $x); + my $py = Page::os_to_px($_->{northing}, $y); + $pins .= Page::display_pin($q, $px, $py, 'red', $count_prob++); } } my $fixed = select_all( @@ -717,97 +676,13 @@ sub map_pins { 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); - $pins .= display_pin($q, $px, $py, 'green', $count_fixed++); + my $px = Page::os_to_px($_->{easting}, $x); + my $py = Page::os_to_px($_->{northing}, $y); + $pins .= Page::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 = '<img class="pin" src="/i/pin' . $cols{$col} - . $num . '.gif" alt="Problem" style="top:' . ($py-59) - . 'px; right:' . ($px-31) . 'px; position: absolute;">'; - return $out unless $_ && $_->{id} && $col ne 'blue'; - my $url = NewURL($q, id=>$_->{id}, x=>undef, y=>undef); - $out = '<a title="' . $_->{title} . '" href="' . $url . '">' . $out . '</a>'; - 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 ||= ''; - $x = 0 if ($x<=0); - $y = 0 if ($y<=0); - 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; - return '<div id="side">' if (!$tileids->[0][0] || !$tileids->[0][1] || !$tileids->[1][0] || !$tileids->[1][1]); - 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 = $q->param('pc') || ''; - my $pc_enc = ent($pc); - $out .= <<EOF; -<form action="./" method="post" id="mapForm"$encoding> -<input type="hidden" name="submit_map" value="1"> -<input type="hidden" name="x" value="$x"> -<input type="hidden" name="y" value="$y"> -<input type="hidden" name="pc" value="$pc_enc"> -EOF - $img_type = '<input type="image"'; - } else { - $img_type = '<img'; - } - my $imgw = '254px'; - my $imgh = '254px'; - $out .= <<EOF; -<script type="text/javascript"> -var x = $x - 2; var y = $y - 2; -var drag_x = 0; var drag_y = 0; -</script> -<div id="map_box"> - <div id="map"><div id="drag"> - $img_type alt="NW map tile" id="t2.2" name="tile_$tl" src="$tl_src" style="top:0px; left:0px;">$img_type alt="NE map tile" id="t2.3" name="tile_$tr" src="$tr_src" style="top:0px; left:$imgw;"><br>$img_type alt="SW map tile" id="t3.2" name="tile_$bl" src="$bl_src" style="top:$imgh; left:0px;">$img_type alt="SE map tile" id="t3.3" name="tile_$br" src="$br_src" style="top:$imgh; left:$imgw;"> - $pins - </div></div> - <p>© Crown copyright. All rights reserved. - Department for Constitutional Affairs 100037819 2007</p> - </div> -EOF - $out .= Page::compass($q, $x, $y) if $compass; - $out .= '<div id="side">'; - return $out; -} - -sub display_map_end { - my ($type) = @_; - my $out = '</div>'; - $out .= '</form>' if ($type); - return $out; -} - sub geocode_choice { my $choices = shift; my $out = '<p>We found more than one match for that location:</p> <ul>'; @@ -833,8 +708,8 @@ sub geocode { throw RABX::Error("We do not cover Northern Ireland, I'm afraid, as our licence doesn't include any maps for the region.") if $island eq 'I'; $easting = $location->{easting}; $northing = $location->{northing}; - my $xx = os_to_tile($easting); - my $yy = os_to_tile($northing); + my $xx = Page::os_to_tile($easting); + my $yy = Page::os_to_tile($northing); $x = int($xx); $y = int($yy); $x -= 1 if ($xx - $x < 0.5); @@ -887,43 +762,9 @@ sub geocode_string { $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; + $x = int(Page::os_to_tile($easting))-1; + $y = int(Page::os_to_tile($northing))-1; } return ($x, $y, $easting, $northing, $error); } -# 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; -} - - @@ -51,6 +51,21 @@ YAHOO.util.Event.onContentReady('mapForm', function() { } }); +YAHOO.util.Event.onContentReady('another_qn', function() { + if (!document.getElementById('been_fixed_no').checked) { + YAHOO.util.Dom.setStyle(this, 'display', 'none'); + } + YAHOO.util.Event.addListener('been_fixed_no', 'click', function(e) { + YAHOO.util.Dom.setStyle('another_qn', 'display', 'block'); + }); + YAHOO.util.Event.addListener('been_fixed_yes', 'click', function(e) { + YAHOO.util.Dom.setStyle('another_qn', 'display', 'none'); + }); + YAHOO.util.Event.addListener('been_fixed_na', 'click', function(e) { + YAHOO.util.Dom.setStyle('another_qn', 'display', 'none'); + }); +}); + var timer; function email_alert_close() { YAHOO.util.Dom.setStyle('email_alert_box', 'display', 'none'); diff --git a/web/questionnaire.cgi b/web/questionnaire.cgi new file mode 100755 index 000000000..2f31fac3d --- /dev/null +++ b/web/questionnaire.cgi @@ -0,0 +1,228 @@ +#!/usr/bin/perl -w + +# questionnaire.cgi: +# Questionnaire for problem creators +# +# Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved. +# Email: matthew@mysociety.org. WWW: http://www.mysociety.org +# +# $Id: questionnaire.cgi,v 1.1 2007-05-04 14:36:56 matthew Exp $ + +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 Page; +use mySociety::AuthToken; +use mySociety::Config; +use mySociety::DBHandle qw(dbh select_all); +use mySociety::MaPit; +use mySociety::Web qw(ent); + +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) + ); +} + +sub main { + my $q = shift; + my $out = ''; + if ($q->param('submit')) { + $out = submit_questionnaire($q); + } else { + $out = display_questionnaire($q); + } + print Page::header($q, _('Questionnaire')); + print $out; + print Page::footer(); + dbh()->rollback(); +} +Page::do_fastcgi(\&main); + +sub check_stuff { + my $q = shift; + + my $id = mySociety::AuthToken::retrieve('questionnaire', $q->param('token')); + throw Error::Simple("I'm afraid we couldn't validate that token. If you've copied the URL from an email, please check that you copied it exactly.\n") unless $id; + + my $questionnaire = dbh()->selectrow_hashref( + 'select id, problem_id, whenanswered from questionnaire where id=?', {}, $id); + my $problem_id = $questionnaire->{problem_id}; + throw Error::Simple("You have already answered this questionnaire. If you have a question, please <a href=/contact>get in touch</a>, or <a href=/?id=$problem_id>view your problem</a>.\n") if $questionnaire->{whenanswered}; + + my $prev_questionnaire = dbh()->selectrow_hashref( + 'select id from questionnaire where problem_id=? and whenanswered is not null', {}, $problem_id); + + my $problem = dbh()->selectrow_hashref( + "select *, extract(epoch from confirmed) as time, extract(epoch from whensent-confirmed) as whensent + from problem where id=? and state in ('confirmed','fixed')", {}, $problem_id); + throw Error::Simple("I'm afraid we couldn't locate your problem in the database.\n") unless $problem; + + return ($questionnaire, $prev_questionnaire, $problem); +} + +sub submit_questionnaire { + my $q = shift; + my @vars = qw(token id been_fixed reported update another); + my %input = map { $_ => scalar $q->param($_) } @vars; + my %input_h = map { $_ => $q->param($_) ? ent($q->param($_)) : '' } @vars; + + my ($error, $questionnaire, $prev_questionnaire, $problem); + try { + ($questionnaire, $prev_questionnaire, $problem) = check_stuff($q); + } catch Error::Simple with { + my $e = shift; + $error = $e; + }; + return $error if $error; + + my @errors; + push @errors, 'Please state whether or not the problem has been fixed' unless $input{been_fixed}; + push @errors, 'Please say whether you\'ve ever reported a problem to your council before' unless $input{reported} || $prev_questionnaire; + push @errors, 'Please indicate whether you\'d like to receive another questionnaire' + if $input{been_fixed} eq 'No' && !$input{another}; + push @errors, 'Please provide some explanation as to why you\'re reopening this report' + if $input{been_fixed} eq 'No' && $problem->{state} eq 'fixed' && !$input{update}; + return display_questionnaire($q, @errors) if @errors; + + my $new_state; + $new_state = 'fixed' if $input{been_fixed} eq 'Yes' && $problem->{state} eq 'confirmed'; + $new_state = 'confirmed' if $input{been_fixed} eq 'No' && $problem->{state} eq 'fixed'; + + # Record state change, if there was one + dbh()->do("update problem set state=? where id=?", {}, $new_state, $problem->{id}) + if $new_state; + + # Record questionnaire response + my $reported = $input{reported} eq 'Yes' ? 't' : + ($input{reported} eq 'No' ? 'f' : undef); + dbh()->do('update questionnaire set whenanswered=ms_current_timestamp(), + ever_reported=? where id=?', {}, $reported, $questionnaire->{id}); + + # Record an update if they've given one, or if there's a state change + my $name = $problem->{anonymous} ? undef : $problem->{name}; + my $update = $input{update} ? $input{update} : 'Questionnaire filled in by problem reporter'; + dbh()->do("insert into comment + (problem_id, name, email, website, text, state, mark_fixed, mark_open) + values (?, ?, ?, ?, ?, 'confirmed', ?, ?)", {}, + $problem->{id}, $name, $problem->{email}, '', $update, + $new_state eq 'fixed' ? 't' : 'f', $new_state eq 'confirmed' ? 't' : 'f' + ) + if $new_state || $input{update}; + + # If they've said they want another questionnaire, mark as such + dbh()->do("update problem set send_questionnaire = 't' where id=?", {}, $problem->{id}) + if $input{been_fixed} eq 'No' && $input{another} eq 'Yes'; + + dbh()->commit(); + return <<EOF; +<p>Thank you very much for filling in our questionnaire. +<a href="/?id=$problem->{id}">View your report on the site</a></p> +EOF +} + +sub display_questionnaire { + my ($q, @errors) = @_; + my @vars = qw(token id been_fixed reported update another); + my %input = map { $_ => scalar $q->param($_) } @vars; + my %input_h = map { $_ => $q->param($_) ? ent($q->param($_)) : '' } @vars; + + my ($error, $questionnaire, $prev_questionnaire, $problem); + try { + ($questionnaire, $prev_questionnaire, $problem) = check_stuff($q); + } catch Error::Simple with { + my $e = shift; + $error = $e; + }; + return $error if $error; + + my $x = Page::os_to_tile($problem->{easting}); + my $y = Page::os_to_tile($problem->{northing}); + my $x_tile = int($x); + my $y_tile = int($y); + my $px = Page::os_to_px($problem->{easting}, $x_tile); + my $py = Page::os_to_px($problem->{northing}, $y_tile); + + my $pins = Page::display_pin($q, $px, $py, $problem->{state} eq 'fixed'?'green':'red'); + my $problem_text = Page::display_problem_text($q, $problem); + my $updates = Page::display_problem_updates($problem->{id}); + my $out = ''; + $out .= Page::display_map($q, x => $x_tile, y => $y_tile, pins => $pins, + px => $px, py => $py, pre => $problem_text, post => $updates ); + my %been_fixed = ( + yes => $input{been_fixed} eq 'Yes' ? ' checked' : '', + no => $input{been_fixed} eq 'No' ? ' checked' : '', + ); + my %reported = ( + yes => $input{reported} eq 'Yes' ? ' checked' : '', + no => $input{reported} eq 'No' ? ' checked' : '', + ); + my %another = ( + yes => $input{another} eq 'Yes' ? ' checked' : '', + no => $input{another} eq 'No' ? ' checked' : '', + ); + $out .= <<EOF; + <style type="text/css">label { float:none;}</style> +<h1>Questionnaire</h1> +<form method="post" action="/questionnaire"> +<input type="hidden" name="token" value="$input_h{token}"> + +<p>The details of your problem are available on the right hand side of this page. +EOF + $out .= 'Please take a look at the updates that have been left.' if $updates; + $out .= '</p>'; + if (@errors) { + $out .= '<ul id="error"><li>' . join('</li><li>', @errors) . '</li></ul>'; + } + $out .= '<p>'; + $out .= 'An update marked this problem as fixed. ' if $problem->{state} eq 'fixed'; + $out .= 'Has the problem been fixed?</p>'; + $out .= <<EOF; +<p align="center"> +<input type="radio" name="been_fixed" id="been_fixed_yes" value="Yes"$been_fixed{yes}> +<label for="been_fixed_yes">Yes</label> +<input type="radio" name="been_fixed" id="been_fixed_no" value="No"$been_fixed{no}> +<label for="been_fixed_no">No</label> +</p> +EOF + $out .= <<EOF unless $prev_questionnaire; +<p>Have you ever reported a problem to your council before?</p> +<p align="center"> +<input type="radio" name="reported" id="reported_yes" value="Yes"$reported{yes}> +<label for="reported_yes">Yes</label> +<input type="radio" name="reported" id="reported_no" value="No"$reported{no}> +<label for="reported_no">No</label> +</p> +EOF + $out .= <<EOF; +<p>If you wish to leave a public update on the problem, please enter it here +(please note it will not be sent to the council) :</p> +<p><textarea name="update" style="width:100%" rows="7" cols="30">$input_h{update}</textarea></p> + +<div id="another_qn"> +<p>Would you like to receive another questionnaire in 4 weeks, reminding you to check the status?</p> +<p align="center"> +<input type="radio" name="another" id="another_yes" value="Yes"$another{yes}> +<label for="another_yes">Yes</label> +<input type="radio" name="another" id="another_no" value="No"$another{no}> +<label for="another_no">No</label> +</p> +</div> + +<p align="right"><input type="submit" name="submit" value="Submit questionnaire"></p> +</form> +EOF + $out .= Page::display_map_end(0); + return $out; +} |