aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xbin/send-questionnaires97
-rw-r--r--conf/httpd.conf3
-rw-r--r--db/schema.sql13
-rw-r--r--perllib/Page.pm223
-rw-r--r--templates/emails/questionnaire20
-rw-r--r--web/css.css2
-rwxr-xr-xweb/index.cgi261
-rw-r--r--web/js.js15
-rwxr-xr-xweb/questionnaire.cgi228
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">&copy; Crown copyright. All rights reserved.
+ Department for Constitutional Affairs 100037819&nbsp;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>&copy; Crown copyright. All rights reserved.
- Department for Constitutional Affairs 100037819&nbsp;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;
-}
-
-
diff --git a/web/js.js b/web/js.js
index eb3a52ea1..2016fc381 100644
--- a/web/js.js
+++ b/web/js.js
@@ -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;
+}