diff options
37 files changed, 446 insertions, 139 deletions
diff --git a/bin/fixmystreet.com/fixture b/bin/fixmystreet.com/fixture new file mode 100755 index 000000000..93982af8a --- /dev/null +++ b/bin/fixmystreet.com/fixture @@ -0,0 +1,39 @@ +#!/usr/bin/env perl +# +# This script will create a test body and its categories, covering the area of +# Westminster, and a user associated with that body, which should help testing +# of report interactions. + +use strict; +use warnings; + +BEGIN { + use File::Basename qw(dirname); + use File::Spec; + my $d = dirname(File::Spec->rel2abs($0)); + require "$d/../../setenv.pl"; +} + +use FixMyStreet::DB; + +my $body = FixMyStreet::DB->resultset("Body")->find_or_create({ name => 'Test City Council' }); +$body->body_areas->find_or_create({ area_id => 2504 }); +foreach ("Potholes", "Street lighting", "Graffiti") { + (my $email = lc $_) =~ s/ /-/g; + $body->contacts->find_or_create({ + category => $_, + email => $email . '@example.net', + confirmed => 't', + deleted => 'f', + whenedited => \'current_timestamp', + editor => 'fixture', + note => 'Created by fixture' + }); +} + +FixMyStreet::DB->resultset("User")->find_or_create({ + email => 'council@example.net', + name => 'Test City Council User', + from_body => $body, + password => 'password', +}); diff --git a/conf/general.yml-example b/conf/general.yml-example index 54bbd6a7f..3b2c597b9 100644 --- a/conf/general.yml-example +++ b/conf/general.yml-example @@ -46,6 +46,9 @@ STAGING_SITE: 1 # reports to live places. Set this to 1 if you want a dev site to route # reports as normal. SEND_REPORTS_ON_STAGING: 0 +# Manual testing of multiple cobrands can be made easier by skipping some +# checks they have in them, if this variable is set +SKIP_CHECKS_ON_STAGING: 0 # What to use as front page/alert example places placeholder # Defaults to High Street, Main Street diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm index b643c9633..46ac10d36 100644 --- a/perllib/FixMyStreet/App/Controller/Admin.pm +++ b/perllib/FixMyStreet/App/Controller/Admin.pm @@ -345,8 +345,6 @@ sub update_contacts : Private { my $category = $self->trim( $c->get_param('category') ); $errors{category} = _("Please choose a category") unless $category; - my $email = $self->trim( $c->get_param('email') ); - $errors{email} = _('Please enter a valid email') unless is_valid_email($email) || $email eq 'REFUSED'; $errors{note} = _('Please enter a message') unless $c->get_param('note'); my $contact = $c->model('DB::Contact')->find_or_new( @@ -356,6 +354,12 @@ sub update_contacts : Private { } ); + my $email = $self->trim( $c->get_param('email') ); + my $send_method = $c->get_param('send_method') || $contact->send_method || $contact->body->send_method || ""; + unless ( $send_method eq 'Open311' ) { + $errors{email} = _('Please enter a valid email') unless is_valid_email($email) || $email eq 'REFUSED'; + } + $contact->email( $email ); $contact->confirmed( $c->get_param('confirmed') ? 1 : 0 ); $contact->deleted( $c->get_param('deleted') ? 1 : 0 ); @@ -683,7 +687,7 @@ sub report_edit : Path('report_edit') : Args(1) { unless ( $c->cobrand->moniker eq 'zurich' - || $c->user->has_permission_to(report_edit => $problem->bodies_str) + || $c->user->has_permission_to(report_edit => $problem->bodies_str_ids) ) { $c->detach( '/page_error_403_access_denied', [] ); } @@ -1030,15 +1034,17 @@ sub users: Path('users') : Args(0) { my %email2user = map { $_->email => $_ } @users; $c->stash->{users} = [ @users ]; - my $emails = $c->model('DB::Abuse')->search( - { email => { ilike => $isearch } } - ) if $c->user->is_superuser; - foreach my $email ($emails->all) { - # Slight abuse of the boolean flagged value - if ($email2user{$email->email}) { - $email2user{$email->email}->flagged( 2 ); - } else { - push @{$c->stash->{users}}, { email => $email->email, flagged => 2 }; + if ( $c->user->is_superuser ) { + my $emails = $c->model('DB::Abuse')->search( + { email => { ilike => $isearch } } + ); + foreach my $email ($emails->all) { + # Slight abuse of the boolean flagged value + if ($email2user{$email->email}) { + $email2user{$email->email}->flagged( 2 ); + } else { + push @{$c->stash->{users}}, { email => $email->email, flagged => 2 }; + } } } @@ -1218,7 +1224,7 @@ sub user_edit : Path('user_edit') : Args(1) { my $user = $c->cobrand->users->find( { id => $id } ); $c->detach( '/page_error_404_not_found', [] ) unless $user; - unless ( $c->user->is_superuser || $c->user->has_body_permission_to('user_edit') ) { + unless ( $c->user->is_superuser || $c->user->has_body_permission_to('user_edit') || $c->cobrand->moniker eq 'zurich' ) { $c->detach('/page_error_403_access_denied', []); } @@ -1253,7 +1259,7 @@ sub user_edit : Path('user_edit') : Args(1) { $user->is_superuser( ( $c->user->is_superuser && $c->get_param('is_superuser') ) || 0 ); # Superusers can set from_body to any value, but other staff can only # set from_body to the same value as their own from_body. - if ( $c->user->is_superuser ) { + if ( $c->user->is_superuser || $c->cobrand->moniker eq 'zurich' ) { $user->from_body( $c->get_param('body') || undef ); } elsif ( $c->user->has_body_permission_to('user_assign_body') && $c->get_param('body') && $c->get_param('body') eq $c->user->from_body->id ) { @@ -1270,14 +1276,14 @@ sub user_edit : Path('user_edit') : Args(1) { if (!$user->from_body) { # Non-staff users aren't allowed any permissions or to be in an area - $user->user_body_permissions->delete_all; + $user->admin_user_body_permissions->delete; $user->area_id(undef); delete $c->stash->{areas}; delete $c->stash->{fetched_areas_body_id}; } elsif ($c->stash->{available_permissions}) { my @all_permissions = map { keys %$_ } values %{ $c->stash->{available_permissions} }; my @user_permissions = grep { $c->get_param("permissions[$_]") ? 1 : undef } @all_permissions; - $user->user_body_permissions->search({ + $user->admin_user_body_permissions->search({ body_id => $user->from_body->id, permission_type => { '!=' => \@user_permissions }, })->delete; @@ -1295,6 +1301,35 @@ sub user_edit : Path('user_edit') : Args(1) { $user->area_id( $valid_areas{$new_area} ? $new_area : undef ); } + # Handle 'trusted' flag(s) + my @trusted_bodies = $c->get_param_list('trusted_bodies'); + if ( $c->user->is_superuser ) { + $user->user_body_permissions->search({ + body_id => { '!=' => \@trusted_bodies }, + permission_type => 'trusted', + })->delete; + foreach my $body_id (@trusted_bodies) { + $user->user_body_permissions->find_or_create({ + body_id => $body_id, + permission_type => 'trusted', + }); + } + } elsif ( $c->user->from_body ) { + my %trusted = map { $_ => 1 } @trusted_bodies; + my $body_id = $c->user->from_body->id; + if ( $trusted{$body_id} ) { + $user->user_body_permissions->find_or_create({ + body_id => $body_id, + permission_type => 'trusted', + }); + } else { + $user->user_body_permissions->search({ + body_id => $body_id, + permission_type => 'trusted', + })->delete; + } + } + unless ($user->email) { $c->stash->{field_errors}->{email} = _('Please enter a valid email'); return; diff --git a/perllib/FixMyStreet/App/Controller/Contact.pm b/perllib/FixMyStreet/App/Controller/Contact.pm index c2cc54832..b98bdbcc7 100644 --- a/perllib/FixMyStreet/App/Controller/Contact.pm +++ b/perllib/FixMyStreet/App/Controller/Contact.pm @@ -236,6 +236,10 @@ sub send_email : Private { my $recipient = $c->cobrand->contact_email; my $recipient_name = $c->cobrand->contact_name(); + if (my $localpart = $c->get_param('recipient')) { + $recipient = join('@', $localpart, FixMyStreet->config('EMAIL_DOMAIN')); + } + $c->stash->{host} = $c->req->header('HOST'); $c->stash->{ip} = $c->req->address; $c->stash->{ip} .= diff --git a/perllib/FixMyStreet/App/Controller/Moderate.pm b/perllib/FixMyStreet/App/Controller/Moderate.pm index dadec5c53..94e6cd62a 100644 --- a/perllib/FixMyStreet/App/Controller/Moderate.pm +++ b/perllib/FixMyStreet/App/Controller/Moderate.pm @@ -54,7 +54,7 @@ sub report : Chained('moderate') : PathPart('report') : CaptureArgs(1) { # ... and immediately, if the user isn't authorized $c->detach unless $c->user_exists; - $c->detach unless $c->user->has_permission_to(moderate => $problem->bodies_str); + $c->detach unless $c->user->has_permission_to(moderate => $problem->bodies_str_ids); $c->forward('/auth/check_csrf_token'); diff --git a/perllib/FixMyStreet/App/Controller/Report.pm b/perllib/FixMyStreet/App/Controller/Report.pm index 1d67afd0e..34392782b 100644 --- a/perllib/FixMyStreet/App/Controller/Report.pm +++ b/perllib/FixMyStreet/App/Controller/Report.pm @@ -133,7 +133,7 @@ sub load_problem_or_display_error : Private { } $c->stash->{problem} = $problem; - if ( $c->user_exists && $c->user->has_permission_to(moderate => $problem->bodies_str) ) { + if ( $c->user_exists && $c->user->has_permission_to(moderate => $problem->bodies_str_ids) ) { $c->stash->{problem_original} = $problem->find_or_new_related( moderation_original_data => { title => $problem->title, @@ -401,7 +401,7 @@ to the current Problem in $c->stash->{problem}. Shows the 403 page if not. sub check_has_permission_to : Private { my ( $self, $c, @permissions ) = @_; - my $bodies = $c->stash->{problem}->bodies_str; + my $bodies = $c->stash->{problem}->bodies_str_ids; my %permissions = map { $_ => $c->user->has_permission_to($_, $bodies) } @permissions if $c->user_exists; diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm index cee72244f..f26120829 100644 --- a/perllib/FixMyStreet/App/Controller/Report/New.pm +++ b/perllib/FixMyStreet/App/Controller/Report/New.pm @@ -190,9 +190,9 @@ sub report_form_ajax : Path('ajax') : Args(0) { my $contribute_as = {}; if ($c->user_exists) { - my $bodies = join(',', keys %{$c->stash->{bodies}}); - my $ca_another_user = $c->user->has_permission_to('contribute_as_another_user', $bodies); - my $ca_body = $c->user->has_permission_to('contribute_as_body', $bodies); + my @bodies = keys %{$c->stash->{bodies}}; + my $ca_another_user = $c->user->has_permission_to('contribute_as_another_user', \@bodies); + my $ca_body = $c->user->has_permission_to('contribute_as_body', \@bodies); $contribute_as->{another_user} = $ca_another_user if $ca_another_user; $contribute_as->{body} = $ca_body if $ca_body; } diff --git a/perllib/FixMyStreet/App/Controller/Report/Update.pm b/perllib/FixMyStreet/App/Controller/Report/Update.pm index 705e6ee99..4c2d92d5e 100644 --- a/perllib/FixMyStreet/App/Controller/Report/Update.pm +++ b/perllib/FixMyStreet/App/Controller/Report/Update.pm @@ -113,7 +113,7 @@ sub process_user : Private { if ( $c->user_exists ) { { my $user = $c->user->obj; - if ($c->stash->{contributing_as_another_user} = $user->contributing_as('another_user', $c, $update->problem->bodies_str)) { + if ($c->stash->{contributing_as_another_user} = $user->contributing_as('another_user', $c, $update->problem->bodies_str_ids)) { # Act as if not logged in (and it will be auto-confirmed later on) last; } @@ -276,7 +276,7 @@ sub process_update : Private { $update->mark_fixed($params{fixed} ? 1 : 0); $update->mark_open($params{reopen} ? 1 : 0); - $c->stash->{contributing_as_body} = $c->user_exists && $c->user->contributing_as('body', $c, $update->problem->bodies_str); + $c->stash->{contributing_as_body} = $c->user_exists && $c->user->contributing_as('body', $c, $update->problem->bodies_str_ids); if ($c->stash->{contributing_as_body}) { $update->name($c->user->from_body->name); $update->anonymous(0); @@ -286,7 +286,7 @@ sub process_update : Private { } if ( $params{state} ) { - $params{state} = 'fixed - council' + $params{state} = 'fixed - council' if $params{state} eq 'fixed' && $c->user && $c->user->belongs_to_body( $update->problem->bodies_str ); $update->problem_state( $params{state} ); } else { diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm index 47e577372..c44842dea 100644 --- a/perllib/FixMyStreet/Cobrand/Default.pm +++ b/perllib/FixMyStreet/Cobrand/Default.pm @@ -717,9 +717,15 @@ sub available_permissions { report_edit_priority => _("Edit report priority"), # future use report_inspect => _("Markup problem details"), report_instruct => _("Instruct contractors to fix problems"), # future use - planned_reports => _("Manage planned reports list"), + planned_reports => _("Manage shortlist"), contribute_as_another_user => _("Create reports/updates on a user's behalf"), contribute_as_body => _("Create reports/updates as the council"), + + # NB this permission is special in that it can be assigned to users + # without their from_body being set. It's included here for + # reference, but left commented out because it's not assigned in the + # same way as other permissions. + # trusted => _("Trusted to make reports that don't need to be inspected"), }, _("Users") => { user_edit => _("Edit other users' details"), diff --git a/perllib/FixMyStreet/Cobrand/FiksGataMi.pm b/perllib/FixMyStreet/Cobrand/FiksGataMi.pm index ba26b7a2c..242735073 100644 --- a/perllib/FixMyStreet/Cobrand/FiksGataMi.pm +++ b/perllib/FixMyStreet/Cobrand/FiksGataMi.pm @@ -29,6 +29,8 @@ sub disambiguate_location { } sub area_types { + my $self = shift; + return $self->next::method() if FixMyStreet->config('STAGING_SITE') && FixMyStreet->config('SKIP_CHECKS_ON_STAGING'); [ 'NKO', 'NFY', 'NRA' ]; } diff --git a/perllib/FixMyStreet/Cobrand/FixaMinGata.pm b/perllib/FixMyStreet/Cobrand/FixaMinGata.pm index 9ffbf00b8..a321d5c7c 100644 --- a/perllib/FixMyStreet/Cobrand/FixaMinGata.pm +++ b/perllib/FixMyStreet/Cobrand/FixaMinGata.pm @@ -30,6 +30,8 @@ sub disambiguate_location { } sub area_types { + my $self = shift; + return $self->next::method() if FixMyStreet->config('STAGING_SITE') && FixMyStreet->config('SKIP_CHECKS_ON_STAGING'); [ 'KOM' ]; } diff --git a/perllib/FixMyStreet/Cobrand/UKCouncils.pm b/perllib/FixMyStreet/Cobrand/UKCouncils.pm index 5d72c4962..42c9c5cbc 100644 --- a/perllib/FixMyStreet/Cobrand/UKCouncils.pm +++ b/perllib/FixMyStreet/Cobrand/UKCouncils.pm @@ -42,11 +42,13 @@ sub restriction { sub problems_restriction { my ($self, $rs) = @_; + return $rs if FixMyStreet->config('STAGING_SITE') && FixMyStreet->config('SKIP_CHECKS_ON_STAGING'); return $rs->to_body($self->council_id); } sub updates_restriction { my ($self, $rs) = @_; + return $rs if FixMyStreet->config('STAGING_SITE') && FixMyStreet->config('SKIP_CHECKS_ON_STAGING'); return $rs->to_body($self->council_id); } @@ -96,6 +98,8 @@ sub enter_postcode_text { sub area_check { my ( $self, $params, $context ) = @_; + return 1 if FixMyStreet->config('STAGING_SITE') && FixMyStreet->config('SKIP_CHECKS_ON_STAGING'); + my $councils = $params->{all_areas}; my $council_match = defined $councils->{$self->council_id}; if ($council_match) { diff --git a/perllib/FixMyStreet/DB/Result/User.pm b/perllib/FixMyStreet/DB/Result/User.pm index 0ba7e252c..2a2d0d5e3 100644 --- a/perllib/FixMyStreet/DB/Result/User.pm +++ b/perllib/FixMyStreet/DB/Result/User.pm @@ -257,15 +257,14 @@ sub permissions { } sub has_permission_to { - my ($self, $permission_type, $body_id) = @_; + my ($self, $permission_type, $body_ids) = @_; return 1 if $self->is_superuser; + return 0 unless $body_ids; - return 0 unless $self->belongs_to_body($body_id); - - my $permission = $self->user_body_permissions->find({ + my $permission = $self->user_body_permissions->find({ permission_type => $permission_type, - body_id => $self->from_body->id, + body_id => $body_ids, }); return $permission ? 1 : 0; } @@ -293,10 +292,25 @@ sub has_body_permission_to { return $self->has_permission_to($permission_type, $self->from_body->id); } +=head2 admin_user_body_permissions + +Some permissions aren't managed in the normal way via the admin, e.g. the +'trusted' permission. This method returns a query that excludes such exceptional +permissions. + +=cut + +sub admin_user_body_permissions { + my $self = shift; + + return $self->user_body_permissions->search({ + permission_type => { '!=' => 'trusted' }, + }); +} + sub contributing_as { my ($self, $other, $c, $bodies) = @_; - $bodies = join(',', keys %$bodies) if ref $bodies eq 'HASH'; - $c->log->error("Bad data $bodies passed to contributing_as") if ref $bodies; + $bodies = [ keys %$bodies ] if ref $bodies eq 'HASH'; my $form_as = $c->get_param('form_as') || ''; return 1 if $form_as eq $other && $self->has_permission_to("contribute_as_$other", $bodies); } @@ -327,7 +341,7 @@ sub adopt { $other->delete; } -# Planned reports +# Planned reports / shortlist # Override the default auto-created function as we only want one live entry per user around add_to_planned_reports => sub { diff --git a/perllib/FixMyStreet/Script/Reports.pm b/perllib/FixMyStreet/Script/Reports.pm index ab0d90ba8..8d3b2ddbc 100644 --- a/perllib/FixMyStreet/Script/Reports.pm +++ b/perllib/FixMyStreet/Script/Reports.pm @@ -144,9 +144,14 @@ sub send(;$) { $reporters{ $sender } ||= $sender->new(); my $inspection_required = $sender_info->{contact}->get_extra_metadata('inspection_required') if $sender_info->{contact}; - if ( $inspection_required && !$row->get_extra_metadata('inspected') ) { - $skip = 1; - debug_print("skipped because not yet inspected", $row->id) if $debug_mode; + if ( $inspection_required ) { + unless ( + $row->get_extra_metadata('inspected') || + $row->user->has_permission_to( trusted => $row->bodies_str_ids ) + ) { + $skip = 1; + debug_print("skipped because not yet inspected", $row->id) if $debug_mode; + } } if ( $reporters{ $sender }->should_skip( $row ) ) { diff --git a/perllib/Utils.pm b/perllib/Utils.pm index 87c1a10d6..84c09d09d 100644 --- a/perllib/Utils.pm +++ b/perllib/Utils.pm @@ -15,6 +15,7 @@ use Encode; use File::Slurp qw(); use mySociety::GeoUtil; use mySociety::Locale; +use FixMyStreet; =head2 convert_latlon_to_en @@ -199,7 +200,7 @@ sub prettify_duration { $s = int(($s+60*60*12)/60/60/24)*60*60*24; } elsif ($nearest eq 'hour') { $s = int(($s+60*30)/60/60)*60*60; - } elsif ($nearest eq 'minute') { + } else { # minute $s = int(($s+30)/60)*60; return _('less than a minute') if $s == 0; } @@ -221,7 +222,7 @@ sub _part { $str = mySociety::Locale::nget("%d day", "%d days", $i); } elsif ($m == 60*60) { $str = mySociety::Locale::nget("%d hour", "%d hours", $i); - } elsif ($m == 60) { + } else { $str = mySociety::Locale::nget("%d minute", "%d minutes", $i); } push @$o, sprintf($str, $i); @@ -229,17 +230,5 @@ sub _part { } } -=head2 read_file - -Reads in a UTF-8 encoded file using File::Slurp and decodes it from UTF-8. -This appears simplest, rather than getting confused with binmodes and so on. - -=cut -sub read_file { - my $filename = shift; - my $data = File::Slurp::read_file( $filename ); - $data = Encode::decode( 'utf8', $data ); - return $data; -} 1; diff --git a/t/app/controller/admin.t b/t/app/controller/admin.t index 8c3cde4b7..7ba84b652 100644 --- a/t/app/controller/admin.t +++ b/t/app/controller/admin.t @@ -1165,6 +1165,7 @@ my %default_perms = ( "permissions[template_edit]" => undef, "permissions[responsepriority_edit]" => undef, "permissions[category_edit]" => undef, + trusted_bodies => undef, ); FixMyStreet::override_config { diff --git a/t/app/controller/contact.t b/t/app/controller/contact.t index dd94fc431..7c2769b9c 100644 --- a/t/app/controller/contact.t +++ b/t/app/controller/contact.t @@ -250,7 +250,7 @@ for my $test ( $mech->get_ok('/contact'); } $mech->submit_form_ok( { with_fields => $test->{fields} } ); - $mech->content_contains('Thank you for your feedback'); + $mech->content_contains('Thank you for your enquiry'); my $email = $mech->get_email; @@ -376,7 +376,7 @@ for my $test ( $mech->clear_emails_ok; $mech->get_ok('/contact'); $mech->submit_form_ok( { with_fields => $test->{fields} } ); - $mech->content_contains('Thank you for your feedback'); + $mech->content_contains('Thank you for your enquiry'); $mech->email_count_is(1); } }; diff --git a/t/app/controller/my_planned.t b/t/app/controller/my_planned.t index 25f82224e..7bd1dd2cd 100644 --- a/t/app/controller/my_planned.t +++ b/t/app/controller/my_planned.t @@ -13,8 +13,8 @@ my $body = $mech->create_body_ok(2237, 'Oxfordshire'); my ($problem) = $mech->create_problems_for_body(1, $body->id, 'Test Title'); $mech->get_ok($problem->url); -$mech->content_lacks('Add to planned reports'); -$mech->content_lacks('Remove from planned reports'); +$mech->content_lacks('Shortlist'); +$mech->content_lacks('Shortlisted'); my $user = $mech->log_in_ok( 'test@example.com' ); $user->update({ from_body => $body }); @@ -39,11 +39,11 @@ $mech->get_ok('/my/planned'); $mech->content_contains('Test Title'); $mech->get_ok($problem->url); -$mech->content_contains('Remove from planned reports'); +$mech->content_contains('Shortlisted'); $mech->submit_form_ok({ with_fields => { change => 'remove' } }); -$mech->content_contains('Add to planned reports'); +$mech->content_contains('Shortlist'); $mech->submit_form_ok({ with_fields => { change => 'add' } }); -$mech->content_contains('Remove from planned reports'); +$mech->content_contains('Shortlisted'); done_testing(); diff --git a/t/app/sendreport/inspection_required.t b/t/app/sendreport/inspection_required.t index 178fa2a1f..88a48e991 100644 --- a/t/app/sendreport/inspection_required.t +++ b/t/app/sendreport/inspection_required.t @@ -52,8 +52,28 @@ subtest 'Report is sent when inspected' => sub { ok $report->whensent, 'Report marked as sent'; }; +subtest 'Uninspected report is sent when made by trusted user' => sub { + $mech->clear_emails_ok; + $report->unset_extra_metadata('inspected'); + $report->whensent( undef ); + $report->update; + + $user->user_body_permissions->find_or_create({ + body => $body, + permission_type => 'trusted', + }); + ok $user->has_permission_to('trusted', $report->bodies_str_ids), 'User can make trusted reports'; + + FixMyStreet::DB->resultset('Problem')->send_reports(); + + $report->discard_changes; + $mech->email_count_is( 1 ); + ok $report->whensent, 'Report marked as sent'; +}; + done_testing(); END { + $mech->delete_user($user); $mech->delete_body($body); } @@ -4,6 +4,9 @@ use strict; use warnings; use Test::More; +use mySociety::Locale; +mySociety::Locale::gettext_domain('FixMyStreet'); + use Utils; my @truncate_tests = ( @@ -34,9 +37,14 @@ foreach my $test (@convert_en_to_latlon_tests) { [ Utils::convert_en_to_latlon_truncated( $e, $n ) ], # [ $lat, $lon ], # "convert ($e,$n) to ($lat,$lon)"; + is_deeply + [ Utils::convert_latlon_to_en( $lat, $lon ) ], + [ $e, $n ], + "convert ($lat,$lon) to ($e,$n)"; } my @cleanup_tests = ( + [ '', '', '' ], [ 'dog shit', 'Dog poo', 'dog poo' ], [ 'dog shit', 'Dog poo', 'with spaces' ], [ 'dog shite', 'Dog poo', 'with extra e' ], @@ -57,4 +65,32 @@ foreach my $test ( @cleanup_tests ) { is Utils::cleanup_text( "This has new\n\n\nlines in it", { allow_multiline => 1 } ), "This has new\n\nLines in it", 'new lines allowed'; + +is Utils::prettify_dt(), "[unknown time]"; +my $dt = DateTime->now; +is Utils::prettify_dt($dt), $dt->strftime("%H:%M today"); + +# Same week test +if ($dt->day_of_week == 7) { # Sunday + $dt = DateTime->now->add(days => 1); +} else { + $dt = DateTime->now->subtract(days => 1); +} +is Utils::prettify_dt($dt), $dt->strftime("%H:%M, %A"); + +$dt = DateTime->now->subtract(days => 100); +is Utils::prettify_dt($dt), $dt->strftime("%H:%M, %A %e %B %Y"); +is Utils::prettify_dt($dt, "date"), $dt->strftime("%A %e %B %Y"); +is Utils::prettify_dt($dt, "zurich"), $dt->strftime("%H:%M, %e. %B %Y"); +is Utils::prettify_dt($dt, "short"), $dt->strftime("%H:%M, %e %b %Y"); +is Utils::prettify_dt($dt, 1), $dt->strftime("%H:%M, %e %b %Y"); +$dt = DateTime->now->subtract(days => 400); +is Utils::prettify_dt($dt), $dt->strftime("%H:%M, %a %e %B %Y"); + +is Utils::prettify_duration(7*86400+3600+60+1, 'week'), '1 week'; +is Utils::prettify_duration(86400+3600+60+1, 'day'), '1 day'; +is Utils::prettify_duration(86400+3600+60+1, 'hour'), '1 day, 1 hour'; +is Utils::prettify_duration(86400+3600+60+1, 'minute'), '1 day, 1 hour, 1 minute'; +is Utils::prettify_duration(20, 'minute'), 'less than a minute'; + done_testing(); diff --git a/t/utils/email.t b/t/utils/email.t new file mode 100644 index 000000000..23814c1d7 --- /dev/null +++ b/t/utils/email.t @@ -0,0 +1,34 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; +use Test::MockModule; + +use Utils::Email; + +my $resolver = Test::MockModule->new('Net::DNS::Resolver'); +$resolver->mock('send', sub { + my ($self, $domain, $type) = @_; + my @rrs; + is $type, 'TXT'; + if ($domain eq '_dmarc.yahoo.com') { + @rrs = ( + Net::DNS::RR->new(name => '_dmarc.yahoo.com', type => 'TXT', txtdata => 'p=reject'), + Net::DNS::RR->new(name => '_dmarc.yahoo.com', type => 'A'), + ); + } elsif ($domain eq 'cname.example.com') { + @rrs = Net::DNS::RR->new(name => 'cname.example.com', type => 'TXT', txtdata => 'p=none'); + } else { + @rrs = Net::DNS::RR->new(name => '_dmarc.example.net', type => 'CNAME', cname => 'cname.example.com'); + } + my $pkt = Net::DNS::Packet->new; + push @{$pkt->{answer}}, @rrs; + return $pkt; +}); + +is Utils::Email::test_dmarc('BAD'), undef; +is Utils::Email::test_dmarc('test@yahoo.com'), 1; +is Utils::Email::test_dmarc('test@example.net'), undef; + +done_testing(); diff --git a/templates/web/base/admin/response_templates_select.html b/templates/web/base/admin/response_templates_select.html index 33f50170e..fbe09268e 100644 --- a/templates/web/base/admin/response_templates_select.html +++ b/templates/web/base/admin/response_templates_select.html @@ -1,3 +1,4 @@ +[% IF problem.response_templates %] <div class="response_templates_select"> <select id="templates_for_[% for %]" class="js-template-name" data-for="[% for %]" name="response_template"> <option value="">[% loc('--Choose a template--') %]</option> @@ -6,3 +7,4 @@ [% END %] </select> </div> +[% END %] diff --git a/templates/web/base/admin/user-form.html b/templates/web/base/admin/user-form.html index 8a9ba5a77..40e0b510a 100644 --- a/templates/web/base/admin/user-form.html +++ b/templates/web/base/admin/user-form.html @@ -23,7 +23,7 @@ <li><label for="phone">[% loc('Phone:') %]</label> <input type='text' id='phone' name='phone' value='[% user.phone | html %]'></li> - [% IF c.user.is_superuser %] + [% IF c.user.is_superuser || c.cobrand.moniker == 'zurich' %] <li> <div class="admin-hint"> <p> @@ -53,7 +53,7 @@ </li> [% END %] - [% IF areas %] + [% IF areas AND c.cobrand.moniker != 'zurich' %] <li> <div class="admin-hint"> <p> @@ -85,10 +85,33 @@ [% loc("You can add an abusive user's email to the abuse list, which automatically hides (and never sends) reports they create.") %] </p> </div> - + [% loc('Flagged:') %] <input type="checkbox" id="flagged" name="flagged"[% user.flagged ? ' checked' : '' %]> </li> + [% UNLESS user.is_superuser %] + <li> + <div class="admin-hint"> + <p> + [% loc("Reports made by trusted users will be sent to the responsible body without being inspected first.") %] + </p> + </div> + [% IF c.user.is_superuser %] + [% loc('Trusted by bodies:') %]<br /> + <select id='body' name='trusted_bodies' multiple> + [% FOR body IN bodies %] + <option value="[% body.id %]"[% ' selected' IF user.has_permission_to('trusted', body.id) %]>[% body.name %]</option> + [% END %] + </select> + [% ELSE %] + <label> + [% loc('Trusted:') %] + <input type="checkbox" id="trusted_bodies" name="trusted_bodies" value="[% c.user.from_body.id %]" [% 'checked' IF user.has_permission_to('trusted', c.user.from_body.id) %]> + </label> + [% END %] + </li> + [% END %] + [% IF c.user.is_superuser %] <li> <div class="admin-hint"> diff --git a/templates/web/base/contact/submit.html b/templates/web/base/contact/submit.html index 5adb87e2d..4bf82dcd6 100644 --- a/templates/web/base/contact/submit.html +++ b/templates/web/base/contact/submit.html @@ -3,7 +3,7 @@ [% IF success %] <div class="confirmation-header"> - <h1>[% loc('Thank you for your feedback') %]</h1> + <h1>[% loc('Thank you for your enquiry') %]</h1> <p>[% loc('We’ll get back to you as soon as we can.') %]</p> </div> diff --git a/templates/web/base/my/planned.html b/templates/web/base/my/planned.html index 2e852ea01..19d29f970 100644 --- a/templates/web/base/my/planned.html +++ b/templates/web/base/my/planned.html @@ -1,7 +1,7 @@ [% SET bodyclass = 'mappage'; PROCESS "maps/${map.type}.html" IF problems.size; - INCLUDE 'header.html', title = loc('Your planned reports') + INCLUDE 'header.html', title = loc('Your shortlist') %] [% IF problems.size %] @@ -14,10 +14,10 @@ <div id="skipped-map"> [% END %] -<h1>[% loc('Your planned reports') %]</h1> +<h1>[% loc('Your shortlist') %]</h1> [% IF ! has_content %] -[% loc('You haven’t planned any reports yet.') %] +[% loc('You haven’t shortlisted any reports yet.') %] [% END %] <section class="full-width"> diff --git a/templates/web/base/questionnaire/index.html b/templates/web/base/questionnaire/index.html index a93039827..217bd827e 100644 --- a/templates/web/base/questionnaire/index.html +++ b/templates/web/base/questionnaire/index.html @@ -48,22 +48,22 @@ [% loc('Has this problem been fixed?') %] </p> -<p class="radio-segmented-control"> +<p class="segmented-control segmented-control--radio"> <input type="radio" name="been_fixed" id="been_fixed_yes" value="Yes"[% ' checked' IF been_fixed == 'Yes' %]> - <label class="inline" for="been_fixed_yes">[% loc('Yes') %]</label> + <label for="been_fixed_yes">[% loc('Yes') %]</label> <input type="radio" name="been_fixed" id="been_fixed_no" value="No"[% ' checked' IF been_fixed == 'No' %]> - <label class="inline" for="been_fixed_no">[% loc('No') %]</label> + <label for="been_fixed_no">[% loc('No') %]</label> <input type="radio" name="been_fixed" id="been_fixed_unknown" value="Unknown"[% ' checked' IF been_fixed == 'Unknown' %]> - <label class="inline" for="been_fixed_unknown">[% loc('Don’t know') %]</label> + <label for="been_fixed_unknown">[% loc('Don’t know') %]</label> </p> [% UNLESS answered_ever_reported %] <p>[% loc('Have you ever reported a problem to a council before, or is this your first time?') %]</p> -<p class="radio-segmented-control"> +<p class="segmented-control segmented-control--radio"> <input type="radio" name="reported" id="reported_yes" value="Yes"[% ' checked' IF reported == 'Yes' %]> - <label class="inline" for="reported_yes">[% loc('Reported before') %]</label> + <label for="reported_yes">[% loc('Reported before') %]</label> <input type="radio" name="reported" id="reported_no" value="No"[% ' checked' IF reported == 'No' %]> - <label class="inline" for="reported_no">[% loc('First time') %]</label> + <label for="reported_no">[% loc('First time') %]</label> </p> [% END %] @@ -99,11 +99,11 @@ <div class="js-another-questionnaire"> <p>[% loc('Would you like to receive another questionnaire in 4 weeks, reminding you to check the status?') %]</p> - <p class="radio-segmented-control"> + <p class="segmented-control segmented-control--radio"> <input type="radio" name="another" id="another_yes" value="Yes"[% ' checked' IF another == 'Yes' %]> - <label class="inline" for="another_yes">[% loc('Yes') %]</label> + <label for="another_yes">[% loc('Yes') %]</label> <input type="radio" name="another" id="another_no" value="No"[% ' checked' IF another == 'No' %]> - <label class="inline" for="another_no">[% loc('No') %]</label> + <label for="another_no">[% loc('No') %]</label> </p> </div> diff --git a/templates/web/base/report/_main.html b/templates/web/base/report/_main.html index e02d4b2b0..a85bca08f 100644 --- a/templates/web/base/report/_main.html +++ b/templates/web/base/report/_main.html @@ -7,20 +7,26 @@ <div class="problem-header clearfix" problem-id="[% problem.id %]"> -[% IF c.user.has_permission_to('planned_reports', problem.bodies_str) %] -<form method="post" action="/my/planned/change" id="planned_form"> +[% IF c.user.has_permission_to('planned_reports', problem.bodies_str_ids) %] +<form method="post" action="/my/planned/change" id="planned_form" class="hidden-label-target"> <input type="hidden" name="id" value="[% problem.id %]"> <input type="hidden" name="token" value="[% csrf_token %]"> <input type="hidden" name="change" value="[% IF c.user.is_planned_report(problem) %]remove[% ELSE %]add[% END %]"> - <p><input type="submit" - data-remove="[% loc('Remove from planned reports') %]" data-add="[% loc('Add to planned reports') %]" - value=" + <p><input + type="submit" + id="shortlist-report" + data-label-remove="[% loc('Remove from shortlist') %]" + data-label-add="[% loc('Add to shortlist') %]" + data-value-remove="[% loc('Shortlisted') %]" + data-value-add="[% loc('Shortlist') %]" [%~ IF c.user.is_planned_report(problem) ~%] - [% loc('Remove from planned reports') %] + value="[% loc('Shortlisted') %]" + aria-label="[% loc('Remove from shortlist') %]" [%~ ELSE ~%] - [% loc('Add to planned reports') %] + value="[% loc('Shortlist') %]" + aria-label="[% loc('Add to shortlist') %]" [%~ END ~%] - "></p> + ></p> </form> [% END %] @@ -106,25 +112,39 @@ </div> [% END %] - [% IF permissions.keys.grep('moderate|report_inspect|report_edit_category|report_edit_priority').size %] - <p class="moderate-display"> + [% IF permissions.moderate %] + </form> + [% END %] + + [% IF + (permissions.moderate) + OR + (!hide_inspect_button AND + permissions.keys.grep('report_inspect|report_edit_category|report_edit_priority').size) + OR + (c.user.has_permission_to('planned_reports', problem.bodies_str_ids)) + %] + <div class="moderate-display segmented-control" role="menu"> [% IF permissions.moderate %] - <input type="button" class="btn moderate" value="Moderate this report"> + <a id="moderate-report" role="menuitem" aria-label="[% loc('Moderate this report') %]">[% loc('Moderate') %]</a> [% END %] [% IF !hide_inspect_button AND permissions.keys.grep('report_inspect|report_edit_category|report_edit_priority').size %] - <a href="/report/[% problem.id %]/inspect" class="btn inspect"> - [%- IF permissions.report_inspect %] - [%- loc('Inspect') %] - [%- ELSE %] - [%- loc('Manage') %] - [%- END ~%] - </a> + <a href="/report/[% problem.id %]/inspect" role="menuitem"> + [%~ IF permissions.report_inspect ~%] + [%~ loc('Inspect') ~%] + [%~ ELSE ~%] + [%~ loc('Manage') ~%] + [%~ END ~%] + </a> [% END %] - </p> - [% END %] - - [% IF permissions.moderate %] - </form> + [% IF c.user.has_permission_to('planned_reports', problem.bodies_str_ids) %] + [%~ IF c.user.is_planned_report(problem) ~%] + <label for="shortlist-report" role="menuitem" aria-label="[% loc('Remove from shortlist') %]">[% loc('Shortlisted') %]</label> + [%~ ELSE ~%] + <label for="shortlist-report" role="menuitem" aria-label="[% loc('Add to shortlist') %]">[% loc('Shortlist') %]</label> + [%~ END ~%] + [% END %] + </div> [% END %] [% TRY %][% PROCESS 'report/_main_after.html' %][% CATCH file %][% END %] diff --git a/templates/web/base/report/new/after_photo.html b/templates/web/base/report/new/after_photo.html index cdebab462..b337977e4 100644 --- a/templates/web/base/report/new/after_photo.html +++ b/templates/web/base/report/new/after_photo.html @@ -1,7 +1,6 @@ <div class="description_tips" aria-label="[% loc('Tips for perfect photos') %]"> <ul class="do"> - <li>[% loc('Show what the problem is') %]</li> - <li>[% loc('Show where it’s located') %]</li> + <li>[% loc('For best results include a close-up and a wide shot') %]</li> </ul> <ul class="dont"> <li>[% loc('Avoid personal information and vehicle number plates') %]</li> diff --git a/templates/web/base/report/new/form_user_loggedin.html b/templates/web/base/report/new/form_user_loggedin.html index 4935a6481..7fc1b1b5f 100644 --- a/templates/web/base/report/new/form_user_loggedin.html +++ b/templates/web/base/report/new/form_user_loggedin.html @@ -5,8 +5,8 @@ [% INCLUDE form_as %] </div> [% ELSE %] - [% can_contribute_as_another_user = c.user.has_permission_to("contribute_as_another_user", bodies.keys.join(",")) %] - [% can_contribute_as_body = c.user.from_body AND c.user.has_permission_to("contribute_as_body", bodies.keys.join(",")) %] + [% can_contribute_as_another_user = c.user.has_permission_to("contribute_as_another_user", bodies.keys) %] + [% can_contribute_as_body = c.user.from_body AND c.user.has_permission_to("contribute_as_body", bodies.keys) %] [% IF can_contribute_as_another_user OR can_contribute_as_body %] [% INCLUDE form_as %] [% END %] diff --git a/templates/web/base/report/update.html b/templates/web/base/report/update.html index 55fdeb3b1..104e1d6f9 100644 --- a/templates/web/base/report/update.html +++ b/templates/web/base/report/update.html @@ -1,4 +1,4 @@ -[% moderating = c.user && c.user.has_permission_to('moderate', problem.bodies_str) %] +[% moderating = c.user && c.user.has_permission_to('moderate', problem.bodies_str_ids) %] [% IF loop.first %] <section class="full-width"> diff --git a/templates/web/base/report/update/form_name.html b/templates/web/base/report/update/form_name.html index 5d4c719ae..19c31998e 100644 --- a/templates/web/base/report/update/form_name.html +++ b/templates/web/base/report/update/form_name.html @@ -2,8 +2,8 @@ [% PROCESS 'user/_anonymity.html' anonymous = update.anonymous %] - [% can_contribute_as_another_user = c.user.has_permission_to("contribute_as_another_user", problem.bodies_str) %] - [% can_contribute_as_body = c.user.from_body AND c.user.has_permission_to("contribute_as_body", problem.bodies_str) %] + [% can_contribute_as_another_user = c.user.has_permission_to("contribute_as_another_user", problem.bodies_str_ids) %] + [% can_contribute_as_body = c.user.from_body AND c.user.has_permission_to("contribute_as_body", problem.bodies_str_ids) %] [% IF can_contribute_as_another_user OR can_contribute_as_body %] <label for="form_as">[% loc('Provide update as') %]</label> diff --git a/templates/web/fixmystreet.com/about/council.html b/templates/web/fixmystreet.com/about/council.html index cd28a7269..651e4f57f 100644 --- a/templates/web/fixmystreet.com/about/council.html +++ b/templates/web/fixmystreet.com/about/council.html @@ -260,6 +260,7 @@ </div> <div class="form-group submit-group"> <input type="hidden" name="subject" value="Councils submission"> + <input type="hidden" name="recipient" value="enquiries"> <input type="hidden" name="dest" value="from_council"> <input type="submit" value="Send"> </div> diff --git a/templates/web/fixmystreet.com/report/new/extra_name.html b/templates/web/fixmystreet.com/report/new/extra_name.html index 80ab1837a..f329541c3 100644 --- a/templates/web/fixmystreet.com/report/new/extra_name.html +++ b/templates/web/fixmystreet.com/report/new/extra_name.html @@ -8,7 +8,7 @@ shared with the council or displayed publicly.</em> </p> [% SET gender = report.get_extra_metadata('gender') %] -<p class="radio-segmented-control" style="font-size: 80%"> +<p class="segmented-control segmented-control--radio" style="font-size: 80%"> <input type="radio" name="gender" id="gender_female" value="female"[% ' checked' IF gender == 'female' %]> <label for="gender_female">Female</label> <input type="radio" name="gender" id="gender_male" value="male"[% ' checked' IF gender == 'male' %]> diff --git a/templates/web/oxfordshire/header.html b/templates/web/oxfordshire/header.html index d5884272b..5b5532b67 100644 --- a/templates/web/oxfordshire/header.html +++ b/templates/web/oxfordshire/header.html @@ -45,7 +45,7 @@ [% IF c.user_exists AND c.user.has_body_permission_to('planned_reports') %] <li> <[% IF c.req.uri.path == '/my/planned' %]span[% ELSE %]a href="/my/planned"[% END - %]>[% loc('Planned reports') %]</[% c.req.uri.path == '/my/planned' ? 'span' : 'a' %]> + %]>[% loc('Shortlist') %]</[% c.req.uri.path == '/my/planned' ? 'span' : 'a' %]> </li> [% END %] <li> diff --git a/templates/web/zurich/maps/noscript_map.html b/templates/web/zurich/maps/noscript_map.html index dcd577c52..4925f9260 100644 --- a/templates/web/zurich/maps/noscript_map.html +++ b/templates/web/zurich/maps/noscript_map.html @@ -1,3 +1,4 @@ +[% IF map.cols %] <div class="noscript square-map__outer"> <div class="square-map__inner"> <div id="[% nsm_prefix %]drag"> @@ -19,6 +20,35 @@ [% INCLUDE 'maps/_compass.html' %] </div> </div> +[% ELSE %] +<div class="noscript"> + <div id="[% nsm_prefix %]drag"> + <[% map.img_type %] + alt="NW map tile" id="[% nsm_prefix %]t2.2" + name="tile_[% map.x_tile - 1 %].[% map.y_tile - 1 %]" + src="[% map.tiles.0 %]" + style="top:0; left:0;"> + <[% map.img_type %] + alt="NE map tile" id="[% nsm_prefix %]t2.3" + name="tile_[% map.x_tile %].[% map.y_tile - 1 %]" + src="[% map.tiles.1 %]" + style="top:0px; left:256px;"> + <br> + <[% map.img_type %] + alt="SW map tile" id="[% nsm_prefix %]t3.2" + name="tile_[% map.x_tile - 1 %].[% map.y_tile %]" + src="[% map.tiles.2 %]" + style="top:256px; left:0;"> + <[% map.img_type %] + alt="SE map tile" id="[% nsm_prefix %]t3.3" + name="tile_[% map.x_tile %].[% map.y_tile %]" + src="[% map.tiles.3 %]" + style="top:256px; left:256px;"> + </div> + <div id="[% nsm_prefix %]pins">[% FOR pin IN map.pins %][% INCLUDE 'maps/pin.html' %][% END %]</div> + [% INCLUDE 'maps/_compass.html' %] +</div> +[% END %] [% BLOCK pin %] [% diff --git a/web/cobrands/fixmystreet/fixmystreet.js b/web/cobrands/fixmystreet/fixmystreet.js index 51ed30880..8d45e4017 100644 --- a/web/cobrands/fixmystreet/fixmystreet.js +++ b/web/cobrands/fixmystreet/fixmystreet.js @@ -281,16 +281,25 @@ $.extend(fixmystreet.set_up, { var $form = $(this), $change = $form.find("input[name='change']" ), $submit = $form.find("input[type='submit']" ), - data = $form.serialize() + '&ajax=1'; + $labels = $('label[for="' + $submit.attr('id') + '"]'), + data = $form.serialize() + '&ajax=1', + changeValue, + buttonLabel, + buttonValue; $.post(this.action, data, function(data) { if (data.outcome == 'add') { - $change.val('remove'); - $submit.val($submit.data('remove')); + changeValue = "remove"; + buttonLabel = $submit.data('label-remove'); + buttonValue = $submit.data('value-remove'); } else if (data.outcome == 'remove') { - $change.val('add'); - $submit.val($submit.data('add')); + changeValue = "add"; + buttonLabel = $submit.data('label-add'); + buttonValue = $submit.data('value-add'); } + $change.val(changeValue); + $submit.val(buttonValue).attr('aria-label', buttonLabel); + $labels.text(buttonValue).attr('aria-label', buttonLabel); }); }); }, @@ -793,7 +802,7 @@ $.extend(fixmystreet.set_up, { function add_handlers (elem, word) { elem.each( function () { var $elem = $(this); - $elem.find('.moderate').click( function () { + $elem.find('#moderate-report').on('click', function () { $elem.find('.moderate-display').hide(); $elem.find('.moderate-edit').show(); }); diff --git a/web/cobrands/sass/_base.scss b/web/cobrands/sass/_base.scss index e5d2c48dd..7462682ee 100644 --- a/web/cobrands/sass/_base.scss +++ b/web/cobrands/sass/_base.scss @@ -1035,6 +1035,11 @@ input.final-submit { .problem-header { margin-bottom: 1em; + + // Clear the admin actions after the floated photo(s). + .segmented-control { + clear: both; + } } .report_meta_info, @@ -1796,41 +1801,49 @@ table.nicetable { margin-left: 0; } -.radio-segmented-control { - overflow: auto; // clear floats (if browser doesn't support flexbox) +.segmented-control { + @include clearfix(); display: flex; // fancy full-width buttons for browsers that support flexbox - input { - position: absolute; - left: -999px; - } - - label { + & > * { display: block; margin: 0; - padding: 0.75em 1.5em; + padding: 0.75em 0.5em; text-align: center; background: #fff linear-gradient(to bottom, #fff 0%, #eee 100%) 0 0 no-repeat; - border: 1px solid #ddd; + border: 1px solid #ccc; border-right-width: 0; // avoid double border between items font-weight: bold; + color: inherit !important; + text-decoration: none !important; + cursor: pointer; float: left; // float fallback for browsers that don't support flexbox flex: 1 0 auto; - @media(max-width: 400px){ - // Shameful hack to stop the control expanding wider than the window - // on narrow devices (eg: 320px iPhone), which would cause horizontal - // scrolling, and clipped text on the new report page, for example. - // Flexbox will add the spacing back in anyway. Ideally we'd only apply - // this style if flexbox is supported, but there's no easy way to do that. - padding: 0.75em 0; - } - - &:hover, &:focus { + &:hover { background: #f3f3f3 linear-gradient(to bottom, #f9f9f9 0%, #e9e9e9 100%) 0 0 no-repeat; } } + & > :first-child { + border-radius: 0.25em 0 0 0.25em; + } + + & > :last-child { + border-radius: 0 0.25em 0.25em 0; + border-right-width: 1px; // reinstate border on last item + } +} + +// A special case of segmented control, where each "button" (or label) is +// preceded by a radio button, which needs to be hidden but interactive. +// Checked checkboxes give their following label a "pressed" appearance. +.segmented-control--radio { + input { + position: absolute; + left: -999px; + } + input:checked + label { color: $primary_text; background: $primary; @@ -1841,17 +1854,14 @@ table.nicetable { } input:checked + label + input + label { - border-left-width: none; // in favour of the realistic coloured border on the selected item + border-left-width: 0; // in favour of the realistic coloured border on the selected label } + // The first label is no longer the first child, so we need to + // fish it out specially with a next sibling selector. input:first-child + label { border-radius: 0.25em 0 0 0.25em; } - - label:last-child { - border-radius: 0 0.25em 0.25em 0; - border-right-width: 1px; // reinstate border on last item - } } .my-account-buttons a { @@ -1861,6 +1871,25 @@ table.nicetable { border-radius: 0.2em; } +// Useful for inserting hidden forms on the page, but still allowing +// inputs inside the form to be toggled by labels outside the form. +// Try using this in combination with .segmented-control for funtimes. +.hidden-label-target { + position: absolute; + top: -100px; + width: 0; + height: 0; + overflow: hidden; + opacity: 0; + + // Tell assistive devices to ignore this element. + visibility: hidden; + + // Hack for IE8 which doesn't allow interaction with + // `visibility:hidden` elements. + visibility: visible\9; +} + @media screen { .print-only { |