aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xbin/update-schema1
-rw-r--r--db/schema.sql18
-rw-r--r--db/schema_0050-add-defect-type-table.sql21
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm113
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm16
-rw-r--r--perllib/FixMyStreet/App/Controller/Report.pm8
-rw-r--r--perllib/FixMyStreet/Cobrand/Oxfordshire.pm22
-rw-r--r--perllib/FixMyStreet/DB/Result/Body.pm10
-rw-r--r--perllib/FixMyStreet/DB/Result/Contact.pm11
-rw-r--r--perllib/FixMyStreet/DB/Result/ContactDefectType.pm46
-rw-r--r--perllib/FixMyStreet/DB/Result/DefectType.pm66
-rw-r--r--perllib/FixMyStreet/DB/Result/Problem.pm29
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/DefectType.pm22
-rw-r--r--perllib/FixMyStreet/TestMech.pm9
-rw-r--r--t/app/model/defecttype.t67
-rw-r--r--templates/web/base/admin/category-multiselect.html10
-rw-r--r--templates/web/base/admin/defecttypes/edit.html37
-rw-r--r--templates/web/base/admin/defecttypes/index.html13
-rw-r--r--templates/web/base/admin/defecttypes/list.html35
-rw-r--r--templates/web/base/defect_type/format.html9
-rw-r--r--templates/web/base/report/_inspect.html23
-rw-r--r--templates/web/oxfordshire/admin/defecttypes/extra_fields.html8
-rw-r--r--templates/web/oxfordshire/defect_type/format.html4
-rw-r--r--web/cobrands/fixmystreet/admin.js2
-rw-r--r--web/cobrands/fixmystreet/fixmystreet.js33
25 files changed, 593 insertions, 40 deletions
diff --git a/bin/update-schema b/bin/update-schema
index 94c42d8ae..171b87576 100755
--- a/bin/update-schema
+++ b/bin/update-schema
@@ -194,6 +194,7 @@ else {
# By querying the database schema, we can see where we're currently at
# (assuming schema change files are never half-applied, which should be the case)
sub get_db_version {
+ return '0050' if table_exists('defect_types');
return '0049' if column_exists('response_priorities', 'external_id');
return '0048' if column_exists('response_templates', 'state');
return '0047' if column_exists('response_priorities', 'description');
diff --git a/db/schema.sql b/db/schema.sql
index 23db82b65..18c1533d9 100644
--- a/db/schema.sql
+++ b/db/schema.sql
@@ -502,3 +502,21 @@ CREATE TABLE contact_response_priorities (
contact_id int REFERENCES contacts(id) NOT NULL,
response_priority_id int REFERENCES response_priorities(id) NOT NULL
);
+
+CREATE TABLE defect_types (
+ id serial not null primary key,
+ body_id int references body(id) not null,
+ name text not null,
+ description text not null,
+ extra text,
+ unique(body_id, name)
+);
+
+CREATE TABLE contact_defect_types (
+ id serial NOT NULL PRIMARY KEY,
+ contact_id int REFERENCES contacts(id) NOT NULL,
+ defect_type_id int REFERENCES defect_types(id) NOT NULL
+);
+
+ALTER TABLE problem
+ ADD COLUMN defect_type_id int REFERENCES defect_types(id);
diff --git a/db/schema_0050-add-defect-type-table.sql b/db/schema_0050-add-defect-type-table.sql
new file mode 100644
index 000000000..d65e17940
--- /dev/null
+++ b/db/schema_0050-add-defect-type-table.sql
@@ -0,0 +1,21 @@
+BEGIN;
+
+CREATE TABLE defect_types (
+ id serial not null primary key,
+ body_id int references body(id) not null,
+ name text not null,
+ description text not null,
+ extra text,
+ unique(body_id, name)
+);
+
+CREATE TABLE contact_defect_types (
+ id serial NOT NULL PRIMARY KEY,
+ contact_id int REFERENCES contacts(id) NOT NULL,
+ defect_type_id int REFERENCES defect_types(id) NOT NULL
+);
+
+ALTER TABLE problem
+ ADD COLUMN defect_type_id int REFERENCES defect_types(id);
+
+COMMIT;
diff --git a/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm b/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm
new file mode 100644
index 000000000..bcfeb3dd8
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm
@@ -0,0 +1,113 @@
+package FixMyStreet::App::Controller::Admin::DefectTypes;
+use Moose;
+use namespace::autoclean;
+use mySociety::ArrayUtils;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+
+sub begin : Private {
+ my ( $self, $c ) = @_;
+
+ $c->forward('/admin/begin');
+}
+
+sub index : Path : Args(0) {
+ my ( $self, $c ) = @_;
+
+ my $user = $c->user;
+
+ if ($user->is_superuser) {
+ $c->forward('/admin/fetch_all_bodies');
+ } elsif ( $user->from_body ) {
+ $c->forward('load_user_body', [ $user->from_body->id ]);
+ $c->res->redirect( $c->uri_for( '', $c->stash->{body}->id ) );
+ } else {
+ $c->detach( '/page_error_404_not_found' );
+ }
+}
+
+sub list : Path : Args(1) {
+ my ($self, $c, $body_id) = @_;
+
+ $c->forward('load_user_body', [ $body_id ]);
+
+ my @defect_types = $c->stash->{body}->defect_types->search(
+ undef,
+ {
+ order_by => 'name'
+ }
+ );
+
+ $c->stash->{defect_types} = \@defect_types;
+}
+
+sub edit : Path : Args(2) {
+ my ( $self, $c, $body_id, $defect_type_id ) = @_;
+
+ $c->forward('load_user_body', [ $body_id ]);
+
+ my $defect_type;
+ if ($defect_type_id eq 'new') {
+ $defect_type = $c->stash->{body}->defect_types->new({});
+ }
+ else {
+ $defect_type = $c->stash->{body}->defect_types->find( $defect_type_id )
+ or $c->detach( '/page_error_404_not_found' );
+ }
+
+ $c->forward('/admin/fetch_contacts');
+ my @contacts = $defect_type->contacts->all;
+ my @live_contacts = $c->stash->{live_contacts}->all;
+ my %active_contacts = map { $_->id => 1 } @contacts;
+ my @all_contacts = map { {
+ id => $_->id,
+ category => $_->category,
+ active => $active_contacts{$_->id},
+ } } @live_contacts;
+ $c->stash->{contacts} = \@all_contacts;
+
+ if ($c->req->method eq 'POST') {
+ $defect_type->name( $c->get_param('name') );
+ $defect_type->description( $c->get_param('description') );
+
+ my @extra_fields = @{ $c->cobrand->call_hook('defect_type_extra_fields') || [] };
+ foreach ( @extra_fields ) {
+ $defect_type->set_extra_metadata( $_ => $c->get_param("extra[$_]") );
+ }
+
+ $defect_type->update_or_insert;
+ my @live_contact_ids = map { $_->id } @live_contacts;
+ my @new_contact_ids = $c->get_param_list('categories');
+ @new_contact_ids = @{ mySociety::ArrayUtils::intersection(\@live_contact_ids, \@new_contact_ids) };
+ $defect_type->contact_defect_types->search({
+ contact_id => { '!=' => \@new_contact_ids },
+ })->delete;
+ foreach my $contact_id (@new_contact_ids) {
+ $defect_type->contact_defect_types->find_or_create({
+ contact_id => $contact_id,
+ });
+ }
+
+ $c->res->redirect( $c->uri_for( '', $c->stash->{body}->id ) );
+ }
+
+ $c->stash->{defect_type} = $defect_type;
+}
+
+sub load_user_body : Private {
+ my ($self, $c, $body_id) = @_;
+
+ my $has_permission = $c->user->has_body_permission_to('defect_type_edit', $body_id);
+
+ unless ( $has_permission ) {
+ $c->detach( '/page_error_404_not_found' );
+ }
+
+ $c->stash->{body} = $c->model('DB::Body')->find($body_id)
+ or $c->detach( '/page_error_404_not_found' );
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm b/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm
index 013d710af..201742c81 100644
--- a/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm
@@ -141,22 +141,32 @@ sub download : Path('download') : Args(0) {
foreach my $report (@$inspections) {
my ($eastings, $northings) = $report->local_coords;
my $description = sprintf("%s %s", $report->external_id || "", $report->get_extra_metadata('detailed_information') || "");
+ my $activity_code = $report->defect_type ?
+ $report->defect_type->get_extra_metadata('activity_code')
+ : 'MC';
+ my $traffic_information = $report->get_extra_metadata('traffic_information') ?
+ 'TM ' . $report->get_extra_metadata('traffic_information')
+ : 'TM none';
+
$csv->combine(
"I", # beginning of defect record
- "MC", # activity code - minor carriageway, also FC (footway)
+ $activity_code, # activity code - minor carriageway, also FC (footway)
"", # empty field, can also be A (seen on MC) or B (seen on FC)
sprintf("%03d", ++$i), # randomised sequence number
"${eastings}E ${northings}N", # defect location field, which we don't capture from inspectors
$report->inspection_log_entry->whenedited->strftime("%H%M"), # defect time raised
"","","","","","","", # empty fields
- $report->get_extra_metadata('traffic_information') ? 'TM required' : 'TM none', # further description
+ $traffic_information,
$description, # defect description
);
push @body, $csv->string;
+ my $defect_type = $report->defect_type ?
+ $report->defect_type->get_extra_metadata('defect_code')
+ : 'SFP2';
$csv->combine(
"J", # georeferencing record
- $report->get_extra_metadata('defect_type') || 'SFP2', # defect type - SFP2: sweep and fill <1m2, POT2 also seen
+ $defect_type, # defect type - SFP2: sweep and fill <1m2, POT2 also seen
$report->response_priority ?
$report->response_priority->external_id :
"2", # priority of defect
diff --git a/perllib/FixMyStreet/App/Controller/Report.pm b/perllib/FixMyStreet/App/Controller/Report.pm
index 1c2e98bd4..fe7576893 100644
--- a/perllib/FixMyStreet/App/Controller/Report.pm
+++ b/perllib/FixMyStreet/App/Controller/Report.pm
@@ -340,10 +340,16 @@ sub inspect : Private {
my %update_params = ();
if ($permissions->{report_inspect}) {
- foreach (qw/detailed_information traffic_information duplicate_of defect_type/) {
+ foreach (qw/detailed_information traffic_information duplicate_of/) {
$problem->set_extra_metadata( $_ => $c->get_param($_) );
}
+ if ( $c->get_param('defect_type') ) {
+ $problem->defect_type($problem->defect_types->find($c->get_param('defect_type')));
+ } else {
+ $problem->defect_type(undef);
+ }
+
if ( $c->get_param('include_update') ) {
$update_text = Utils::cleanup_text( $c->get_param('public_update'), { allow_multiline => 1 } );
if (!$update_text) {
diff --git a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
index d585a5328..78247e39d 100644
--- a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
+++ b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
@@ -161,9 +161,13 @@ sub admin_pages {
# Oxfordshire have a custom admin page for downloading reports in an Exor-
# friendly format which anyone with report_instruct permission can use.
- if ( $user->is_superuser || $user->has_body_permission_to('report_instruct') ) {
+ if ( $user->has_body_permission_to('report_instruct') ) {
$pages->{exordefects} = [ _('Download Exor RDI'), 10 ];
}
+ if ( $user->has_body_permission_to('defect_type_edit') ) {
+ $pages->{defecttypes} = [ _('Defect Types'), 11 ];
+ $pages->{defecttype_edit} = [ undef, undef ];
+ };
return $pages;
}
@@ -190,4 +194,20 @@ sub user_extra_fields {
sub display_days_ago_threshold { 28 }
+sub defect_type_extra_fields {
+ return [
+ 'activity_code',
+ 'defect_code',
+ ];
+};
+
+sub available_permissions {
+ my $self = shift;
+
+ my $perms = $self->next::method();
+ $perms->{Bodies}->{defect_type_edit} = "Add/edit defect types";
+
+ return $perms;
+}
+
1;
diff --git a/perllib/FixMyStreet/DB/Result/Body.pm b/perllib/FixMyStreet/DB/Result/Body.pm
index 6dac8821c..82015ad2d 100644
--- a/perllib/FixMyStreet/DB/Result/Body.pm
+++ b/perllib/FixMyStreet/DB/Result/Body.pm
@@ -75,6 +75,12 @@ __PACKAGE__->has_many(
{ "foreign.body_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
+__PACKAGE__->has_many(
+ "defect_types",
+ "FixMyStreet::DB::Result::DefectType",
+ { "foreign.body_id" => "self.id" },
+ { cascade_copy => 0, cascade_delete => 0 },
+);
__PACKAGE__->belongs_to(
"parent",
"FixMyStreet::DB::Result::Body",
@@ -112,8 +118,8 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-09-06 15:33:04
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ZuzscnLqcx0k512cTZ/kdg
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-02-13 15:11:11
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BOJANVwg3kR/1VjDq0LykA
sub url {
my ( $self, $c, $args ) = @_;
diff --git a/perllib/FixMyStreet/DB/Result/Contact.pm b/perllib/FixMyStreet/DB/Result/Contact.pm
index f7e8ac5b5..a620b7358 100644
--- a/perllib/FixMyStreet/DB/Result/Contact.pm
+++ b/perllib/FixMyStreet/DB/Result/Contact.pm
@@ -56,6 +56,12 @@ __PACKAGE__->belongs_to(
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
);
__PACKAGE__->has_many(
+ "contact_defect_types",
+ "FixMyStreet::DB::Result::ContactDefectType",
+ { "foreign.contact_id" => "self.id" },
+ { cascade_copy => 0, cascade_delete => 0 },
+);
+__PACKAGE__->has_many(
"contact_response_priorities",
"FixMyStreet::DB::Result::ContactResponsePriority",
{ "foreign.contact_id" => "self.id" },
@@ -69,8 +75,8 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-09-06 15:33:04
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ocmQGeFJtO3wmvyx6W+EKQ
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-02-13 15:11:11
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:f9VepR/oPyr3z6PUpJ4w2A
__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn");
__PACKAGE__->rabx_column('extra');
@@ -82,6 +88,7 @@ with 'FixMyStreet::Roles::Extra';
__PACKAGE__->many_to_many( response_templates => 'contact_response_templates', 'response_template' );
__PACKAGE__->many_to_many( response_priorities => 'contact_response_priorities', 'response_priority' );
+__PACKAGE__->many_to_many( defect_types => 'contact_defect_types', 'defect_type' );
sub get_metadata_for_input {
my $self = shift;
diff --git a/perllib/FixMyStreet/DB/Result/ContactDefectType.pm b/perllib/FixMyStreet/DB/Result/ContactDefectType.pm
new file mode 100644
index 000000000..2199f0b42
--- /dev/null
+++ b/perllib/FixMyStreet/DB/Result/ContactDefectType.pm
@@ -0,0 +1,46 @@
+use utf8;
+package FixMyStreet::DB::Result::ContactDefectType;
+
+# Created by DBIx::Class::Schema::Loader
+# DO NOT MODIFY THE FIRST PART OF THIS FILE
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->table("contact_defect_types");
+__PACKAGE__->add_columns(
+ "id",
+ {
+ data_type => "integer",
+ is_auto_increment => 1,
+ is_nullable => 0,
+ sequence => "contact_defect_types_id_seq",
+ },
+ "contact_id",
+ { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
+ "defect_type_id",
+ { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
+);
+__PACKAGE__->set_primary_key("id");
+__PACKAGE__->belongs_to(
+ "contact",
+ "FixMyStreet::DB::Result::Contact",
+ { id => "contact_id" },
+ { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
+);
+__PACKAGE__->belongs_to(
+ "defect_type",
+ "FixMyStreet::DB::Result::DefectType",
+ { id => "defect_type_id" },
+ { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
+);
+
+
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-02-13 15:11:11
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:VIczmM0OXXpWgQVpop3SMw
+
+
+# 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/DefectType.pm b/perllib/FixMyStreet/DB/Result/DefectType.pm
new file mode 100644
index 000000000..a2969f59e
--- /dev/null
+++ b/perllib/FixMyStreet/DB/Result/DefectType.pm
@@ -0,0 +1,66 @@
+use utf8;
+package FixMyStreet::DB::Result::DefectType;
+
+# Created by DBIx::Class::Schema::Loader
+# DO NOT MODIFY THE FIRST PART OF THIS FILE
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+__PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn");
+__PACKAGE__->table("defect_types");
+__PACKAGE__->add_columns(
+ "id",
+ {
+ data_type => "integer",
+ is_auto_increment => 1,
+ is_nullable => 0,
+ sequence => "defect_types_id_seq",
+ },
+ "body_id",
+ { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
+ "name",
+ { data_type => "text", is_nullable => 0 },
+ "description",
+ { data_type => "text", is_nullable => 0 },
+ "extra",
+ { data_type => "text", is_nullable => 1 },
+);
+__PACKAGE__->set_primary_key("id");
+__PACKAGE__->add_unique_constraint("defect_types_body_id_name_key", ["body_id", "name"]);
+__PACKAGE__->belongs_to(
+ "body",
+ "FixMyStreet::DB::Result::Body",
+ { id => "body_id" },
+ { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
+);
+__PACKAGE__->has_many(
+ "contact_defect_types",
+ "FixMyStreet::DB::Result::ContactDefectType",
+ { "foreign.defect_type_id" => "self.id" },
+ { cascade_copy => 0, cascade_delete => 0 },
+);
+__PACKAGE__->has_many(
+ "problems",
+ "FixMyStreet::DB::Result::Problem",
+ { "foreign.defect_type_id" => "self.id" },
+ { cascade_copy => 0, cascade_delete => 0 },
+);
+
+
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-02-13 15:11:11
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BBLjb/aAoTKJZerdYCeBMQ
+
+__PACKAGE__->many_to_many( contacts => 'contact_defect_types', 'contact' );
+
+__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn");
+__PACKAGE__->rabx_column('extra');
+
+use Moo;
+use namespace::clean -except => [ 'meta' ];
+
+with 'FixMyStreet::Roles::Extra';
+
+
+1;
diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm
index 0ab52628e..84db41490 100644
--- a/perllib/FixMyStreet/DB/Result/Problem.pm
+++ b/perllib/FixMyStreet/DB/Result/Problem.pm
@@ -108,6 +108,8 @@ __PACKAGE__->add_columns(
{ data_type => "text", is_nullable => 1 },
"response_priority_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
+ "defect_type_id",
+ { data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->has_many(
@@ -116,6 +118,17 @@ __PACKAGE__->has_many(
{ "foreign.problem_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
+__PACKAGE__->belongs_to(
+ "defect_type",
+ "FixMyStreet::DB::Result::DefectType",
+ { id => "defect_type_id" },
+ {
+ is_deferrable => 0,
+ join_type => "LEFT",
+ on_delete => "NO ACTION",
+ on_update => "NO ACTION",
+ },
+);
__PACKAGE__->has_many(
"moderation_original_datas",
"FixMyStreet::DB::Result::ModerationOriginalData",
@@ -153,8 +166,8 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-09-07 11:01:40
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:iH9c4VZZN/ONnhN6g89DFw
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-02-13 15:11:11
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:8zzWlJX7OQOdvrGxKuZUmg
# Add fake relationship to stored procedure table
__PACKAGE__->has_one(
@@ -754,6 +767,18 @@ sub response_priorities {
return $self->result_source->schema->resultset('ResponsePriority')->for_bodies($self->bodies_str_ids, $self->category);
}
+=head2 defect_types
+
+Returns all DefectTypes attached to this problem's category/contact, in
+alphabetical order of name.
+
+=cut
+
+sub defect_types {
+ my $self = shift;
+ return $self->result_source->schema->resultset('DefectType')->for_bodies($self->bodies_str_ids, $self->category);
+}
+
# returns true if the external id is the council's ref, i.e., useful to publish it
# (by way of an example, the Oxfordshire send method returns a useful reference when
# it succeeds, so that is the ref we should show on the problem report page).
diff --git a/perllib/FixMyStreet/DB/ResultSet/DefectType.pm b/perllib/FixMyStreet/DB/ResultSet/DefectType.pm
new file mode 100644
index 000000000..a873ef252
--- /dev/null
+++ b/perllib/FixMyStreet/DB/ResultSet/DefectType.pm
@@ -0,0 +1,22 @@
+package FixMyStreet::DB::ResultSet::DefectType;
+use base 'DBIx::Class::ResultSet';
+
+use strict;
+use warnings;
+
+sub for_bodies {
+ my ($rs, $bodies, $category) = @_;
+ my $attrs = {
+ 'me.body_id' => $bodies,
+ };
+ if ($category) {
+ $attrs->{'contact.category'} = [ $category, undef ];
+ }
+ $rs->search($attrs, {
+ order_by => 'name',
+ join => { 'contact_defect_types' => 'contact' },
+ distinct => 1,
+ });
+}
+
+1;
diff --git a/perllib/FixMyStreet/TestMech.pm b/perllib/FixMyStreet/TestMech.pm
index 902ae7a5a..881572a38 100644
--- a/perllib/FixMyStreet/TestMech.pm
+++ b/perllib/FixMyStreet/TestMech.pm
@@ -611,6 +611,7 @@ sub delete_body {
my $body = shift;
$mech->delete_problems_for_body($body->id);
+ $mech->delete_defect_type($_) for $body->defect_types;
$mech->delete_contact($_) for $body->contacts;
$mech->delete_user($_) for $body->users;
$_->delete for $body->response_templates;
@@ -642,6 +643,14 @@ sub delete_problems_for_body {
}
}
+sub delete_defect_type {
+ my $mech = shift;
+ my $defect_type = shift;
+
+ $defect_type->contact_defect_types->delete_all;
+ $defect_type->delete;
+}
+
sub create_contact_ok {
my $self = shift;
my %contact_params = (
diff --git a/t/app/model/defecttype.t b/t/app/model/defecttype.t
new file mode 100644
index 000000000..0f66ac684
--- /dev/null
+++ b/t/app/model/defecttype.t
@@ -0,0 +1,67 @@
+use strict;
+use warnings;
+use Test::More;
+
+use FixMyStreet::App;
+use FixMyStreet::TestMech;
+my $mech = FixMyStreet::TestMech->new;
+
+my $oxfordshire = $mech->create_body_ok(2237, 'Oxfordshire County Council', id => 2237);
+my $potholes_contact = $mech->create_contact_ok( body_id => $oxfordshire->id, category => 'Potholes', email => 'potholes@example.com' );
+my $traffic_lights_contact =$mech->create_contact_ok( body_id => $oxfordshire->id, category => 'Traffic lights', email => 'lights@example.com' );
+
+my $potholes_defect_type = FixMyStreet::App->model('DB::DefectType')->find_or_create(
+ {
+ body_id => 2237,
+ name => 'Potholes',
+ description => 'This defect type is to do with potholes'
+ }
+);
+$potholes_defect_type->contact_defect_types->find_or_create({
+ contact_id => $potholes_contact->id,
+});
+
+my $general_defect_type = FixMyStreet::App->model('DB::DefectType')->find_or_create(
+ {
+ body_id => 2237,
+ name => 'All categories',
+ description => 'This defect type is for all categories'
+ }
+);
+
+
+subtest 'for_bodies returns correct results' => sub {
+ my $defect_types = FixMyStreet::App->model('DB::DefectType')->for_bodies(
+ [ $oxfordshire->id ],
+ 'Potholes'
+ );
+
+ is $defect_types->count, 2, 'Both defect types are included for Potholes category';
+
+ $defect_types = FixMyStreet::App->model('DB::DefectType')->for_bodies(
+ [ $oxfordshire->id ],
+ 'Traffic lights'
+ );
+
+ is $defect_types->count, 1, 'Only 1 defect type is included for Traffic lights category';
+ is $defect_types->first->name, $general_defect_type->name, 'Correct defect type is returned for Traffic lights category';
+};
+
+subtest 'Problem->defect_types behaves correctly' => sub {
+ my ($problem) = $mech->create_problems_for_body(1, $oxfordshire->id, 'Test', {
+ category => 'Potholes',
+ });
+
+ is $problem->defect_types->count, 2, 'Both defect types are available for the problem';
+
+ $problem->update({ category => 'Traffic lights' });
+ is $problem->defect_types->count, 1, 'Only 1 defect type is included for Traffic lights category';
+ is $problem->defect_types->first->name, $general_defect_type->name, 'Correct defect type is returned for Traffic lights category';
+};
+
+
+END {
+ $mech->delete_body( $oxfordshire );
+
+ done_testing();
+}
diff --git a/templates/web/base/admin/category-multiselect.html b/templates/web/base/admin/category-multiselect.html
new file mode 100644
index 000000000..98416204f
--- /dev/null
+++ b/templates/web/base/admin/category-multiselect.html
@@ -0,0 +1,10 @@
+<p>
+ <strong>[% loc('Categories:') %]</strong>
+</p>
+<p>
+ <select class="form-control js-multiple" name="categories" id="categories" multiple data-all="[% loc('All categories') %]">
+ [% FOR contact IN contacts %]
+ <option value="[% contact.id %]" [% 'selected' IF contact.active %]>[% contact.category | html %]</option>
+ [% END %]
+ </select>
+</p>
diff --git a/templates/web/base/admin/defecttypes/edit.html b/templates/web/base/admin/defecttypes/edit.html
new file mode 100644
index 000000000..65c8a5ab7
--- /dev/null
+++ b/templates/web/base/admin/defecttypes/edit.html
@@ -0,0 +1,37 @@
+[% INCLUDE 'admin/header.html' title=tprintf(loc('Defect Type for %s'), body.name) -%]
+[% dt = defect_type %]
+
+[% UNLESS dt.id %]<h3>[% loc('New defect type') %]</h3>[% END %]
+
+<form method="post"
+ action="[% c.uri_for('', body.id, dt.id || 'new' ) %]"
+ enctype="application/x-www-form-urlencoded"
+ accept-charset="utf-8"
+ class="validate">
+
+ <p>
+ <strong>[% loc('Name:') %] </strong>
+ <input type="text" name="name" class="required form-control" size="30" value="[% dt.name | html %]">
+ </p>
+ <p>
+ <strong>[% loc('Description:') %] </strong>
+ <input type="text" name="description" class="form-control" size="30" value="[% dt.description | html %]">
+ </p>
+
+ <div class="admin-hint">
+ <p>
+ [% loc('If you only want this defect type to be an option for specific categories, pick them here. By default they will show for all categories.') %]
+ </p>
+ </div>
+
+ [% INCLUDE 'admin/category-multiselect.html' %]
+
+ [% TRY %][% INCLUDE 'admin/defecttypes/extra_fields.html' %][% CATCH file %][% END %]
+
+ <p>
+ <input type="hidden" name="token" value="[% csrf_token %]" >
+ <input type="submit" class="btn" name="save" value="[% dt.id ? loc('Save changes') : loc('Create defect type') %]" >
+ </p>
+</form>
+
+[% INCLUDE 'admin/footer.html' %]
diff --git a/templates/web/base/admin/defecttypes/index.html b/templates/web/base/admin/defecttypes/index.html
new file mode 100644
index 000000000..2e6ce7e1b
--- /dev/null
+++ b/templates/web/base/admin/defecttypes/index.html
@@ -0,0 +1,13 @@
+[% INCLUDE 'admin/header.html' title=loc('Defect Types') -%]
+
+<ul>
+ [% FOR body IN bodies %]
+ <li>
+ <a href="[% c.uri_for('', body.id) %]">[% body.name %]</a>
+ [% defect_types_count = body.defect_types.count %]
+ [% IF defect_types_count %]([% defect_types_count %])[% END %]
+ </li>
+ [% END %]
+</ul>
+
+[% INCLUDE 'admin/footer.html' %]
diff --git a/templates/web/base/admin/defecttypes/list.html b/templates/web/base/admin/defecttypes/list.html
new file mode 100644
index 000000000..1a9cb4fa7
--- /dev/null
+++ b/templates/web/base/admin/defecttypes/list.html
@@ -0,0 +1,35 @@
+[% INCLUDE 'admin/header.html' title=tprintf(loc('Defect Types for %s'), body.name) -%]
+
+<table>
+ <thead>
+ <tr>
+ <th> [% loc('Name') %] </th>
+ <th> [% loc('Description') %] </th>
+ <th> [% loc('Categories') %] </th>
+ <th> &nbsp; </th>
+ </tr>
+ </thead>
+ <tbody>
+ [% PROCESS 'defect_type/format.html' %]
+ [% FOR d IN defect_types %]
+ <tr>
+ <td> [% defect_type_format(defect_type=d) %] </td>
+ <td> [% d.description | html %] </td>
+ <td>
+ [% UNLESS d.contacts.size %]
+ <em>[% loc('All categories') %]</em>
+ [% ELSE %]
+ [% FOR contact IN d.contacts %]
+ [% contact.category %][% ',' UNLESS loop.last %]
+ [% END %]
+ [% END %]
+ </td>
+ <td> <a href="[% c.uri_for('', body.id, d.id) %]" class="btn">[% loc('Edit') %]</a> </td>
+ </tr>
+ [% END %]
+ </tbody>
+</table>
+
+<a href="[% c.uri_for('', body.id, 'new') %]" class="btn">[% loc('New defect type') %]</a>
+
+[% INCLUDE 'admin/footer.html' %]
diff --git a/templates/web/base/defect_type/format.html b/templates/web/base/defect_type/format.html
new file mode 100644
index 000000000..3c0781501
--- /dev/null
+++ b/templates/web/base/defect_type/format.html
@@ -0,0 +1,9 @@
+[%
+# This template can be overridden by cobrands if they've added extra fields
+# to the DefectType model (e.g Cobrand::Oxfordshire->defect_type_extra_fields)
+# which should be used to represent this DefectType
+# to the user in the inspect form.
+~%]
+[% MACRO defect_type_format BLOCK ~%]
+[%~ defect_type.name | html ~%]
+[%~ END %] \ No newline at end of file
diff --git a/templates/web/base/report/_inspect.html b/templates/web/base/report/_inspect.html
index 625887eff..5e97de3f4 100644
--- a/templates/web/base/report/_inspect.html
+++ b/templates/web/base/report/_inspect.html
@@ -62,18 +62,17 @@
[% END %]
[% IF permissions.report_inspect %]
- [% IF c.cobrand.defect_types %]
- <p>
- <label for="defect_type">[% loc('Defect type') %]</label>
- [% defect_type = problem.get_extra_metadata('defect_type') %]
- <select id="defect_type" name="defect_type" class="form-control">
- <option value=""[% ' selected' IF NOT defect_type %]>-</option>
- [% FOREACH dt IN c.cobrand.defect_types.pairs %]
- <option[% ' selected' IF defect_type == dt.key %] value="[% dt.key | html %]">[% dt.value | html %]</option>
- [% END %]
- </select>
- </p>
- [% END %]
+ [% PROCESS 'defect_type/format.html' %]
+ <p>
+ <label for="defect_type">[% loc('Defect type') %]</label>
+ <select id="defect_type" name="defect_type" class="form-control">
+ <option value=""[% ' selected' IF NOT problem.defect_type %]>-</option>
+ [% FOREACH defect_type IN problem.defect_types %]
+ <option[% ' selected' IF problem.defect_type_id == defect_type.id %] value="[% defect_type.id %]">[% defect_type_format() %]</option>
+ [% END %]
+ </select>
+ </p>
+
<p>
<label for="state">[% loc('State') %]</label>
[% INCLUDE 'report/inspect/state_groups_select.html' %]
diff --git a/templates/web/oxfordshire/admin/defecttypes/extra_fields.html b/templates/web/oxfordshire/admin/defecttypes/extra_fields.html
new file mode 100644
index 000000000..73cc54f0c
--- /dev/null
+++ b/templates/web/oxfordshire/admin/defecttypes/extra_fields.html
@@ -0,0 +1,8 @@
+<p>
+ <strong>[% loc('Activity Code:') %] </strong>
+ <input type="text" name="extra[activity_code]" class="form-control" size="30" value="[% dt.get_extra_metadata('activity_code') | html %]">
+</p>
+<p>
+ <strong>[% loc('Defect Code:') %] </strong>
+ <input type="text" name="extra[defect_code]" class="form-control" size="30" value="[% dt.get_extra_metadata('defect_code') | html %]">
+</p>
diff --git a/templates/web/oxfordshire/defect_type/format.html b/templates/web/oxfordshire/defect_type/format.html
new file mode 100644
index 000000000..9cbf2d873
--- /dev/null
+++ b/templates/web/oxfordshire/defect_type/format.html
@@ -0,0 +1,4 @@
+[% MACRO defect_type_format BLOCK ~%]
+[%~ defect_type.get_extra_metadata('defect_code') | html %] - [% defect_type.get_extra_metadata('activity_code') | html %]
+([% defect_type.name | html %])
+[%~ END %] \ No newline at end of file
diff --git a/web/cobrands/fixmystreet/admin.js b/web/cobrands/fixmystreet/admin.js
index 02eb30766..f7fcaf276 100644
--- a/web/cobrands/fixmystreet/admin.js
+++ b/web/cobrands/fixmystreet/admin.js
@@ -48,6 +48,8 @@ $(function(){
}
});
+ $("select.js-multiple[multiple]").make_multi();
+
// on a body's page, hide/show deleted contact categories
var $table_with_deleted_contacts = $('table tr.is-deleted td.contact-category').closest('table');
if ($table_with_deleted_contacts.length == 1) {
diff --git a/web/cobrands/fixmystreet/fixmystreet.js b/web/cobrands/fixmystreet/fixmystreet.js
index c09eeb803..e92395661 100644
--- a/web/cobrands/fixmystreet/fixmystreet.js
+++ b/web/cobrands/fixmystreet/fixmystreet.js
@@ -109,6 +109,20 @@ function isR2L() {
$drawer.hide();
});
});
+ },
+
+ make_multi: function() {
+ var $this = $(this),
+ all = $this.data('all');
+ $this.multiSelect({
+ allText: all,
+ noneText: all,
+ positionMenuWithin: $('#side'),
+ presets: [{
+ name: all,
+ options: []
+ }]
+ });
}
});
@@ -535,23 +549,8 @@ $.extend(fixmystreet.set_up, {
// to refresh the map when the filter inputs are changed.
$(".report-list-filters [type=submit]").hide();
- function make_multi(id) {
- var $id = $('#' + id),
- all = $id.data('all'),
- none = $id.data('none') || all,
- allOpts = $id.data('allOptions') || [];
- $id.multiSelect({
- allText: all,
- noneText: none,
- positionMenuWithin: $('#side'),
- presets: [{
- name: all,
- options: allOpts
- }]
- });
- }
- make_multi('statuses');
- make_multi('filter_categories');
+ $('#statuses').make_multi();
+ $('#filter_categories').make_multi();
},
mobile_ui_tweaks: function() {