diff options
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(); @@ -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&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 Binary files differnew file mode 100644 index 000000000..99305f24a --- /dev/null +++ b/web/cobrands/hackney/hackney-search-icon.png diff --git a/web/cobrands/hackney/images/hackney-logo-white.png b/web/cobrands/hackney/images/hackney-logo-white.png Binary files differnew file mode 100644 index 000000000..3a41df786 --- /dev/null +++ b/web/cobrands/hackney/images/hackney-logo-white.png 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. |