aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xbin/populate_bing_cache58
-rwxr-xr-xbin/send-reports12
m---------commonlib0
-rw-r--r--data/openlayers.fixmystreet.cfg1
-rw-r--r--db/schema.sql3
-rw-r--r--db/schema_0012-add_gecode_column_to_problem.sql7
-rw-r--r--notes/INSTALL6
-rw-r--r--perl-external/files.txt2
-rw-r--r--perl-external/minicpan/modules/02packages.details.txt.gzbin20395 -> 20439 bytes
-rw-r--r--perl-external/modules.txt2
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin.pm41
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/New.pm7
-rw-r--r--perllib/FixMyStreet/App/Controller/Reports.pm11
-rwxr-xr-xperllib/FixMyStreet/App/Controller/Rss.pm10
-rw-r--r--perllib/FixMyStreet/Cobrand/Default.pm67
-rw-r--r--perllib/FixMyStreet/Cobrand/Reading.pm29
-rw-r--r--perllib/FixMyStreet/Cobrand/Southampton.pm1
-rw-r--r--perllib/FixMyStreet/DB/Result/Problem.pm27
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/AlertType.pm43
-rw-r--r--perllib/FixMyStreet/Map/FMS.pm10
-rw-r--r--perllib/Open311.pm20
-rw-r--r--t/app/controller/alert_new.t3
-rw-r--r--t/app/controller/auth.t12
-rw-r--r--t/app/controller/report_new.t50
-rw-r--r--t/app/controller/rss.t125
-rw-r--r--t/app/model/alert_type.t209
-rw-r--r--t/cobrand/closest.t78
-rw-r--r--t/open311.t20
-rw-r--r--templates/web/default/admin/list_updates.html4
-rw-r--r--templates/web/default/admin/report_edit.html5
-rwxr-xr-xtemplates/web/default/around/display_location.html2
-rw-r--r--templates/web/default/auth/general.html4
-rw-r--r--templates/web/default/common_header_tags.html7
-rw-r--r--templates/web/default/contact/index.html10
-rw-r--r--templates/web/default/header.html1
-rw-r--r--templates/web/default/js_validation_msgs.html20
-rw-r--r--templates/web/default/report/display.html10
-rw-r--r--templates/web/default/report/new/category.html2
-rw-r--r--templates/web/default/report/new/category_extras.html4
-rw-r--r--templates/web/default/report/new/fill_in_details.html4
-rw-r--r--templates/web/default/report/new/fill_in_details_form.html18
-rw-r--r--templates/web/default/report/updates.html12
-rw-r--r--templates/web/emptyhomes/header.html2
-rw-r--r--templates/web/emptyhomes/index.html24
-rw-r--r--templates/web/reading/footer.html15
-rw-r--r--templates/web/reading/reports/cobrand_stats.html5
-rw-r--r--templates/web/southampton/report/photo.html6
-rw-r--r--web/cobrands/emptyhomes/css.css11
-rw-r--r--web/cobrands/southampton/css.scss4
-rw-r--r--web/css/_main.scss2
-rw-r--r--web/css/core.scss15
-rw-r--r--web/i/appstore.pngbin0 -> 6680 bytes
-rw-r--r--web/js/OpenLayers.fixmystreet.js10
-rw-r--r--web/js/fixmystreet.js96
-rw-r--r--web/js/jquery.validate.js1188
-rw-r--r--web/js/jquery.validate.min.js51
-rw-r--r--web/js/map-OpenLayers.js6
-rw-r--r--web/js/map-bing-ol.js10
58 files changed, 2303 insertions, 99 deletions
diff --git a/bin/populate_bing_cache b/bin/populate_bing_cache
new file mode 100755
index 000000000..a3bef6759
--- /dev/null
+++ b/bin/populate_bing_cache
@@ -0,0 +1,58 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+require 5.8.0;
+
+use Data::Dumper;
+
+use FixMyStreet::App;
+use FixMyStreet::Geocode::Bing;
+
+my $reports = FixMyStreet::App->model('DB::Problem')->search(
+ {
+ geocode => undef,
+ confirmed => { '!=', undef },
+ latitude => { '!=', 0 },
+ longitude => { '!=', 0 },
+ },
+ {
+ select => [qw/id geocode confirmed latitude longitude/],
+ order_by => { -desc => 'confirmed' }
+ }
+);
+
+my $num_reports = $reports->count();
+print "Found $num_reports lacking geocode information\n";
+
+my $time_to_do = ( $num_reports * 10 ) / 60 / 60;
+if ( $time_to_do > 24 ) {
+ my $days = $time_to_do / 24;
+ my $hours = $time_to_do % 24;
+ printf( "Should take %d days and %d hours to finish\n", $days, $hours );
+}
+elsif ( $time_to_do < 1 ) {
+ printf( "Should take %d minutes to finish\n", $time_to_do * 60 );
+}
+else {
+ my $mins = ( $num_reports * 10 ) / 60 % 60;
+ printf( "Should take %d hours and %d minutes to finish\n",
+ $time_to_do, $mins );
+}
+
+while ( my $report = $reports->next ) {
+ $num_reports--;
+ next unless $report->latitude && $report->longitude;
+ next if $report->geocode;
+
+ my $j = FixMyStreet::Geocode::Bing::reverse( $report->latitude,
+ $report->longitude );
+
+ $report->geocode($j);
+ $report->update;
+
+ print "$num_reports left to populate\n" unless $num_reports % 100;
+ sleep 10;
+}
+
+print "done\n";
diff --git a/bin/send-reports b/bin/send-reports
index 648e83192..427d02ec8 100755
--- a/bin/send-reports
+++ b/bin/send-reports
@@ -92,7 +92,7 @@ while (my $row = $unsent->next) {
}
if ( $row->used_map ) {
- $h{closest_address} = $cobrand->find_closest( $h{latitude}, $h{longitude} );
+ $h{closest_address} = $cobrand->find_closest( $h{latitude}, $h{longitude}, $row );
}
my (@to, @recips, $template, $areas_info, @open311_councils);
@@ -273,8 +273,18 @@ while (my $row = $unsent->next) {
$open311->endpoints( { services => 'Services', requests => 'Requests' } );
}
+ # required to get round issues with CRM constraints
+ if ( $row->council =~ /2218/ ) {
+ $row->user->name( $row->user->id . ' ' . $row->user->name );
+ }
+
my $resp = $open311->send_service_request( $row, \%h, $contact->email );
+ # make sure we don't save user changes from above
+ if ( $row->council =~ /2218/ ) {
+ $row->discard_changes();
+ }
+
if ( $resp ) {
$row->external_id( $resp );
$result *= 0;
diff --git a/commonlib b/commonlib
-Subproject f2532c104a1268b536f79b13c52bdc0d7fb4d7a
+Subproject 7486b07a4a865f977df04c3c34de759126c014e
diff --git a/data/openlayers.fixmystreet.cfg b/data/openlayers.fixmystreet.cfg
index c2c06a9ed..faeb3ed50 100644
--- a/data/openlayers.fixmystreet.cfg
+++ b/data/openlayers.fixmystreet.cfg
@@ -16,6 +16,7 @@ OpenLayers/Control/Attribution.js
OpenLayers/Control/DragFeature.js
OpenLayers/Control/Navigation.js
OpenLayers/Control/PanZoom.js
+OpenLayers/Control/PinchZoom.js
OpenLayers/Control/Permalink.js
OpenLayers/Control/SelectFeature.js
OpenLayers/Format/JSON.js
diff --git a/db/schema.sql b/db/schema.sql
index fcd137919..395d1c07b 100644
--- a/db/schema.sql
+++ b/db/schema.sql
@@ -188,7 +188,8 @@ create table problem (
whensent timestamp,
send_questionnaire boolean not null default 't',
extra text, -- extra fields required for open311
- flagged boolean not null default 'f'
+ flagged boolean not null default 'f',
+ geocode bytea
);
create index problem_state_latitude_longitude_idx on problem(state, latitude, longitude);
create index problem_user_id_idx on problem ( user_id );
diff --git a/db/schema_0012-add_gecode_column_to_problem.sql b/db/schema_0012-add_gecode_column_to_problem.sql
new file mode 100644
index 000000000..730212ead
--- /dev/null
+++ b/db/schema_0012-add_gecode_column_to_problem.sql
@@ -0,0 +1,7 @@
+
+begin;
+
+ALTER table problem
+ ADD column geocode BYTEA;
+
+commit;
diff --git a/notes/INSTALL b/notes/INSTALL
index 261865700..56be765db 100644
--- a/notes/INSTALL
+++ b/notes/INSTALL
@@ -14,9 +14,9 @@ conf/packages is a list of Debian packages that are needed, so install them if
you're on Debian/Ubuntu. You'll also probably need to install lots of CPAN
modules, see the section on that below.
-FixMyStreet expects a PostgreSQL database, so set one of them up - the schema
-is available in db/schema.sql. You will also need to load in db/alert_types.sql
-to populate the alert types table.
+FixMyStreet expects a PostgreSQL database with the postGIS extension, so set one
+of them up - the schema is available in db/schema.sql. You will also need to load
+in db/alert_types.sql to populate the alert types table.
Copy conf/general-example to conf/general and set it up appropriately:
* provide the relevant database connection details
diff --git a/perl-external/files.txt b/perl-external/files.txt
index f594e80af..781a2564e 100644
--- a/perl-external/files.txt
+++ b/perl-external/files.txt
@@ -126,6 +126,7 @@
/authors/id/G/GA/GAAS/Digest-HMAC-1.02.tar.gz
/authors/id/G/GA/GAAS/Encode-Locale-1.01.tar.gz
/authors/id/G/GA/GAAS/File-Listing-6.00.tar.gz
+/authors/id/G/GA/GAAS/HTML-Form-6.00.tar.gz
/authors/id/G/GA/GAAS/HTML-Parser-3.68.tar.gz
/authors/id/G/GA/GAAS/HTTP-Cookies-6.00.tar.gz
/authors/id/G/GA/GAAS/HTTP-Daemon-6.00.tar.gz
@@ -144,6 +145,7 @@
/authors/id/G/GE/GETTY/HTTP-Body-1.11.tar.gz
/authors/id/G/GR/GRODITI/MooseX-Types-Common-0.001002.tar.gz
/authors/id/G/GR/GROMMEL/Math-Round-0.06.tar.gz
+/authors/id/G/GW/GWILLIAMS/DateTime-Format-W3CDTF-0.06.tar.gz
/authors/id/I/IL/ILMARI/Class-Unload-0.07.tar.gz
/authors/id/I/IN/INGY/Spiffy-0.30.tar.gz
/authors/id/I/IN/INGY/Test-Base-0.59.tar.gz
diff --git a/perl-external/minicpan/modules/02packages.details.txt.gz b/perl-external/minicpan/modules/02packages.details.txt.gz
index d78040bab..dda827b07 100644
--- a/perl-external/minicpan/modules/02packages.details.txt.gz
+++ b/perl-external/minicpan/modules/02packages.details.txt.gz
Binary files differ
diff --git a/perl-external/modules.txt b/perl-external/modules.txt
index 4568fc706..f2e6a92ad 100644
--- a/perl-external/modules.txt
+++ b/perl-external/modules.txt
@@ -27,6 +27,7 @@ DBIx::Class::Storage::DBI
DateTime::Format::HTTP
DateTime::Format::ISO8601
DateTime::Format::Pg
+DateTime::Format::W3CDTF
Email::Address
Email::MIME
Email::Send
@@ -34,6 +35,7 @@ Email::Simple
Email::Valid
File::Path
HTML::Entities
+HTML::Form
HTTP::Server::Simple
HTTP::Server::Simple::CGI
IO::String
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm
index a34737844..c988b23c1 100644
--- a/perllib/FixMyStreet/App/Controller/Admin.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin.pm
@@ -572,6 +572,9 @@ sub report_edit : Path('report_edit') : Args(1) {
elsif ( $c->req->param('banuser') ) {
$c->forward('ban_user');
}
+ elsif ( $c->req->param('rotate_photo') ) {
+ $c->forward('rotate_photo');
+ }
elsif ( $c->req->param('submit') ) {
$c->forward('check_token');
@@ -1161,6 +1164,31 @@ sub check_email_for_abuse : Private {
return 1;
}
+=head2 rotate_photo
+
+Rotate a photo 90 degrees left or right
+
+=cut
+
+sub rotate_photo : Private {
+ my ( $self, $c ) =@_;
+
+ my $direction = $c->req->param('rotate_photo');
+
+ return unless $direction =~ /Left/ or $direction =~ /Right/;
+
+ my $photo = _rotate_image( $c->stash->{problem}->photo, $direction =~ /Left/ ? -90 : 90 );
+
+ if ( $photo ) {
+ $c->stash->{rotated} = 1;
+ $c->stash->{problem}->photo( $photo );
+ $c->stash->{problem}->update();
+ }
+
+ return 1;
+}
+
+
=head2 check_page_allowed
Checks if the current catalyst action is in the list of allowed pages and
@@ -1207,6 +1235,19 @@ sub trim {
return $e;
}
+sub _rotate_image {
+ my ($photo, $direction) = @_;
+ use Image::Magick;
+ my $image = Image::Magick->new;
+ $image->BlobToImage($photo);
+ my $err = $image->Rotate($direction);
+ return 0 if $err;
+ my @blobs = $image->ImageToBlob();
+ undef $image;
+ return $blobs[0];
+}
+
+
=head1 AUTHOR
Struan Donald
diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm
index c6ede88f7..e982d6a4c 100644
--- a/perllib/FixMyStreet/App/Controller/Report/New.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/New.pm
@@ -911,6 +911,13 @@ sub check_for_errors : Private {
%{ $c->stash->{report}->check_for_errors },
);
+ # if they're got the login details wrong when signing in then
+ # we don't care about the name field even though it's validated
+ # by the user object
+ if ( $c->req->param('submit_sign_in') and $field_errors{password} ) {
+ delete $field_errors{name};
+ }
+
# add the photo error if there is one.
if ( my $photo_error = delete $c->stash->{photo_error} ) {
$field_errors{photo} = $photo_error;
diff --git a/perllib/FixMyStreet/App/Controller/Reports.pm b/perllib/FixMyStreet/App/Controller/Reports.pm
index 0a4ae4609..0587a627a 100644
--- a/perllib/FixMyStreet/App/Controller/Reports.pm
+++ b/perllib/FixMyStreet/App/Controller/Reports.pm
@@ -110,6 +110,8 @@ sub ward : Path : Args(2) {
$c->stash->{council_url} = '/reports/' . $council_short;
+ $c->stash->{stats} = $c->cobrand->get_report_stats();
+
my $pins = $c->stash->{pins};
$c->stash->{page} = 'reports'; # So the map knows to make clickable pins
@@ -349,17 +351,12 @@ sub load_and_group_problems : Private {
$c->stash->{pager} = $problems->pager;
$problems = $problems->cursor; # Raw DB cursor for speed
- my ( %fixed, %open, @pins, $total, $cobrand_total );
+ my ( %fixed, %open, @pins );
my $re_councils = join('|', keys %{$c->stash->{areas_info}});
my @cols = ( 'id', 'council', 'state', 'areas', 'latitude', 'longitude', 'title', 'cobrand', 'duration', 'age' );
while ( my @problem = $problems->next ) {
my %problem = zip @cols, @problem;
$c->log->debug( $problem{'cobrand'} . ', cobrand is ' . $c->cobrand->moniker );
- if ( $problem{'cobrand'} && $problem{'cobrand'} eq $c->cobrand->moniker ) {
- $cobrand_total++;
- } else {
- $total++;
- }
if ( !$problem{council} ) {
# Problem was not sent to any council, add to possible councils
$problem{councils} = 0;
@@ -382,8 +379,6 @@ sub load_and_group_problems : Private {
fixed => \%fixed,
open => \%open,
pins => \@pins,
- cobrand_count => $cobrand_total || 0,
- total_count => $total || 0,
);
return 1;
diff --git a/perllib/FixMyStreet/App/Controller/Rss.pm b/perllib/FixMyStreet/App/Controller/Rss.pm
index 7fddbed97..822780b81 100755
--- a/perllib/FixMyStreet/App/Controller/Rss.pm
+++ b/perllib/FixMyStreet/App/Controller/Rss.pm
@@ -251,6 +251,12 @@ sub add_row : Private {
(my $link = $alert_type->item_link) =~ s/{{(.*?)}}/$row->{$1}/g;
(my $desc = _($alert_type->item_description)) =~ s/{{(.*?)}}/$row->{$1}/g;
my $url = $c->uri_for( $link );
+
+ if ( $row->{postcode} ) {
+ my $pc = $c->cobrand->format_postcode( $row->{postcode} );
+ $title .= ", $pc";
+ }
+
my %item = (
title => ent($title),
link => $url,
@@ -266,8 +272,8 @@ sub add_row : Private {
}
if ( $row->{used_map} ) {
- #my $address = $c->cobrand->find_closest_address_for_rss( $row->{latitude}, $row->{longitude} );
- #$item{description} .= ent("\n<br>$address");
+ my $address = $c->cobrand->find_closest_address_for_rss( $row->{latitude}, $row->{longitude}, $row );
+ $item{description} .= ent("\n<br>$address") if $address;
}
my $recipient_name = $c->cobrand->contact_name;
diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm
index 1e87468ac..b5a1cd8d3 100644
--- a/perllib/FixMyStreet/Cobrand/Default.pm
+++ b/perllib/FixMyStreet/Cobrand/Default.pm
@@ -545,10 +545,15 @@ Used by send-reports to attach nearest things to the bottom of the report
=cut
sub find_closest {
- my ( $self, $latitude, $longitude ) = @_;
+ my ( $self, $latitude, $longitude, $problem ) = @_;
my $str = '';
if ( my $j = FixMyStreet::Geocode::Bing::reverse( $latitude, $longitude ) ) {
+ # cache the bing results for use in alerts
+ if ( $problem ) {
+ $problem->geocode( $j );
+ $problem->update;
+ }
if ($j->{resourceSets}[0]{resources}[0]{name}) {
$str .= sprintf(_("Nearest road to the pin placed on the map (automatically generated by Bing Maps): %s"),
$j->{resourceSets}[0]{resources}[0]{name}) . "\n\n";
@@ -576,23 +581,55 @@ Used by rss feeds to provide a bit more context
=cut
sub find_closest_address_for_rss {
- my ( $self, $latitude, $longitude ) = @_;
+ my ( $self, $latitude, $longitude, $problem ) = @_;
my $str = '';
- if ( my $j = FixMyStreet::Geocode::Bing::reverse( $latitude, $longitude, 1 ) ) {
- if ($j->{resourceSets}[0]{resources}[0]{name}) {
- my $address = $j->{resourceSets}[0]{resources}[0]{address};
- my @address;
- push @address, $address->{addressLine} if $address->{addressLine} ne 'Street';
- push @address, $address->{locality};
- $str .= sprintf(_("Nearest road to the pin placed on the map (automatically generated by Bing Maps): %s"),
- join( ', ', @address ) );
- }
+ my $j;
+ if ( $problem && ref($problem) =~ /FixMyStreet/ && $problem->can( 'geocode' ) ) {
+ $j = $problem->geocode;
+ } else {
+ $problem = FixMyStreet::App->model('DB::Problem')->find( { id => $problem->{id} } );
+ $j = $problem->geocode;
+ }
+
+ # if we've not cached it then we don't want to look it up in order to avoid
+ # hammering the bing api
+ # if ( !$j ) {
+ # $j = FixMyStreet::Geocode::Bing::reverse( $latitude, $longitude, 1 );
+
+ # $problem->geocode( $j );
+ # $problem->update;
+ # }
+
+ if ($j && $j->{resourceSets}[0]{resources}[0]{name}) {
+ my $address = $j->{resourceSets}[0]{resources}[0]{address};
+ my @address;
+ push @address, $address->{addressLine} if $address->{addressLine} and $address->{addressLine} !~ /^Street$/i;
+ push @address, $address->{locality} if $address->{locality};
+ $str .= sprintf(_("Nearest road to the pin placed on the map (automatically generated by Bing Maps): %s"),
+ join( ', ', @address ) ) if @address;
}
return $str;
}
+=head2 format_postcode
+
+Takes a postcode string and if it looks like a valid postcode then transforms it
+into the canonical postcode.
+
+=cut
+
+sub format_postcode {
+ my ( $self, $postcode ) = @_;
+
+ if ( $postcode ) {
+ $postcode = mySociety::PostcodeUtil::canonicalise_postcode($postcode)
+ if $postcode && mySociety::PostcodeUtil::is_valid_postcode($postcode);
+ }
+
+ return $postcode;
+}
=head2 council_check
Paramters are COUNCILS, QUERY, CONTEXT. Return a boolean indicating whether
@@ -960,5 +997,13 @@ to be resized then return 0;
sub default_photo_resize { return 0; }
+=head2 get_report_stats
+
+Get stats to display on the council reports page
+
+=cut
+
+sub get_report_stats { return 0; }
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/Reading.pm b/perllib/FixMyStreet/Cobrand/Reading.pm
index f4fd0dc7a..8e98931fd 100644
--- a/perllib/FixMyStreet/Cobrand/Reading.pm
+++ b/perllib/FixMyStreet/Cobrand/Reading.pm
@@ -77,5 +77,32 @@ sub recent_photos {
return $self->problems->recent_photos( $num, $lat, $lon, $dist );
}
-1;
+sub get_report_stats {
+ my $self = shift;
+
+ my ( $cobrand, $main_site ) = ( 0, 0 );
+
+ $self->{c}->log->debug( 'X' x 60 );
+ my $stats = $self->{c}->model('DB::Problem')->search(
+ { confirmed => { '>=', '2011-11-01' } },
+ {
+ select => [ { count => 'id', -as => 'cobrand_count' }, 'cobrand' ],
+ group_by => [qw/cobrand/]
+ }
+ );
+
+ while ( my $stat = $stats->next ) {
+ if ( $stat->cobrand eq $self->moniker ) {
+ $cobrand += $stat->get_column( 'cobrand_count' );
+ } else {
+ $main_site += $stat->get_column( 'cobrand_count' );
+ }
+ }
+ return {
+ cobrand => $cobrand,
+ main_site => $main_site,
+ };
+}
+
+1;
diff --git a/perllib/FixMyStreet/Cobrand/Southampton.pm b/perllib/FixMyStreet/Cobrand/Southampton.pm
index bd461f5e2..213dd533b 100644
--- a/perllib/FixMyStreet/Cobrand/Southampton.pm
+++ b/perllib/FixMyStreet/Cobrand/Southampton.pm
@@ -64,6 +64,7 @@ sub all_councils_report {
sub disambiguate_location {
return {
+ town => 'Southampton',
centre => '50.913822,-1.400493',
span => '0.084628,0.15701',
bounds => [ '50.871508,-1.478998', '50.956136,-1.321988' ],
diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm
index 9ff19efb6..7ceabf1da 100644
--- a/perllib/FixMyStreet/DB/Result/Problem.pm
+++ b/perllib/FixMyStreet/DB/Result/Problem.pm
@@ -82,6 +82,8 @@ __PACKAGE__->add_columns(
{ data_type => "text", is_nullable => 1 },
"flagged",
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
+ "geocode",
+ { data_type => "bytea", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->has_many(
@@ -104,8 +106,8 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07010 @ 2011-07-29 16:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ifvx9FOlbui66hPyzNIAPA
+# Created by DBIx::Class::Schema::Loader v0.07010 @ 2011-09-19 14:38:43
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:nq8Ufn/SEoDGSrrGlHIxag
# Add fake relationship to stored procedure table
__PACKAGE__->has_one(
@@ -134,6 +136,27 @@ __PACKAGE__->filter_column(
},
}
);
+
+__PACKAGE__->filter_column(
+ geocode => {
+ filter_from_storage => sub {
+ my $self = shift;
+ my $ser = shift;
+ return undef unless defined $ser;
+ my $h = new IO::String($ser);
+ return RABX::wire_rd($h);
+ },
+ filter_to_storage => sub {
+ my $self = shift;
+ my $data = shift;
+ my $ser = '';
+ my $h = new IO::String($ser);
+ RABX::wire_wr( $data, $h );
+ return $ser;
+ },
+ }
+);
+
use DateTime::TimeZone;
use Image::Size;
use Moose;
diff --git a/perllib/FixMyStreet/DB/ResultSet/AlertType.pm b/perllib/FixMyStreet/DB/ResultSet/AlertType.pm
index c1a5d65c9..32654e534 100644
--- a/perllib/FixMyStreet/DB/ResultSet/AlertType.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/AlertType.pm
@@ -9,6 +9,8 @@ use mySociety::EmailUtil;
use mySociety::Gaze;
use mySociety::Locale;
use mySociety::MaPit;
+use IO::String;
+use RABX;
# Child must have confirmed, id, email, state(!) columns
# If parent/child, child table must also have name and text
@@ -82,12 +84,21 @@ sub email_alerts ($) {
}
my $url = $cobrand->base_url_for_emails( $row->{alert_cobrand_data} );
+ # this is currently only for new_updates
if ($row->{item_text}) {
$data{problem_url} = $url . "/report/" . $row->{id};
$data{data} .= $row->{item_name} . ' : ' if $row->{item_name} && !$row->{item_anonymous};
$data{data} .= $row->{item_text} . "\n\n------\n\n";
+ # this is ward and council problems
} else {
- $data{data} .= $url . "/report/" . $row->{id} . " - $row->{title}\n\n";
+ my $postcode = $cobrand->format_postcode( $row->{postcode} );
+ $postcode = ", $postcode" if $postcode;
+ $data{data} .= $url . "/report/" . $row->{id} . " - $row->{title}$postcode\n\n";
+ if ( exists $row->{geocode} && $row->{geocode} && $ref =~ /ward|council/ ) {
+ my $nearest_st = _get_address_from_gecode( $row->{geocode} );
+ $data{data} .= $nearest_st if $nearest_st;
+ }
+ $data{data} .= "\n\n------\n\n";
}
if (!$data{alert_user_id}) {
%data = (%data, %$row);
@@ -134,7 +145,7 @@ sub email_alerts ($) {
};
my $states = "'" . join( "', '", FixMyStreet::DB::Result::Problem::visible_states() ) . "'";
my %data = ( template => $template, data => '', alert_id => $alert->id, alert_email => $alert->user->email, lang => $alert->lang, cobrand => $alert->cobrand, cobrand_data => $alert->cobrand_data );
- my $q = "select problem.id, problem.title from problem_find_nearby(?, ?, ?) as nearby, problem, users
+ my $q = "select problem.id, problem.postcode, problem.geocode, problem.title from problem_find_nearby(?, ?, ?) as nearby, problem, users
where nearby.problem_id = problem.id
and problem.user_id = users.id
and problem.state in ($states)
@@ -150,7 +161,14 @@ sub email_alerts ($) {
alert_id => $alert->id,
parameter => $row->{id},
} );
- $data{data} .= $url . "/report/" . $row->{id} . " - $row->{title}\n\n";
+ my $postcode = $cobrand->format_postcode( $row->{postcode} );
+ $postcode = ", $postcode" if $postcode;
+ $data{data} .= $url . "/report/" . $row->{id} . " - $row->{title}$postcode\n\n";
+ if ( exists $row->{geocode} && $row->{geocode} ) {
+ my $nearest_st = _get_address_from_gecode( $row->{geocode} );
+ $data{data} .= $nearest_st if $nearest_st;
+ }
+ $data{data} .= "\n\n------\n\n";
}
_send_aggregated_alert_email(%data) if $data{data};
}
@@ -210,4 +228,23 @@ sub _send_aggregated_alert_email(%) {
}
}
+sub _get_address_from_gecode {
+ my $geocode = shift;
+
+ return '' unless defined $geocode;
+ my $h = new IO::String($geocode);
+ my $data = RABX::wire_rd($h);
+
+ my $str = '';
+
+ my $address = $data->{resourceSets}[0]{resources}[0]{address};
+ my @address;
+ push @address, $address->{addressLine} if $address->{addressLine} && $address->{addressLine} ne 'Street';
+ push @address, $address->{locality} if $address->{locality};
+ $str .= sprintf(_("Nearest road to the pin placed on the map (automatically generated by Bing Maps): %s\n\n"),
+ join( ', ', @address ) ) if @address;
+
+ return $str;
+}
+
1;
diff --git a/perllib/FixMyStreet/Map/FMS.pm b/perllib/FixMyStreet/Map/FMS.pm
index d5edac763..24842c861 100644
--- a/perllib/FixMyStreet/Map/FMS.pm
+++ b/perllib/FixMyStreet/Map/FMS.pm
@@ -47,11 +47,13 @@ sub map_tiles {
"http://tilma.mysociety.org/sv/$z/$x/$y.png",
];
} else {
+ my $url = "g=701";
+ $url .= "&productSet=mmOS" if $z > 10;
return [
- "http://ecn.t0.tiles.virtualearth.net/tiles/r" . get_quadkey($x-1, $y-1, $z) . ".png?g=701&productSet=mmOS",
- "http://ecn.t1.tiles.virtualearth.net/tiles/r" . get_quadkey($x, $y-1, $z) . ".png?g=701&productSet=mmOS",
- "http://ecn.t2.tiles.virtualearth.net/tiles/r" . get_quadkey($x-1, $y, $z) . ".png?g=701&productSet=mmOS",
- "http://ecn.t3.tiles.virtualearth.net/tiles/r" . get_quadkey($x, $y, $z) . ".png?g=701&productSet=mmOS",
+ "http://ecn.t0.tiles.virtualearth.net/tiles/r" . get_quadkey($x-1, $y-1, $z) . ".png?$url",
+ "http://ecn.t1.tiles.virtualearth.net/tiles/r" . get_quadkey($x, $y-1, $z) . ".png?$url",
+ "http://ecn.t2.tiles.virtualearth.net/tiles/r" . get_quadkey($x-1, $y, $z) . ".png?$url",
+ "http://ecn.t3.tiles.virtualearth.net/tiles/r" . get_quadkey($x, $y, $z) . ".png?$url",
];
}
}
diff --git a/perllib/Open311.pm b/perllib/Open311.pm
index f3f642895..e26e3e4c6 100644
--- a/perllib/Open311.pm
+++ b/perllib/Open311.pm
@@ -14,6 +14,8 @@ has test_mode => ( is => 'ro', isa => 'Bool' );
has test_uri_used => ( is => 'rw', 'isa' => 'Str' );
has test_get_returns => ( is => 'rw' );
has endpoints => ( is => 'rw', default => sub { { services => 'services.xml', requests => 'requests.xml' } } );
+has debug => ( is => 'ro', isa => 'Bool', default => 0 );
+has debug_details => ( is => 'rw', 'isa' => 'Str', default => '' );
sub get_service_list {
my $self = shift;
@@ -48,12 +50,16 @@ Submitted via FixMyStreet
EOT
;
+ my ( $firstname, $lastname ) = ( $problem->user->name =~ /(\w+)\s+(.+)/ );
+
my $params = {
lat => $problem->latitude,
long => $problem->longitude,
email => $problem->user->email,
description => $description,
service_code => $service_code,
+ first_name => $firstname,
+ last_name => $lastname || '',
};
if ( $problem->user->phone ) {
@@ -89,7 +95,7 @@ EOT
}
}
- warn sprintf( "Failed to submit problem %s over Open311, response\n: %s", $problem->id, $response );
+ warn sprintf( "Failed to submit problem %s over Open311, response\n: %s\n%s", $problem->id, $response, $self->debug_details );
return 0;
}
}
@@ -134,6 +140,8 @@ sub _get {
$uri->path( $uri->path . $path );
$uri->query_form( $params );
+ $self->debug_details( $self->debug_details . "\nrequest:" . $uri->as_string );
+
my $content;
if ( $self->test_mode ) {
$content = $self->test_get_returns->{ $path };
@@ -160,14 +168,20 @@ sub _post {
%{ $params }
];
+ $self->debug_details( $self->debug_details . "\nrequest:" . $req->as_string );
+
my $ua = LWP::UserAgent->new();
my $res = $ua->request( $req );
if ( $res->is_success ) {
return $res->decoded_content;
} else {
- warn "request failed: " . $res->status_line;
- warn $self->_process_error( $res->decoded_content );
+ warn sprintf(
+ "request failed: %s\nerror: %s\n%s",
+ $res->status_line,
+ $self->_process_error( $res->decoded_content ),
+ $self->debug_details
+ );
return 0;
}
}
diff --git a/t/app/controller/alert_new.t b/t/app/controller/alert_new.t
index 580a5ad9a..950666d2d 100644
--- a/t/app/controller/alert_new.t
+++ b/t/app/controller/alert_new.t
@@ -452,8 +452,9 @@ subtest "Test normal alert signups and that alerts are sent" => sub {
$count++ if $_->body =~ /The following updates have been left on this problem:/;
$count++ if $_->body =~ /The following new problems have been reported to City of\s*Edinburgh Council:/;
$count++ if $_->body =~ /The following nearby problems have been added:/;
+ $count++ if $_->body =~ / - Testing, EH1 1BB/;
}
- is $count, 3, 'Three emails with the right things in them';
+ is $count, 5, 'Five emails with the right things in them';
my $email = $emails[0];
like $email->body, qr/Other User/, 'Update name given';
diff --git a/t/app/controller/auth.t b/t/app/controller/auth.t
index fef45ac90..efc5e60e6 100644
--- a/t/app/controller/auth.t
+++ b/t/app/controller/auth.t
@@ -24,16 +24,16 @@ $mech->not_logged_in_ok;
$mech->get_ok('/auth');
for my $test (
- [ '' => 'enter your email' ],
- [ 'not an email' => 'check your email address is correct' ],
- [ 'bob@foo' => 'check your email address is correct' ],
- [ 'bob@foonaoedudnueu.co.uk' => 'check your email address is correct' ],
+ [ '' => 'Please enter your email' ],
+ [ 'not an email' => 'Please check your email address is correct' ],
+ [ 'bob@foo' => 'Please check your email address is correct' ],
+ [ 'bob@foonaoedudnueu.co.uk' => 'Please check your email address is correct' ],
)
{
my ( $email, $error_message ) = @$test;
pass "--- testing bad email '$email' gives error '$error_message'";
$mech->get_ok('/auth');
- $mech->content_lacks($error_message);
+ is_deeply $mech->form_errors, [], 'no errors initially';
$mech->submit_form_ok(
{
form_name => 'general_auth',
@@ -43,7 +43,7 @@ for my $test (
"try to create an account with email '$email'"
);
is $mech->uri->path, '/auth', "still on auth page";
- $mech->content_contains($error_message);
+ is_deeply $mech->form_errors, [ $error_message ], 'no errors initially';
}
# create a new account
diff --git a/t/app/controller/report_new.t b/t/app/controller/report_new.t
index 9ec8fa4a8..15237e041 100644
--- a/t/app/controller/report_new.t
+++ b/t/app/controller/report_new.t
@@ -1,4 +1,5 @@
use strict;
+use utf8; # sign in error message has &ndash; in it
use warnings;
use Test::More;
use utf8;
@@ -428,6 +429,55 @@ foreach my $test (
};
}
+# this test to make sure that we don't see spurious error messages about
+# the name being blank when there is a sign in error
+subtest "test password errors for a user who is signing in as they report" => sub {
+ $mech->log_out_ok;
+ $mech->clear_emails_ok;
+
+ # check that the user does not exist
+ my $test_email = 'test-2@example.com';
+
+ my $user = FixMyStreet::App->model('DB::User')->find_or_create( { email => $test_email } );
+ ok $user, "test user does exist";
+
+ # setup the user.
+ ok $user->update( {
+ name => 'Joe Bloggs',
+ phone => '01234 567 890',
+ password => 'secret2',
+ } ), "set user details";
+
+ # submit initial pc form
+ $mech->get_ok('/around');
+ $mech->submit_form_ok( { with_fields => { pc => 'EH1 1BB', } },
+ "submit location" );
+
+ # click through to the report page
+ $mech->follow_link_ok( { text => 'skip this step', },
+ "follow 'skip this step' link" );
+
+ $mech->submit_form_ok(
+ {
+ button => 'submit_sign_in',
+ with_fields => {
+ title => 'Test Report',
+ detail => 'Test report details.',
+ photo => '',
+ email => 'test-2@example.com',
+ password_sign_in => 'secret1',
+ category => 'Street lighting',
+ }
+ },
+ "submit with wrong password"
+ );
+
+ # check that we got the errors expected
+ is_deeply $mech->form_errors, [
+ 'There was a problem with your email/password combination. Passwords and user accounts are a brand new service, so you probably do not have one yet – please fill in the right hand side of this form to get one.'
+ ], "check there were errors";
+};
+
subtest "test report creation for a user who is signing in as they report" => sub {
$mech->log_out_ok;
$mech->clear_emails_ok;
diff --git a/t/app/controller/rss.t b/t/app/controller/rss.t
new file mode 100644
index 000000000..c6ab20574
--- /dev/null
+++ b/t/app/controller/rss.t
@@ -0,0 +1,125 @@
+use strict;
+use warnings;
+use Test::More;
+
+use FixMyStreet::TestMech;
+
+my $mech = FixMyStreet::TestMech->new;
+
+my $dt = DateTime->new(
+ year => 2011,
+ month => 10,
+ day => 10
+);
+
+my $user1 = FixMyStreet::App->model('DB::User')
+ ->find_or_create( { email => 'reporter@example.com', name => 'Reporter User' } );
+
+my $report = FixMyStreet::App->model('DB::Problem')->find_or_create( {
+ postcode => 'eh1 1BB',
+ council => '2651',
+ areas => ',11808,135007,14419,134935,2651,20728,',
+ category => 'Street lighting',
+ title => 'Testing',
+ detail => 'Testing Detail',
+ used_map => 1,
+ name => $user1->name,
+ anonymous => 0,
+ state => 'confirmed',
+ confirmed => $dt,
+ lastupdate => $dt,
+ whensent => $dt->clone->add( minutes => 5 ),
+ lang => 'en-gb',
+ service => '',
+ cobrand => 'default',
+ cobrand_data => '',
+ send_questionnaire => 1,
+ latitude => '55.951963',
+ longitude => '-3.189944',
+ user_id => $user1->id,
+} );
+
+
+$mech->get_ok("/rss/pc/EH11BB/2");
+$mech->content_contains( "Testing, 10th October, EH1 1BB" );
+$mech->content_lacks( 'Nearest road to the pin' );
+
+$report->geocode(
+{
+ 'traceId' => 'ae7c4880b70b423ebc8ab4d80961b3e9|LTSM001158|02.00.71.1600|LTSMSNVM002010, LTSMSNVM001477',
+ 'statusDescription' => 'OK',
+ 'brandLogoUri' => 'http://dev.virtualearth.net/Branding/logo_powered_by.png',
+ 'resourceSets' => [
+ {
+ 'resources' => [
+ {
+ 'geocodePoints' => [
+ {
+ 'calculationMethod' => 'Interpolation',
+ 'coordinates' => [
+ '55.9532357007265',
+ '-3.18906001746655'
+ ],
+ 'usageTypes' => [
+ 'Display',
+ 'Route'
+ ],
+ 'type' => 'Point'
+ }
+ ],
+ 'entityType' => 'Address',
+ 'name' => '18 N Bridge, Edinburgh EH1 1',
+ 'point' => {
+ 'coordinates' => [
+ '55.9532357007265',
+ '-3.18906001746655'
+ ],
+ 'type' => 'Point'
+ },
+ 'bbox' => [
+ '55.9493729831558',
+ '-3.19825819222605',
+ '55.9570984182972',
+ '-3.17986184270704'
+ ],
+ 'matchCodes' => [
+ 'Good'
+ ],
+ 'address' => {
+ 'countryRegion' => 'United Kingdom',
+ 'adminDistrict2' => 'Edinburgh City',
+ 'adminDistrict' => 'Scotland',
+ 'addressLine' => '18 North Bridge',
+ 'formattedAddress' => '18 N Bridge, Edinburgh EH1 1',
+ 'postalCode' => 'EH1 1',
+ 'locality' => 'Edinburgh'
+ },
+ 'confidence' => 'Medium',
+ '__type' => 'Location:http://schemas.microsoft.com/search/local/ws/rest/v1'
+ }
+ ],
+ 'estimatedTotal' => 1
+ }
+ ],
+ 'copyright' => "Copyright \x{a9} 2011 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.",
+ 'statusCode' => 200,
+ 'authenticationResultCode' => 'ValidCredentials'
+ }
+);
+$report->postcode('eh11bb');
+$report->update();
+
+$mech->get_ok("/rss/pc/EH11BB/2");
+$mech->content_contains( "Testing, 10th October, EH1 1BB" );
+$mech->content_contains( '18 North Bridge, Edinburgh' );
+
+$report->postcode('Princes St, Edinburgh');
+$report->update();
+
+$mech->get_ok("/rss/pc/EH11BB/2");
+$mech->content_contains( "Testing, 10th October, Princes St, Edinburgh" );
+
+$report->delete();
+$user1->delete();
+
+done_testing();
diff --git a/t/app/model/alert_type.t b/t/app/model/alert_type.t
index c7bfe171c..1adc584d7 100644
--- a/t/app/model/alert_type.t
+++ b/t/app/model/alert_type.t
@@ -140,5 +140,214 @@ for my $test (
};
}
+my $now = DateTime->now();
+$report->confirmed( $now->ymd . ' ' . $now->hms );
+$report->update();
+
+my $council_alert = FixMyStreet::App->model('DB::Alert')->find_or_create(
+ {
+ user => $user2,
+ parameter => 2504,
+ parameter2 => 2504,
+ alert_type => 'council_problems',
+ whensubscribed => $dt->ymd . ' ' . $dt->hms,
+ confirmed => 1,
+ }
+);
+
+for my $test (
+ {
+ postcode => 'SW1A 1AA',
+ expected_postcode => 'SW1A 1AA',
+ },
+ {
+ postcode => 'sw1a 1AA',
+ expected_postcode => 'SW1A 1AA',
+ },
+ {
+ postcode => 'SW1A 1aa',
+ expected_postcode => 'SW1A 1AA',
+ },
+ {
+ postcode => 'SW1A1AA',
+ expected_postcode => 'SW1A 1AA',
+ },
+ {
+ postcode => 'Buckingham Gate',
+ expected_postcode => 'Buckingham Gate',
+ },
+ {
+ postcode => 'Buckingham gate',
+ expected_postcode => 'Buckingham gate',
+ },
+) {
+ subtest "correct text for postcode $test->{postcode}" => sub {
+ $mech->clear_emails_ok;
+
+ my $sent = FixMyStreet::App->model('DB::AlertSent')->search(
+ {
+ alert_id => $council_alert->id,
+ parameter => $report->id,
+ }
+ )->delete;
+
+ $report->postcode( $test->{postcode} );
+ $report->update;
+
+ FixMyStreet::App->model('DB::AlertType')->email_alerts();
+
+ $mech->email_count_is( 1 );
+ my $email = $mech->get_email;
+ my $pc = $test->{expected_postcode};
+ my $title = $report->title;
+ my $body = $email->body;
+
+ like $body, qr#report/$report_id - $title, $pc#, 'email contains expected postcode';
+ };
+}
+
+$report->postcode( 'SW1A 1AA' );
+$report->update;
+
+$report->geocode(
+{
+ 'traceId' => 'ae7c4880b70b423ebc8ab4d80961b3e9|LTSM001158|02.00.71.1600|LTSMSNVM002010, LTSMSNVM001477',
+ 'statusDescription' => 'OK',
+ 'brandLogoUri' => 'http://dev.virtualearth.net/Branding/logo_powered_by.png',
+ 'resourceSets' => [
+ {
+ 'resources' => [
+ {
+ 'geocodePoints' => [
+ {
+ 'calculationMethod' => 'Interpolation',
+ 'coordinates' => [
+ '55.9532357007265',
+ '-3.18906001746655'
+ ],
+ 'usageTypes' => [
+ 'Display',
+ 'Route'
+ ],
+ 'type' => 'Point'
+ }
+ ],
+ 'entityType' => 'Address',
+ 'name' => '18 N Bridge, Edinburgh EH1 1',
+ 'point' => {
+ 'coordinates' => [
+ '55.9532357007265',
+ '-3.18906001746655'
+ ],
+ 'type' => 'Point'
+ },
+ 'bbox' => [
+ '55.9493729831558',
+ '-3.19825819222605',
+ '55.9570984182972',
+ '-3.17986184270704'
+ ],
+ 'matchCodes' => [
+ 'Good'
+ ],
+ 'address' => {
+ 'countryRegion' => 'United Kingdom',
+ 'adminDistrict2' => 'Edinburgh City',
+ 'adminDistrict' => 'Scotland',
+ 'addressLine' => '18 North Bridge',
+ 'formattedAddress' => '18 N Bridge, Edinburgh EH1 1',
+ 'postalCode' => 'EH1 1',
+ 'locality' => 'Edinburgh'
+ },
+ 'confidence' => 'Medium',
+ '__type' => 'Location:http://schemas.microsoft.com/search/local/ws/rest/v1'
+ }
+ ],
+ 'estimatedTotal' => 1
+ }
+ ],
+ 'copyright' => "Copyright \x{a9} 2011 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.",
+ 'statusCode' => 200,
+ 'authenticationResultCode' => 'ValidCredentials'
+ }
+);
+$report->update();
+
+foreach my $test (
+ {
+ desc => 'all fields',
+ addressLine => '18 North Bridge',
+ locality => 'Edinburgh',
+ nearest => qr/18 North Bridge, Edinburgh/,
+ },
+ {
+ desc => 'address with Street only',
+ addressLine => 'Street',
+ locality => 'Edinburgh',
+ nearest => qr/: Edinburgh/,
+ },
+ {
+ desc => 'locality only',
+ addressLine => undef,
+ locality => 'Edinburgh',
+ nearest => qr/: Edinburgh/,
+ },
+ {
+ desc => 'address only',
+ addressLine => '18 North Bridge',
+ locality => undef,
+ nearest => qr/: 18 North Bridge\n/,
+ },
+ {
+ desc => 'no fields',
+ addressLine => undef,
+ locality => undef,
+ nearest => '',
+ },
+ {
+ desc => 'no address',
+ no_address => 1,
+ nearest => '',
+ },
+) {
+ subtest "correct Nearest Road text with $test->{desc}" => sub {
+ $mech->clear_emails_ok;
+
+ my $sent = FixMyStreet::App->model('DB::AlertSent')->search(
+ {
+ alert_id => $council_alert->id,
+ parameter => $report->id,
+ }
+ )->delete;
+
+ my $g = $report->geocode;
+ if ( $test->{no_address} ) {
+ $g->{resourceSets}[0]{resources}[0]{address} = undef;
+ } else {
+ $g->{resourceSets}[0]{resources}[0]{address}->{addressLine} = $test->{addressLine};
+ $g->{resourceSets}[0]{resources}[0]{address}->{locality} = $test->{locality};
+ }
+
+ # if we don't do this then it ignores the change
+ $report->geocode( undef );
+ $report->geocode( $g );
+ $report->update();
+
+ FixMyStreet::App->model('DB::AlertType')->email_alerts();
+
+ $mech->email_count_is( 1 );
+ my $email = $mech->get_email;
+ my $body = $email->body;
+
+ if ( $test->{nearest} ) {
+ like $body, $test->{nearest}, 'correct nearest line';
+ } else {
+ unlike $body, qr/Nearest Road/, 'no nearest line';
+ }
+ };
+}
+
+$report->comments->delete();
+$report->delete();
done_testing();
diff --git a/t/cobrand/closest.t b/t/cobrand/closest.t
new file mode 100644
index 000000000..c7ba10cc4
--- /dev/null
+++ b/t/cobrand/closest.t
@@ -0,0 +1,78 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+use mySociety::Locale;
+use FixMyStreet::App;
+
+use_ok 'FixMyStreet::Cobrand';
+
+mySociety::Locale::gettext_domain( 'FixMyStreet' );
+
+my $c = FixMyStreet::Cobrand::Default->new();
+
+my $user =
+ FixMyStreet::App->model('DB::User')
+ ->find_or_create( { email => 'test@example.com', name => 'Test User' } );
+ok $user, "created test user";
+
+my $dt = DateTime->new(
+ year => 2011,
+ month => 04,
+ day => 16,
+ hour => 15,
+ minute => 47,
+ second => 23
+);
+
+my $report = FixMyStreet::App->model('DB::Problem')->find_or_create(
+ {
+ postcode => 'SW1A 1AA',
+ council => '2504',
+ areas => ',105255,11806,11828,2247,2504,',
+ category => 'Other',
+ title => 'Test 2',
+ detail => 'Test 2 Detail',
+ used_map => 't',
+ name => 'Test User',
+ anonymous => 'f',
+ state => 'closed',
+ confirmed => $dt->ymd . ' ' . $dt->hms,
+ lang => 'en-gb',
+ service => '',
+ cobrand => 'default',
+ cobrand_data => '',
+ send_questionnaire => 't',
+ latitude => '51.5016605453401',
+ longitude => '-0.142497580865087',
+ user_id => $user->id,
+ }
+);
+my $report_id = $report->id;
+ok $report, "created test report - $report_id";
+
+$report->geocode( undef );
+
+ok !$report->geocode, 'no gecode entry for report';
+
+my $near = $c->find_closest( $report->latitude, $report->longitude, $report );
+
+ok $report->geocode, 'geocode entry added to report';
+ok $report->geocode->{resourceSets}, 'geocode entry looks like right sort of thing';
+
+like $near, qr/Constitution Hill/i, 'nearest street looks right';
+like $near, qr/Nearest postcode .*: SW1A 1AA/i, 'nearest postcode looks right';
+
+$near = $c->find_closest_address_for_rss( $report->latitude, $report->longitude, $report );
+
+like $near, qr/Constitution Hill/i, 'nearest street for RSS looks right';
+unlike $near, qr/Nearest postcode/i, 'no nearest postcode in RSS text';
+
+$report->geocode( undef );
+$near = $c->find_closest_address_for_rss( $report->latitude, $report->longitude, $report );
+
+ok !$near, 'no closest address for RSS if not cached';
+
+# all done
+done_testing();
diff --git a/t/open311.t b/t/open311.t
index f7a8cd815..30de330b6 100644
--- a/t/open311.t
+++ b/t/open311.t
@@ -2,7 +2,9 @@
use strict;
use warnings;
-use Test::More tests => 4;
+use Test::More;
+use Test::Warn;
+use FixMyStreet::App;
use FindBin;
use lib "$FindBin::Bin/../perllib";
@@ -21,4 +23,20 @@ EOT
is $o->_process_error( $err_text ), "400: Service Code cannot be null -- can't proceed with the request.\n", 'error text parsing';
is $o->_process_error( '503 - service unavailable' ), 'unknown error', 'error text parsing of bad error';
+my $o2 = Open311->new( endpoint => 'http://192.168.50.1/open311/', jurisdiction => 'example.org' );
+my $u = FixMyStreet::App->model('DB::User')->new( { email => 'test@example.org', name => 'A User' } );
+
+my $p = FixMyStreet::App->model('DB::Problem')->new( {
+ latitude => 1,
+ longitude => 1,
+ title => 'title',
+ detail => 'detail',
+ user => $u,
+} );
+
+my $expected_error = qr{.*request failed: 500 Can.t connect to 192.168.50.1:80 \([^)]*\).*};
+
+warning_like {$o2->send_service_request( $p, { url => 'http://example.com/' }, 1 )} $expected_error, 'warning generated on failed call';
+
+done_testing();
diff --git a/templates/web/default/admin/list_updates.html b/templates/web/default/admin/list_updates.html
index 4f27b9595..0a05ba4ac 100644
--- a/templates/web/default/admin/list_updates.html
+++ b/templates/web/default/admin/list_updates.html
@@ -15,8 +15,8 @@
<th>*</th>
</tr>
[% FOREACH update IN updates -%]
- <tr[% ' class="hidden"' IF update.state == 'hidden' || ( problem.state && problem.state == 'hidden' ) %]>
- <td>[%- IF update.state == 'confirmed' -%]
+ <tr[% ' class="hidden"' IF update.state == 'hidden' || update.problem.state == 'hidden' %]>
+ <td>[%- IF update.state == 'confirmed' && update.problem.state != 'hidden' -%]
[%- cobrand_data = update.cobrand_data %]
[%- cobrand_data = c.data_for_generic_update IF !update.cobrand %]
<a href="[% c.uri_for_email( '/report', update.problem.id, cobrand_data ) %]#update_[% update.id %]">[% update.id %]</a>
diff --git a/templates/web/default/admin/report_edit.html b/templates/web/default/admin/report_edit.html
index 470ad311a..79207192d 100644
--- a/templates/web/default/admin/report_edit.html
+++ b/templates/web/default/admin/report_edit.html
@@ -43,6 +43,11 @@
[% IF problem.photo %]
[% photo = problem.get_photo_params %]
<li><img alt="" height="[% photo.height %]" width="[% photo.width %]" src="[% c.cobrand.base_url %][% photo.url %]">
+<br>
+[% IF rotated %]Photo may be cached. View image directly to check<br>[% END %]
+<input type="submit" name="rotate_photo" value="Rotate Left" />
+<input type="submit" name="rotate_photo" value="Rotate Right" />
+<br>
<input type="checkbox" id="remove_photo" name="remove_photo" value="1">
<label for="remove_photo">[% loc("Remove photo (can't be undone!)") %]</label></li>
[% END %]
diff --git a/templates/web/default/around/display_location.html b/templates/web/default/around/display_location.html
index f091de0f2..3d9c82187 100755
--- a/templates/web/default/around/display_location.html
+++ b/templates/web/default/around/display_location.html
@@ -35,7 +35,7 @@
robots => 'noindex,nofollow';
%]
-<form action="[% c.uri_for('/report/new') %]" method="post" name="mapForm" id="mapForm" enctype="multipart/form-data">
+<form action="[% c.uri_for('/report/new') %]" method="post" name="mapForm" id="mapForm" enctype="multipart/form-data" class="validate">
[% IF c.req.params.map_override %]
<input type="hidden" name="map_override" value="[% c.req.params.map_override | html %]">
[% END %]
diff --git a/templates/web/default/auth/general.html b/templates/web/default/auth/general.html
index 6c9d4497a..5407e56e1 100644
--- a/templates/web/default/auth/general.html
+++ b/templates/web/default/auth/general.html
@@ -2,7 +2,7 @@
<h1>[% loc('Sign in') %]</h1>
-<form action="[% c.uri_for() %]" method="post" name="general_auth">
+<form action="[% c.uri_for() %]" method="post" name="general_auth" class="validate">
<input type="hidden" name="r" value="[% c.req.params.r | html %]">
[% IF email_error;
@@ -25,7 +25,7 @@
<div class="form-field">
<label class="n" for="email">[% loc('Your email address:') %]</label>
- <input type="email" size="30" id="email" name="email" value="[% email | html %]">
+ <input type="email" class="required email" size="30" id="email" name="email" value="[% email | html %]">
</div>
<div id="form_sign_in">
diff --git a/templates/web/default/common_header_tags.html b/templates/web/default/common_header_tags.html
index e6278847d..95b59d9dd 100644
--- a/templates/web/default/common_header_tags.html
+++ b/templates/web/default/common_header_tags.html
@@ -1,5 +1,10 @@
+[% INCLUDE 'js_validation_msgs.html' %]
+
<meta http-equiv="content-type" content="text/html; charset=utf-8">
-<script type="text/javascript" src="/jslib/jquery-1.6.2.min.js"></script>
+<script type="text/javascript" src="/jslib/jquery-1.7.0.min.js"></script>
+
+<script src="[% version('/js/jquery.validate.min.js') %]" type="text/javascript" charset="utf-8"></script>
+
<script type="text/javascript" src="[% version('/js/geo.min.js') %]"></script>
<script type="text/javascript" src="[% version('/js/fixmystreet.js') %]"></script>
diff --git a/templates/web/default/contact/index.html b/templates/web/default/contact/index.html
index dc64dd554..2c0c2ff02 100644
--- a/templates/web/default/contact/index.html
+++ b/templates/web/default/contact/index.html
@@ -5,7 +5,7 @@
<h1>[% loc('Contact the team') %]</h1>
-<form method="post" action="/contact/submit">
+<form method="post" action="/contact/submit" class="validate">
[% INCLUDE 'errors.html' %]
@@ -66,7 +66,7 @@
[% END %]
<div class="form-field">
<label for="form_name">[% loc('Your name:') %]</label>
-<input type="text" name="name" id="form_name" value="[% form_name | html %]" size="30"></div>
+<input type="text" class="required" name="name" id="form_name" value="[% form_name | html %]" size="30"></div>
[% IF field_errors.em %]
@@ -74,14 +74,14 @@
[% END %]
<div class="form-field">
<label for="form_email">[% loc('Your&nbsp;email:') %]</label>
-<input type="text" name="em" id="form_email" value="[% em | html %]" size="30"></div>
+<input type="text" class="required email" name="em" id="form_email" value="[% em | html %]" size="30"></div>
[% IF field_errors.subject %]
<div class="form-error">[% field_errors.subject %]</div>
[% END %]
<div class="form-field">
<label for="form_subject">[% loc('Subject:') %]</label>
-<input type="text" name="subject" id="form_subject" value="[% subject | html %]" size="30"></div>
+<input type="text" class="required" name="subject" id="form_subject" value="[% subject | html %]" size="30"></div>
[% IF field_errors.message %]
<div class="form-error">[% field_errors.message %]</div>
@@ -89,7 +89,7 @@
<div class="form-field">
<label for="form_message">[% loc('Message:') %]</label>
-<textarea name="message" id="form_message" rows="7" cols="50">[% message | html %]</textarea></div>
+<textarea class="required" name="message" id="form_message" rows="7" cols="50">[% message | html %]</textarea></div>
<div class="checkbox"><input type="submit" value="[% loc('Post') %]"></div>
[% c.cobrand.form_elements('contactForm') %]
diff --git a/templates/web/default/header.html b/templates/web/default/header.html
index f54793fe4..e3c364363 100644
--- a/templates/web/default/header.html
+++ b/templates/web/default/header.html
@@ -22,6 +22,7 @@
[% END %]
[% INCLUDE 'tracking_code.html' %]
+
</head>
<body>
diff --git a/templates/web/default/js_validation_msgs.html b/templates/web/default/js_validation_msgs.html
new file mode 100644
index 000000000..2466ce175
--- /dev/null
+++ b/templates/web/default/js_validation_msgs.html
@@ -0,0 +1,20 @@
+<script type="text/javascript">
+ validation_strings = {
+ update: '[% loc('Please enter a message') %]',
+ title: '[% loc('Please enter a subject') %]',
+ detail: '[% loc('Please enter some details') %]',
+ name: {
+ required: '[% loc('Please enter your name') %]',
+ validName: '[% loc('Please enter your full name, councils need this information - if you do not wish your name to be shown on the site, untick the box') %]',
+ },
+ category: '[% loc('Please choose a category') %]',
+ rznvy: {
+ required: '[% loc('Please enter your email') %]',
+ email: '[% loc('Please enter a valid email') %]',
+ },
+ email: {
+ required: '[% loc('Please enter your email') %]',
+ email: '[% loc('Please enter a valid email') %]',
+ }
+ };
+</script>
diff --git a/templates/web/default/report/display.html b/templates/web/default/report/display.html
index aeff140fe..1e320e1b4 100644
--- a/templates/web/default/report/display.html
+++ b/templates/web/default/report/display.html
@@ -65,7 +65,7 @@
[% INCLUDE 'errors.html' %]
- <form method="post" action="[% c.uri_for( '/report/update' ) %]" name="updateForm" class="fieldset"[% IF c.cobrand.allow_photo_upload %] enctype="multipart/form-data"[% END %]>
+ <form method="post" action="[% c.uri_for( '/report/update' ) %]" name="updateForm" class="fieldset validate"[% IF c.cobrand.allow_photo_upload %] enctype="multipart/form-data"[% END %]>
<input type="hidden" name="submit_update" value="1">
<input type="hidden" name="id" value="[% problem.id | html %]">
@@ -75,7 +75,7 @@
[% END %]
<div class="form-field">
<label for="form_update">[% loc( 'Update:' ) %]</label>
- <textarea name="update" id="form_update" rows="7" cols="30">[% update.text | html %]</textarea>
+ <textarea name="update" id="form_update" rows="7" cols="30" required>[% update.text | html %]</textarea>
</div>
[% IF c.user && c.user.belongs_to_council( problem.council ) %]
@@ -132,7 +132,7 @@
[% END %]
<div class="form-field">
<label for="form_rznvy">[% loc('Your email:' ) %]</label>
- <input type="email" name="rznvy" id="form_rznvy" value="[% update.user.email | html %]" size="30">
+ <input type="email" name="rznvy" id="form_rznvy" value="[% update.user.email | html %]" size="30" required>
</div>
<div id="form_sign_in">
@@ -159,7 +159,7 @@
</p>
<p>
- <input type="submit" name="submit_sign_in" value="[% loc('Post') %]">
+ <input type="submit" name="submit_sign_in" id="submit_sign_in" value="[% loc('Post') %]">
</p>
</div>
@@ -181,7 +181,7 @@
<p style="clear:both"><small>[% loc('Providing a password is optional, but doing so will allow you to more easily report problems, leave updates and manage your reports.') %]</small></p>
<p>
- <input type="submit" name="submit_register" value="[% loc('Post') %]">
+ <input type="submit" name="submit_register" id="submit_register" value="[% loc('Post') %]">
</p>
</div>
diff --git a/templates/web/default/report/new/category.html b/templates/web/default/report/new/category.html
index 6ec0eb19f..095cd7c2e 100644
--- a/templates/web/default/report/new/category.html
+++ b/templates/web/default/report/new/category.html
@@ -1,7 +1,7 @@
[% FILTER collapse %]
[% IF category_options.size %]
<label for='form_category'>[% category_label | html %]</label>
- <select name='category' id='form_category'>
+ <select name='category' id='form_category'[% ' onchange="form_category_onchange()"' IF category_extras.size %]>
[% FOREACH cat_op IN category_options %]
<option value='[% cat_op | html %]'[% ' selected' IF report.category == cat_op %]>[% cat_op | html %]</option>
[% END %]
diff --git a/templates/web/default/report/new/category_extras.html b/templates/web/default/report/new/category_extras.html
index 479b9f46c..7be69e30c 100644
--- a/templates/web/default/report/new/category_extras.html
+++ b/templates/web/default/report/new/category_extras.html
@@ -12,13 +12,13 @@
<div class="form-field">
<label for="form_[% meta_name %]">[% meta.description _ ':' %]</label>
[% IF meta.exists('values') %]
- <select name="[% meta_name %]" id="form_[% meta_name %]">
+ <select name="[% meta_name %]" id="form_[% meta_name %]"[% meta.required == 'true' ? ' required' : '' %]>
[% FOR option IN meta.values.value.keys %]
<option value="[% meta.values.value.$option.key %]">[% option %]</option>
[% END %]
</select>
[% ELSE %]
- <input type="text" value="[% report_meta.$meta_name | html %]" name="[% meta_name %]" id="form_[% meta_name %]">
+ <input type="text" value="[% report_meta.$meta_name | html %]" name="[% meta_name %]" id="form_[% meta_name %]"[% meta.required == 'true' ? ' required' : '' %]>
[% END %]
</div>
[%- END %]
diff --git a/templates/web/default/report/new/fill_in_details.html b/templates/web/default/report/new/fill_in_details.html
index a9a113283..701a9bafa 100644
--- a/templates/web/default/report/new/fill_in_details.html
+++ b/templates/web/default/report/new/fill_in_details.html
@@ -4,14 +4,14 @@
%]
[% IF report.used_map %]
-<form action="[% c.uri_for('/report/new') %]" method="post" name="mapForm" id="mapForm"[% IF c.cobrand.allow_photo_upload %] enctype="multipart/form-data"[% END %]>
+<form action="[% c.uri_for('/report/new') %]" method="post" name="mapForm" id="mapForm"[% IF c.cobrand.allow_photo_upload %] enctype="multipart/form-data"[% END %] class="validate">
[% IF c.req.params.map_override %]
<input type="hidden" name="map_override" value="[% c.req.params.map_override | html %]">
[% END %]
<input type="hidden" name="pc" value="[% pc | html %]">
[% c.cobrand.form_elements('mapForm') %]
[% ELSE %]
-<form action="[% c.uri_for('/report/new') %]" method="post" name="mapSkippedForm"[% IF c.cobrand.allow_photo_upload %] enctype="multipart/form-data"[% END %]>
+<form action="[% c.uri_for('/report/new') %]" method="post" name="mapSkippedForm"[% IF c.cobrand.allow_photo_upload %] enctype="multipart/form-data"[% END %] class="validate">
<input type="hidden" name="pc" value="[% pc | html %]">
<input type="hidden" name="skipped" value="1">
[% c.cobrand.form_elements('mapSkippedForm') %]
diff --git a/templates/web/default/report/new/fill_in_details_form.html b/templates/web/default/report/new/fill_in_details_form.html
index e2d8cb3ec..b6b77c75a 100644
--- a/templates/web/default/report/new/fill_in_details_form.html
+++ b/templates/web/default/report/new/fill_in_details_form.html
@@ -50,7 +50,7 @@
<div class="form-field">
<label for="form_title">[% loc('Subject:') %]</label>
- <input type="text" value="[% report.title | html %]" name="title" id="form_title" size="25">
+ <input type="text" value="[% report.title | html %]" name="title" id="form_title" size="25" required>
</div>
[% IF field_errors.detail %]
@@ -59,13 +59,13 @@
<div class="form-field">
<label for="form_detail">[% loc('Details:') %]</label>
- <textarea name="detail" id="form_detail" rows="7" cols="26">[% report.detail | html %]</textarea>
+ <textarea name="detail" id="form_detail" rows="7" cols="26" required>[% report.detail | html %]</textarea>
</div>
[% IF js %]
<div class="form-field" id="form_category_row">
<label for="form_category">[% loc('Category:') %]</label>
- <select name="category" id="form_category"><option>[% loc('Loading...') %]</option></select>
+ <select name="category" id="form_category" required><option>[% loc('Loading...') %]</option></select>
</div>
[% ELSE %]
[% IF category_options.size %]
@@ -122,7 +122,7 @@
<div class="form-field">
<label for="form_email">[% loc('Your email:') %]</label>
- <input type="email" value="[% report.user.email | html %]" name="email" id="form_email" size="25">
+ <input type="email" value="[% report.user.email | html %]" name="email" id="form_email" size="25" required>
</div>
[% INCLUDE 'report/new/notes.html' %]
@@ -136,10 +136,10 @@
<div class='form-error'>[% field_errors.password %]</div>
[% END %]
- <p>
+ <div class="form-field">
<label class="n" for="password_sign_in">[% loc('<strong>Yes</strong>, I have a password:') %]</label>
<input type="password" name="password_sign_in" id="password_sign_in" value="" size="25">
- </p>
+ </div>
<div class="fieldset">
@@ -151,7 +151,7 @@
</p>
<p>
- <input type="submit" name="submit_sign_in" value="[% loc('Submit') %]">
+ <input type="submit" id="submit_sign_in" name="submit_sign_in" value="[% loc('Submit') %]">
</p>
</div>
@@ -173,7 +173,7 @@
<p style="clear:both"><small>[% loc('Providing a password is optional, but doing so will allow you to more easily report problems, leave updates and manage your reports.') %]</small></p>
<p>
- <input type="submit" name="submit_register" value="[% loc('Submit') %]">
+ <input type="submit" id="submit_register" name="submit_register" value="[% loc('Submit') %]">
</p>
</div>
@@ -201,7 +201,7 @@
<div class="form-field">
<label for="form_name">[% loc('Your name:') %]</label>
- <input type="text" value="[% report.name | html %]" name="name" id="form_name" size="25">
+ <input type="text" class="validName" value="[% report.name | html %]" name="name" id="form_name" size="25">
</div>
<div class="checkbox">
diff --git a/templates/web/default/report/updates.html b/templates/web/default/report/updates.html
index 803ed197e..4fd3c75d4 100644
--- a/templates/web/default/report/updates.html
+++ b/templates/web/default/report/updates.html
@@ -18,18 +18,18 @@
[%- ", " _ tprintf(loc( 'marked as %s' ), update.meta_problem_state) IF update.problem_state %]
</em></p>
- [% IF c.cobrand.allow_update_reporting %]
- <p>
- <a rel="nofollow" class="unsuitable-problem" href="[% c.uri_for( '/contact?id=' _ update.problem_id _ ';update_id' _ update.id ) %]">[% loc('Offensive? Unsuitable? Tell us') %]</a>
- </p>
- [% END %]
-
</div>
<div class="update-text">
[% add_links( update.text ) | html_para %]
[% INCLUDE 'report/photo.html' object=update %]
+
+ [% IF c.cobrand.allow_update_reporting %]
+ <p align="right">
+ <small><a rel="nofollow" class="unsuitable-problem" href="[% c.uri_for( '/contact', { id => update.problem_id, update_id => update.id } ) %]">[% loc('Offensive? Unsuitable? Tell us') %]</a></small>
+ </p>
+ [% END %]
</div>
</div>
[% '</div>' IF loop.last %]
diff --git a/templates/web/emptyhomes/header.html b/templates/web/emptyhomes/header.html
index 7f4106f85..d7fbcb6af 100644
--- a/templates/web/emptyhomes/header.html
+++ b/templates/web/emptyhomes/header.html
@@ -6,7 +6,7 @@
<head>
<link rel="stylesheet" type="text/css" href="[% version('/css/core.css') %]">
- <link rel="stylesheet" type="text/css" href="/cobrands/emptyhomes/css.css">
+ <link rel="stylesheet" type="text/css" href="[% version('/cobrands/emptyhomes/css.css') %]">
[% INCLUDE 'common_header_tags.html' %]
</head>
diff --git a/templates/web/emptyhomes/index.html b/templates/web/emptyhomes/index.html
index 34cb0a1c0..7c4d6881b 100644
--- a/templates/web/emptyhomes/index.html
+++ b/templates/web/emptyhomes/index.html
@@ -1,5 +1,27 @@
[% INCLUDE 'header.html', title => '' %]
+[% IF c.req.uri.host == 'reportemptyhomes.com' or c.req.uri.host == 'emptyhomes.matthew.fixmystreet.com' %]
+
+<h2>Channel 4: The Great British Property Scandal</h2>
+
+<div class="video"><object id="flashObj" width="480" height="270" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,47,0"><param name="movie" value="http://c.brightcove.com/services/viewer/federated_f9?isVid=1&isUI=1" /><param name="bgcolor" value="#FFFFFF" /><param name="flashVars" value="@videoPlayer=1293919404001&playerID=1242807532001&playerKey=AQ~~,AAABIWs5YNk~,K8Yb_Dc0PlMA8gCUiCBbnEcXR1bU7HRm&domain=embed&dynamicStreaming=true" /><param name="base" value="http://admin.brightcove.com" /><param name="seamlesstabbing" value="false" /><param name="allowFullScreen" value="true" /><param name="swLiveConnect" value="true" /><param name="allowScriptAccess" value="always" /><embed src="http://c.brightcove.com/services/viewer/federated_f9?isVid=1&isUI=1" bgcolor="#FFFFFF" flashVars="@videoPlayer=1293919404001&playerID=1242807532001&playerKey=AQ~~,AAABIWs5YNk~,K8Yb_Dc0PlMA8gCUiCBbnEcXR1bU7HRm&domain=embed&dynamicStreaming=true" base="http://admin.brightcove.com" name="flashObj" width="480" height="270" seamlesstabbing="false" type="application/x-shockwave-flash" allowFullScreen="true" allowScriptAccess="always" swLiveConnect="true" pluginspage="http://www.macromedia.com/shockwave/download/index.cgi?P1_Prod_Version=ShockwaveFlash"></embed></object></div>
+
+<p>We&#8217;re really excited to be offical advisors to the forthcoming new
+empty homes TV series<br><strong>The Great British Property Scandal</strong>!</p>
+<p>The series will highlight the nearly two million British families who don’t
+have adequate housing, and the million-odd homes lying empty across the UK.</p>
+
+<ul class="channel4">
+
+<li><a href="http://www.channel4.com/programmes/the-great-british-property-scandal/articles/report-an-empty">Report an empty property</a></li>
+<li><a href="http://cy.reportemptyhomes.com/">Rhoi gwybod am eiddo gwag</a></li>
+
+<li><a href="http://itunes.apple.com/gb/app/empty-homes-spotter/id482550587?mt=8"><img src="/i/appstore.png" hspace="5" alt="" align="right">Download the iPhone app</a> from the App Store.</li>
+
+</ul>
+
+[% ELSE %]
+
[% IF error %]
<p class="error">[% error %]</p>
[% END %]
@@ -37,4 +59,6 @@
</div>
+[% END %]
+
[% INCLUDE 'footer.html' %]
diff --git a/templates/web/reading/footer.html b/templates/web/reading/footer.html
index 2d58eb0c7..ec523aa97 100644
--- a/templates/web/reading/footer.html
+++ b/templates/web/reading/footer.html
@@ -28,5 +28,20 @@
<div class="clear"></div>
</div>
</div>
+
+<!-- START OF eVisitAnalyst CODE -->
+<script language="JavaScript" type="text/javascript">
+var ID_section_15243615 = "";//Place section here.
+var ID_tt_15243615 = "t";
+var ID_uid_15243615 = "23137";
+</script>
+<script src="http://extra.evisitanalyst.com/eva.js" type="text/javascript"></script>
+<script src="http://extra.evisitanalyst.com/tag/evatrackclick.js" type="text/javascript"></script>
+<noscript>
+ <img src="http://extra.evisitanalyst.com/eva51/collect/?userid=23137&tt=t"
+ border="0" alt="eVisit" width="1" height="1">
+</noscript>
+<!-- END OF eVisitAnalyst CODE -->
+
</body>
</html>
diff --git a/templates/web/reading/reports/cobrand_stats.html b/templates/web/reading/reports/cobrand_stats.html
index becb724cf..80976c3a6 100644
--- a/templates/web/reading/reports/cobrand_stats.html
+++ b/templates/web/reading/reports/cobrand_stats.html
@@ -1,4 +1,5 @@
<ul>
- <li>Reports submitted via <a href="[% uri_for('/') %]">reading.fixmystreet.com</a>: [% cobrand_count %]</li>
- <li>Reports submitted via <a href="http://www.fixmystreet.com/">www.fixmystreet.com</a>: [% total_count %]</li>
+ <li>Reports submitted via <a href="[% uri_for('/') %]">reading.fixmystreet.com</a>: [% stats.cobrand %]</li>
+ <li>Reports submitted via <a href="http://www.fixmystreet.com/">www.fixmystreet.com</a>: [% stats.main_site %]<br /><br />
+ Statistics date from launch of Reading FixMyStreet.</li>
</ul>
diff --git a/templates/web/southampton/report/photo.html b/templates/web/southampton/report/photo.html
new file mode 100644
index 000000000..07b6a8558
--- /dev/null
+++ b/templates/web/southampton/report/photo.html
@@ -0,0 +1,6 @@
+[% IF c.cobrand.allow_photo_display && object.photo %]
+[% photo = object.get_photo_params %]
+<p>
+ <img alt="" height="[% photo.height %]" width="[% photo.width %]" src="[% photo.url %]">
+</p>
+[% END %]
diff --git a/web/cobrands/emptyhomes/css.css b/web/cobrands/emptyhomes/css.css
index e59497880..ba761814a 100644
--- a/web/cobrands/emptyhomes/css.css
+++ b/web/cobrands/emptyhomes/css.css
@@ -223,3 +223,14 @@ a:hover, a:active {
margin-bottom: 1em;
}
+.video {
+ float: right;
+ margin-left: 35px;
+}
+
+.channel4 {
+ font-size: 125%;
+}
+.channel4 > li:first-child + li {
+ margin-bottom: 1em;
+}
diff --git a/web/cobrands/southampton/css.scss b/web/cobrands/southampton/css.scss
index 0760c982c..3bc2f1b54 100644
--- a/web/cobrands/southampton/css.scss
+++ b/web/cobrands/southampton/css.scss
@@ -8,6 +8,7 @@ $darker: #768EB5;
#map_box {
width: $map_width + 2px;
+ margin-bottom: 10px;
}
#map, #drag {
width: $map_width;
@@ -60,4 +61,7 @@ $darker: #768EB5;
padding: 0.5em;
}
+ #update_form {
+ clear: right;
+ }
}
diff --git a/web/css/_main.scss b/web/css/_main.scss
index 8d3b00418..305a9e43e 100644
--- a/web/css/_main.scss
+++ b/web/css/_main.scss
@@ -98,7 +98,7 @@ select, input, textarea {
#meta {
list-style-type: none;
- margin: 0.25em 0 0 1em;
+ margin: 30px 0 0.5em 1em; /* (was 0.25em 0 0 1em) forced to drop below promo (fix before the freeze) image */
padding: 0;
font-size: 0.875em;
li {
diff --git a/web/css/core.scss b/web/css/core.scss
index c1856a2c5..675471b40 100644
--- a/web/css/core.scss
+++ b/web/css/core.scss
@@ -45,11 +45,22 @@ $map_width: 500px;
color: #cc0000;
margin: 5px 1em 5px 1em;
padding: 2px 5px 2px 5px;
- float: left;
- background-color: #ffeeee;
text-align: left;
}
+ div.label-valid {
+ background-color: white;
+ }
+
+ div.label-valid-hidden {
+ display: none;
+ visibility: hidden;
+ height: 0px;
+ width: 0px;
+ margin: 0px;
+ padding: 0px;
+ }
+
div.form-field {
clear: both;
}
diff --git a/web/i/appstore.png b/web/i/appstore.png
new file mode 100644
index 000000000..3e97c3688
--- /dev/null
+++ b/web/i/appstore.png
Binary files differ
diff --git a/web/js/OpenLayers.fixmystreet.js b/web/js/OpenLayers.fixmystreet.js
index 9616df8b6..eab691def 100644
--- a/web/js/OpenLayers.fixmystreet.js
+++ b/web/js/OpenLayers.fixmystreet.js
@@ -1100,7 +1100,15 @@ return true;}
return false;},deactivate:function(){var deactivated=OpenLayers.Strategy.prototype.deactivate.call(this);if(deactivated){this.layer.events.un({"refresh":this.load,"visibilitychanged":this.load,scope:this});}
return deactivated;},load:function(options){var layer=this.layer;layer.events.triggerEvent("loadstart");layer.protocol.read(OpenLayers.Util.applyDefaults({callback:OpenLayers.Function.bind(this.merge,this,layer.map.getProjectionObject()),filter:layer.filter},options));layer.events.un({"visibilitychanged":this.load,scope:this});},merge:function(mapProjection,resp){var layer=this.layer;layer.destroyFeatures();var features=resp.features;if(features&&features.length>0){if(!mapProjection.equals(layer.projection)){var geom;for(var i=0,len=features.length;i<len;++i){geom=features[i].geometry;if(geom){geom.transform(layer.projection,mapProjection);}}}
layer.addFeatures(features);}
-layer.events.triggerEvent("loadend");},CLASS_NAME:"OpenLayers.Strategy.Fixed"});OpenLayers.Date={toISOString:(function(){if("toISOString"in Date.prototype){return function(date){return date.toISOString();};}else{function pad(num,len){var str=num+"";while(str.length<len){str="0"+str;}
+layer.events.triggerEvent("loadend");},CLASS_NAME:"OpenLayers.Strategy.Fixed"});OpenLayers.Handler.Pinch=OpenLayers.Class(OpenLayers.Handler,{started:false,stopDown:false,pinching:false,last:null,start:null,initialize:function(control,callbacks,options){OpenLayers.Handler.prototype.initialize.apply(this,arguments);},touchstart:function(evt){var propagate=true;this.pinching=false;if(OpenLayers.Event.isMultiTouch(evt)){this.started=true;this.last=this.start={distance:this.getDistance(evt.touches),delta:0,scale:1};this.callback("start",[evt,this.start]);propagate=!this.stopDown;}else{this.started=false;this.start=null;this.last=null;}
+OpenLayers.Event.stop(evt);return propagate;},touchmove:function(evt){if(this.started&&OpenLayers.Event.isMultiTouch(evt)){this.pinching=true;var current=this.getPinchData(evt);this.callback("move",[evt,current]);this.last=current;OpenLayers.Event.stop(evt);}
+return true;},touchend:function(evt){if(this.started){this.started=false;this.pinching=false;this.callback("done",[evt,this.start,this.last]);this.start=null;this.last=null;}
+return true;},activate:function(){var activated=false;if(OpenLayers.Handler.prototype.activate.apply(this,arguments)){this.pinching=false;activated=true;}
+return activated;},deactivate:function(){var deactivated=false;if(OpenLayers.Handler.prototype.deactivate.apply(this,arguments)){this.started=false;this.pinching=false;this.start=null;this.last=null;deactivated=true;}
+return deactivated;},getDistance:function(touches){var t0=touches[0];var t1=touches[1];return Math.sqrt(Math.pow(t0.clientX-t1.clientX,2)+
+Math.pow(t0.clientY-t1.clientY,2));},getPinchData:function(evt){var distance=this.getDistance(evt.touches);var scale=distance/this.start.distance;return{distance:distance,delta:this.last.distance-distance,scale:scale};},CLASS_NAME:"OpenLayers.Handler.Pinch"});OpenLayers.Control.PinchZoom=OpenLayers.Class(OpenLayers.Control,{type:OpenLayers.Control.TYPE_TOOL,containerOrigin:null,pinchOrigin:null,currentCenter:null,autoActivate:true,initialize:function(options){OpenLayers.Control.prototype.initialize.apply(this,arguments);this.handler=new OpenLayers.Handler.Pinch(this,{start:this.pinchStart,move:this.pinchMove,done:this.pinchDone},this.handlerOptions);},activate:function(){var activated=OpenLayers.Control.prototype.activate.apply(this,arguments);if(activated){this.map.events.on({moveend:this.updateContainerOrigin,scope:this});this.updateContainerOrigin();}
+return activated;},deactivate:function(){var deactivated=OpenLayers.Control.prototype.deactivate.apply(this,arguments);if(this.map&&this.map.events){this.map.events.un({moveend:this.updateContainerOrigin,scope:this});}
+return deactivated;},updateContainerOrigin:function(){var container=this.map.layerContainerDiv;this.containerOrigin={x:parseInt(container.style.left,10),y:parseInt(container.style.top,10)};},pinchStart:function(evt,pinchData){this.pinchOrigin=evt.xy;this.currentCenter=evt.xy;},pinchMove:function(evt,pinchData){var scale=pinchData.scale;var containerOrigin=this.containerOrigin;var pinchOrigin=this.pinchOrigin;var current=evt.xy;var dx=Math.round((current.x-pinchOrigin.x)+(scale-1)*(containerOrigin.x-pinchOrigin.x));var dy=Math.round((current.y-pinchOrigin.y)+(scale-1)*(containerOrigin.y-pinchOrigin.y));this.applyTransform("translate("+dx+"px, "+dy+"px) scale("+scale+")");this.currentCenter=current;},applyTransform:function(transform){var style=this.map.layerContainerDiv.style;style['-webkit-transform']=transform;style['-moz-transform']=transform;},pinchDone:function(evt,start,last){this.applyTransform("");var zoom=this.map.getZoomForResolution(this.map.getResolution()/last.scale,true);if(zoom!==this.map.getZoom()||!this.currentCenter.equals(this.pinchOrigin)){var resolution=this.map.getResolutionForZoom(zoom);var location=this.map.getLonLatFromPixel(this.pinchOrigin);var zoomPixel=this.currentCenter;var size=this.map.getSize();location.lon+=resolution*((size.w/2)-zoomPixel.x);location.lat-=resolution*((size.h/2)-zoomPixel.y);this.map.setCenter(location,zoom);}},CLASS_NAME:"OpenLayers.Control.PinchZoom"});OpenLayers.Date={toISOString:(function(){if("toISOString"in Date.prototype){return function(date){return date.toISOString();};}else{function pad(num,len){var str=num+"";while(str.length<len){str="0"+str;}
return str;}
return function(date){var str;if(isNaN(date.getTime())){str="Invalid Date";}else{str=date.getUTCFullYear()+"-"+
pad(date.getUTCMonth()+1,2)+"-"+
diff --git a/web/js/fixmystreet.js b/web/js/fixmystreet.js
index 97d2892af..524bcdafa 100644
--- a/web/js/fixmystreet.js
+++ b/web/js/fixmystreet.js
@@ -4,11 +4,20 @@
*/
function form_category_onchange() {
- $.getJSON('/report/new/category_extras', {
- latitude: fixmystreet.latitude,
- longitude: fixmystreet.longitude,
- category: this.options[ this.selectedIndex ].text,
- }, function(data) {
+ var cat = $('#form_category');
+ var args = {
+ category: cat.val()
+ };
+
+ if ( typeof fixmystreet !== 'undefined' ) {
+ args['latitude'] = fixmystreet.latitude;
+ args['longitude'] = fixmystreet.longitude;
+ } else {
+ args['latitude'] = $('input[name="latitude"]').val();
+ args['longitude'] = $('input[name="longitude"]').val();
+ }
+
+ $.getJSON('/report/new/category_extras', args, function(data) {
if ( data.category_extra ) {
if ( $('#category_meta').size() ) {
$('#category_meta').html( data.category_extra);
@@ -28,12 +37,14 @@ $(function(){
$('#pc').focus();
$('input[type=submit]').removeAttr('disabled');
+ /*
$('#mapForm').submit(function() {
if (this.submit_problem) {
$('input[type=submit]', this).prop("disabled", true);
}
return true;
});
+ */
if (!$('#been_fixed_no').prop('checked') && !$('#been_fixed_unknown').prop('checked')) {
$('#another_qn').hide();
@@ -53,6 +64,81 @@ $(function(){
$('#email_alert_box').hide('fast');
}
+ // FIXME - needs to use translated string
+ jQuery.validator.addMethod('validCategory', function(value, element) {
+ return this.optional(element) || value != '-- Pick a category --'; }, validation_strings['category'] );
+
+ jQuery.validator.addMethod('validName', function(value, element) {
+ var validNamePat = /\ba\s*n+on+((y|o)mo?u?s)?(ly)?\b/i;
+ return this.optional(element) || value.length > 5 && value.match( /\S/ ) && !value.match( validNamePat ) }, validation_strings['category'] );
+
+ var form_submitted = 0;
+
+ $("form.validate").validate({
+ rules: {
+ title: { required: true },
+ detail: { required: true },
+ email: { required: true },
+ update: { required: true },
+ rznvy: { required: true }
+ },
+ messages: validation_strings,
+ onkeyup: false,
+ errorElement: 'div',
+ errorClass: 'form-error',
+ // we do this to stop things jumping around on blur
+ success: function (err) { if ( form_submitted ) { err.addClass('label-valid').html( '&nbsp;' ); } else { err.addClass('label-valid-hidden'); } },
+ errorPlacement: function( error, element ) {
+ /* And all because the .before thing doesn't seem to work in
+ mobile safari on iOS 5. However outerHTML is not cross
+ browser so we have to have two solutions :( */
+ if ( element[0].outerHTML ) {
+ var html = element.parent('div').html();
+ element.parent('div').html( error[0].outerHTML + html );
+ } else {
+ element.parent('div').before( error );
+ }
+ },
+ submitHandler: function(form) {
+ if (form.submit_problem) {
+ $('input[type=submit]', form).prop("disabled", true);
+ }
+
+ form.submit();
+ },
+ // make sure we can see the error message when we focus on invalid elements
+ showErrors: function( errorMap, errorList ) {
+ submitted && errorList.length && $(window).scrollTop( $(errorList[0].element).offset().top - 40 );
+ this.defaultShowErrors();
+ submitted = false;
+ },
+ invalidHandler: function(form, validator) { submitted = true; }
+ });
+
+ $('input[type=submit]').click( function(e) { form_submitted = 1; } );
+
+ /* set correct required status depending on what we submit
+ * NB: need to add things to form_category as the JS updating
+ * of this we do after a map click removes them */
+ $('#submit_sign_in').click( function(e) {
+ $('#form_category').addClass('required validCategory').removeClass('valid');
+ $('#form_name').removeClass();
+ } );
+
+ $('#submit_register').click( function(e) {
+ $('#form_category').addClass('required validCategory').removeClass('valid');
+ $('#form_name').addClass('required validName');
+ } );
+
+ $('#problem_submit > input[type="submit"]').click( function(e) {
+ $('#form_category').addClass('required validCategory').removeClass('valid');
+ $('#form_name').addClass('required validName');
+ } );
+
+ $('#update_post').click( function(e) {
+ $('#form_name').addClass('required').removeClass('valid');
+ } );
+
$('#email_alert').click(function(e) {
if (!$('#email_alert_box').length)
return true;
diff --git a/web/js/jquery.validate.js b/web/js/jquery.validate.js
new file mode 100644
index 000000000..b7ed45b4a
--- /dev/null
+++ b/web/js/jquery.validate.js
@@ -0,0 +1,1188 @@
+/**
+ * jQuery Validation Plugin 1.9.0
+ *
+ * http://bassistance.de/jquery-plugins/jquery-plugin-validation/
+ * http://docs.jquery.com/Plugins/Validation
+ *
+ * Copyright (c) 2006 - 2011 Jörn Zaefferer
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ */
+
+(function($) {
+
+$.extend($.fn, {
+ // http://docs.jquery.com/Plugins/Validation/validate
+ validate: function( options ) {
+
+ // if nothing is selected, return nothing; can't chain anyway
+ if (!this.length) {
+ options && options.debug && window.console && console.warn( "nothing selected, can't validate, returning nothing" );
+ return;
+ }
+
+ // check if a validator for this form was already created
+ var validator = $.data(this[0], 'validator');
+ if ( validator ) {
+ return validator;
+ }
+
+ // Add novalidate tag if HTML5.
+ this.attr('novalidate', 'novalidate');
+
+ validator = new $.validator( options, this[0] );
+ $.data(this[0], 'validator', validator);
+
+ if ( validator.settings.onsubmit ) {
+
+ var inputsAndButtons = this.find("input, button");
+
+ // allow suppresing validation by adding a cancel class to the submit button
+ inputsAndButtons.filter(".cancel").click(function () {
+ validator.cancelSubmit = true;
+ });
+
+ // when a submitHandler is used, capture the submitting button
+ if (validator.settings.submitHandler) {
+ inputsAndButtons.filter(":submit").click(function () {
+ validator.submitButton = this;
+ });
+ }
+
+ // validate the form on submit
+ this.submit( function( event ) {
+ if ( validator.settings.debug )
+ // prevent form submit to be able to see console output
+ event.preventDefault();
+
+ function handle() {
+ if ( validator.settings.submitHandler ) {
+ if (validator.submitButton) {
+ // insert a hidden input as a replacement for the missing submit button
+ var hidden = $("<input type='hidden'/>").attr("name", validator.submitButton.name).val(validator.submitButton.value).appendTo(validator.currentForm);
+ }
+ validator.settings.submitHandler.call( validator, validator.currentForm );
+ if (validator.submitButton) {
+ // and clean up afterwards; thanks to no-block-scope, hidden can be referenced
+ hidden.remove();
+ }
+ return false;
+ }
+ return true;
+ }
+
+ // prevent submit for invalid forms or custom submit handlers
+ if ( validator.cancelSubmit ) {
+ validator.cancelSubmit = false;
+ return handle();
+ }
+ if ( validator.form() ) {
+ if ( validator.pendingRequest ) {
+ validator.formSubmitted = true;
+ return false;
+ }
+ return handle();
+ } else {
+ validator.focusInvalid();
+ return false;
+ }
+ });
+ }
+
+ return validator;
+ },
+ // http://docs.jquery.com/Plugins/Validation/valid
+ valid: function() {
+ if ( $(this[0]).is('form')) {
+ return this.validate().form();
+ } else {
+ var valid = true;
+ var validator = $(this[0].form).validate();
+ this.each(function() {
+ valid &= validator.element(this);
+ });
+ return valid;
+ }
+ },
+ // attributes: space seperated list of attributes to retrieve and remove
+ removeAttrs: function(attributes) {
+ var result = {},
+ $element = this;
+ $.each(attributes.split(/\s/), function(index, value) {
+ result[value] = $element.attr(value);
+ $element.removeAttr(value);
+ });
+ return result;
+ },
+ // http://docs.jquery.com/Plugins/Validation/rules
+ rules: function(command, argument) {
+ var element = this[0];
+
+ if (command) {
+ var settings = $.data(element.form, 'validator').settings;
+ var staticRules = settings.rules;
+ var existingRules = $.validator.staticRules(element);
+ switch(command) {
+ case "add":
+ $.extend(existingRules, $.validator.normalizeRule(argument));
+ staticRules[element.name] = existingRules;
+ if (argument.messages)
+ settings.messages[element.name] = $.extend( settings.messages[element.name], argument.messages );
+ break;
+ case "remove":
+ if (!argument) {
+ delete staticRules[element.name];
+ return existingRules;
+ }
+ var filtered = {};
+ $.each(argument.split(/\s/), function(index, method) {
+ filtered[method] = existingRules[method];
+ delete existingRules[method];
+ });
+ return filtered;
+ }
+ }
+
+ var data = $.validator.normalizeRules(
+ $.extend(
+ {},
+ $.validator.metadataRules(element),
+ $.validator.classRules(element),
+ $.validator.attributeRules(element),
+ $.validator.staticRules(element)
+ ), element);
+
+ // make sure required is at front
+ if (data.required) {
+ var param = data.required;
+ delete data.required;
+ data = $.extend({required: param}, data);
+ }
+
+ return data;
+ }
+});
+
+// Custom selectors
+$.extend($.expr[":"], {
+ // http://docs.jquery.com/Plugins/Validation/blank
+ blank: function(a) {return !$.trim("" + a.value);},
+ // http://docs.jquery.com/Plugins/Validation/filled
+ filled: function(a) {return !!$.trim("" + a.value);},
+ // http://docs.jquery.com/Plugins/Validation/unchecked
+ unchecked: function(a) {return !a.checked;}
+});
+
+// constructor for validator
+$.validator = function( options, form ) {
+ this.settings = $.extend( true, {}, $.validator.defaults, options );
+ this.currentForm = form;
+ this.init();
+};
+
+$.validator.format = function(source, params) {
+ if ( arguments.length == 1 )
+ return function() {
+ var args = $.makeArray(arguments);
+ args.unshift(source);
+ return $.validator.format.apply( this, args );
+ };
+ if ( arguments.length > 2 && params.constructor != Array ) {
+ params = $.makeArray(arguments).slice(1);
+ }
+ if ( params.constructor != Array ) {
+ params = [ params ];
+ }
+ $.each(params, function(i, n) {
+ source = source.replace(new RegExp("\\{" + i + "\\}", "g"), n);
+ });
+ return source;
+};
+
+$.extend($.validator, {
+
+ defaults: {
+ messages: {},
+ groups: {},
+ rules: {},
+ errorClass: "error",
+ validClass: "valid",
+ errorElement: "label",
+ focusInvalid: true,
+ errorContainer: $( [] ),
+ errorLabelContainer: $( [] ),
+ onsubmit: true,
+ ignore: ":hidden",
+ ignoreTitle: false,
+ onfocusin: function(element, event) {
+ this.lastActive = element;
+
+ // hide error label and remove error class on focus if enabled
+ if ( this.settings.focusCleanup && !this.blockFocusCleanup ) {
+ this.settings.unhighlight && this.settings.unhighlight.call( this, element, this.settings.errorClass, this.settings.validClass );
+ this.addWrapper(this.errorsFor(element)).hide();
+ }
+ },
+ onfocusout: function(element, event) {
+ if ( !this.checkable(element) && (element.name in this.submitted || !this.optional(element)) ) {
+ this.element(element);
+ }
+ },
+ onkeyup: function(element, event) {
+ if ( element.name in this.submitted || element == this.lastElement ) {
+ this.element(element);
+ }
+ },
+ onclick: function(element, event) {
+ // click on selects, radiobuttons and checkboxes
+ if ( element.name in this.submitted )
+ this.element(element);
+ // or option elements, check parent select in that case
+ else if (element.parentNode.name in this.submitted)
+ this.element(element.parentNode);
+ },
+ highlight: function(element, errorClass, validClass) {
+ if (element.type === 'radio') {
+ this.findByName(element.name).addClass(errorClass).removeClass(validClass);
+ } else {
+ $(element).addClass(errorClass).removeClass(validClass);
+ }
+ },
+ unhighlight: function(element, errorClass, validClass) {
+ if (element.type === 'radio') {
+ this.findByName(element.name).removeClass(errorClass).addClass(validClass);
+ } else {
+ $(element).removeClass(errorClass).addClass(validClass);
+ }
+ }
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Validator/setDefaults
+ setDefaults: function(settings) {
+ $.extend( $.validator.defaults, settings );
+ },
+
+ messages: {
+ required: "This field is required.",
+ remote: "Please fix this field.",
+ email: "Please enter a valid email address.",
+ url: "Please enter a valid URL.",
+ date: "Please enter a valid date.",
+ dateISO: "Please enter a valid date (ISO).",
+ number: "Please enter a valid number.",
+ digits: "Please enter only digits.",
+ creditcard: "Please enter a valid credit card number.",
+ equalTo: "Please enter the same value again.",
+ accept: "Please enter a value with a valid extension.",
+ maxlength: $.validator.format("Please enter no more than {0} characters."),
+ minlength: $.validator.format("Please enter at least {0} characters."),
+ rangelength: $.validator.format("Please enter a value between {0} and {1} characters long."),
+ range: $.validator.format("Please enter a value between {0} and {1}."),
+ max: $.validator.format("Please enter a value less than or equal to {0}."),
+ min: $.validator.format("Please enter a value greater than or equal to {0}.")
+ },
+
+ autoCreateRanges: false,
+
+ prototype: {
+
+ init: function() {
+ this.labelContainer = $(this.settings.errorLabelContainer);
+ this.errorContext = this.labelContainer.length && this.labelContainer || $(this.currentForm);
+ this.containers = $(this.settings.errorContainer).add( this.settings.errorLabelContainer );
+ this.submitted = {};
+ this.valueCache = {};
+ this.pendingRequest = 0;
+ this.pending = {};
+ this.invalid = {};
+ this.reset();
+
+ var groups = (this.groups = {});
+ $.each(this.settings.groups, function(key, value) {
+ $.each(value.split(/\s/), function(index, name) {
+ groups[name] = key;
+ });
+ });
+ var rules = this.settings.rules;
+ $.each(rules, function(key, value) {
+ rules[key] = $.validator.normalizeRule(value);
+ });
+
+ function delegate(event) {
+ var validator = $.data(this[0].form, "validator"),
+ eventType = "on" + event.type.replace(/^validate/, "");
+ validator.settings[eventType] && validator.settings[eventType].call(validator, this[0], event);
+ }
+ $(this.currentForm)
+ .validateDelegate("[type='text'], [type='password'], [type='file'], select, textarea, " +
+ "[type='number'], [type='search'] ,[type='tel'], [type='url'], " +
+ "[type='email'], [type='datetime'], [type='date'], [type='month'], " +
+ "[type='week'], [type='time'], [type='datetime-local'], " +
+ "[type='range'], [type='color'] ",
+ "focusin focusout keyup", delegate)
+ .validateDelegate("[type='radio'], [type='checkbox'], select, option", "click", delegate);
+
+ if (this.settings.invalidHandler)
+ $(this.currentForm).bind("invalid-form.validate", this.settings.invalidHandler);
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Validator/form
+ form: function() {
+ this.checkForm();
+ $.extend(this.submitted, this.errorMap);
+ this.invalid = $.extend({}, this.errorMap);
+ if (!this.valid())
+ $(this.currentForm).triggerHandler("invalid-form", [this]);
+ this.showErrors();
+ return this.valid();
+ },
+
+ checkForm: function() {
+ this.prepareForm();
+ for ( var i = 0, elements = (this.currentElements = this.elements()); elements[i]; i++ ) {
+ this.check( elements[i] );
+ }
+ return this.valid();
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Validator/element
+ element: function( element ) {
+ element = this.validationTargetFor( this.clean( element ) );
+ this.lastElement = element;
+ this.prepareElement( element );
+ this.currentElements = $(element);
+ var result = this.check( element );
+ if ( result ) {
+ delete this.invalid[element.name];
+ } else {
+ this.invalid[element.name] = true;
+ }
+ if ( !this.numberOfInvalids() ) {
+ // Hide error containers on last error
+ this.toHide = this.toHide.add( this.containers );
+ }
+ this.showErrors();
+ return result;
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Validator/showErrors
+ showErrors: function(errors) {
+ if(errors) {
+ // add items to error list and map
+ $.extend( this.errorMap, errors );
+ this.errorList = [];
+ for ( var name in errors ) {
+ this.errorList.push({
+ message: errors[name],
+ element: this.findByName(name)[0]
+ });
+ }
+ // remove items from success list
+ this.successList = $.grep( this.successList, function(element) {
+ return !(element.name in errors);
+ });
+ }
+ this.settings.showErrors
+ ? this.settings.showErrors.call( this, this.errorMap, this.errorList )
+ : this.defaultShowErrors();
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Validator/resetForm
+ resetForm: function() {
+ if ( $.fn.resetForm )
+ $( this.currentForm ).resetForm();
+ this.submitted = {};
+ this.lastElement = null;
+ this.prepareForm();
+ this.hideErrors();
+ this.elements().removeClass( this.settings.errorClass );
+ },
+
+ numberOfInvalids: function() {
+ return this.objectLength(this.invalid);
+ },
+
+ objectLength: function( obj ) {
+ var count = 0;
+ for ( var i in obj )
+ count++;
+ return count;
+ },
+
+ hideErrors: function() {
+ this.addWrapper( this.toHide ).hide();
+ },
+
+ valid: function() {
+ return this.size() == 0;
+ },
+
+ size: function() {
+ return this.errorList.length;
+ },
+
+ focusInvalid: function() {
+ if( this.settings.focusInvalid ) {
+ try {
+ $(this.findLastActive() || this.errorList.length && this.errorList[0].element || [])
+ .filter(":visible")
+ .focus()
+ // manually trigger focusin event; without it, focusin handler isn't called, findLastActive won't have anything to find
+ .trigger("focusin");
+ } catch(e) {
+ // ignore IE throwing errors when focusing hidden elements
+ }
+ }
+ },
+
+ findLastActive: function() {
+ var lastActive = this.lastActive;
+ return lastActive && $.grep(this.errorList, function(n) {
+ return n.element.name == lastActive.name;
+ }).length == 1 && lastActive;
+ },
+
+ elements: function() {
+ var validator = this,
+ rulesCache = {};
+
+ // select all valid inputs inside the form (no submit or reset buttons)
+ return $(this.currentForm)
+ .find("input, select, textarea")
+ .not(":submit, :reset, :image, [disabled]")
+ .not( this.settings.ignore )
+ .filter(function() {
+ !this.name && validator.settings.debug && window.console && console.error( "%o has no name assigned", this);
+
+ // select only the first element for each name, and only those with rules specified
+ if ( this.name in rulesCache || !validator.objectLength($(this).rules()) )
+ return false;
+
+ rulesCache[this.name] = true;
+ return true;
+ });
+ },
+
+ clean: function( selector ) {
+ return $( selector )[0];
+ },
+
+ errors: function() {
+ return $( this.settings.errorElement + "." + this.settings.errorClass, this.errorContext );
+ },
+
+ reset: function() {
+ this.successList = [];
+ this.errorList = [];
+ this.errorMap = {};
+ this.toShow = $([]);
+ this.toHide = $([]);
+ this.currentElements = $([]);
+ },
+
+ prepareForm: function() {
+ this.reset();
+ this.toHide = this.errors().add( this.containers );
+ },
+
+ prepareElement: function( element ) {
+ this.reset();
+ this.toHide = this.errorsFor(element);
+ },
+
+ check: function( element ) {
+ element = this.validationTargetFor( this.clean( element ) );
+
+ var rules = $(element).rules();
+ var dependencyMismatch = false;
+ for (var method in rules ) {
+ var rule = { method: method, parameters: rules[method] };
+ try {
+ var result = $.validator.methods[method].call( this, element.value.replace(/\r/g, ""), element, rule.parameters );
+
+ // if a method indicates that the field is optional and therefore valid,
+ // don't mark it as valid when there are no other rules
+ if ( result == "dependency-mismatch" ) {
+ dependencyMismatch = true;
+ continue;
+ }
+ dependencyMismatch = false;
+
+ if ( result == "pending" ) {
+ this.toHide = this.toHide.not( this.errorsFor(element) );
+ return;
+ }
+
+ if( !result ) {
+ this.formatAndAdd( element, rule );
+ return false;
+ }
+ } catch(e) {
+ this.settings.debug && window.console && console.log("exception occured when checking element " + element.id
+ + ", check the '" + rule.method + "' method", e);
+ throw e;
+ }
+ }
+ if (dependencyMismatch)
+ return;
+ if ( this.objectLength(rules) )
+ this.successList.push(element);
+ return true;
+ },
+
+ // return the custom message for the given element and validation method
+ // specified in the element's "messages" metadata
+ customMetaMessage: function(element, method) {
+ if (!$.metadata)
+ return;
+
+ var meta = this.settings.meta
+ ? $(element).metadata()[this.settings.meta]
+ : $(element).metadata();
+
+ return meta && meta.messages && meta.messages[method];
+ },
+
+ // return the custom message for the given element name and validation method
+ customMessage: function( name, method ) {
+ var m = this.settings.messages[name];
+ return m && (m.constructor == String
+ ? m
+ : m[method]);
+ },
+
+ // return the first defined argument, allowing empty strings
+ findDefined: function() {
+ for(var i = 0; i < arguments.length; i++) {
+ if (arguments[i] !== undefined)
+ return arguments[i];
+ }
+ return undefined;
+ },
+
+ defaultMessage: function( element, method) {
+ return this.findDefined(
+ this.customMessage( element.name, method ),
+ this.customMetaMessage( element, method ),
+ // title is never undefined, so handle empty string as undefined
+ !this.settings.ignoreTitle && element.title || undefined,
+ $.validator.messages[method],
+ "<strong>Warning: No message defined for " + element.name + "</strong>"
+ );
+ },
+
+ formatAndAdd: function( element, rule ) {
+ var message = this.defaultMessage( element, rule.method ),
+ theregex = /\$?\{(\d+)\}/g;
+ if ( typeof message == "function" ) {
+ message = message.call(this, rule.parameters, element);
+ } else if (theregex.test(message)) {
+ message = jQuery.format(message.replace(theregex, '{$1}'), rule.parameters);
+ }
+ this.errorList.push({
+ message: message,
+ element: element
+ });
+
+ this.errorMap[element.name] = message;
+ this.submitted[element.name] = message;
+ },
+
+ addWrapper: function(toToggle) {
+ if ( this.settings.wrapper )
+ toToggle = toToggle.add( toToggle.parent( this.settings.wrapper ) );
+ return toToggle;
+ },
+
+ defaultShowErrors: function() {
+ for ( var i = 0; this.errorList[i]; i++ ) {
+ var error = this.errorList[i];
+ this.settings.highlight && this.settings.highlight.call( this, error.element, this.settings.errorClass, this.settings.validClass );
+ this.showLabel( error.element, error.message );
+ }
+ if( this.errorList.length ) {
+ this.toShow = this.toShow.add( this.containers );
+ }
+ if (this.settings.success) {
+ for ( var i = 0; this.successList[i]; i++ ) {
+ this.showLabel( this.successList[i] );
+ }
+ }
+ if (this.settings.unhighlight) {
+ for ( var i = 0, elements = this.validElements(); elements[i]; i++ ) {
+ this.settings.unhighlight.call( this, elements[i], this.settings.errorClass, this.settings.validClass );
+ }
+ }
+ this.toHide = this.toHide.not( this.toShow );
+ this.hideErrors();
+ this.addWrapper( this.toShow ).show();
+ },
+
+ validElements: function() {
+ return this.currentElements.not(this.invalidElements());
+ },
+
+ invalidElements: function() {
+ return $(this.errorList).map(function() {
+ return this.element;
+ });
+ },
+
+ showLabel: function(element, message) {
+ var label = this.errorsFor( element );
+ if ( label.length ) {
+ // refresh error/success class
+ label.removeClass( this.settings.validClass ).addClass( this.settings.errorClass );
+
+ // check if we have a generated label, replace the message then
+ label.attr("generated") && label.html(message);
+ } else {
+ // create label
+ label = $("<" + this.settings.errorElement + "/>")
+ .attr({"for": this.idOrName(element), generated: true})
+ .addClass(this.settings.errorClass)
+ .html(message || "");
+ if ( this.settings.wrapper ) {
+ // make sure the element is visible, even in IE
+ // actually showing the wrapped element is handled elsewhere
+ label = label.hide().show().wrap("<" + this.settings.wrapper + "/>").parent();
+ }
+ if ( !this.labelContainer.append(label).length )
+ this.settings.errorPlacement
+ ? this.settings.errorPlacement(label, $(element) )
+ : label.insertAfter(element);
+ }
+ if ( !message && this.settings.success ) {
+ label.text("");
+ typeof this.settings.success == "string"
+ ? label.addClass( this.settings.success )
+ : this.settings.success( label );
+ }
+ this.toShow = this.toShow.add(label);
+ },
+
+ errorsFor: function(element) {
+ var name = this.idOrName(element);
+ return this.errors().filter(function() {
+ return $(this).attr('for') == name;
+ });
+ },
+
+ idOrName: function(element) {
+ return this.groups[element.name] || (this.checkable(element) ? element.name : element.id || element.name);
+ },
+
+ validationTargetFor: function(element) {
+ // if radio/checkbox, validate first element in group instead
+ if (this.checkable(element)) {
+ element = this.findByName( element.name ).not(this.settings.ignore)[0];
+ }
+ return element;
+ },
+
+ checkable: function( element ) {
+ return /radio|checkbox/i.test(element.type);
+ },
+
+ findByName: function( name ) {
+ // select by name and filter by form for performance over form.find("[name=...]")
+ var form = this.currentForm;
+ return $(document.getElementsByName(name)).map(function(index, element) {
+ return element.form == form && element.name == name && element || null;
+ });
+ },
+
+ getLength: function(value, element) {
+ switch( element.nodeName.toLowerCase() ) {
+ case 'select':
+ return $("option:selected", element).length;
+ case 'input':
+ if( this.checkable( element) )
+ return this.findByName(element.name).filter(':checked').length;
+ }
+ return value.length;
+ },
+
+ depend: function(param, element) {
+ return this.dependTypes[typeof param]
+ ? this.dependTypes[typeof param](param, element)
+ : true;
+ },
+
+ dependTypes: {
+ "boolean": function(param, element) {
+ return param;
+ },
+ "string": function(param, element) {
+ return !!$(param, element.form).length;
+ },
+ "function": function(param, element) {
+ return param(element);
+ }
+ },
+
+ optional: function(element) {
+ return !$.validator.methods.required.call(this, $.trim(element.value), element) && "dependency-mismatch";
+ },
+
+ startRequest: function(element) {
+ if (!this.pending[element.name]) {
+ this.pendingRequest++;
+ this.pending[element.name] = true;
+ }
+ },
+
+ stopRequest: function(element, valid) {
+ this.pendingRequest--;
+ // sometimes synchronization fails, make sure pendingRequest is never < 0
+ if (this.pendingRequest < 0)
+ this.pendingRequest = 0;
+ delete this.pending[element.name];
+ if ( valid && this.pendingRequest == 0 && this.formSubmitted && this.form() ) {
+ $(this.currentForm).submit();
+ this.formSubmitted = false;
+ } else if (!valid && this.pendingRequest == 0 && this.formSubmitted) {
+ $(this.currentForm).triggerHandler("invalid-form", [this]);
+ this.formSubmitted = false;
+ }
+ },
+
+ previousValue: function(element) {
+ return $.data(element, "previousValue") || $.data(element, "previousValue", {
+ old: null,
+ valid: true,
+ message: this.defaultMessage( element, "remote" )
+ });
+ }
+
+ },
+
+ classRuleSettings: {
+ required: {required: true},
+ email: {email: true},
+ url: {url: true},
+ date: {date: true},
+ dateISO: {dateISO: true},
+ dateDE: {dateDE: true},
+ number: {number: true},
+ numberDE: {numberDE: true},
+ digits: {digits: true},
+ creditcard: {creditcard: true}
+ },
+
+ addClassRules: function(className, rules) {
+ className.constructor == String ?
+ this.classRuleSettings[className] = rules :
+ $.extend(this.classRuleSettings, className);
+ },
+
+ classRules: function(element) {
+ var rules = {};
+ var classes = $(element).attr('class');
+ classes && $.each(classes.split(' '), function() {
+ if (this in $.validator.classRuleSettings) {
+ $.extend(rules, $.validator.classRuleSettings[this]);
+ }
+ });
+ return rules;
+ },
+
+ attributeRules: function(element) {
+ var rules = {};
+ var $element = $(element);
+
+ for (var method in $.validator.methods) {
+ var value;
+ // If .prop exists (jQuery >= 1.6), use it to get true/false for required
+ if (method === 'required' && typeof $.fn.prop === 'function') {
+ value = $element.prop(method);
+ } else {
+ value = $element.attr(method);
+ }
+ if (value) {
+ rules[method] = value;
+ } else if ($element[0].getAttribute("type") === method) {
+ rules[method] = true;
+ }
+ }
+
+ // maxlength may be returned as -1, 2147483647 (IE) and 524288 (safari) for text inputs
+ if (rules.maxlength && /-1|2147483647|524288/.test(rules.maxlength)) {
+ delete rules.maxlength;
+ }
+
+ return rules;
+ },
+
+ metadataRules: function(element) {
+ if (!$.metadata) return {};
+
+ var meta = $.data(element.form, 'validator').settings.meta;
+ return meta ?
+ $(element).metadata()[meta] :
+ $(element).metadata();
+ },
+
+ staticRules: function(element) {
+ var rules = {};
+ var validator = $.data(element.form, 'validator');
+ if (validator.settings.rules) {
+ rules = $.validator.normalizeRule(validator.settings.rules[element.name]) || {};
+ }
+ return rules;
+ },
+
+ normalizeRules: function(rules, element) {
+ // handle dependency check
+ $.each(rules, function(prop, val) {
+ // ignore rule when param is explicitly false, eg. required:false
+ if (val === false) {
+ delete rules[prop];
+ return;
+ }
+ if (val.param || val.depends) {
+ var keepRule = true;
+ switch (typeof val.depends) {
+ case "string":
+ keepRule = !!$(val.depends, element.form).length;
+ break;
+ case "function":
+ keepRule = val.depends.call(element, element);
+ break;
+ }
+ if (keepRule) {
+ rules[prop] = val.param !== undefined ? val.param : true;
+ } else {
+ delete rules[prop];
+ }
+ }
+ });
+
+ // evaluate parameters
+ $.each(rules, function(rule, parameter) {
+ rules[rule] = $.isFunction(parameter) ? parameter(element) : parameter;
+ });
+
+ // clean number parameters
+ $.each(['minlength', 'maxlength', 'min', 'max'], function() {
+ if (rules[this]) {
+ rules[this] = Number(rules[this]);
+ }
+ });
+ $.each(['rangelength', 'range'], function() {
+ if (rules[this]) {
+ rules[this] = [Number(rules[this][0]), Number(rules[this][1])];
+ }
+ });
+
+ if ($.validator.autoCreateRanges) {
+ // auto-create ranges
+ if (rules.min && rules.max) {
+ rules.range = [rules.min, rules.max];
+ delete rules.min;
+ delete rules.max;
+ }
+ if (rules.minlength && rules.maxlength) {
+ rules.rangelength = [rules.minlength, rules.maxlength];
+ delete rules.minlength;
+ delete rules.maxlength;
+ }
+ }
+
+ // To support custom messages in metadata ignore rule methods titled "messages"
+ if (rules.messages) {
+ delete rules.messages;
+ }
+
+ return rules;
+ },
+
+ // Converts a simple string to a {string: true} rule, e.g., "required" to {required:true}
+ normalizeRule: function(data) {
+ if( typeof data == "string" ) {
+ var transformed = {};
+ $.each(data.split(/\s/), function() {
+ transformed[this] = true;
+ });
+ data = transformed;
+ }
+ return data;
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Validator/addMethod
+ addMethod: function(name, method, message) {
+ $.validator.methods[name] = method;
+ $.validator.messages[name] = message != undefined ? message : $.validator.messages[name];
+ if (method.length < 3) {
+ $.validator.addClassRules(name, $.validator.normalizeRule(name));
+ }
+ },
+
+ methods: {
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/required
+ required: function(value, element, param) {
+ // check if dependency is met
+ if ( !this.depend(param, element) )
+ return "dependency-mismatch";
+ switch( element.nodeName.toLowerCase() ) {
+ case 'select':
+ // could be an array for select-multiple or a string, both are fine this way
+ var val = $(element).val();
+ return val && val.length > 0;
+ case 'input':
+ if ( this.checkable(element) )
+ return this.getLength(value, element) > 0;
+ default:
+ return $.trim(value).length > 0;
+ }
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/remote
+ remote: function(value, element, param) {
+ if ( this.optional(element) )
+ return "dependency-mismatch";
+
+ var previous = this.previousValue(element);
+ if (!this.settings.messages[element.name] )
+ this.settings.messages[element.name] = {};
+ previous.originalMessage = this.settings.messages[element.name].remote;
+ this.settings.messages[element.name].remote = previous.message;
+
+ param = typeof param == "string" && {url:param} || param;
+
+ if ( this.pending[element.name] ) {
+ return "pending";
+ }
+ if ( previous.old === value ) {
+ return previous.valid;
+ }
+
+ previous.old = value;
+ var validator = this;
+ this.startRequest(element);
+ var data = {};
+ data[element.name] = value;
+ $.ajax($.extend(true, {
+ url: param,
+ mode: "abort",
+ port: "validate" + element.name,
+ dataType: "json",
+ data: data,
+ success: function(response) {
+ validator.settings.messages[element.name].remote = previous.originalMessage;
+ var valid = response === true;
+ if ( valid ) {
+ var submitted = validator.formSubmitted;
+ validator.prepareElement(element);
+ validator.formSubmitted = submitted;
+ validator.successList.push(element);
+ validator.showErrors();
+ } else {
+ var errors = {};
+ var message = response || validator.defaultMessage( element, "remote" );
+ errors[element.name] = previous.message = $.isFunction(message) ? message(value) : message;
+ validator.showErrors(errors);
+ }
+ previous.valid = valid;
+ validator.stopRequest(element, valid);
+ }
+ }, param));
+ return "pending";
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/minlength
+ minlength: function(value, element, param) {
+ return this.optional(element) || this.getLength($.trim(value), element) >= param;
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/maxlength
+ maxlength: function(value, element, param) {
+ return this.optional(element) || this.getLength($.trim(value), element) <= param;
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/rangelength
+ rangelength: function(value, element, param) {
+ var length = this.getLength($.trim(value), element);
+ return this.optional(element) || ( length >= param[0] && length <= param[1] );
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/min
+ min: function( value, element, param ) {
+ return this.optional(element) || value >= param;
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/max
+ max: function( value, element, param ) {
+ return this.optional(element) || value <= param;
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/range
+ range: function( value, element, param ) {
+ return this.optional(element) || ( value >= param[0] && value <= param[1] );
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/email
+ email: function(value, element) {
+ // contributed by Scott Gonzalez: http://projects.scottsplayground.com/email_address_validation/
+ return this.optional(element) || /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(value);
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/url
+ url: function(value, element) {
+ // contributed by Scott Gonzalez: http://projects.scottsplayground.com/iri/
+ return this.optional(element) || /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value);
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/date
+ date: function(value, element) {
+ return this.optional(element) || !/Invalid|NaN/.test(new Date(value));
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/dateISO
+ dateISO: function(value, element) {
+ return this.optional(element) || /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(value);
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/number
+ number: function(value, element) {
+ return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/.test(value);
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/digits
+ digits: function(value, element) {
+ return this.optional(element) || /^\d+$/.test(value);
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/creditcard
+ // based on http://en.wikipedia.org/wiki/Luhn
+ creditcard: function(value, element) {
+ if ( this.optional(element) )
+ return "dependency-mismatch";
+ // accept only spaces, digits and dashes
+ if (/[^0-9 -]+/.test(value))
+ return false;
+ var nCheck = 0,
+ nDigit = 0,
+ bEven = false;
+
+ value = value.replace(/\D/g, "");
+
+ for (var n = value.length - 1; n >= 0; n--) {
+ var cDigit = value.charAt(n);
+ var nDigit = parseInt(cDigit, 10);
+ if (bEven) {
+ if ((nDigit *= 2) > 9)
+ nDigit -= 9;
+ }
+ nCheck += nDigit;
+ bEven = !bEven;
+ }
+
+ return (nCheck % 10) == 0;
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/accept
+ accept: function(value, element, param) {
+ param = typeof param == "string" ? param.replace(/,/g, '|') : "png|jpe?g|gif";
+ return this.optional(element) || value.match(new RegExp(".(" + param + ")$", "i"));
+ },
+
+ // http://docs.jquery.com/Plugins/Validation/Methods/equalTo
+ equalTo: function(value, element, param) {
+ // bind to the blur event of the target in order to revalidate whenever the target field is updated
+ // TODO find a way to bind the event just once, avoiding the unbind-rebind overhead
+ var target = $(param).unbind(".validate-equalTo").bind("blur.validate-equalTo", function() {
+ $(element).valid();
+ });
+ return value == target.val();
+ }
+
+ }
+
+});
+
+// deprecated, use $.validator.format instead
+$.format = $.validator.format;
+
+})(jQuery);
+
+// ajax mode: abort
+// usage: $.ajax({ mode: "abort"[, port: "uniqueport"]});
+// if mode:"abort" is used, the previous request on that port (port can be undefined) is aborted via XMLHttpRequest.abort()
+;(function($) {
+ var pendingRequests = {};
+ // Use a prefilter if available (1.5+)
+ if ( $.ajaxPrefilter ) {
+ $.ajaxPrefilter(function(settings, _, xhr) {
+ var port = settings.port;
+ if (settings.mode == "abort") {
+ if ( pendingRequests[port] ) {
+ pendingRequests[port].abort();
+ }
+ pendingRequests[port] = xhr;
+ }
+ });
+ } else {
+ // Proxy ajax
+ var ajax = $.ajax;
+ $.ajax = function(settings) {
+ var mode = ( "mode" in settings ? settings : $.ajaxSettings ).mode,
+ port = ( "port" in settings ? settings : $.ajaxSettings ).port;
+ if (mode == "abort") {
+ if ( pendingRequests[port] ) {
+ pendingRequests[port].abort();
+ }
+ return (pendingRequests[port] = ajax.apply(this, arguments));
+ }
+ return ajax.apply(this, arguments);
+ };
+ }
+})(jQuery);
+
+// provides cross-browser focusin and focusout events
+// IE has native support, in other browsers, use event caputuring (neither bubbles)
+
+// provides delegate(type: String, delegate: Selector, handler: Callback) plugin for easier event delegation
+// handler is only called when $(event.target).is(delegate), in the scope of the jquery-object for event.target
+;(function($) {
+ // only implement if not provided by jQuery core (since 1.4)
+ // TODO verify if jQuery 1.4's implementation is compatible with older jQuery special-event APIs
+ if (!jQuery.event.special.focusin && !jQuery.event.special.focusout && document.addEventListener) {
+ $.each({
+ focus: 'focusin',
+ blur: 'focusout'
+ }, function( original, fix ){
+ $.event.special[fix] = {
+ setup:function() {
+ this.addEventListener( original, handler, true );
+ },
+ teardown:function() {
+ this.removeEventListener( original, handler, true );
+ },
+ handler: function(e) {
+ arguments[0] = $.event.fix(e);
+ arguments[0].type = fix;
+ return $.event.handle.apply(this, arguments);
+ }
+ };
+ function handler(e) {
+ e = $.event.fix(e);
+ e.type = fix;
+ return $.event.handle.call(this, e);
+ }
+ });
+ };
+ $.extend($.fn, {
+ validateDelegate: function(delegate, type, handler) {
+ return this.bind(type, function(event) {
+ var target = $(event.target);
+ if (target.is(delegate)) {
+ return handler.apply(target, arguments);
+ }
+ });
+ }
+ });
+})(jQuery);
diff --git a/web/js/jquery.validate.min.js b/web/js/jquery.validate.min.js
new file mode 100644
index 000000000..edd645255
--- /dev/null
+++ b/web/js/jquery.validate.min.js
@@ -0,0 +1,51 @@
+/**
+ * jQuery Validation Plugin 1.9.0
+ *
+ * http://bassistance.de/jquery-plugins/jquery-plugin-validation/
+ * http://docs.jquery.com/Plugins/Validation
+ *
+ * Copyright (c) 2006 - 2011 Jörn Zaefferer
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ */
+(function(c){c.extend(c.fn,{validate:function(a){if(this.length){var b=c.data(this[0],"validator");if(b)return b;this.attr("novalidate","novalidate");b=new c.validator(a,this[0]);c.data(this[0],"validator",b);if(b.settings.onsubmit){a=this.find("input, button");a.filter(".cancel").click(function(){b.cancelSubmit=true});b.settings.submitHandler&&a.filter(":submit").click(function(){b.submitButton=this});this.submit(function(d){function e(){if(b.settings.submitHandler){if(b.submitButton)var f=c("<input type='hidden'/>").attr("name",
+b.submitButton.name).val(b.submitButton.value).appendTo(b.currentForm);b.settings.submitHandler.call(b,b.currentForm);b.submitButton&&f.remove();return false}return true}b.settings.debug&&d.preventDefault();if(b.cancelSubmit){b.cancelSubmit=false;return e()}if(b.form()){if(b.pendingRequest){b.formSubmitted=true;return false}return e()}else{b.focusInvalid();return false}})}return b}else a&&a.debug&&window.console&&console.warn("nothing selected, can't validate, returning nothing")},valid:function(){if(c(this[0]).is("form"))return this.validate().form();
+else{var a=true,b=c(this[0].form).validate();this.each(function(){a&=b.element(this)});return a}},removeAttrs:function(a){var b={},d=this;c.each(a.split(/\s/),function(e,f){b[f]=d.attr(f);d.removeAttr(f)});return b},rules:function(a,b){var d=this[0];if(a){var e=c.data(d.form,"validator").settings,f=e.rules,g=c.validator.staticRules(d);switch(a){case "add":c.extend(g,c.validator.normalizeRule(b));f[d.name]=g;if(b.messages)e.messages[d.name]=c.extend(e.messages[d.name],b.messages);break;case "remove":if(!b){delete f[d.name];
+return g}var h={};c.each(b.split(/\s/),function(j,i){h[i]=g[i];delete g[i]});return h}}d=c.validator.normalizeRules(c.extend({},c.validator.metadataRules(d),c.validator.classRules(d),c.validator.attributeRules(d),c.validator.staticRules(d)),d);if(d.required){e=d.required;delete d.required;d=c.extend({required:e},d)}return d}});c.extend(c.expr[":"],{blank:function(a){return!c.trim(""+a.value)},filled:function(a){return!!c.trim(""+a.value)},unchecked:function(a){return!a.checked}});c.validator=function(a,
+b){this.settings=c.extend(true,{},c.validator.defaults,a);this.currentForm=b;this.init()};c.validator.format=function(a,b){if(arguments.length==1)return function(){var d=c.makeArray(arguments);d.unshift(a);return c.validator.format.apply(this,d)};if(arguments.length>2&&b.constructor!=Array)b=c.makeArray(arguments).slice(1);if(b.constructor!=Array)b=[b];c.each(b,function(d,e){a=a.replace(RegExp("\\{"+d+"\\}","g"),e)});return a};c.extend(c.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",
+validClass:"valid",errorElement:"label",focusInvalid:true,errorContainer:c([]),errorLabelContainer:c([]),onsubmit:true,ignore:":hidden",ignoreTitle:false,onfocusin:function(a){this.lastActive=a;if(this.settings.focusCleanup&&!this.blockFocusCleanup){this.settings.unhighlight&&this.settings.unhighlight.call(this,a,this.settings.errorClass,this.settings.validClass);this.addWrapper(this.errorsFor(a)).hide()}},onfocusout:function(a){if(!this.checkable(a)&&(a.name in this.submitted||!this.optional(a)))this.element(a)},
+onkeyup:function(a){if(a.name in this.submitted||a==this.lastElement)this.element(a)},onclick:function(a){if(a.name in this.submitted)this.element(a);else a.parentNode.name in this.submitted&&this.element(a.parentNode)},highlight:function(a,b,d){a.type==="radio"?this.findByName(a.name).addClass(b).removeClass(d):c(a).addClass(b).removeClass(d)},unhighlight:function(a,b,d){a.type==="radio"?this.findByName(a.name).removeClass(b).addClass(d):c(a).removeClass(b).addClass(d)}},setDefaults:function(a){c.extend(c.validator.defaults,
+a)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date (ISO).",number:"Please enter a valid number.",digits:"Please enter only digits.",creditcard:"Please enter a valid credit card number.",equalTo:"Please enter the same value again.",accept:"Please enter a value with a valid extension.",maxlength:c.validator.format("Please enter no more than {0} characters."),
+minlength:c.validator.format("Please enter at least {0} characters."),rangelength:c.validator.format("Please enter a value between {0} and {1} characters long."),range:c.validator.format("Please enter a value between {0} and {1}."),max:c.validator.format("Please enter a value less than or equal to {0}."),min:c.validator.format("Please enter a value greater than or equal to {0}.")},autoCreateRanges:false,prototype:{init:function(){function a(e){var f=c.data(this[0].form,"validator"),g="on"+e.type.replace(/^validate/,
+"");f.settings[g]&&f.settings[g].call(f,this[0],e)}this.labelContainer=c(this.settings.errorLabelContainer);this.errorContext=this.labelContainer.length&&this.labelContainer||c(this.currentForm);this.containers=c(this.settings.errorContainer).add(this.settings.errorLabelContainer);this.submitted={};this.valueCache={};this.pendingRequest=0;this.pending={};this.invalid={};this.reset();var b=this.groups={};c.each(this.settings.groups,function(e,f){c.each(f.split(/\s/),function(g,h){b[h]=e})});var d=
+this.settings.rules;c.each(d,function(e,f){d[e]=c.validator.normalizeRule(f)});c(this.currentForm).validateDelegate("[type='text'], [type='password'], [type='file'], select, textarea, [type='number'], [type='search'] ,[type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], [type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'] ","focusin focusout keyup",a).validateDelegate("[type='radio'], [type='checkbox'], select, option","click",
+a);this.settings.invalidHandler&&c(this.currentForm).bind("invalid-form.validate",this.settings.invalidHandler)},form:function(){this.checkForm();c.extend(this.submitted,this.errorMap);this.invalid=c.extend({},this.errorMap);this.valid()||c(this.currentForm).triggerHandler("invalid-form",[this]);this.showErrors();return this.valid()},checkForm:function(){this.prepareForm();for(var a=0,b=this.currentElements=this.elements();b[a];a++)this.check(b[a]);return this.valid()},element:function(a){this.lastElement=
+a=this.validationTargetFor(this.clean(a));this.prepareElement(a);this.currentElements=c(a);var b=this.check(a);if(b)delete this.invalid[a.name];else this.invalid[a.name]=true;if(!this.numberOfInvalids())this.toHide=this.toHide.add(this.containers);this.showErrors();return b},showErrors:function(a){if(a){c.extend(this.errorMap,a);this.errorList=[];for(var b in a)this.errorList.push({message:a[b],element:this.findByName(b)[0]});this.successList=c.grep(this.successList,function(d){return!(d.name in a)})}this.settings.showErrors?
+this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){c.fn.resetForm&&c(this.currentForm).resetForm();this.submitted={};this.lastElement=null;this.prepareForm();this.hideErrors();this.elements().removeClass(this.settings.errorClass)},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(a){var b=0,d;for(d in a)b++;return b},hideErrors:function(){this.addWrapper(this.toHide).hide()},valid:function(){return this.size()==
+0},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{c(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").focus().trigger("focusin")}catch(a){}},findLastActive:function(){var a=this.lastActive;return a&&c.grep(this.errorList,function(b){return b.element.name==a.name}).length==1&&a},elements:function(){var a=this,b={};return c(this.currentForm).find("input, select, textarea").not(":submit, :reset, :image, [disabled]").not(this.settings.ignore).filter(function(){!this.name&&
+a.settings.debug&&window.console&&console.error("%o has no name assigned",this);if(this.name in b||!a.objectLength(c(this).rules()))return false;return b[this.name]=true})},clean:function(a){return c(a)[0]},errors:function(){return c(this.settings.errorElement+"."+this.settings.errorClass,this.errorContext)},reset:function(){this.successList=[];this.errorList=[];this.errorMap={};this.toShow=c([]);this.toHide=c([]);this.currentElements=c([])},prepareForm:function(){this.reset();this.toHide=this.errors().add(this.containers)},
+prepareElement:function(a){this.reset();this.toHide=this.errorsFor(a)},check:function(a){a=this.validationTargetFor(this.clean(a));var b=c(a).rules(),d=false,e;for(e in b){var f={method:e,parameters:b[e]};try{var g=c.validator.methods[e].call(this,a.value.replace(/\r/g,""),a,f.parameters);if(g=="dependency-mismatch")d=true;else{d=false;if(g=="pending"){this.toHide=this.toHide.not(this.errorsFor(a));return}if(!g){this.formatAndAdd(a,f);return false}}}catch(h){this.settings.debug&&window.console&&console.log("exception occured when checking element "+
+a.id+", check the '"+f.method+"' method",h);throw h;}}if(!d){this.objectLength(b)&&this.successList.push(a);return true}},customMetaMessage:function(a,b){if(c.metadata){var d=this.settings.meta?c(a).metadata()[this.settings.meta]:c(a).metadata();return d&&d.messages&&d.messages[b]}},customMessage:function(a,b){var d=this.settings.messages[a];return d&&(d.constructor==String?d:d[b])},findDefined:function(){for(var a=0;a<arguments.length;a++)if(arguments[a]!==undefined)return arguments[a]},defaultMessage:function(a,
+b){return this.findDefined(this.customMessage(a.name,b),this.customMetaMessage(a,b),!this.settings.ignoreTitle&&a.title||undefined,c.validator.messages[b],"<strong>Warning: No message defined for "+a.name+"</strong>")},formatAndAdd:function(a,b){var d=this.defaultMessage(a,b.method),e=/\$?\{(\d+)\}/g;if(typeof d=="function")d=d.call(this,b.parameters,a);else if(e.test(d))d=jQuery.format(d.replace(e,"{$1}"),b.parameters);this.errorList.push({message:d,element:a});this.errorMap[a.name]=d;this.submitted[a.name]=
+d},addWrapper:function(a){if(this.settings.wrapper)a=a.add(a.parent(this.settings.wrapper));return a},defaultShowErrors:function(){for(var a=0;this.errorList[a];a++){var b=this.errorList[a];this.settings.highlight&&this.settings.highlight.call(this,b.element,this.settings.errorClass,this.settings.validClass);this.showLabel(b.element,b.message)}if(this.errorList.length)this.toShow=this.toShow.add(this.containers);if(this.settings.success)for(a=0;this.successList[a];a++)this.showLabel(this.successList[a]);
+if(this.settings.unhighlight){a=0;for(b=this.validElements();b[a];a++)this.settings.unhighlight.call(this,b[a],this.settings.errorClass,this.settings.validClass)}this.toHide=this.toHide.not(this.toShow);this.hideErrors();this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return c(this.errorList).map(function(){return this.element})},showLabel:function(a,b){var d=this.errorsFor(a);if(d.length){d.removeClass(this.settings.validClass).addClass(this.settings.errorClass);
+d.attr("generated")&&d.html(b)}else{d=c("<"+this.settings.errorElement+"/>").attr({"for":this.idOrName(a),generated:true}).addClass(this.settings.errorClass).html(b||"");if(this.settings.wrapper)d=d.hide().show().wrap("<"+this.settings.wrapper+"/>").parent();this.labelContainer.append(d).length||(this.settings.errorPlacement?this.settings.errorPlacement(d,c(a)):d.insertAfter(a))}if(!b&&this.settings.success){d.text("");typeof this.settings.success=="string"?d.addClass(this.settings.success):this.settings.success(d)}this.toShow=
+this.toShow.add(d)},errorsFor:function(a){var b=this.idOrName(a);return this.errors().filter(function(){return c(this).attr("for")==b})},idOrName:function(a){return this.groups[a.name]||(this.checkable(a)?a.name:a.id||a.name)},validationTargetFor:function(a){if(this.checkable(a))a=this.findByName(a.name).not(this.settings.ignore)[0];return a},checkable:function(a){return/radio|checkbox/i.test(a.type)},findByName:function(a){var b=this.currentForm;return c(document.getElementsByName(a)).map(function(d,
+e){return e.form==b&&e.name==a&&e||null})},getLength:function(a,b){switch(b.nodeName.toLowerCase()){case "select":return c("option:selected",b).length;case "input":if(this.checkable(b))return this.findByName(b.name).filter(":checked").length}return a.length},depend:function(a,b){return this.dependTypes[typeof a]?this.dependTypes[typeof a](a,b):true},dependTypes:{"boolean":function(a){return a},string:function(a,b){return!!c(a,b.form).length},"function":function(a,b){return a(b)}},optional:function(a){return!c.validator.methods.required.call(this,
+c.trim(a.value),a)&&"dependency-mismatch"},startRequest:function(a){if(!this.pending[a.name]){this.pendingRequest++;this.pending[a.name]=true}},stopRequest:function(a,b){this.pendingRequest--;if(this.pendingRequest<0)this.pendingRequest=0;delete this.pending[a.name];if(b&&this.pendingRequest==0&&this.formSubmitted&&this.form()){c(this.currentForm).submit();this.formSubmitted=false}else if(!b&&this.pendingRequest==0&&this.formSubmitted){c(this.currentForm).triggerHandler("invalid-form",[this]);this.formSubmitted=
+false}},previousValue:function(a){return c.data(a,"previousValue")||c.data(a,"previousValue",{old:null,valid:true,message:this.defaultMessage(a,"remote")})}},classRuleSettings:{required:{required:true},email:{email:true},url:{url:true},date:{date:true},dateISO:{dateISO:true},dateDE:{dateDE:true},number:{number:true},numberDE:{numberDE:true},digits:{digits:true},creditcard:{creditcard:true}},addClassRules:function(a,b){a.constructor==String?this.classRuleSettings[a]=b:c.extend(this.classRuleSettings,
+a)},classRules:function(a){var b={};(a=c(a).attr("class"))&&c.each(a.split(" "),function(){this in c.validator.classRuleSettings&&c.extend(b,c.validator.classRuleSettings[this])});return b},attributeRules:function(a){var b={};a=c(a);for(var d in c.validator.methods){var e;if(e=d==="required"&&typeof c.fn.prop==="function"?a.prop(d):a.attr(d))b[d]=e;else if(a[0].getAttribute("type")===d)b[d]=true}b.maxlength&&/-1|2147483647|524288/.test(b.maxlength)&&delete b.maxlength;return b},metadataRules:function(a){if(!c.metadata)return{};
+var b=c.data(a.form,"validator").settings.meta;return b?c(a).metadata()[b]:c(a).metadata()},staticRules:function(a){var b={},d=c.data(a.form,"validator");if(d.settings.rules)b=c.validator.normalizeRule(d.settings.rules[a.name])||{};return b},normalizeRules:function(a,b){c.each(a,function(d,e){if(e===false)delete a[d];else if(e.param||e.depends){var f=true;switch(typeof e.depends){case "string":f=!!c(e.depends,b.form).length;break;case "function":f=e.depends.call(b,b)}if(f)a[d]=e.param!==undefined?
+e.param:true;else delete a[d]}});c.each(a,function(d,e){a[d]=c.isFunction(e)?e(b):e});c.each(["minlength","maxlength","min","max"],function(){if(a[this])a[this]=Number(a[this])});c.each(["rangelength","range"],function(){if(a[this])a[this]=[Number(a[this][0]),Number(a[this][1])]});if(c.validator.autoCreateRanges){if(a.min&&a.max){a.range=[a.min,a.max];delete a.min;delete a.max}if(a.minlength&&a.maxlength){a.rangelength=[a.minlength,a.maxlength];delete a.minlength;delete a.maxlength}}a.messages&&delete a.messages;
+return a},normalizeRule:function(a){if(typeof a=="string"){var b={};c.each(a.split(/\s/),function(){b[this]=true});a=b}return a},addMethod:function(a,b,d){c.validator.methods[a]=b;c.validator.messages[a]=d!=undefined?d:c.validator.messages[a];b.length<3&&c.validator.addClassRules(a,c.validator.normalizeRule(a))},methods:{required:function(a,b,d){if(!this.depend(d,b))return"dependency-mismatch";switch(b.nodeName.toLowerCase()){case "select":return(a=c(b).val())&&a.length>0;case "input":if(this.checkable(b))return this.getLength(a,
+b)>0;default:return c.trim(a).length>0}},remote:function(a,b,d){if(this.optional(b))return"dependency-mismatch";var e=this.previousValue(b);this.settings.messages[b.name]||(this.settings.messages[b.name]={});e.originalMessage=this.settings.messages[b.name].remote;this.settings.messages[b.name].remote=e.message;d=typeof d=="string"&&{url:d}||d;if(this.pending[b.name])return"pending";if(e.old===a)return e.valid;e.old=a;var f=this;this.startRequest(b);var g={};g[b.name]=a;c.ajax(c.extend(true,{url:d,
+mode:"abort",port:"validate"+b.name,dataType:"json",data:g,success:function(h){f.settings.messages[b.name].remote=e.originalMessage;var j=h===true;if(j){var i=f.formSubmitted;f.prepareElement(b);f.formSubmitted=i;f.successList.push(b);f.showErrors()}else{i={};h=h||f.defaultMessage(b,"remote");i[b.name]=e.message=c.isFunction(h)?h(a):h;f.showErrors(i)}e.valid=j;f.stopRequest(b,j)}},d));return"pending"},minlength:function(a,b,d){return this.optional(b)||this.getLength(c.trim(a),b)>=d},maxlength:function(a,
+b,d){return this.optional(b)||this.getLength(c.trim(a),b)<=d},rangelength:function(a,b,d){a=this.getLength(c.trim(a),b);return this.optional(b)||a>=d[0]&&a<=d[1]},min:function(a,b,d){return this.optional(b)||a>=d},max:function(a,b,d){return this.optional(b)||a<=d},range:function(a,b,d){return this.optional(b)||a>=d[0]&&a<=d[1]},email:function(a,b){return this.optional(b)||/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(a)},
+url:function(a,b){return this.optional(b)||/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(a)},
+date:function(a,b){return this.optional(b)||!/Invalid|NaN/.test(new Date(a))},dateISO:function(a,b){return this.optional(b)||/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(a)},number:function(a,b){return this.optional(b)||/^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/.test(a)},digits:function(a,b){return this.optional(b)||/^\d+$/.test(a)},creditcard:function(a,b){if(this.optional(b))return"dependency-mismatch";if(/[^0-9 -]+/.test(a))return false;var d=0,e=0,f=false;a=a.replace(/\D/g,"");for(var g=a.length-1;g>=
+0;g--){e=a.charAt(g);e=parseInt(e,10);if(f)if((e*=2)>9)e-=9;d+=e;f=!f}return d%10==0},accept:function(a,b,d){d=typeof d=="string"?d.replace(/,/g,"|"):"png|jpe?g|gif";return this.optional(b)||a.match(RegExp(".("+d+")$","i"))},equalTo:function(a,b,d){d=c(d).unbind(".validate-equalTo").bind("blur.validate-equalTo",function(){c(b).valid()});return a==d.val()}}});c.format=c.validator.format})(jQuery);
+(function(c){var a={};if(c.ajaxPrefilter)c.ajaxPrefilter(function(d,e,f){e=d.port;if(d.mode=="abort"){a[e]&&a[e].abort();a[e]=f}});else{var b=c.ajax;c.ajax=function(d){var e=("port"in d?d:c.ajaxSettings).port;if(("mode"in d?d:c.ajaxSettings).mode=="abort"){a[e]&&a[e].abort();return a[e]=b.apply(this,arguments)}return b.apply(this,arguments)}}})(jQuery);
+(function(c){!jQuery.event.special.focusin&&!jQuery.event.special.focusout&&document.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(e){e=c.event.fix(e);e.type=b;return c.event.handle.call(this,e)}c.event.special[b]={setup:function(){this.addEventListener(a,d,true)},teardown:function(){this.removeEventListener(a,d,true)},handler:function(e){arguments[0]=c.event.fix(e);arguments[0].type=b;return c.event.handle.apply(this,arguments)}}});c.extend(c.fn,{validateDelegate:function(a,
+b,d){return this.bind(b,function(e){var f=c(e.target);if(f.is(a))return d.apply(f,arguments)})}})})(jQuery);
diff --git a/web/js/map-OpenLayers.js b/web/js/map-OpenLayers.js
index b911b7c71..c8bdb85df 100644
--- a/web/js/map-OpenLayers.js
+++ b/web/js/map-OpenLayers.js
@@ -308,6 +308,12 @@ OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, {
fixmystreet_activate_drag();
}
fixmystreet_update_pin(lonlat);
+ // check to see if markers are visible. We click the
+ // link so that it updates the text in case they go
+ // back
+ if ( ! fixmystreet.markers.getVisibility() ) {
+ $('#hide_pins_link').click();
+ }
if (fixmystreet.page == 'new') {
return;
}
diff --git a/web/js/map-bing-ol.js b/web/js/map-bing-ol.js
index 391f837c6..94b777134 100644
--- a/web/js/map-bing-ol.js
+++ b/web/js/map-bing-ol.js
@@ -88,11 +88,13 @@ OpenLayers.Layer.Bing = OpenLayers.Class(OpenLayers.Layer.XYZ, {
"http://c.tilma.mysociety.org/sv/${z}/${x}/${y}.png"
];
} else {
+ var type = '';
+ if (z > 10) type = '&productSet=mmOS';
var url = [
- "http://ecn.t0.tiles.virtualearth.net/tiles/r${id}.png?g=701&productSet=mmOS",
- "http://ecn.t1.tiles.virtualearth.net/tiles/r${id}.png?g=701&productSet=mmOS",
- "http://ecn.t2.tiles.virtualearth.net/tiles/r${id}.png?g=701&productSet=mmOS",
- "http://ecn.t3.tiles.virtualearth.net/tiles/r${id}.png?g=701&productSet=mmOS"
+ "http://ecn.t0.tiles.virtualearth.net/tiles/r${id}.png?g=701" + type,
+ "http://ecn.t1.tiles.virtualearth.net/tiles/r${id}.png?g=701" + type,
+ "http://ecn.t2.tiles.virtualearth.net/tiles/r${id}.png?g=701" + type,
+ "http://ecn.t3.tiles.virtualearth.net/tiles/r${id}.png?g=701" + type
];
}
var s = '' + x + y + z;