aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/Roles.pm96
-rw-r--r--perllib/FixMyStreet/App/Form/I18N.pm13
-rw-r--r--perllib/FixMyStreet/App/Form/Role.pm66
-rw-r--r--perllib/FixMyStreet/App/Form/Widget/Field/CheckboxGroup.pm62
-rw-r--r--perllib/FixMyStreet/Cobrand/Default.pm3
-rw-r--r--perllib/FixMyStreet/DB/Result/Role.pm2
-rw-r--r--perllib/FixMyStreet/DB/Result/User.pm1
-rw-r--r--t/app/controller/admin/roles.t96
-rw-r--r--templates/web/base/admin/roles/form.html32
-rw-r--r--templates/web/base/admin/roles/index.html34
10 files changed, 403 insertions, 2 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Admin/Roles.pm b/perllib/FixMyStreet/App/Controller/Admin/Roles.pm
new file mode 100644
index 000000000..15c96a4ed
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Admin/Roles.pm
@@ -0,0 +1,96 @@
+package FixMyStreet::App::Controller::Admin::Roles;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+use FixMyStreet::App::Form::Role;
+
+sub auto :Private {
+ my ($self, $c) = @_;
+
+ my $user = $c->user;
+ if ($user->is_superuser) {
+ $c->stash(rs => $c->model('DB::Role')->search_rs({}, { join => 'body', order_by => ['body.name', 'me.name'] }));
+ } elsif ($user->from_body) {
+ $c->stash(rs => $user->from_body->roles->search_rs({}, { order_by => 'name' }));
+ }
+}
+
+sub index :Path :Args(0) {
+ my ($self, $c) = @_;
+
+ my $p = $c->cobrand->available_permissions;
+ my %labels;
+ foreach my $group (sort keys %$p) {
+ my $group_vals = $p->{$group};
+ foreach (sort keys %$group_vals) {
+ $labels{$_} = $group_vals->{$_};
+ }
+ }
+
+ $c->stash(
+ roles => [ $c->stash->{rs}->all ],
+ labels => \%labels,
+ );
+}
+
+sub create :Local :Args(0) {
+ my ($self, $c, $id) = @_;
+
+ my $role = $c->stash->{rs}->new_result({});
+ return $self->form($c, $role);
+}
+
+sub item :PathPart('admin/roles') :Chained :CaptureArgs(1) {
+ my ($self, $c, $id) = @_;
+
+ my $obj = $c->stash->{rs}->find($id)
+ or $c->detach('/page_error_404_not_found', []);
+ $c->stash(obj => $obj);
+}
+
+sub edit :PathPart('') :Chained('item') :Args(0) {
+ my ($self, $c) = @_;
+ return $self->form($c, $c->stash->{obj});
+}
+
+sub form {
+ my ($self, $c, $role) = @_;
+
+ if ($c->get_param('delete_role')) {
+ $role->delete;
+ $c->response->redirect($c->uri_for($self->action_for('list')));
+ $c->detach;
+ }
+
+ my $perms = [];
+ my $p = $c->cobrand->available_permissions;
+ foreach my $group (sort keys %$p) {
+ my $group_vals = $p->{$group};
+ my @foo;
+ foreach (sort keys %$group_vals) {
+ push @foo, { value => $_, label => $group_vals->{$_} };
+ }
+ push @$perms, { group => $group, options => \@foo };
+ }
+ my $opts = {
+ field_list => [
+ '+permissions' => { options => $perms },
+ ],
+ };
+
+ if (!$c->user->is_superuser && $c->user->from_body) {
+ push @{$opts->{field_list}}, '+body', { inactive => 1 };
+ $opts->{body_id} = $c->user->from_body->id;
+ }
+
+ my $form = FixMyStreet::App::Form::Role->new(%$opts);
+ $c->stash(template => 'admin/roles/form.html', form => $form);
+ $form->process(item => $role, params => $c->req->params);
+ return unless $form->validated;
+
+ $c->response->redirect($c->uri_for($self->action_for('list')));
+}
+
+1;
diff --git a/perllib/FixMyStreet/App/Form/I18N.pm b/perllib/FixMyStreet/App/Form/I18N.pm
new file mode 100644
index 000000000..b37f7ac53
--- /dev/null
+++ b/perllib/FixMyStreet/App/Form/I18N.pm
@@ -0,0 +1,13 @@
+package FixMyStreet::App::Form::I18N;
+
+use Moo;
+
+sub maketext {
+ my ($self, $msg, @args) = @_;
+
+ no if ($] >= 5.022), warnings => 'redundant';
+ return sprintf(_($msg), @args);
+}
+
+1;
+
diff --git a/perllib/FixMyStreet/App/Form/Role.pm b/perllib/FixMyStreet/App/Form/Role.pm
new file mode 100644
index 000000000..f0711af15
--- /dev/null
+++ b/perllib/FixMyStreet/App/Form/Role.pm
@@ -0,0 +1,66 @@
+package FixMyStreet::App::Form::Role;
+
+use HTML::FormHandler::Moose;
+use FixMyStreet::App::Form::I18N;
+extends 'HTML::FormHandler::Model::DBIC';
+use namespace::autoclean;
+
+has 'body_id' => ( isa => 'Int', is => 'ro' );
+
+has '+widget_name_space' => ( default => sub { ['FixMyStreet::App::Form::Widget'] } );
+has '+widget_tags' => ( default => sub { { wrapper_tag => 'p' } } );
+has '+item_class' => ( default => 'Role' );
+has_field 'name' => ( required => 1 );
+has_field 'body' => ( type => 'Select', empty_select => 'Select a body', required => 1 );
+has_field 'permissions' => (
+ type => 'Multiple',
+ widget => 'CheckboxGroup',
+ tags => { inline => 1, wrapper_tag => 'fieldset', },
+);
+
+before 'update_model' => sub {
+ my $self = shift;
+ $self->item->body_id($self->body_id) if $self->body_id;
+};
+
+sub _build_language_handle { FixMyStreet::App::Form::I18N->new }
+
+has '+unique_messages' => (
+ default => sub {
+ { roles_body_id_name_key => "Role names must be unique" };
+ }
+);
+
+sub validate {
+ my $self = shift;
+
+ my $rs = $self->resultset;
+ my $value = $self->value;
+
+ return 0 if $self->body_id; # The core validation catches this, because body_id is set on $self->item
+ return 0 if $self->item_id && $self->item->body_id == $value->{body}; # Correctly caught by core validation
+
+ # Okay, due to a bug we need to check this ourselves
+ # https://github.com/gshank/html-formhandler-model-dbic/issues/20
+ my @id_clause = ();
+ @id_clause = HTML::FormHandler::Model::DBIC::_id_clause( $rs, $self->item_id ) if defined $self->item;
+
+ my %form_columns = (body => 'body_id', name => 'name');
+ my %where = map { $form_columns{$_} =>
+ exists( $value->{$_} ) ? $value->{$_} : undef ||
+ ( $self->item ? $self->item->get_column($form_columns{$_}) : undef )
+ } keys %form_columns;
+
+ my $count = $rs->search( \%where )->search( {@id_clause} )->count;
+ return 0 if $count < 1;
+
+ my $field = $self->field('name');
+ my $constraint = 'roles_body_id_name_key';
+ my $field_error = $self->unique_message_for_constraint($constraint);
+ $field->add_error( $field_error, $constraint );
+ return 1;
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/perllib/FixMyStreet/App/Form/Widget/Field/CheckboxGroup.pm b/perllib/FixMyStreet/App/Form/Widget/Field/CheckboxGroup.pm
new file mode 100644
index 000000000..1dc55e49b
--- /dev/null
+++ b/perllib/FixMyStreet/App/Form/Widget/Field/CheckboxGroup.pm
@@ -0,0 +1,62 @@
+package FixMyStreet::App::Form::Widget::Field::CheckboxGroup;
+
+use Moose::Role;
+with 'HTML::FormHandler::Widget::Field::CheckboxGroup';
+use namespace::autoclean;
+
+sub render_element {
+ my ( $self, $result ) = @_;
+ $result ||= $self->result;
+
+ my $output = '<ul class="permissions-checkboxes">';
+ foreach my $option ( @{ $self->{options} } ) {
+ if ( my $label = $option->{group} ) {
+ $label = $self->_localize( $label ) if $self->localize_labels;
+ $output .= qq{\n<li>$label\n<ul class="no-margin no-bullets">};
+ $output .= qq{\n<li>(<a href="#" data-select-all>} . _('all') . '</a> / ';
+ $output .= '<a href="#" data-select-none>' . _('none') . '</a>)</li>';
+ foreach my $group_opt ( @{ $option->{options} } ) {
+ $output .= '<li>';
+ $output .= $self->render_option( $group_opt, $result );
+ $output .= "</li>\n";
+ }
+ $output .= qq{</ul>\n</li>};
+ }
+ else {
+ $output .= $self->render_option( $option, $result );
+ }
+ }
+ $output .= '</ul>';
+ $self->reset_options_index;
+ return $output;
+}
+
+1;
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+FixMyStreet::App::Form::Widget::Field::CheckboxGroup - checkbox group field role
+
+=head1 SYNOPSIS
+
+Subclass of HTML::FormHandler::Widget::Field::CheckboxGroup, but printed
+as a nested <ul>.
+
+=head1 AUTHOR
+
+FormHandler Contributors - see HTML::FormHandler
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is copyright (c) 2017 by Gerda Shank.
+
+This is free software; you can redistribute it and/or modify it under
+the same terms as the Perl 5 programming language system itself.
+
+=cut
diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm
index a08a8dff7..cc7f03adb 100644
--- a/perllib/FixMyStreet/Cobrand/Default.pm
+++ b/perllib/FixMyStreet/Cobrand/Default.pm
@@ -647,7 +647,7 @@ sub admin_pages {
my $pages = {
'summary' => [_('Summary'), 0],
'timeline' => [_('Timeline'), 5],
- 'stats' => [_('Stats'), 8],
+ 'stats' => [_('Stats'), 8.5],
};
# There are some pages that only super users can see
@@ -680,6 +680,7 @@ sub admin_pages {
if ( $user->has_body_permission_to('user_edit') ) {
$pages->{reports} = [ _('Reports'), 2 ];
$pages->{users} = [ _('Users'), 6 ];
+ $pages->{roles} = [ _('Roles'), 7 ];
$pages->{user_edit} = [ undef, undef ];
}
if ( $self->allow_report_extra_fields && $user->has_body_permission_to('category_edit') ) {
diff --git a/perllib/FixMyStreet/DB/Result/Role.pm b/perllib/FixMyStreet/DB/Result/Role.pm
index 17a752f02..e35b0b195 100644
--- a/perllib/FixMyStreet/DB/Result/Role.pm
+++ b/perllib/FixMyStreet/DB/Result/Role.pm
@@ -48,6 +48,6 @@ __PACKAGE__->has_many(
# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-05-23 18:03:28
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:KkzVQZuzExH8PhZLJsnZgg
+__PACKAGE__->many_to_many( users => 'user_roles', 'user' );
-# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;
diff --git a/perllib/FixMyStreet/DB/Result/User.pm b/perllib/FixMyStreet/DB/Result/User.pm
index 5719b3150..edcd20fdf 100644
--- a/perllib/FixMyStreet/DB/Result/User.pm
+++ b/perllib/FixMyStreet/DB/Result/User.pm
@@ -149,6 +149,7 @@ use namespace::clean -except => [ 'meta' ];
with 'FixMyStreet::Roles::Extra';
__PACKAGE__->many_to_many( planned_reports => 'user_planned_reports', 'report' );
+__PACKAGE__->many_to_many( roles => 'user_roles', 'role' );
sub cost {
FixMyStreet->test_mode ? 1 : 12;
diff --git a/t/app/controller/admin/roles.t b/t/app/controller/admin/roles.t
new file mode 100644
index 000000000..a7740a572
--- /dev/null
+++ b/t/app/controller/admin/roles.t
@@ -0,0 +1,96 @@
+use FixMyStreet::TestMech;
+
+my $mech = FixMyStreet::TestMech->new;
+
+my $superuser = $mech->create_user_ok('superuser@example.com', name => 'Super User', is_superuser => 1);
+
+my $body = $mech->create_body_ok(2237, 'Oxfordshire County Council');
+my $body2 = $mech->create_body_ok(2482, 'Bromley Council');
+my $editor = $mech->create_user_ok('counciluser@example.com', name => 'Council User', from_body => $body);
+$editor->user_body_permissions->create({
+ body => $body,
+ permission_type => 'user_edit',
+});
+
+FixMyStreet::DB->resultset("Role")->create({
+ body => $body,
+ name => 'Role A',
+ permissions => ['moderate', 'user_edit'],
+});
+FixMyStreet::DB->resultset("Role")->create({
+ body => $body2,
+ name => 'Role Z',
+ permissions => ['report_inspect', 'planned_reports'],
+});
+
+FixMyStreet::override_config {
+ ALLOWED_COBRANDS => 'oxfordshire',
+}, sub {
+
+ $mech->log_in_ok( $editor->email );
+
+ subtest 'role index page' => sub {
+ $mech->get_ok("/admin/roles");
+ $mech->content_contains('Role A');
+ $mech->content_contains('Moderate report details');
+ $mech->content_lacks('Role Z');
+ $mech->content_lacks('Manage shortlist');
+ $mech->content_lacks('Add/edit response templates'); # About to be added
+ };
+
+ subtest 'create a role' => sub {
+ $mech->follow_link_ok({ text => 'Create' });
+ $mech->content_lacks('Body');
+ $mech->submit_form_ok({ with_fields => { name => 'Role A' }});
+ $mech->content_contains('Role names must be unique');
+ $mech->submit_form_ok({ with_fields => {
+ name => 'Role B',
+ permissions => [ ['template_edit', 'user_manage_permissions'] ],
+ }});
+
+ $mech->content_contains('Role B');
+ $mech->content_contains('Add/edit response templates');
+ };
+
+ subtest 'editing a role preselects correct options' => sub {
+ $mech->follow_link_ok({ text => 'Edit' });
+ $mech->content_like(qr/value="moderate"[^>]*checked/);
+ $mech->content_like(qr/value="user_edit"[^>]*checked/);
+ };
+
+ subtest 'editing a role to same name as another fails' => sub {
+ $mech->submit_form_ok({ with_fields => { name => 'Role B' }});
+ $mech->content_contains('Role names must be unique');
+ };
+
+ subtest 'delete a role' => sub {
+ $mech->submit_form_ok({ button => 'delete_role' });
+ $mech->content_lacks('Role A');
+ };
+
+};
+
+subtest 'superuser can see all bodies' => sub {
+ $mech->log_in_ok( $superuser->email );
+
+ $mech->get_ok("/admin/roles");
+ $mech->content_contains('Oxfordshire');
+ $mech->content_contains('Bromley');
+ $mech->content_contains('Role B');
+ $mech->content_contains('Role Z');
+ $mech->follow_link_ok({ text => 'Create' });
+ $mech->content_contains('Body');
+ $mech->content_contains('Bromley');
+
+ $mech->submit_form_ok({ with_fields => { body => $body->id, name => 'Role B' }});
+ $mech->content_contains('Role names must be unique');
+
+ $mech->submit_form_ok({ with_fields => {
+ name => 'Role C',
+ body => $body2->id,
+ permissions => 'contribute_as_body',
+ }});
+ $mech->content_contains('Role C');
+};
+
+done_testing();
diff --git a/templates/web/base/admin/roles/form.html b/templates/web/base/admin/roles/form.html
new file mode 100644
index 000000000..cb50689a6
--- /dev/null
+++ b/templates/web/base/admin/roles/form.html
@@ -0,0 +1,32 @@
+[% INCLUDE 'admin/header.html' title=loc('Roles') -%]
+
+<form method="post">
+ <div class="admin-hint">
+ <p>[% loc("The role's <strong>name</strong> is used to refer to this group of permissions elsewhere in the admin.") %]</p>
+ </div>
+ [% form.field('name').render %]
+
+ [% IF form.field('body').is_active %]
+ [% form.field('body').render %]
+ [% END %]
+
+ <div class="admin-hint">
+ <p>[% loc("Users with this role can perform the following actions within their assigned body or area.") %]</p>
+ </div>
+ [% form.field('permissions').render %]
+
+ [% form.field('submit').render %]
+
+ <p>
+ <input class="btn" type="submit" name="submit" value="[% loc('Save changes') %]">
+ </p>
+ [% IF form.item.id %]
+ <p>
+ <input class="btn-danger" type="submit" name="delete_role" value="[% loc('Delete') %]" data-confirm="[% loc('Are you sure?') %]">
+ </p>
+ [% END %]
+</form>
+
+<p><a href="[% c.uri_for(c.controller.action_for('list')) %]">Return to roles list</a></p>
+
+[% INCLUDE 'admin/footer.html' %]
diff --git a/templates/web/base/admin/roles/index.html b/templates/web/base/admin/roles/index.html
new file mode 100644
index 000000000..54a4b6ace
--- /dev/null
+++ b/templates/web/base/admin/roles/index.html
@@ -0,0 +1,34 @@
+[% INCLUDE 'admin/header.html' title=loc('Roles') -%]
+
+<table>
+ <tr>
+ <th>[% loc('Role') %]</th>
+ <th>[% loc('Permissions') %]</th>
+ <th>&nbsp;</th>
+ </tr>
+[% FOREACH role IN roles -%]
+ [% IF c.user.is_superuser AND last_name != role.body.name %]
+ <tr>
+ <td colspan="3"><strong>[% role.body.name %]</strong></td>
+ </tr>
+ [% SET last_name = role.body.name %]
+ [% END %]
+ <tr>
+ <td>[% role.name %]</td>
+ <td><ul class="no-margin no-bullets">
+ [% FOR perm IN role.permissions.sort %]
+ <li>[% labels.$perm %]
+ [% END %]
+ </ul></td>
+ <td>
+ <a href="[% c.uri_for(c.controller.action_for('edit'), [role.id]) %]">Edit</a>
+ </td>
+ </tr>
+[% END -%]
+</table>
+
+<p>
+ <a href="[% c.uri_for(c.controller.action_for('create')) %]">Create</a>
+</p>
+
+[% INCLUDE 'admin/footer.html' %]