aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.cypress/cypress/integration/hackney.js19
-rw-r--r--CHANGELOG.md2
-rwxr-xr-xbin/browser-tests4
-rwxr-xr-xbin/fixmystreet.com/fixture1
-rw-r--r--bin/import_categories5
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth/Social.pm34
-rw-r--r--perllib/FixMyStreet/Cobrand/Default.pm3
-rw-r--r--perllib/FixMyStreet/Cobrand/Hackney.pm187
-rw-r--r--perllib/FixMyStreet/Cobrand/UKCouncils.pm2
-rw-r--r--perllib/FixMyStreet/Cobrand/Zurich.pm2
-rw-r--r--perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm2
-rw-r--r--perllib/FixMyStreet/Email.pm4
-rw-r--r--perllib/FixMyStreet/Geocode/OSM.pm1
-rw-r--r--perllib/FixMyStreet/Queue/Item/Report.pm2
-rw-r--r--perllib/FixMyStreet/Script/Alerts.pm2
-rw-r--r--perllib/FixMyStreet/SendReport/Email.pm5
-rw-r--r--perllib/OIDC/Lite/Client/WebServer/AuthCodeFlow.pm (renamed from perllib/OIDC/Lite/Client/WebServer/Azure.pm)13
-rw-r--r--perllib/Open311/PopulateServiceList.pm8
-rwxr-xr-xperllib/Open311/PostServiceRequestUpdates.pm2
-rw-r--r--t/Mock/MapIt.pm1
-rw-r--r--t/Mock/OpenIDConnect.pm48
-rw-r--r--t/app/controller/admin/report_edit.t20
-rw-r--r--t/app/controller/auth_social.t68
-rw-r--r--t/app/controller/moderate.t3
-rw-r--r--t/cobrand/get_body_sender.t18
-rw-r--r--t/cobrand/hackney.t292
-rw-r--r--t/email.t6
-rw-r--r--templates/email/hackney/_email_bottom.html28
-rw-r--r--templates/email/hackney/_email_color_overrides.html25
-rw-r--r--templates/email/hackney/_email_setting_overrides.html7
-rw-r--r--templates/email/hackney/signature.txt2
-rw-r--r--templates/email/hackney/site-name.txt1
-rw-r--r--templates/web/base/admin/reports/edit.html15
-rw-r--r--templates/web/fixmystreet-uk-councils/about/privacy.html6
-rw-r--r--templates/web/hackney/around/intro.html5
-rw-r--r--templates/web/hackney/auth/general.html88
-rw-r--r--templates/web/hackney/footer_extra.html10
-rw-r--r--templates/web/hackney/footer_extra_js.html7
-rw-r--r--templates/web/hackney/header_extra.html2
-rw-r--r--templates/web/hackney/report/form/user.html29
-rw-r--r--templates/web/hackney/site-name.html1
-rw-r--r--templates/web/hackney/tracking_code.html11
-rw-r--r--web/cobrands/fixmystreet-uk-councils/alloy.js95
-rw-r--r--web/cobrands/hackney/_colours.scss45
-rw-r--r--web/cobrands/hackney/assets.js246
-rw-r--r--web/cobrands/hackney/base.scss222
-rw-r--r--web/cobrands/hackney/hackney-search-icon.pngbin0 -> 464 bytes
-rw-r--r--web/cobrands/hackney/images/hackney-logo-white.pngbin0 -> 18214 bytes
-rw-r--r--web/cobrands/hackney/layout.scss143
-rw-r--r--web/cobrands/northamptonshire/assets.js21
50 files changed, 1685 insertions, 78 deletions
diff --git a/.cypress/cypress/integration/hackney.js b/.cypress/cypress/integration/hackney.js
new file mode 100644
index 000000000..a4293b028
--- /dev/null
+++ b/.cypress/cypress/integration/hackney.js
@@ -0,0 +1,19 @@
+describe('When you look at the Hackney site', function() {
+
+ beforeEach(function() {
+ cy.server();
+ cy.route('/report/new/ajax*').as('report-ajax');
+ cy.visit('http://hackney.localhost:3001/');
+ cy.contains('Hackney Council');
+ cy.should('not.contain', 'Hackney Borough');
+ cy.get('[name=pc]').type('E8 1DY');
+ cy.get('[name=pc]').parents('form').submit();
+ });
+
+ it('uses the correct name', function() {
+ cy.get('#map_box').click();
+ cy.wait('@report-ajax');
+ cy.get('select:eq(4)').select('Potholes');
+ cy.contains('sent to Hackney Council');
+ });
+});
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 93292785c..e77fcfa42 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@
- Skip accounts without email when sending inactive emails.
- Include file extensions in Dropzone accepted photo config.
- Fix photo orientation in modern browsers.
+ - Improve compatibility with G Suite OpenID Connect authentication. #3032
- Admin improvements:
- Display user name/email for contributed as reports. #2990
- Interface for enabling anonymous reports for certain categories. #2989
@@ -28,6 +29,7 @@
- Links inside `#front-main` can be customised using `$primary_link_*` Sass variables. #3007
- Add option to show front end testing code coverage. #3036
- Add function to fetch user's country from Gaze.
+ - Add cobrand hook to specify custom domain for VERP emails.
- Open311 improvements:
- Use devolved data on update sending.
- UK:
diff --git a/bin/browser-tests b/bin/browser-tests
index 32b844127..e0b9bedfe 100755
--- a/bin/browser-tests
+++ b/bin/browser-tests
@@ -11,7 +11,7 @@ my ($cobrand, $coords, $area_id, $name, $mapit_url, $coverage);
BEGIN {
$config_file = 'conf/general.yml-example';
- $cobrand = [ 'borsetshire', 'fixmystreet', 'northamptonshire', 'bathnes', 'buckinghamshire', 'hounslow', 'isleofwight', 'peterborough', 'tfl' ];
+ $cobrand = [ 'borsetshire', 'fixmystreet', 'northamptonshire', 'bathnes', 'buckinghamshire', 'hounslow', 'isleofwight', 'peterborough', 'tfl', 'hackney' ];
$coords = '51.532851,-2.284277';
$area_id = 2608;
$name = 'Borsetshire';
@@ -190,7 +190,7 @@ browser-tests [running options] [fixture options] [cypress options]
--help this help message
Fixture option:
- --cobrand Cobrand(s) to use, default is fixmystreet,northamptonshire,bathnes,buckinghamshire,isleofwight,peterborough,tfl
+ --cobrand Cobrand(s) to use, default is fixmystreet,northamptonshire,bathnes,buckinghamshire,isleofwight,peterborough,tfl,hackney
--coords Default co-ordinates for created reports
--area_id Area ID to use for created body
--name Name to use for created body
diff --git a/bin/fixmystreet.com/fixture b/bin/fixmystreet.com/fixture
index 082fc6309..e8dd3f364 100755
--- a/bin/fixmystreet.com/fixture
+++ b/bin/fixmystreet.com/fixture
@@ -99,6 +99,7 @@ if ($opt->test_fixtures) {
{ area_id => 2257, categories => ['Flytipping', 'Graffiti'], name => 'Chiltern District Council' },
{ area_id => 2397, categories => [ 'Graffiti' ], name => 'Northampton Borough Council' },
{ area_id => 2483, categories => [ 'Potholes', 'Other' ], name => 'Hounslow Borough Council' },
+ { area_id => 2508, categories => [ 'Potholes', 'Other' ], name => 'Hackney Council' },
{ area_id => 2636, categories => [ 'Potholes', 'Private', 'Extra' ], name => 'Isle of Wight Council' },
{ area_id => 2566, categories => [ 'Fallen branch', 'Light Out', 'Light Dim', 'Fallen Tree', 'Damaged Tree' ], name => 'Peterborough City Council' },
{ area_id => 2498, categories => [ 'Incorrect timetable', 'Glass broken', 'Mobile Crane Operation' ], name => 'TfL' },
diff --git a/bin/import_categories b/bin/import_categories
index 744759f1f..23a1089f5 100644
--- a/bin/import_categories
+++ b/bin/import_categories
@@ -46,11 +46,10 @@ if (!$opt->commit) {
my $config = decode_json(path($ARGV[0])->slurp_utf8);
my $body = FixMyStreet::DB->resultset('Body')->find({ name => $opt->body });
+die "Couldn't find body " . $opt->body unless $body;
$body->contacts->delete_all if $opt->delete;
-die "Couldn't find body" unless $body;
-
my $groups = $config->{groups};
if ($groups) {
for my $group (keys %$groups) {
@@ -72,12 +71,14 @@ sub make_categories {
category => $cat->{category}
});
$child_cat->email($cat->{email});
+ $child_cat->send_method($cat->{devolved}) if $cat->{devolved};
$child_cat->state('confirmed');
$child_cat->editor($0);
$child_cat->whenedited(\'current_timestamp');
$child_cat->note($child_cat->in_storage ? 'Updated by import_categories' : 'Created by import_categories');
say colored("WARNING", 'red') . " " . $child_cat->category . " already exists" if $child_cat->in_storage and $child_cat->category ne 'Other (TfL)';
$child_cat->extra(undef) if $child_cat->in_storage;
+ $child_cat->set_extra_metadata(open311_protect => 1) if $cat->{open311_protect};
if ($group) {
my $groups = $child_cat->groups;
diff --git a/perllib/FixMyStreet/App/Controller/Auth/Social.pm b/perllib/FixMyStreet/App/Controller/Auth/Social.pm
index 06e67573f..ce94fe256 100644
--- a/perllib/FixMyStreet/App/Controller/Auth/Social.pm
+++ b/perllib/FixMyStreet/App/Controller/Auth/Social.pm
@@ -6,7 +6,7 @@ BEGIN { extends 'Catalyst::Controller'; }
use Net::Facebook::Oauth2;
use Net::Twitter::Lite::WithAPIv1_1;
-use OIDC::Lite::Client::WebServer::Azure;
+use OIDC::Lite::Client::WebServer::AuthCodeFlow;
use URI::Escape;
use mySociety::AuthToken;
@@ -167,7 +167,7 @@ sub oidc : Private {
my $config = $c->cobrand->feature('oidc_login');
- OIDC::Lite::Client::WebServer::Azure->new(
+ OIDC::Lite::Client::WebServer::AuthCodeFlow->new(
id => $config->{client_id},
secret => $config->{secret},
authorize_uri => $config->{auth_uri},
@@ -179,7 +179,9 @@ sub oidc_sign_in : Private {
my ( $self, $c ) = @_;
$c->detach( '/page_error_403_access_denied', [] ) if FixMyStreet->config('SIGNUPS_DISABLED');
- $c->detach( '/page_error_400_bad_request', [] ) unless $c->cobrand->feature('oidc_login');
+
+ my $cfg = $c->cobrand->feature('oidc_login');
+ $c->detach( '/page_error_400_bad_request', [] ) unless $cfg;
my $oidc = $c->forward('oidc');
my $nonce = $self->generate_nonce();
@@ -190,6 +192,15 @@ sub oidc_sign_in : Private {
extra => {
response_mode => 'form_post',
nonce => $nonce,
+ # auth_extra_params provides a way to pass custom parameters
+ # to the OIDC endpoint for the intial authentication request.
+ # This allows, for example, a custom scope to be used,
+ # or the `hd` parameter which customises the appearance of
+ # the login form.
+ # This is primarily useful for Google G Suite authentication - see
+ # available parameters here:
+ # https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters
+ %{ $cfg->{auth_extra_params} || {} } ,
},
);
@@ -201,14 +212,14 @@ sub oidc_sign_in : Private {
# The OIDC endpoint may require a specific URI to be called to log the user
# out when they log out of FMS.
- if ( my $redirect_uri = $c->cobrand->feature('oidc_login')->{logout_uri} ) {
+ if ( my $redirect_uri = $cfg->{logout_uri} ) {
$redirect_uri .= "?post_logout_redirect_uri=";
$redirect_uri .= URI::Escape::uri_escape( $c->uri_for('/auth/sign_out') );
$oauth{logout_redirect_uri} = $redirect_uri;
}
# The OIDC endpoint may provide a specific URI for changing the user's password.
- if ( my $password_change_uri = $c->cobrand->feature('oidc_login')->{password_change_uri} ) {
+ if ( my $password_change_uri = $cfg->{password_change_uri} ) {
$oauth{change_password_uri} = $oidc->uri_to_redirect(
uri => $password_change_uri,
redirect_uri => $c->uri_for('/auth/OIDC'),
@@ -279,6 +290,7 @@ sub oidc_callback: Path('/auth/OIDC') : Args(0) {
eval {
$id_token = $oidc->get_access_token(
code => $c->get_param('code'),
+ redirect_uri => $c->uri_for('/auth/OIDC')
);
};
if ($@) {
@@ -294,10 +306,18 @@ sub oidc_callback: Path('/auth/OIDC') : Args(0) {
# check that the nonce matches what we set in the user session
$c->detach('/page_error_500_internal_error', ['invalid id_token']) unless $id_token->payload->{nonce} eq $c->session->{oauth}{nonce};
+ if (my $domains = $c->cobrand->feature('oidc_login')->{allowed_domains}) {
+ # Check that the hd payload is present in the token and matches the
+ # list of allowed domains from the config
+ my $hd = $id_token->payload->{hd};
+ my %allowed_domains = map { $_ => 1} @$domains;
+ $c->detach('oauth_failure') unless $allowed_domains{$hd};
+ }
+
# Some claims need parsing into a friendlier format
- # XXX check how much of this is Westminster/Azure-specific
- my $name = join(" ", $id_token->payload->{given_name}, $id_token->payload->{family_name});
+ my $name = $id_token->payload->{name} || join(" ", $id_token->payload->{given_name}, $id_token->payload->{family_name});
my $email = $id_token->payload->{email};
+
# WCC Azure provides a single email address as an array for some reason
my $emails = $id_token->payload->{emails};
if ($emails && @$emails) {
diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm
index 73340338b..bd8b7e4b4 100644
--- a/perllib/FixMyStreet/Cobrand/Default.pm
+++ b/perllib/FixMyStreet/Cobrand/Default.pm
@@ -957,9 +957,10 @@ Get stats to display on the council reports page
sub get_report_stats { return 0; }
sub get_body_sender {
- my ( $self, $body, $category ) = @_;
+ my ( $self, $body, $problem ) = @_;
# look up via category
+ my $category = $problem->category;
my $contact = $body->contacts->search( { category => $category } )->first;
if ( $body->can_be_devolved && $contact && $contact->send_method ) {
return { method => $contact->send_method, config => $contact, contact => $contact };
diff --git a/perllib/FixMyStreet/Cobrand/Hackney.pm b/perllib/FixMyStreet/Cobrand/Hackney.pm
new file mode 100644
index 000000000..234573e3e
--- /dev/null
+++ b/perllib/FixMyStreet/Cobrand/Hackney.pm
@@ -0,0 +1,187 @@
+package FixMyStreet::Cobrand::Hackney;
+use parent 'FixMyStreet::Cobrand::Whitelabel';
+
+use strict;
+use warnings;
+use mySociety::EmailUtil qw(is_valid_email);
+
+sub council_area_id { return 2508; }
+sub council_area { return 'Hackney'; }
+sub council_name { return 'Hackney Council'; }
+sub council_url { return 'hackney'; }
+sub send_questionnaires { 0 }
+
+sub disambiguate_location {
+ my $self = shift;
+ my $string = shift;
+
+ my $town = 'Hackney';
+
+ # Teale Street is on the boundary with Tower Hamlets and
+ # shows the 'please use fixmystreet.com' message, but Hackney
+ # do provide services on that road.
+ ($string, $town) = ('E2 9AA', '') if $string =~ /^teale\s+st/i;
+
+ return {
+ %{ $self->SUPER::disambiguate_location() },
+ string => $string,
+ town => $town,
+ centre => '51.552267,-0.063316',
+ bounds => [ 51.519814, -0.104511, 51.577784, -0.016527 ],
+ };
+}
+
+sub do_not_reply_email { shift->feature('do_not_reply_email') }
+
+sub verp_email_domain { shift->feature('verp_email_domain') }
+
+sub get_geocoder {
+ return 'OSM'; # default of Bing gives poor results, let's try overriding.
+}
+
+sub geocoder_munge_query_params {
+ my ($self, $params) = @_;
+
+ $params->{addressdetails} = 1;
+}
+
+sub geocoder_munge_results {
+ my ($self, $result) = @_;
+ if (my $a = $result->{address}) {
+ if ($a->{road} && $a->{suburb} && $a->{postcode}) {
+ $result->{display_name} = "$a->{road}, $a->{suburb}, $a->{postcode}";
+ return;
+ }
+ }
+ $result->{display_name} = '' unless $result->{display_name} =~ /Hackney/;
+ $result->{display_name} =~ s/, United Kingdom$//;
+ $result->{display_name} =~ s/, London, Greater London, England//;
+ $result->{display_name} =~ s/, London Borough of Hackney//;
+}
+
+
+sub open311_config {
+ my ($self, $row, $h, $params) = @_;
+
+ $params->{multi_photos} = 1;
+}
+
+sub open311_extra_data {
+ my ($self, $row, $h, $extra, $contact) = @_;
+
+ my $open311_only = [
+ { name => 'report_url',
+ value => $h->{url} },
+ { name => 'title',
+ value => $row->title },
+ { name => 'description',
+ value => $row->detail },
+ { name => 'category',
+ value => $row->category },
+ ];
+
+ # Make sure contact 'email' set correctly for Open311
+ if (my $sent_to = $row->get_extra_metadata('sent_to')) {
+ $row->unset_extra_metadata('sent_to');
+ my $code = $sent_to->{$contact->email};
+ $contact->email($code) if $code;
+ }
+
+ return $open311_only;
+}
+
+sub map_type { 'OSM' }
+
+sub default_map_zoom { 5 }
+
+sub admin_user_domain { 'hackney.gov.uk' }
+
+sub social_auth_enabled {
+ my $self = shift;
+
+ return $self->feature('oidc_login') ? 1 : 0;
+}
+
+sub anonymous_account {
+ my $self = shift;
+ return {
+ email => $self->feature('anonymous_account') . '@' . $self->admin_user_domain,
+ name => 'Anonymous user',
+ };
+}
+
+sub open311_skip_existing_contact {
+ my ($self, $contact) = @_;
+
+ # For Hackney we want the 'protected' flag to prevent any changes to this
+ # contact at all.
+ return $contact->get_extra_metadata("open311_protect") ? 1 : 0;
+}
+
+sub open311_filter_contacts_for_deletion {
+ my ($self, $contacts) = @_;
+
+ # Don't delete open311 protected contacts when importing
+ return $contacts->search({
+ extra => { -not_like => '%T15:open311_protect,I1:1%' },
+ });
+}
+
+sub problem_is_within_area_type {
+ my ($self, $problem, $type) = @_;
+ my $layer_map = {
+ park => "greenspaces:hackney_park",
+ estate => "housing:lbh_estate",
+ };
+ my $layer = $layer_map->{$type};
+ return unless $layer;
+
+ my ($x, $y) = $problem->local_coords;
+
+ my $cfg = {
+ url => "https://map.hackney.gov.uk/geoserver/wfs",
+ srsname => "urn:ogc:def:crs:EPSG::27700",
+ typename => $layer,
+ outputformat => "json",
+ filter => "<Filter xmlns:gml=\"http://www.opengis.net/gml\"><Intersects><PropertyName>geom</PropertyName><gml:Point srsName=\"27700\"><gml:coordinates>$x,$y</gml:coordinates></gml:Point></Intersects></Filter>",
+ };
+
+ my $features = $self->_fetch_features($cfg, $x, $y) || [];
+ return scalar @$features ? 1 : 0;
+}
+
+sub get_body_sender {
+ my ( $self, $body, $problem ) = @_;
+
+ my $contact = $body->contacts->search( { category => $problem->category } )->first;
+
+ my $parts = join '\s*', qw(^ park : (.*?) ; estate : (.*?) ; other : (.*?) $);
+ my $regex = qr/$parts/i;
+ if (my ($park, $estate, $other) = $contact->email =~ $regex) {
+ my $to = $other;
+ if ($self->problem_is_within_area_type($problem, 'park')) {
+ $to = $park;
+ } elsif ($self->problem_is_within_area_type($problem, 'estate')) {
+ $to = $estate;
+ }
+ $problem->set_extra_metadata(sent_to => { $contact->email => $to });
+ if (is_valid_email($to)) {
+ return { method => 'Email', contact => $contact };
+ }
+ }
+ return $self->SUPER::get_body_sender($body, $problem);
+}
+
+# Translate email address to actual delivery address
+sub munge_sendreport_params {
+ my ($self, $row, $h, $params) = @_;
+
+ my $sent_to = $row->get_extra_metadata('sent_to') or return;
+ $row->unset_extra_metadata('sent_to');
+ for my $recip (@{$params->{To}}) {
+ my ($email, $name) = @$recip;
+ $recip->[0] = $sent_to->{$email} if $sent_to->{$email};
+ }
+}
+
+1;
diff --git a/perllib/FixMyStreet/Cobrand/UKCouncils.pm b/perllib/FixMyStreet/Cobrand/UKCouncils.pm
index 21dd2d455..7456d9ddf 100644
--- a/perllib/FixMyStreet/Cobrand/UKCouncils.pm
+++ b/perllib/FixMyStreet/Cobrand/UKCouncils.pm
@@ -392,7 +392,7 @@ sub _fetch_features_url {
SRSNAME => $cfg->{srsname},
TYPENAME => $cfg->{typename},
VERSION => "1.1.0",
- outputformat => "geojson",
+ outputformat => $cfg->{outputformat} || "geojson",
$cfg->{filter} ? ( Filter => $cfg->{filter} ) : ( BBOX => $cfg->{bbox} ),
);
diff --git a/perllib/FixMyStreet/Cobrand/Zurich.pm b/perllib/FixMyStreet/Cobrand/Zurich.pm
index 3cf678f9c..42932ec41 100644
--- a/perllib/FixMyStreet/Cobrand/Zurich.pm
+++ b/perllib/FixMyStreet/Cobrand/Zurich.pm
@@ -217,7 +217,7 @@ sub allow_photo_display {
}
sub get_body_sender {
- my ( $self, $body, $category ) = @_;
+ my ( $self, $body, $problem ) = @_;
return { method => 'Zurich' };
}
diff --git a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
index 1805e1fd2..6d14e6a9f 100644
--- a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
+++ b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
@@ -163,7 +163,7 @@ sub compare_extra {
my $new = $other->get_extra_metadata;
my $both = { %$old, %$new };
- my @all_keys = sort keys %$both;
+ my @all_keys = grep { $_ ne 'sent_to' } sort keys %$both;
my @s;
foreach (@all_keys) {
if ($old->{$_} && $new->{$_}) {
diff --git a/perllib/FixMyStreet/Email.pm b/perllib/FixMyStreet/Email.pm
index 3d7b48539..0cc6a880c 100644
--- a/perllib/FixMyStreet/Email.pm
+++ b/perllib/FixMyStreet/Email.pm
@@ -79,7 +79,9 @@ sub _render_template {
}
sub unique_verp_id {
- sprintf('fms-%s@%s', generate_verp_token(@_), FixMyStreet->config('EMAIL_DOMAIN'));
+ my $parts = shift;
+ my $domain = shift || FixMyStreet->config('EMAIL_DOMAIN');
+ sprintf('fms-%s@%s', generate_verp_token(@$parts), $domain);
}
sub _unique_id {
diff --git a/perllib/FixMyStreet/Geocode/OSM.pm b/perllib/FixMyStreet/Geocode/OSM.pm
index 20e653cf6..06162d74c 100644
--- a/perllib/FixMyStreet/Geocode/OSM.pm
+++ b/perllib/FixMyStreet/Geocode/OSM.pm
@@ -45,6 +45,7 @@ sub string {
if $params->{bounds};
$query_params{countrycodes} = $params->{country}
if $params->{country};
+ $c->cobrand->call_hook(geocoder_munge_query_params => \%query_params);
$url .= join('&', map { "$_=$query_params{$_}" } sort keys %query_params);
$c->stash->{geocoder_url} = $url;
diff --git a/perllib/FixMyStreet/Queue/Item/Report.pm b/perllib/FixMyStreet/Queue/Item/Report.pm
index e38987838..60e9ad3dc 100644
--- a/perllib/FixMyStreet/Queue/Item/Report.pm
+++ b/perllib/FixMyStreet/Queue/Item/Report.pm
@@ -172,7 +172,7 @@ sub _create_reporters {
my @dear;
my %reporters = ();
while (my $body = $bodies->next) {
- my $sender_info = $self->cobrand->get_body_sender( $body, $row->category );
+ my $sender_info = $self->cobrand_handler->get_body_sender( $body, $row );
my $sender = "FixMyStreet::SendReport::" . $sender_info->{method};
if ( ! exists $self->senders->{ $sender } ) {
diff --git a/perllib/FixMyStreet/Script/Alerts.pm b/perllib/FixMyStreet/Script/Alerts.pm
index d07728092..03373a8cc 100644
--- a/perllib/FixMyStreet/Script/Alerts.pm
+++ b/perllib/FixMyStreet/Script/Alerts.pm
@@ -327,7 +327,7 @@ sub _send_aggregated_alert_email(%) {
} );
$data{unsubscribe_url} = $cobrand->base_url( $data{cobrand_data} ) . '/A/' . $token->token;
- my $sender = FixMyStreet::Email::unique_verp_id('alert', $data{alert_id});
+ my $sender = FixMyStreet::Email::unique_verp_id([ 'alert', $data{alert_id} ], $cobrand->call_hook('verp_email_domain'));
my $result = FixMyStreet::Email::send_cron(
$data{schema},
"$data{template}.txt",
diff --git a/perllib/FixMyStreet/SendReport/Email.pm b/perllib/FixMyStreet/SendReport/Email.pm
index 81a25f896..2d5e85f3e 100644
--- a/perllib/FixMyStreet/SendReport/Email.pm
+++ b/perllib/FixMyStreet/SendReport/Email.pm
@@ -53,10 +53,11 @@ sub send_from {
sub envelope_sender {
my ($self, $row) = @_;
+ my $cobrand = $row->get_cobrand_logged;
if ($row->user->email && $row->user->email_verified) {
- return FixMyStreet::Email::unique_verp_id('report', $row->id);
+ return FixMyStreet::Email::unique_verp_id([ 'report', $row->id ], $cobrand->call_hook('verp_email_domain'));
}
- return $row->get_cobrand_logged->do_not_reply_email;
+ return $cobrand->do_not_reply_email;
}
sub send {
diff --git a/perllib/OIDC/Lite/Client/WebServer/Azure.pm b/perllib/OIDC/Lite/Client/WebServer/AuthCodeFlow.pm
index b19dce90e..33a9a788f 100644
--- a/perllib/OIDC/Lite/Client/WebServer/Azure.pm
+++ b/perllib/OIDC/Lite/Client/WebServer/AuthCodeFlow.pm
@@ -1,4 +1,4 @@
-package OIDC::Lite::Client::WebServer::Azure;
+package OIDC::Lite::Client::WebServer::AuthCodeFlow;
use strict;
use warnings;
@@ -8,12 +8,15 @@ use OIDC::Lite::Client::IDTokenResponseParser;
=head1 NAME
-OIDC::Lite::Client::WebServer::Azure - extension to auth against Azure AD B2C
+OIDC::Lite::Client::WebServer::AuthCodeFlow - extension to auth against an
+identity provider using the authorization code flow, such as Azure AD B2C or
+Google OAuth 2.0.
+More info: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowSteps
OIDC::Lite doesn't appear to support the authorisation code flow to get an
-ID token - only an access token. Azure returns all its claims in the id_token
-and doesn't support a UserInfo endpoint, so this extension adds support for
-parsing the id_token when calling get_access_token.
+ID token - only an access token. This flow returns all its claims in the id_token
+(and may not support a UserInfo endpoint e.g. Azure AD B2C), so this extension
+adds support for parsing the id_token when calling get_access_token.
=cut
diff --git a/perllib/Open311/PopulateServiceList.pm b/perllib/Open311/PopulateServiceList.pm
index 20fca90b3..a3672770c 100644
--- a/perllib/Open311/PopulateServiceList.pm
+++ b/perllib/Open311/PopulateServiceList.pm
@@ -145,6 +145,8 @@ sub _handle_existing_contact {
my $service_name = $self->_normalize_service_name;
my $protected = $contact->get_extra_metadata("open311_protect");
+ return if $self->_current_body_cobrand && $self->_current_body_cobrand->call_hook(open311_skip_existing_contact => $contact);
+
print $self->_current_body->id . " already has a contact for service code " . $self->_current_service->{service_code} . "\n" if $self->verbose >= 2;
if ( $contact->state eq 'deleted' || $service_name ne $contact->category || $self->_current_service->{service_code} ne $contact->email ) {
@@ -370,7 +372,11 @@ sub _delete_contacts_not_in_service_list {
sub _delete_contacts_not_in_service_list_cobrand_overrides {
my ( $self, $found_contacts ) = @_;
- return $found_contacts;
+ if ($self->_current_body_cobrand && $self->_current_body_cobrand->can('open311_filter_contacts_for_deletion')) {
+ return $self->_current_body_cobrand->open311_filter_contacts_for_deletion($found_contacts);
+ } else {
+ return $found_contacts;
+ }
}
1;
diff --git a/perllib/Open311/PostServiceRequestUpdates.pm b/perllib/Open311/PostServiceRequestUpdates.pm
index d7345ea4d..a31bca8f7 100755
--- a/perllib/Open311/PostServiceRequestUpdates.pm
+++ b/perllib/Open311/PostServiceRequestUpdates.pm
@@ -50,7 +50,7 @@ sub open311_params {
my $conf = $body;
if ($comment) {
my $cobrand_logged = $comment->get_cobrand_logged;
- my $sender = $cobrand_logged->get_body_sender($body, $comment->problem->category);
+ my $sender = $cobrand_logged->get_body_sender($body, $comment->problem);
$conf = $sender->{config};
}
diff --git a/t/Mock/MapIt.pm b/t/Mock/MapIt.pm
index b54ba0ddb..ed95e71fc 100644
--- a/t/Mock/MapIt.pm
+++ b/t/Mock/MapIt.pm
@@ -44,6 +44,7 @@ my @PLACES = (
[ 'NN1 2NS', 52.238301, -0.889992, 2234, 'Northamptonshire County Council', 'CTY', 2397, 'Northampton Borough Council', 'DIS' ],
[ '?', 52.238827, -0.894970, 2234, 'Northamptonshire County Council', 'CTY', 2397, 'Northampton Borough Council', 'DIS' ],
[ '?', 52.23025, -1.015826, 2234, 'Northamptonshire County Council', 'CTY', 2397, 'Northampton Borough Council', 'DIS' ],
+ [ 'E8 1DY', 51.552267, -0.063316, 2508, 'Hackney Borough Council', 'LBO' ],
[ 'TW7 5JN', 51.482286, -0.328163, 2483, 'Hounslow Borough Council', 'LBO' ],
[ '?', 51.48111, -0.327219, 2483, 'Hounslow Borough Council', 'LBO' ],
[ '?', 51.482045, -0.327219, 2483, 'Hounslow Borough Council', 'LBO' ],
diff --git a/t/Mock/OpenIDConnect.pm b/t/Mock/OpenIDConnect.pm
index ba7d03b1d..61a67f329 100644
--- a/t/Mock/OpenIDConnect.pm
+++ b/t/Mock/OpenIDConnect.pm
@@ -27,6 +27,11 @@ sub dispatch_request {
return [ 200, [ 'Content-Type' => 'text/html' ], [ 'OpenID Connect login page' ] ];
},
+ sub (GET + /oauth2/v2.0/authorize_google + ?*) {
+ my ($self) = @_;
+ return [ 200, [ 'Content-Type' => 'text/html' ], [ 'OpenID Connect login page' ] ];
+ },
+
sub (GET + /oauth2/v2.0/logout + ?*) {
my ($self) = @_;
return [ 200, [ 'Content-Type' => 'text/html' ], [ 'OpenID Connect logout page' ] ];
@@ -72,6 +77,49 @@ sub dispatch_request {
my $json = $self->json->encode($data);
return [ 200, [ 'Content-Type' => 'application/json' ], [ $json ] ];
},
+
+ sub (POST + /oauth2/v2.0/token_google + ?*) {
+ my ($self) = @_;
+ my $header = {
+ typ => "JWT",
+ alg => "RS256",
+ kid => "XXXfakeKEY1234",
+ };
+ my $now = DateTime->now->epoch;
+ my $payload = {
+ exp => $now + 3600,
+ nbf => $now,
+ locale => 'en-GB',
+ ver => "1.0",
+ iss => 'https://accounts.google.com',
+ sub => "my_google_user_id",
+ aud => "example_client_id",
+ iat => $now,
+ auth_time => $now,
+ given_name => "Andy",
+ family_name => "Dwyer",
+ name => "Andy Dwyer",
+ nonce => 'MyAwesomeRandomValue',
+ hd => 'example.org',
+ };
+ $payload->{email} = 'pkg-tappcontrollerauth_socialt-oidc_google@example.org' if $self->returns_email;
+ $payload->{email_verified} = JSON->true if $self->returns_email;
+ my $signature = "dummy";
+ my $id_token = join(".", (
+ encode_base64($self->json->encode($header), ''),
+ encode_base64($self->json->encode($payload), ''),
+ encode_base64($signature, '')
+ ));
+ my $data = {
+ id_token => $id_token,
+ token_type => "Bearer",
+ not_before => $now,
+ id_token_expires_in => 3600,
+ profile_info => encode_base64($self->json->encode({}), ''),
+ };
+ my $json = $self->json->encode($data);
+ return [ 200, [ 'Content-Type' => 'application/json' ], [ $json ] ];
+ },
}
__PACKAGE__->run_if_script;
diff --git a/t/app/controller/admin/report_edit.t b/t/app/controller/admin/report_edit.t
index 01f091412..f8101ab76 100644
--- a/t/app/controller/admin/report_edit.t
+++ b/t/app/controller/admin/report_edit.t
@@ -686,16 +686,28 @@ subtest "Test display of fields extra data" => sub {
$mech->get_ok("/admin/report_edit/$report_id");
$mech->content_contains('Extra data: No');
- $report->push_extra_fields( {
- name => 'report_url',
- value => 'http://example.com',
- });
+ $report->push_extra_fields(
+ {
+ name => 'report_url',
+ value => 'http://example.com',
+ },
+ {
+ name => 'sent_to',
+ value => [ 'onerecipient@example.org' ],
+ },
+ {
+ name => 'sent_too',
+ value => [ 'onemorerecipient@example.org', 'another@example.org' ],
+ },
+ );
$report->update;
$report->discard_changes;
$mech->get_ok("/admin/report_edit/$report_id");
$mech->content_contains('report_url</strong>: http://example.com');
+ $mech->content_contains('sent_to</strong>: onerecipient@example.org');
+ $mech->content_contains('sent_too</strong>: onemorerecipient@example.org, another@example.org');
$report->set_extra_fields( {
description => 'Report URL',
diff --git a/t/app/controller/auth_social.t b/t/app/controller/auth_social.t
index 200863029..1f6889dcc 100644
--- a/t/app/controller/auth_social.t
+++ b/t/app/controller/auth_social.t
@@ -15,9 +15,12 @@ FixMyStreet::App->log->disable('info');
END { FixMyStreet::App->log->enable('info'); }
my $body = $mech->create_body_ok(2504, 'Westminster City Council');
+my $body2 = $mech->create_body_ok(2508, 'Hackney Council');
my ($report) = $mech->create_problems_for_body(1, $body->id, 'My Test Report');
my $test_email = $report->user->email;
+my ($report2) = $mech->create_problems_for_body(1, $body2->id, 'My Test Report');
+my $test_email2 = $report->user->email;
my $contact = $mech->create_contact_ok(
body_id => $body->id, category => 'Damaged bin', email => 'BIN',
@@ -26,11 +29,21 @@ my $contact = $mech->create_contact_ok(
{ code => 'bin_service', description => 'Service needed', required => 'False' },
]
);
+$mech->create_contact_ok(
+ body_id => $body2->id, category => 'Damaged bin', email => 'BIN',
+ extra => [
+ { code => 'bin_type', description => 'Type of bin', required => 'True' },
+ { code => 'bin_service', description => 'Service needed', required => 'False' },
+ ]
+);
# Two options, incidentally, so that the template "Only one option, select it"
# code doesn't kick in and make the tests pass
my $contact2 = $mech->create_contact_ok(
body_id => $body->id, category => 'Whatever', email => 'WHATEVER',
);
+$mech->create_contact_ok(
+ body_id => $body2->id, category => 'Whatever', email => 'WHATEVER',
+);
my $resolver = Test::MockModule->new('Email::Valid');
my $social = Test::MockModule->new('FixMyStreet::App::Controller::Auth::Social');
@@ -88,6 +101,44 @@ for my $test (
user_extras => [
[westminster_account_id => "1c304134-ef12-c128-9212-123908123901"],
],
+}, {
+ type => 'oidc',
+ config => {
+ ALLOWED_COBRANDS => 'hackney',
+ MAPIT_URL => 'http://mapit.uk/',
+ COBRAND_FEATURES => {
+ anonymous_account => {
+ hackney => 'test',
+ },
+ oidc_login => {
+ hackney => {
+ client_id => 'example_client_id',
+ secret => 'example_secret_key',
+ auth_uri => 'http://oidc.example.org/oauth2/v2.0/authorize_google',
+ token_uri => 'http://oidc.example.org/oauth2/v2.0/token_google',
+ allowed_domains => [ 'example.org' ],
+ }
+ },
+ do_not_reply_email => {
+ hackney => 'fms-hackney-DO-NOT-REPLY@hackney-example.com',
+ },
+ verp_email_domain => {
+ hackney => 'hackney-example.com',
+ },
+ }
+ },
+ email => $mech->uniquify_email('oidc_google@example.org'),
+ uid => "hackney:example_client_id:my_google_user_id",
+ mock => 't::Mock::OpenIDConnect',
+ mock_hosts => ['oidc.example.org'],
+ host => 'oidc.example.org',
+ error_callback => '/auth/OIDC?error=ERROR',
+ success_callback => '/auth/OIDC?code=response-code&state=login',
+ redirect_pattern => qr{oidc\.example\.org/oauth2/v2\.0/authorize_google},
+ pc => 'E8 1DY',
+ # Need to use a different report that's within Hackney
+ report => $report2,
+ report_email => $test_email2,
}
) {
@@ -100,6 +151,7 @@ for my $state ( 'refused', 'no email', 'existing UID', 'okay' ) {
next if $page eq 'update' && !$test->{update};
subtest "test $test->{type} '$state' login for page '$page'" => sub {
+ my $test_report = $test->{report} || $report;
# Lots of user changes happening here, make sure we don't confuse
# Catalyst with a cookie session user that no longer exists
$mech->log_out_ok;
@@ -115,9 +167,9 @@ for my $state ( 'refused', 'no email', 'existing UID', 'okay' ) {
$mech->delete_user($test->{email});
}
if ($page eq 'my' && $state eq 'existing UID') {
- $report->update({ user_id => FixMyStreet::DB->resultset( 'User' )->find( { email => $test->{email} } )->id });
+ $test_report->update({ user_id => FixMyStreet::DB->resultset( 'User' )->find( { email => $test->{email} } )->id });
} else {
- $report->update({ user_id => FixMyStreet::DB->resultset( 'User' )->find( { email => $test_email } )->id });
+ $test_report->update({ user_id => FixMyStreet::DB->resultset( 'User' )->find( { email => ($report->{test_email} || $test_email) } )->id });
}
# Set up a mock to catch (most, see below) requests to the OAuth API
@@ -139,7 +191,7 @@ for my $state ( 'refused', 'no email', 'existing UID', 'okay' ) {
$mech->get_ok('/my');
} elsif ($page eq 'report') {
$mech->get_ok('/');
- $mech->submit_form_ok( { with_fields => { pc => 'SW1A1AA' } }, "submit location" );
+ $mech->submit_form_ok( { with_fields => { pc => $test->{pc} || 'SW1A1AA' } }, "submit location" );
$mech->follow_link_ok( { text_regex => qr/skip this step/i, }, "follow 'skip this step' link" );
$mech->submit_form(with_fields => {
category => 'Damaged bin',
@@ -150,7 +202,7 @@ for my $state ( 'refused', 'no email', 'existing UID', 'okay' ) {
bin_type => 'Salt bin',
};
} else {
- $mech->get_ok('/report/' . $report->id);
+ $mech->get_ok('/report/' . $test_report->id);
$fields = {
update => 'Test update',
};
@@ -243,17 +295,17 @@ for my $state ( 'refused', 'no email', 'existing UID', 'okay' ) {
}
}
if ($state eq 'existing UID') {
- my $report_id = $report->id;
- $mech->content_contains( $report->title );
+ my $report_id = $test_report->id;
+ $mech->content_contains( $test_report->title );
$mech->content_contains( "/report/$report_id" );
}
- if ($test->{type} eq 'oidc') {
+ if ($test->{type} eq 'oidc' && $test->{password_change_pattern}) {
ok $mech->find_link( text => 'Change password', url_regex => $test->{password_change_pattern} );
}
}
$mech->get('/auth/sign_out');
- if ($test->{type} eq 'oidc' && $state ne 'refused' && $state ne 'no email') {
+ if ($test->{type} eq 'oidc' && $test->{logout_redirect_pattern} && $state ne 'refused' && $state ne 'no email') {
# XXX the 'no email' situation is skipped because of some confusion
# with the hosts/sessions that I've not been able to get to the bottom of.
# The code does behave as expected when testing manually, however.
diff --git a/t/app/controller/moderate.t b/t/app/controller/moderate.t
index 8e84bd392..43ae1c980 100644
--- a/t/app/controller/moderate.t
+++ b/t/app/controller/moderate.t
@@ -51,7 +51,7 @@ sub create_report {
longitude => '0.007831',
user_id => $user2->id,
photo => '74e3362283b6ef0c48686fb0e161da4043bbcc97.jpeg',
- extra => { moon => 'waxing full' },
+ extra => { moon => 'waxing full', sent_to => [ 'authority@example.org' ] },
});
}
my $report = create_report();
@@ -115,6 +115,7 @@ subtest 'Problem moderation' => sub {
}});
$mech->base_like( qr{\Q$REPORT_URL\E} );
$mech->content_like(qr/Moderated by Bromley Council/);
+ $mech->content_lacks('sent_to = ARRAY(0x');
$report->discard_changes;
is $report->title, 'Good good';
diff --git a/t/cobrand/get_body_sender.t b/t/cobrand/get_body_sender.t
index 06ffb42a5..a1e8f2320 100644
--- a/t/cobrand/get_body_sender.t
+++ b/t/cobrand/get_body_sender.t
@@ -6,31 +6,21 @@ use_ok 'FixMyStreet::Cobrand';
my $c = FixMyStreet::Cobrand::FixMyStreet->new();
-FixMyStreet::DB->resultset('BodyArea')->search( { body_id => 1000 } )->delete;
-FixMyStreet::DB->resultset('Body')->search( { name => 'Body of a Thousand' } )->delete;
-
my $body = FixMyStreet::DB->resultset('Body')->find_or_create({
id => 1000,
name => 'Body of a Thousand',
});
-my $body_area = $body->body_areas->find_or_create({ area_id => 1000 });
+
+my $problem = FixMyStreet::DB->resultset('Problem')->new({});
FixMyStreet::override_config {
MAPIT_TYPES => [ 'LBO' ],
MAPIT_URL => 'http://mapit.uk/', # Not actually used as no special casing at present
}, sub {
- is_deeply $c->get_body_sender( $body ), { method => 'Email', contact => undef }, 'defaults to email';
- $body_area->update({ area_id => 2481 }); # Croydon LBO
- is_deeply $c->get_body_sender( $body ), { method => 'Email', contact => undef }, 'still email if London borough';
+ is_deeply $c->get_body_sender( $body, $problem ), { method => 'Email', contact => undef }, 'defaults to email';
};
$body->send_method( 'TestMethod' );
-is $c->get_body_sender( $body )->{ method }, 'TestMethod', 'uses send_method in preference to London';
-
-$body_area->update({ area_id => 1000 }); # Nothing
-is $c->get_body_sender( $body )->{ method }, 'TestMethod', 'uses send_method in preference to Email';
-
-$body_area->delete;
-$body->delete;
+is $c->get_body_sender( $body, $problem )->{ method }, 'TestMethod', 'uses send_method in preference to Email';
done_testing();
diff --git a/t/cobrand/hackney.t b/t/cobrand/hackney.t
new file mode 100644
index 000000000..b5a629e33
--- /dev/null
+++ b/t/cobrand/hackney.t
@@ -0,0 +1,292 @@
+use utf8;
+use CGI::Simple;
+use DateTime;
+use Test::MockModule;
+use FixMyStreet::TestMech;
+use Open311;
+use Open311::GetServiceRequests;
+use Open311::GetServiceRequestUpdates;
+use Open311::PostServiceRequestUpdates;
+use FixMyStreet::Script::Alerts;
+use FixMyStreet::Script::Reports;
+
+# disable info logs for this test run
+FixMyStreet::App->log->disable('info');
+END { FixMyStreet::App->log->enable('info'); }
+
+ok( my $mech = FixMyStreet::TestMech->new, 'Created mech object' );
+
+my $params = {
+ send_method => 'Open311',
+ send_comments => 1,
+ api_key => 'KEY',
+ endpoint => 'endpoint',
+ jurisdiction => 'home',
+ can_be_devolved => 1,
+};
+
+my $hackney = $mech->create_body_ok(2508, 'Hackney Council', $params);
+my $contact = $mech->create_contact_ok(
+ body_id => $hackney->id,
+ category => 'Potholes',
+ email => 'pothole@example.org',
+);
+$contact->set_extra_fields( ( {
+ code => 'urgent',
+ datatype => 'string',
+ description => 'question',
+ variable => 'true',
+ required => 'false',
+ order => 1,
+ datatype_description => 'datatype',
+} ) );
+$contact->update;
+
+my $user = $mech->create_user_ok('user@example.org', name => 'Test User');
+my $hackney_user = $mech->create_user_ok('hackney_user@example.org', name => 'Hackney User', from_body => $hackney);
+$hackney_user->user_body_permissions->create({
+ body => $hackney,
+ permission_type => 'moderate',
+});
+
+my $contact2 = $mech->create_contact_ok(
+ body_id => $hackney->id,
+ category => 'Roads',
+ email => 'roads@example.org',
+ send_method => 'Email',
+);
+
+my $admin_user = $mech->create_user_ok('admin-user@example.org', name => 'Admin User', from_body => $hackney);
+
+my @reports = $mech->create_problems_for_body(1, $hackney->id, 'A Hackney report', {
+ confirmed => '2019-10-25 09:00',
+ lastupdate => '2019-10-25 09:00',
+ latitude => 51.552267,
+ longitude => -0.063316,
+ user => $user,
+ external_id => 101202303
+});
+
+subtest "check clicking all reports link" => sub {
+ FixMyStreet::override_config {
+ MAPIT_URL => 'http://mapit.uk/',
+ ALLOWED_COBRANDS => ['hackney'],
+ }, sub {
+ $mech->get_ok('/');
+ $mech->follow_link_ok({ text => 'All reports' });
+ };
+
+ $mech->content_contains("A Hackney report", "Hackney report there");
+ $mech->content_contains("Hackney Council", "is still on cobrand");
+};
+
+subtest "check moderation label uses correct name" => sub {
+ my $REPORT_URL = '/report/' . $reports[0]->id;
+ FixMyStreet::override_config {
+ MAPIT_URL => 'http://mapit.uk/',
+ ALLOWED_COBRANDS => ['hackney'],
+ COBRAND_FEATURES => {
+ do_not_reply_email => {
+ hackney => 'fms-hackney-DO-NOT-REPLY@hackney-example.com',
+ },
+ verp_email_domain => {
+ hackney => 'hackney-example.com',
+ },
+ },
+ }, sub {
+ $mech->log_out_ok;
+ $mech->log_in_ok( $hackney_user->email );
+ $mech->get_ok($REPORT_URL);
+ $mech->content_lacks('show-moderation');
+ $mech->follow_link_ok({ text_regex => qr/^Moderate$/ });
+ $mech->content_contains('show-moderation');
+ $mech->submit_form_ok({ with_fields => {
+ problem_title => 'Good good',
+ problem_detail => 'Good good improved',
+ }});
+ $mech->base_like( qr{\Q$REPORT_URL\E} );
+ $mech->content_like(qr/Moderated by Hackney Council/);
+ };
+};
+
+$_->delete for @reports;
+
+my $system_user = $mech->create_user_ok('system_user@example.org');
+
+my ($p) = $mech->create_problems_for_body(1, $hackney->id, '', { cobrand => 'hackney' });
+my $alert = FixMyStreet::DB->resultset('Alert')->create( {
+ parameter => $p->id,
+ alert_type => 'new_updates',
+ user => $user,
+ cobrand => 'hackney',
+} )->confirm;
+
+subtest "sends branded alert emails" => sub {
+ $mech->create_comment_for_problem($p, $system_user, 'Other User', 'This is some update text', 'f', 'confirmed', undef);
+ $mech->clear_emails_ok;
+
+ FixMyStreet::override_config {
+ MAPIT_URL => 'http://mapit.uk/',
+ ALLOWED_COBRANDS => ['hackney','fixmystreet'],
+ }, sub {
+ FixMyStreet::Script::Alerts::send();
+ };
+
+ my $email = $mech->get_email;
+ ok $email, "got an email";
+ like $mech->get_text_body_from_email($email), qr/Hackney Council/, "emails are branded";
+};
+
+$p->comments->delete;
+$p->delete;
+
+subtest "sends branded confirmation emails" => sub {
+ $mech->log_out_ok;
+ $mech->clear_emails_ok;
+ $mech->get_ok('/around');
+ FixMyStreet::override_config {
+ ALLOWED_COBRANDS => [ 'hackney' ],
+ MAPIT_URL => 'http://mapit.uk/',
+ COBRAND_FEATURES => {
+ do_not_reply_email => {
+ hackney => 'fms-hackney-DO-NOT-REPLY@hackney-example.com',
+ },
+ verp_email_domain => {
+ hackney => 'hackney-example.com',
+ },
+ },
+ }, sub {
+ $mech->submit_form_ok( { with_fields => { pc => 'E8 1DY', } },
+ "submit location" );
+
+ # click through to the report page
+ $mech->follow_link_ok( { text_regex => qr/skip this step/i, },
+ "follow 'skip this step' link" );
+
+ $mech->submit_form_ok(
+ {
+ button => 'submit_register',
+ with_fields => {
+ title => 'Test Report',
+ detail => 'Test report details.',
+ photo1 => '',
+ name => 'Joe Bloggs',
+ username => 'test-1@example.com',
+ category => 'Roads',
+ }
+ },
+ "submit good details"
+ );
+
+ my $email = $mech->get_email;
+ ok $email, "got an email";
+ like $mech->get_text_body_from_email($email), qr/Hackney Council/, "emails are branded";
+
+ my $url = $mech->get_link_from_email($email);
+ $mech->get_ok($url);
+ $mech->clear_emails_ok;
+ };
+};
+
+FixMyStreet::override_config {
+ STAGING_FLAGS => { send_reports => 1 },
+ MAPIT_URL => 'http://mapit.uk/',
+ ALLOWED_COBRANDS => ['hackney', 'fixmystreet'],
+}, sub {
+ subtest "special send handling" => sub {
+ my $cbr = Test::MockModule->new('FixMyStreet::Cobrand::Hackney');
+ my $p = FixMyStreet::DB->resultset("Problem")->search(undef, { order_by => { -desc => 'id' } })->first;
+ $contact2->update({ email => 'park:parks@example;estate:estates@example;other:OTHER', send_method => '' });
+
+ subtest 'in a park' => sub {
+ $cbr->mock('_fetch_features', sub {
+ my ($self, $cfg, $x, $y) = @_;
+ return [{
+ properties => { park_id => 'park' },
+ geometry => {
+ type => 'Polygon',
+ coordinates => [ [ [ $x-1, $y-1 ], [ $x+1, $y+1 ] ] ],
+ }
+ }] if $cfg->{typename} eq 'greenspaces:hackney_park';
+ });
+ FixMyStreet::Script::Reports::send();
+ my $email = $mech->get_email;
+ is $email->header('To'), '"Hackney Council" <parks@example>';
+ $mech->clear_emails_ok;
+ $p->discard_changes;
+ $p->update({ whensent => undef });
+ };
+
+ subtest 'in an estate' => sub {
+ $cbr->mock('_fetch_features', sub {
+ my ($self, $cfg, $x, $y) = @_;
+ return [{
+ properties => { id => 'estate' },
+ geometry => {
+ type => 'Polygon',
+ coordinates => [ [ [ $x-1, $y-1 ], [ $x+1, $y+1 ] ] ],
+ }
+ }] if $cfg->{typename} eq 'housing:lbh_estate';
+ });
+ FixMyStreet::Script::Reports::send();
+ my $email = $mech->get_email;
+ is $email->header('To'), '"Hackney Council" <estates@example>';
+ $mech->clear_emails_ok;
+ $p->discard_changes;
+ $p->update({ whensent => undef });
+ };
+
+ subtest 'elsewhere' => sub {
+ $cbr->mock('_fetch_features', sub {
+ my ($self, $cfg, $x, $y) = @_;
+ return []; # Not in park or estate
+ });
+ my $test_data = FixMyStreet::Script::Reports::send();
+ my $req = $test_data->{test_req_used};
+ my $c = CGI::Simple->new($req->content);
+ is $c->param('service_code'), 'OTHER';
+ };
+ };
+};
+
+#subtest "sends branded report sent emails" => sub {
+ #$mech->clear_emails_ok;
+ #FixMyStreet::override_config {
+ #STAGING_FLAGS => { send_reports => 1 },
+ #MAPIT_URL => 'http://mapit.uk/',
+ #ALLOWED_COBRANDS => ['hackney','fixmystreet'],
+ #}, sub {
+ #FixMyStreet::Script::Reports::send();
+ #};
+ #my $email = $mech->get_email;
+ #ok $email, "got an email";
+ #like $mech->get_text_body_from_email($email), qr/Hackney Council/, "emails are branded";
+#};
+
+subtest "check category extra uses correct name" => sub {
+ my @extras = ( {
+ code => 'test',
+ datatype => 'string',
+ description => 'question',
+ variable => 'true',
+ required => 'false',
+ order => 1,
+ datatype_description => 'datatype',
+ } );
+ $contact2->set_extra_fields( @extras );
+ $contact2->update;
+
+ my $extra_details;
+
+ FixMyStreet::override_config {
+ MAPIT_URL => 'http://mapit.uk/',
+ ALLOWED_COBRANDS => ['hackney','fixmystreet'],
+ }, sub {
+ $extra_details = $mech->get_ok_json('/report/new/category_extras?category=Roads&latitude=51.552267&longitude=-0.063316');
+ };
+
+ like $extra_details->{category_extra}, qr/Hackney Council/, 'correct name in category extras';
+};
+
+
+done_testing();
diff --git a/t/email.t b/t/email.t
index 7e8e60a8a..ec86af288 100644
--- a/t/email.t
+++ b/t/email.t
@@ -14,4 +14,10 @@ my ($type, $id) = FixMyStreet::Email::check_verp_token($token);
is $type, "report", 'Correct type from token';
is $id, 123, 'Correct ID from token';
+my $verpid = FixMyStreet::Email::unique_verp_id([ "report", 123 ]);
+is $verpid, 'fms-report-123-8fb274c6@example.org', 'VERP id okay';
+
+$verpid = FixMyStreet::Email::unique_verp_id([ "report", 123 ], "example.net");
+is $verpid, 'fms-report-123-8fb274c6@example.net', 'VERP id okay with custom domain';
+
done_testing();
diff --git a/templates/email/hackney/_email_bottom.html b/templates/email/hackney/_email_bottom.html
new file mode 100644
index 000000000..64936c470
--- /dev/null
+++ b/templates/email/hackney/_email_bottom.html
@@ -0,0 +1,28 @@
+ </tr>
+ </table>
+ </th>
+ <th class="spacer-cell"></th>
+ </tr>
+ </table>
+ <table [% wrapper_table | safe %] style="[% wrapper_style %]">
+ <tr>
+ <th class="spacer-cell"></th>
+ <th width="[% wrapper_max_width %]" style="[% td_style %][% hint_style %]" class="hint">
+ [%~ IF email_footer %]
+ [% email_footer | safe %]
+ [%~ ELSE %]
+ This email was sent automatically, from an unmonitored email account. Please do not reply to it.
+ [%~ END %]
+ </th>
+ <th class="spacer-cell"></th>
+ </tr>
+ <tr>
+ <th class="spacer-cell"></th>
+ <th width="[% wrapper_max_width %]" style="[% td_style %][% hint_style %]">
+ Powered by <a href="http://www.fixmystreet.com">FixMyStreet</a>
+ </th>
+ <th class="spacer-cell"></th>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/templates/email/hackney/_email_color_overrides.html b/templates/email/hackney/_email_color_overrides.html
new file mode 100644
index 000000000..1af818eca
--- /dev/null
+++ b/templates/email/hackney/_email_color_overrides.html
@@ -0,0 +1,25 @@
+[%
+
+color_green = '#00b341'
+color_black = '#000000'
+color_white = '#FFFFFF'
+color_hackney_pale_green = '#f2f7f0'
+color_hackney_dark_green = '#00664f'
+
+body_background_color = color_hackney_pale_green
+body_text_color = color_black
+
+header_background_color = color_black
+header_text_color = color_white
+
+secondary_column_background_color = color_white
+
+button_background_color = color_hackney_dark_green
+button_text_color = color_white
+
+logo_file = 'hackney-logo-white.png'
+logo_width = "200" # pixel measurement, but without 'px' suffix
+logo_height = "36" # pixel measurement, but without 'px' suffix
+header_padding = "20px 30px"
+
+%]
diff --git a/templates/email/hackney/_email_setting_overrides.html b/templates/email/hackney/_email_setting_overrides.html
new file mode 100644
index 000000000..00eeed9cc
--- /dev/null
+++ b/templates/email/hackney/_email_setting_overrides.html
@@ -0,0 +1,7 @@
+[%
+
+only_column_style = "$only_column_style border: 1px solid $column_divider_color; border-top: none;"
+primary_column_style = "$primary_column_style border: 1px solid $column_divider_color; border-top: none;"
+secondary_column_style = "vertical-align: top; width: 50%; background-color: $secondary_column_background_color; color: $secondary_column_text_color; border: 1px solid $column_divider_color; border-top: none; border-left: none;"
+
+%]
diff --git a/templates/email/hackney/signature.txt b/templates/email/hackney/signature.txt
new file mode 100644
index 000000000..78a02659f
--- /dev/null
+++ b/templates/email/hackney/signature.txt
@@ -0,0 +1,2 @@
+
+Hackney Council
diff --git a/templates/email/hackney/site-name.txt b/templates/email/hackney/site-name.txt
new file mode 100644
index 000000000..29d7f1480
--- /dev/null
+++ b/templates/email/hackney/site-name.txt
@@ -0,0 +1 @@
+Report A Problem
diff --git a/templates/web/base/admin/reports/edit.html b/templates/web/base/admin/reports/edit.html
index d2b866d01..6e7cff4cf 100644
--- a/templates/web/base/admin/reports/edit.html
+++ b/templates/web/base/admin/reports/edit.html
@@ -131,7 +131,20 @@ class="admin-offsite-link">[% problem.latitude %], [% problem.longitude %]</a>
<li><label class="inline-text" for="category">[% loc('Category:') %]</label>
[% INCLUDE 'admin/report-category.html' %]
</li>
-<li>[% loc('Extra data:') %] [% IF extra_fields.size %]<ul>[% FOREACH field IN extra_fields %]<li><strong>[% field.name %]</strong>: [% field.val %]</li>[% END %]</ul>[% ELSE %]No[% END %]</li>
+<li>[% loc('Extra data:') ~%]
+ [%~ IF extra_fields.size ~%]
+ <ul>
+ [%~ FOREACH field IN extra_fields ~%]
+ <li><strong>[%~ field.name ~%]</strong>: [% IF field.val.0.defined ~%]
+ [%~ field.val.list.join(", ") ~%]
+ [%~ ELSE ~%]
+ [%~ field.val ~%]
+ [%~ END ~%]
+ </li>
+ [%~ END ~%]
+ </ul>
+ [%~ ELSE %] No[% END ~%]
+</li>
<li><label class="inline-text" for="anonymous">[% loc('Anonymous:') %]</label>
<select class="form-control" name="anonymous" id="anonymous">
<option [% 'selected ' IF problem.anonymous %]value="1">[% loc('Yes') %]</option>
diff --git a/templates/web/fixmystreet-uk-councils/about/privacy.html b/templates/web/fixmystreet-uk-councils/about/privacy.html
index 4a5ed9d2c..1290ee7fb 100644
--- a/templates/web/fixmystreet-uk-councils/about/privacy.html
+++ b/templates/web/fixmystreet-uk-councils/about/privacy.html
@@ -231,6 +231,11 @@ When you make a report
</h2>
<p>
+[% IF c.cobrand.moniker == 'hackney' %]
+ In using FixMyStreet for any of the functions listed above (sending a
+ report, leaving an update, email alerts or site registration), we are
+ processing your data under the legal basis 6(1)(a) – consent.
+[% ELSE %]
In using FixMyStreet for any of the functions listed above (sending a
report, leaving an update, email alerts or site registration), we are
processing your data under the legal basis 6(1)(f) – legitimate interests.
@@ -243,6 +248,7 @@ When you make a report
communities, so it's easy to see what the common problems are in a given
area, and how quickly they get fixed. Other local residents can browse, read
and comment on problems – and perhaps even offer a solution.
+[% END %]
</p>
<h2>
diff --git a/templates/web/hackney/around/intro.html b/templates/web/hackney/around/intro.html
new file mode 100644
index 000000000..d4510ac9a
--- /dev/null
+++ b/templates/web/hackney/around/intro.html
@@ -0,0 +1,5 @@
+ <div id="postcode-intro">
+ <h1> Report, view, or discuss local problems</h1>
+ <h2> (like potholes, fly tipping, broken paving slabs, or street lighting) </h2>
+ </div>
+
diff --git a/templates/web/hackney/auth/general.html b/templates/web/hackney/auth/general.html
new file mode 100644
index 000000000..1a9e4a060
--- /dev/null
+++ b/templates/web/hackney/auth/general.html
@@ -0,0 +1,88 @@
+[% INCLUDE 'header.html', bodyclass='authpage', title = loc('Sign in or create an account') %]
+
+<h1>
+ [% loc('Sign in') %]
+ <small>
+ [% tprintf(loc('or <a href="%s">create an account</a>'), '/auth/create') %]
+ </small>
+</h1>
+
+[% TRY %][% INCLUDE 'auth/_general_top.html' %][% CATCH file %][% END %]
+
+[% IF oauth_need_email %]
+ <p class="form-error">[% loc('We need your email address, please give it below.') %]</p>
+[% END %]
+[% IF oauth_failure %]
+ <p class="form-error">[% loc('Sorry, we could not log you in. Please fill in the form below.') %]</p>
+[% END %]
+
+<form action="/auth" method="post" name="general_auth" class="validate">
+ <fieldset>
+
+ <input type="hidden" name="r" value="[% c.req.params.r | html %]">
+
+ [% loc_username_error = INCLUDE 'auth/_username_error.html' default='email' %]
+
+[% IF c.config.SMS_AUTHENTICATION %]
+ [% SET username_label = loc('Your email or mobile') %]
+[% ELSE %]
+ [% SET username_label = loc('Your email') %]
+[% END %]
+
+ <label class="n" for="username">[% username_label %]</label>
+ [% IF loc_username_error %]
+ <div class="form-error">[% loc_username_error %]</div>
+ [% END %]
+ <input type="text" class="form-control required" id="username" name="username" value="[% username | html %]" autocomplete="username"
+ [%~ IF c.cobrand.moniker != 'borsetshire' %] autofocus[% END %]>
+
+ <div id="form_sign_in">
+ [% IF oauth_need_email %]
+ [% INCLUDE form_sign_in_no %]
+ <input type="hidden" name="oauth_need_email" value="1">
+ [% ELSE %]
+ [% INCLUDE form_sign_in_yes %]
+ [% INCLUDE form_sign_in_no %]
+ [% INCLUDE form_sign_in_hackney_staff %]
+ [% END %]
+ </div>
+ </fieldset>
+</form>
+
+[% INCLUDE 'footer.html' %]
+
+[% BLOCK form_sign_in_yes %]
+ <p class="hidden-nojs js-sign-in-password-hide">
+ <input class="btn btn--primary btn--block js-sign-in-password-btn" type="submit" name="sign_in_by_password" value="[% loc('Sign in with a password') %]">
+ </p>
+ <div class="hidden-js js-sign-in-password">
+ <label for="password_sign_in">[% loc('Your password') %]</label>
+
+ <div class="form-txt-submit-box">
+ <input type="password" name="password_sign_in" class="form-control" id="password_sign_in" value="" autocomplete="current-password">
+ <input class="green-btn" type="submit" name="sign_in_by_password" value="[% loc('Sign in') %]">
+ </div>
+
+ <p>
+ <a href="/auth/forgot">[% loc('Forgotten your password?') %]</a>
+ </p>
+ </div>
+[% END %]
+
+[% BLOCK form_sign_in_no %]
+ <p><input class="fake-link" type="submit" name="sign_in_by_code" value="
+ [%~ IF c.config.SMS_AUTHENTICATION %]
+ [%~ loc('Email me a link or text me a code to sign in') %]
+ [%~ ELSE %]
+ [%~ loc('Email me a link to sign in') %]
+ [%~ END ~%]
+ "></p>
+[% END %]
+
+[% BLOCK form_sign_in_hackney_staff %]
+ [% IF c.cobrand.feature('oidc_login') %]
+ <button name="social_sign_in" id="oidc_sign_in" value="oidc" class="fake-link sso-staff-sign-in">
+ Hackney Staff Sign-in
+ </button>
+ [% END %]
+[% END %] \ No newline at end of file
diff --git a/templates/web/hackney/footer_extra.html b/templates/web/hackney/footer_extra.html
new file mode 100644
index 000000000..1e7c53aad
--- /dev/null
+++ b/templates/web/hackney/footer_extra.html
@@ -0,0 +1,10 @@
+ <div class="hackney-footer">
+ <div class="container">
+ <a href="https://hackney.gov.uk/" alt="Hackney.gov.uk" class="hackney-footer__logo">Hackney</a>
+ [% IF NOT bodyclass.match('mappage') %]
+ <p class="footer-powered-by">
+ Powered by <a class="platform-logo" href="https://www.fixmystreet.com/pro/">FixMyStreet Platform</a>
+ </p>
+ [% END %]
+ </div>
+</div> \ No newline at end of file
diff --git a/templates/web/hackney/footer_extra_js.html b/templates/web/hackney/footer_extra_js.html
new file mode 100644
index 000000000..61b8dacea
--- /dev/null
+++ b/templates/web/hackney/footer_extra_js.html
@@ -0,0 +1,7 @@
+[%
+IF bodyclass.match('mappage');
+ scripts.push(
+ version('/cobrands/fixmystreet-uk-councils/alloy.js'),
+ );
+END %]
+[% PROCESS 'footer_extra_js_base.html' highways=1 tfl=1 cobrand_js=1 validation=1 %]
diff --git a/templates/web/hackney/header_extra.html b/templates/web/hackney/header_extra.html
new file mode 100644
index 000000000..73d214ae0
--- /dev/null
+++ b/templates/web/hackney/header_extra.html
@@ -0,0 +1,2 @@
+[% INCLUDE 'tracking_code.html' %]
+<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,600,700&amp;display=swap" rel="stylesheet">
diff --git a/templates/web/hackney/report/form/user.html b/templates/web/hackney/report/form/user.html
new file mode 100644
index 000000000..bced8c189
--- /dev/null
+++ b/templates/web/hackney/report/form/user.html
@@ -0,0 +1,29 @@
+<!-- report/form/user.html -->
+<div class="js-new-report-user-hidden form-section-preview form-section-preview--next
+ [%~ ' hidden-nojs' IF c.user_exists OR NOT c.cobrand.social_auth_enabled %]">
+ <h2 class="form-section-heading form-section-heading--private hidden-nojs">
+ [% loc('Next:') %] [% loc('Tell us about you') %]
+ </h2>
+ <button type="button" class="btn btn--block hidden-nojs js-new-report-user-show">[% loc('Continue') %]</button>
+[% IF NOT c.user_exists AND c.cobrand.feature('oidc_login') %]
+ <button name="social_sign_in" id="oidc_sign_in" value="oidc" class="fake-link sso-staff-sign-in">
+ Hackney Staff Sign-in
+ </button>
+[% END %]
+ <div class="js-show-if-anonymous
+ [%~ ' hidden-js' UNLESS type == 'report' AND c.cobrand.allow_anonymous_reports == 'button' %]">
+ <small id="or">[% loc('or') %]</small>
+ <button name="report_anonymously" value="yes" class="btn btn--block js-new-report-submit">[% loc('Report anonymously') %]</button>
+ <small>[% loc('No personal details will be stored, and you will not receive updates about this report.') %]</small>
+ </div>
+</div>
+
+[% IF (c.user_exists OR NOT c.cobrand.social_auth_enabled) AND type == 'report' AND c.cobrand.allow_anonymous_reports == 'button' %]
+<div class="form-section-preview form-section-preview--next hidden-js">
+ <button name="report_anonymously" value="yes" class="btn btn--block">[% loc('Report anonymously') %]</button>
+ <small>[% loc('No personal details will be stored, and you will not receive updates about this report.') %]</small>
+ <small id="or">[% loc('or') %]</small>
+</div>
+[% END %]
+
+<!-- /report/form/user.html -->
diff --git a/templates/web/hackney/site-name.html b/templates/web/hackney/site-name.html
new file mode 100644
index 000000000..29d7f1480
--- /dev/null
+++ b/templates/web/hackney/site-name.html
@@ -0,0 +1 @@
+Report A Problem
diff --git a/templates/web/hackney/tracking_code.html b/templates/web/hackney/tracking_code.html
new file mode 100644
index 000000000..ac9a5bcf4
--- /dev/null
+++ b/templates/web/hackney/tracking_code.html
@@ -0,0 +1,11 @@
+[% IF c.config.BASE_URL == "https://www.fixmystreet.com" %]
+<!-- Global site tag (gtag.js) - Google Analytics -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=UA-171536255-1"></script>
+<script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+
+ gtag('config', 'UA-171536255-1');
+</script>
+[% END %]
diff --git a/web/cobrands/fixmystreet-uk-councils/alloy.js b/web/cobrands/fixmystreet-uk-councils/alloy.js
index ace0062de..88d0b017f 100644
--- a/web/cobrands/fixmystreet-uk-councils/alloy.js
+++ b/web/cobrands/fixmystreet-uk-councils/alloy.js
@@ -3,6 +3,7 @@
OpenLayers.Protocol.Alloy = OpenLayers.Class(OpenLayers.Protocol.HTTP, {
currentRequests: [],
+ tileSize: 512,
abort: function() {
if (this.currentRequests.length) {
for (var j = 0; j < this.currentRequests.length; j++) {
@@ -63,8 +64,8 @@ OpenLayers.Protocol.Alloy = OpenLayers.Class(OpenLayers.Protocol.HTTP, {
var adjustY = reverse ? 0 : 0.5;
var xFromOrigin = Math.floor((bounds[0] - origin.lon) / resolution + adjustX);
var yFromOrigin = Math.floor((bounds[1] - origin.lat) / resolution + adjustY);
- var tileCoordX = Math.floor(xFromOrigin / 512);
- var tileCoordY = Math.floor(yFromOrigin / 512) * -1;
+ var tileCoordX = Math.floor(xFromOrigin / this.tileSize);
+ var tileCoordY = Math.floor(yFromOrigin / this.tileSize) * -1;
if (reverse) {
tileCoordX -= 1;
@@ -88,13 +89,18 @@ OpenLayers.Strategy.Alloy = OpenLayers.Class(OpenLayers.Strategy.FixMyStreet, {
this.failCount = 0;
this.layer.destroyFeatures();
},
+ // allow sub classes to override the remote projection for converting the geometry
+ // of the features
+ getRemoteProjection: function() {
+ return this.layer.projection;
+ },
merge: function(resp) {
this.count++;
// This if/else clause lifted from OpenLayers.Strategy.BBOX
if (resp.success()) {
var features = resp.features;
if(features && features.length > 0) {
- var remote = this.layer.projection;
+ var remote = this.getRemoteProjection();
var local = this.layer.map.getProjectionObject();
if(!local.equals(remote)) {
var geom;
@@ -133,4 +139,87 @@ fixmystreet.alloy_defaults = {
strategy_class: OpenLayers.Strategy.Alloy
};
+/* for Alloy V2 */
+OpenLayers.Format.AlloyV2 = OpenLayers.Class(OpenLayers.Format.GeoJSON, {
+ read: function(json, type, filter) {
+ var results = null;
+ var obj = null;
+ if (typeof json == "string") {
+ obj = OpenLayers.Format.JSON.prototype.read.apply(this, [json, filter]);
+ } else {
+ obj = json;
+ }
+
+ if(!obj) {
+ OpenLayers.Console.error("Bad JSON: " + json);
+ } else {
+ results = [];
+ for(var i=0, len=obj.results.length; i<len; ++i) {
+ try {
+ results.push(this.parseFeature(obj.results[i]));
+ } catch(err) {
+ results = null;
+ OpenLayers.Console.error(err);
+ }
+ }
+ }
+ return results;
+ }
+});
+
+OpenLayers.Protocol.AlloyV2 = OpenLayers.Class(OpenLayers.Protocol.Alloy, {
+ tileSize: 128,
+ getURL: function(coords, options) {
+ return OpenLayers.String.format(options.base, {'layerid': options.layerid, 'styleid': options.styleid, 'z': 17, 'x': coords[0], 'y': coords[1]});
+ }
+});
+
+OpenLayers.Strategy.AlloyV2 = OpenLayers.Class(OpenLayers.Strategy.Alloy, {
+ initialize: function(name, options) {
+ this.remote = new OpenLayers.Projection("EPSG:4326");
+ OpenLayers.Strategy.Alloy.prototype.initialize.apply(this, arguments);
+ },
+ // the layer uses EPSG:3857 for generating the tile location but the features
+ // use EPSG:4326
+ getRemoteProjection: function() {
+ return this.remote;
+ }
+});
+
+fixmystreet.alloyv2_defaults = {
+ format_class: OpenLayers.Format.AlloyV2,
+ srsName: "EPSG:3857",
+ strategy_class: OpenLayers.Strategy.AlloyV2
+};
+
+fixmystreet.alloy_add_layers = function(defaults, layers) {
+ $.each(layers, function(index, layer) {
+ if ( layer.categories ) {
+ var options = {
+ http_options: {
+ layerid: layer.layerid || layer.layer
+ },
+ asset_type: layer.asset_type || "spot",
+ asset_category: layer.categories,
+ asset_item: layer.item_name || layer.layer_name.toLowerCase()
+ };
+ // Alloy v2
+ if (layer.styleid) {
+ options.http_options.styleid = layer.styleid;
+ }
+ // Alloy v1
+ if (layer.version) {
+ options.http_options.layerVersion = layer.version;
+ }
+ if (layer.max_resolution) {
+ options.max_resolution = layer.max_resolution;
+ }
+ if (layer.snap_threshold || layer.snap_threshold === 0) {
+ options.snap_threshold = layer.snap_threshold;
+ }
+ fixmystreet.assets.add(defaults, options);
+ }
+ });
+};
+
})();
diff --git a/web/cobrands/hackney/_colours.scss b/web/cobrands/hackney/_colours.scss
new file mode 100644
index 000000000..4c0af7b03
--- /dev/null
+++ b/web/cobrands/hackney/_colours.scss
@@ -0,0 +1,45 @@
+/* COLOURS */
+
+$menu-image: 'menu-black';
+
+// Primary
+$white: #fff;
+$green: #00b341;
+$grey: #666664;
+
+
+// Secondary
+$yellow: #ffc845;
+$blue: #0072ce;
+$pale_green: #f2f7f0;
+$alt_green: #328b15;
+$light_green: #84bd00;
+$dark_green: #00664f;
+$teal :#1e98a7;
+$black: #000;
+$red: #be3a34;
+
+$primary: $green;
+$primary_b: #000;
+$primary_text: $black;
+
+$base_bg: $white;
+$base_fg: $black;
+
+$link-color: $blue;
+$link-visited_color: $dark-green;
+$link-hover-color: $green;
+
+$nav_background_colour: $black;
+$nav_colour: $white;
+$nav_hover_background_colour: $black;
+
+$col_click_map: $green;
+
+$header-top-border: false;
+
+$montserrat: 'Montserrat', Arial, sans-serif;
+
+$heading-font: $montserrat;
+$body-font: $montserrat;
+$meta-font: $montserrat; \ No newline at end of file
diff --git a/web/cobrands/hackney/assets.js b/web/cobrands/hackney/assets.js
new file mode 100644
index 000000000..9941594f0
--- /dev/null
+++ b/web/cobrands/hackney/assets.js
@@ -0,0 +1,246 @@
+(function(){
+
+if (!fixmystreet.maps) {
+ return;
+}
+
+/** These layers are from the Hackney WFS feed, for non-Alloy categories: */
+var wfs_defaults = {
+ http_options: {
+ url: "https://map.hackney.gov.uk/geoserver/wfs",
+ params: {
+ SERVICE: "WFS",
+ VERSION: "1.1.0",
+ REQUEST: "GetFeature",
+ SRSNAME: "urn:ogc:def:crs:EPSG::27700"
+ }
+},
+ asset_type: 'spot',
+ max_resolution: 2.388657133579254,
+ asset_id_field: 'id',
+ attributes: {},
+ geometryName: 'geom',
+ srsName: "EPSG:27700",
+ strategy_class: OpenLayers.Strategy.FixMyStreet,
+ body: "Hackney Council",
+ asset_item: "item"
+};
+
+
+fixmystreet.assets.add(wfs_defaults, {
+ http_options: {
+ params: {
+ TYPENAME: "greenspaces:park_bin",
+ }
+ },
+ asset_category: "Overflowing bin",
+ attributes: {}
+});
+
+fixmystreet.assets.add(wfs_defaults, {
+ http_options: {
+ params: {
+ TYPENAME: "amenity:public_toilet",
+ }
+ },
+ asset_category: ["Public toilets", "Toilets"],
+ attributes: {}
+});
+
+fixmystreet.assets.add(wfs_defaults, {
+ http_options: {
+ params: {
+ TYPENAME: "parking:pay_display_machine_liberator",
+ }
+ },
+ asset_category: "Pay & Display Machines",
+ attributes: {}
+});
+
+fixmystreet.assets.add(wfs_defaults, {
+ http_options: {
+ params: {
+ TYPENAME: "recycling:estate_recycling_bin",
+ }
+ },
+ asset_category: "Bin Contamination",
+ attributes: {}
+});
+
+fixmystreet.assets.add(wfs_defaults, {
+ http_options: {
+ params: {
+ TYPENAME: "amenity:sport_facility",
+ }
+ },
+ asset_category: "Sport Area",
+ attributes: {}
+});
+
+fixmystreet.assets.add(wfs_defaults, {
+ http_options: {
+ params: {
+ TYPENAME: "greenspaces:park_bench",
+ }
+ },
+ asset_category: "Park Furniture (bench)",
+ attributes: {}
+});
+
+fixmystreet.assets.add(wfs_defaults, {
+ http_options: {
+ params: {
+ TYPENAME: "greenspaces:park_bin",
+ }
+ },
+ asset_category: "Park Furniture (bin)",
+ attributes: {}
+});
+
+fixmystreet.assets.add(wfs_defaults, {
+ http_options: {
+ params: {
+ TYPENAME: "greenspaces:park_picnic_bench",
+ }
+ },
+ asset_category: "Park Furniture (picnic bench)",
+ attributes: {}
+});
+
+fixmystreet.assets.add(wfs_defaults, {
+ http_options: {
+ params: {
+ TYPENAME: "transport:bike_hangar_scheme",
+ }
+ },
+ asset_category: "Cycle Hangars",
+ attributes: {}
+});
+
+fixmystreet.assets.add(wfs_defaults, {
+ http_options: {
+ params: {
+ TYPENAME: "greenspaces:park_bench",
+ }
+ },
+ asset_category: "Benches",
+ attributes: {}
+});
+
+
+/** These layers are served directly from Alloy: */
+
+// View all layers with something like:
+// curl https://tilma.staging.mysociety.org/resource-proxy/proxy.php\?https://hackney.assets/ | jq '.results[] | .layer.code, ( .layer.styles[] | { id, name } ) '
+var layers = [
+ {
+ "categories": ["Street Lighting", "Lamposts"],
+ "item_name": "street light",
+ "layer_name": "Street Lights",
+ "styleid": "5d308d57fe2ad8046c67cdb5",
+ "layerid": "layers_streetLightingAssets"
+ },
+ {
+ "categories": ["Illuminated Bollards", "Non-illuminated Bollards"],
+ "item_name": "bollard",
+ "layer_name": "Bollards",
+ "styleid": "5d308d57fe2ad8046c67cdb9",
+ "layerid": "layers_streetLightingAssets"
+ },
+ {
+ "categories": ["Benches"],
+ "item_name": "bench",
+ "layer_name": "Bench",
+ "styleid": "5e8b16f0ca31500f60b3f589",
+ "layerid": "layers_bench_5e8b15f0ca31500f60b3f568"
+ },
+ {
+ "categories": ["Potholes"],
+ "item_name": "road",
+ "layer_name": "Carriageway",
+ "styleid": "5d53d28bfe2ad80fc4573184",
+ "layerid": "layers_carriageway_5d53cc74fe2ad80c3403b77d"
+ },
+ {
+ "categories": ["Road Markings / Lines"],
+ "item_name": "road",
+ "layer_name": "Markings",
+ "styleid": "5d308dd7fe2ad8046c67da33",
+ "layerid": "layers_highwayAssets"
+ },
+ {
+ "categories": ["Pavement"],
+ "item_name": "pavement",
+ "layer_name": "Footways",
+ "styleid": "5d308dd6fe2ad8046c67da2a",
+ "layerid": "layers_highwayAssets"
+ },
+ {
+ "categories": ["Cycle Tracks"],
+ "item_name": "cycle track",
+ "layer_name": "Cycle Tracks",
+ "styleid": "5d308dd6fe2ad8046c67da29",
+ "layerid": "layers_highwayAssets"
+ },
+ {
+ "categories": ["Drains and gutters"],
+ "item_name": "drain",
+ "layer_name": "Gullies",
+ "styleid": "5d308dd6fe2ad8046c67da2e",
+ "layerid": "layers_highwayAssets"
+ },
+ {
+ "categories": ["Verges"],
+ "item_name": "verge",
+ "layer_name": "Verges",
+ "styleid": "5d308dd7fe2ad8046c67da36",
+ "layerid": "layers_highwayAssets"
+ },
+ {
+ "categories": ["Road Hump Fault / Damage"],
+ "item_name": "road hump",
+ "layer_name": "Traffic Calming",
+ "styleid": "5d308dd7fe2ad8046c67da35",
+ "layerid": "layers_highwayAssets"
+ },
+ {
+ "categories": ["Broken or Faulty Barrier Gates"],
+ "item_name": "barrier gate",
+ "layer_name": "Gates",
+ "styleid": "5d308dd6fe2ad8046c67da2c",
+ "layerid": "layers_highwayAssets"
+ },
+ {
+ "categories": ["Belisha Beacon"],
+ "item_name": "beacon",
+ "layer_name": "Belisha Beacon",
+ "styleid": "5d308d57fe2ad8046c67cdb6",
+ "layerid": "layers_streetLightingAssets"
+ },
+ {
+ "categories": ["Loose or Damaged Kerb Stones"],
+ "item_name": "kerb",
+ "layer_name": "Kerbs",
+ "styleid": "5d308dd6fe2ad8046c67da30",
+ "layerid": "layers_highwayAssets"
+ }
+];
+
+var hackney_defaults = $.extend(true, {}, fixmystreet.alloyv2_defaults, {
+ class: OpenLayers.Layer.NCCVectorAsset,
+ protocol_class: OpenLayers.Protocol.AlloyV2,
+ http_options: {
+ base: "https://tilma.staging.mysociety.org/resource-proxy/proxy.php?https://hackney.assets/${layerid}/${x}/${y}/${z}/cluster?styleIds=${styleid}"
+ },
+ non_interactive: false,
+ body: "Hackney Council",
+ attributes: {
+ asset_resource_id: function() {
+ return this.fid;
+ }
+ }
+});
+
+fixmystreet.alloy_add_layers(hackney_defaults, layers);
+
+})();
diff --git a/web/cobrands/hackney/base.scss b/web/cobrands/hackney/base.scss
new file mode 100644
index 000000000..4bc3fc8b1
--- /dev/null
+++ b/web/cobrands/hackney/base.scss
@@ -0,0 +1,222 @@
+@import "../sass/h5bp";
+@import "./_colours";
+@import "../sass/mixins";
+
+@import "../sass/base";
+
+
+#site-header {
+ box-shadow:
+ 0 0 0 6px $white,
+ 0 0 0 10px $dark-green,
+ 0 0 0 13px $white,
+ 0 0 0 16px $green,
+ 0 0 0 19px $white,
+ 0 0 0 21px $light-green;
+}
+
+#site-logo {
+ background: transparent url('images/hackney-logo-white.png') 0 50% no-repeat;
+ background-size: 200px 36px;
+ width: 200px;
+ &:focus {
+ outline: 4px solid $yellow;
+ }
+}
+
+.nav-menu--mysoc {
+ a {
+ color: $primary_text;
+ background-color: $primary;
+ }
+}
+
+#front-main {
+ background-color: $white;
+ margin: 0;
+ padding: 1em;
+ text-align: inherit;
+
+ h1 {
+ color: $black;
+ }
+
+ #postcodeForm {
+ margin: 0;
+ padding: 0;
+ background: transparent;
+ color: inherit;
+
+ div {
+ border: none;
+ background: transparent;
+ position: relative;
+ background: #fff;
+ box-shadow: 1px 1px 5px 1px rgba(104, 104, 104, 0.4);
+
+ input#pc {
+ box-sizing: border-box;
+ padding: 10px 22px;
+ }
+
+ input#sub {
+ width: 0;
+ padding-left: 50px;
+ overflow: hidden;
+ @include flex(0 0 auto);
+ background: $black url('hackney-search-icon.png') no-repeat 50% 50%;
+ background-size: 25px 25px;
+ color: $black;
+ &:hover,
+ &:focus {
+ background: $dark-green url('hackney-search-icon.png') no-repeat 50% 50%;
+ background-size: 25px 25px;
+ color: $dark_green;
+ }
+ &:focus {
+ outline: 4px solid $yellow;
+ }
+ }
+ }
+ }
+
+ a#geolocate_link {
+ background: transparent;
+ display: block;
+ padding: 0;
+ margin-top: 0.5em;
+ font-family: inherit;
+ font-size: 1em;
+ border-radius: 0;
+ color: $dark_green;
+
+ &:hover {
+ background: transparent;
+ text-decoration: underline;
+ }
+ }
+
+ .form-hint {
+ color: inherit;
+ }
+}
+
+.hackney-footer {
+ background-color: $dark_green;
+ color: #fff;
+ padding: 1em 0;
+}
+
+.hackney-footer__logo {
+ background: transparent url('images/hackney-logo-white.png') 0 50% no-repeat;
+ background-size: 200px 36px;
+ width: 200px;
+ height: 54px;
+ text-indent: -999999px;
+ display: inline-block;
+ &:focus {
+ outline: 4px solid $yellow;
+ }
+}
+
+a.platform-logo {
+ color: #fff;
+}
+
+#front_stats {
+ background-color: $dark_green;
+ color: #fff;
+}
+
+.green-btn,
+.btn {
+ border-radius: 4px;
+ font-size: 1.1875em;
+ font-weight: 400;
+ line-height: 1.4375em;
+ vertical-align: top;
+ display: inline-block;
+ position: relative;
+ border: 1px solid $dark_green;
+ box-shadow: inset #003d2f 0 -2px 0 0;
+ &:hover {
+ background: none;
+ background-color: #005a30;
+ color: #fff !important;
+ }
+
+ &:focus {
+ border-color:#ffdd00;
+ color:#0b0c0c !important;
+ background: none;
+ background-color:#ffdd00;
+ box-shadow: 0 2px 0#0b0c0c;
+ }
+}
+
+.btn--primary,
+.btn-primary,
+.green-btn {
+ background: $dark_green;
+ border: 2px solid transparent;
+ color: #ffffff;
+ box-shadow: 0 2px 0 #002d18;
+}
+
+textarea.form-control,
+.dropzone,
+input.form-control {
+ border: 2px solid #0b0c0c;
+ border-radius: 0;
+ box-shadow: none;
+ &:focus {
+ outline: 3px solid#ffdd00;
+ outline-offset: 0;
+ box-shadow: inset 0 0 0 2px;
+ }
+}
+
+label, legend, .label {
+ font-weight: 300;
+}
+
+.big-green-banner {
+ background-color: $dark_green;
+ text-transform: none;
+}
+
+div.form-error, p.form-error {
+ color: $red;
+ background-color: transparent;
+ padding: 0;
+ width: 100%;
+
+ a {
+ color: $red;
+ text-decoration: underline;
+ }
+}
+
+
+input.form-error, textarea.form-error,
+select.form-error {
+ border-color: $red;
+ border-radius: 0 !important;
+}
+
+.box-warning {
+ background-color: rgba(164, 214, 94, 0.5);
+}
+
+.dz-clickable:hover, .dz-drag-hover {
+ border-color: $light-green;
+}
+
+.dz-clickable:hover .dz-message u, .dz-drag-hover .dz-message u {
+ color: $green;
+}
+
+.sso-staff-sign-in {
+ font-size: 0.9em;
+ margin: 1em 0;
+}
diff --git a/web/cobrands/hackney/hackney-search-icon.png b/web/cobrands/hackney/hackney-search-icon.png
new file mode 100644
index 000000000..99305f24a
--- /dev/null
+++ b/web/cobrands/hackney/hackney-search-icon.png
Binary files differ
diff --git a/web/cobrands/hackney/images/hackney-logo-white.png b/web/cobrands/hackney/images/hackney-logo-white.png
new file mode 100644
index 000000000..3a41df786
--- /dev/null
+++ b/web/cobrands/hackney/images/hackney-logo-white.png
Binary files differ
diff --git a/web/cobrands/hackney/layout.scss b/web/cobrands/hackney/layout.scss
new file mode 100644
index 000000000..8b6fa6278
--- /dev/null
+++ b/web/cobrands/hackney/layout.scss
@@ -0,0 +1,143 @@
+@import "_colours";
+@import "../sass/layout";
+
+#main-nav {
+ display: block; // remove flex so nav touches top of parent
+ min-height: 0; // no vertical align, so no need for a height
+ margin-top: 0; // don't bother overlapping the border
+ height: auto;
+ margin-top: 0.5em;
+ .frontpage & {
+ margin-top: 1em;
+ }
+ .ie9 & {
+ position: static;
+ float: $right;
+
+ & > * {
+ position: static;
+ -ms-transform: none;
+ }
+ }
+
+ a {
+ font-weight: 600;
+ }
+}
+
+.nav-menu--mysoc {
+ padding: 0em 0.5em;
+ margin-#{$left}: 0.25em;
+ background-color: $primary;
+ @include border-radius(0 0 0.375em 0.375em);
+ a {
+ background-color: transparent;
+ color: $primary_text;
+ text-transform: uppercase;
+ font-size: 0.6875em;
+ padding: 1.3em 0.7em 1em;
+ &:hover {
+ color: #fff;
+ background-color: transparent;
+ }
+ }
+}
+
+body.frontpage {
+ #site-logo {
+ margin: 0.5em 0 0.5em;
+ width: 200px;
+ height: 54px;
+ background: transparent url('images/hackney-logo-white.png') 0 50% no-repeat;
+ background-size: 200px 36px;
+ }
+}
+
+#site-header {
+ padding: 4px 0;
+ margin-bottom: 2em;
+ .mappage & {
+ margin-bottom: 0;
+ }
+}
+
+#front-main {
+ background-color: $white;
+ padding: 50px 0;
+ border-bottom: 3px solid $light_green;
+ label {
+ font-size: 18px;
+ font-weight: 600;
+ }
+
+ h1 {
+ font-size: 40px;
+ font-weight: 600;
+ }
+
+ #postcodeForm {
+ overflow: visible;
+ margin: 0;
+
+ div {
+ margin: 0;
+ }
+ }
+
+ #front-main-container {
+ padding: 0 1em;
+ }
+}
+
+ol.big-numbers>li:before {
+ color: $dark_green;
+}
+
+.nav-menu--main span {
+ color: $light_green;
+ font-weight: 600;
+}
+
+#front_stats {
+ border-color: $dark_green;
+ big {
+ color: $dark_green;
+ }
+}
+
+.nav-menu--main a.report-a-problem-btn {
+ background-color: transparent;
+ color: white;
+ &:hover,
+ &:active,
+ &:focus {
+ background-color: transparent;
+ color: white;
+ }
+}
+
+#front-howto h2,
+#front-recently h2 {
+ font-weight: 600;
+}
+
+#front_stats {
+ background-color: transparent;
+}
+
+.big-green-banner {
+ background-color: $dark_green;
+}
+
+.footer-powered-by {
+ position: relative;
+ top: -40px;
+ right: 0;
+ text-align: right;
+
+}
+
+a.platform-logo {
+ text-align: left;
+
+} \ No newline at end of file
diff --git a/web/cobrands/northamptonshire/assets.js b/web/cobrands/northamptonshire/assets.js
index 2b6cdeb69..377e1091c 100644
--- a/web/cobrands/northamptonshire/assets.js
+++ b/web/cobrands/northamptonshire/assets.js
@@ -439,26 +439,7 @@ var northants_defaults = $.extend(true, {}, fixmystreet.alloy_defaults, {
}
});
-$.each(layers, function(index, layer) {
- if ( layer.categories ) {
- var options = {
- http_options: {
- layerid: layer.layer,
- layerVersion: layer.version,
- },
- asset_type: layer.asset_type || 'spot',
- asset_category: layer.categories,
- asset_item: layer.item_name || layer.layer_name.toLowerCase(),
- };
- if (layer.max_resolution) {
- options.max_resolution = layer.max_resolution;
- }
- if (layer.snap_threshold || layer.snap_threshold === 0) {
- options.snap_threshold = layer.snap_threshold;
- }
- fixmystreet.assets.add(northants_defaults, options);
- }
-});
+fixmystreet.alloy_add_layers(northants_defaults, layers);
// NCC roads layers which prevent report submission unless we have selected
// an asset.