diff options
Diffstat (limited to 'perllib/FixMyStreet/DB')
20 files changed, 767 insertions, 248 deletions
diff --git a/perllib/FixMyStreet/DB/Factories.pm b/perllib/FixMyStreet/DB/Factories.pm new file mode 100644 index 000000000..ec4dd630a --- /dev/null +++ b/perllib/FixMyStreet/DB/Factories.pm @@ -0,0 +1,173 @@ +use FixMyStreet::DB; + +package FixMyStreet::DB::Factory::Base; + +use parent "DBIx::Class::Factory"; + +sub find_or_create { + my ($class, $fields) = @_; + my $key_field = $class->key_field; + my $id = $class->get_fields($fields)->{$key_field}; + my $rs = $class->_class_data->{resultset}; + my $obj = $rs->find({ $key_field => $id }); + return $obj if $obj; + return $class->create($fields); +} + +####################### + +package FixMyStreet::DB::Factory::Problem; + +use parent "DBIx::Class::Factory"; + +__PACKAGE__->resultset(FixMyStreet::DB->resultset("Problem")); + +__PACKAGE__->exclude(['body']); + +__PACKAGE__->fields({ + postcode => '', + title => __PACKAGE__->seq(sub { 'Title #' . (shift()+1) }), + detail => __PACKAGE__->seq(sub { 'Detail #' . (shift()+1) }), + name => __PACKAGE__->callback(sub { shift->get('user')->name }), + bodies_str => __PACKAGE__->callback(sub { shift->get('body')->id }), + confirmed => \'current_timestamp', + whensent => \'current_timestamp', + state => 'confirmed', + cobrand => 'default', + latitude => 0, + longitude => 0, + areas => '', + used_map => 't', + anonymous => 'f', + category => 'Other', +}); + +####################### + +package FixMyStreet::DB::Factory::Body; + +use parent -norequire, "FixMyStreet::DB::Factory::Base"; +use mySociety::MaPit; + +__PACKAGE__->resultset(FixMyStreet::DB->resultset("Body")); + +__PACKAGE__->exclude(['area_id', 'categories']); + +__PACKAGE__->fields({ + name => __PACKAGE__->callback(sub { + my $area_id = shift->get('area_id'); + my $area = mySociety::MaPit::call('area', $area_id); + $area->{name}; + }), + body_areas => __PACKAGE__->callback(sub { + my $area_id = shift->get('area_id'); + [ { area_id => $area_id } ] + }), + contacts => __PACKAGE__->callback(sub { + my $categories = shift->get('categories'); + push @$categories, 'Other' unless @$categories; + [ map { FixMyStreet::DB::Factory::Contact->get_fields({ category => $_ }) } @$categories ]; + }), +}); + +sub key_field { 'id' } + +####################### + +package FixMyStreet::DB::Factory::Contact; + +use parent "DBIx::Class::Factory"; + +__PACKAGE__->resultset(FixMyStreet::DB->resultset("Contact")); + +__PACKAGE__->fields({ + body_id => __PACKAGE__->callback(sub { + my $fields = shift; + return $fields->get('body')->id if $fields->get('body'); + }), + category => 'Other', + email => __PACKAGE__->callback(sub { + my $category = shift->get('category'); + (my $email = lc $_) =~ s/ /-/g; + lc $category . '@example.org'; + }), + state => 'confirmed', + editor => 'Factory', + whenedited => \'current_timestamp', + note => 'Created by factory', +}); + +####################### + +package FixMyStreet::DB::Factory::ResponseTemplate; + +use parent -norequire, "FixMyStreet::DB::Factory::Base"; + +__PACKAGE__->resultset(FixMyStreet::DB->resultset("ResponseTemplate")); + +__PACKAGE__->fields({ + text => __PACKAGE__->seq(sub { 'Template text #' . (shift()+1) }), +}); + +####################### + +package FixMyStreet::DB::Factory::ResponsePriority; + +use parent "DBIx::Class::Factory"; + +__PACKAGE__->resultset(FixMyStreet::DB->resultset("ResponsePriority")); + +__PACKAGE__->fields({ + name => __PACKAGE__->seq(sub { 'Priority #' . (shift()+1) }), + description => __PACKAGE__->seq(sub { 'Description #' . (shift()+1) }), +}); + +####################### + +package FixMyStreet::DB::Factory::Comment; + +use parent "DBIx::Class::Factory"; + +__PACKAGE__->resultset(FixMyStreet::DB->resultset("Comment")); + +__PACKAGE__->fields({ + anonymous => 'f', + name => __PACKAGE__->callback(sub { shift->get('user')->name }), + text => __PACKAGE__->seq(sub { 'Comment #' . (shift()+1) }), + confirmed => \'current_timestamp', + state => 'confirmed', + cobrand => 'default', + mark_fixed => 0, +}); + +####################### + +package FixMyStreet::DB::Factory::User; + +use parent -norequire, "FixMyStreet::DB::Factory::Base"; + +__PACKAGE__->resultset(FixMyStreet::DB->resultset("User")); + +__PACKAGE__->exclude(['body', 'permissions']); + +__PACKAGE__->fields({ + name => 'User', + email => 'user@example.org', + password => 'password', + from_body => __PACKAGE__->callback(sub { + my $fields = shift; + if (my $body = $fields->get('body')) { + return $body->id; + } + }), + user_body_permissions => __PACKAGE__->callback(sub { + my $fields = shift; + my $body = $fields->get('body'); + my $permissions = $fields->get('permissions'); + [ map { { body_id => $body->id, permission_type => $_ } } @$permissions ]; + }), +}); + +sub key_field { 'email' } + +1; diff --git a/perllib/FixMyStreet/DB/Result/Body.pm b/perllib/FixMyStreet/DB/Result/Body.pm index 82015ad2d..6481d5cfc 100644 --- a/perllib/FixMyStreet/DB/Result/Body.pm +++ b/perllib/FixMyStreet/DB/Result/Body.pm @@ -121,18 +121,52 @@ __PACKAGE__->has_many( # Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-02-13 15:11:11 # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BOJANVwg3kR/1VjDq0LykA +use Moo; +use namespace::clean; + +with 'FixMyStreet::Roles::Translatable'; + sub url { my ( $self, $c, $args ) = @_; # XXX $areas_info was used here for Norway parent - needs body parents, I guess return $c->uri_for( '/reports/' . $c->cobrand->short_name( $self ), $args || {} ); } +__PACKAGE__->might_have( + "translations", + "FixMyStreet::DB::Result::Translation", + sub { + my $args = shift; + return { + "$args->{foreign_alias}.object_id" => { -ident => "$args->{self_alias}.id" }, + "$args->{foreign_alias}.tbl" => { '=' => \"?" }, + "$args->{foreign_alias}.col" => { '=' => \"?" }, + "$args->{foreign_alias}.lang" => { '=' => \"?" }, + }; + }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +around name => \&translate_around; + sub areas { my $self = shift; my %ids = map { $_->area_id => 1 } $self->body_areas->all; return \%ids; } +sub first_area_children { + my ( $self, $c ) = @_; + + my $area_id = $self->body_areas->first->area_id; + + my $children = mySociety::MaPit::call('area/children', $area_id, + type => $c->cobrand->area_types_children, + ); + + return $children; +} + =head2 get_cobrand_handler Get a cobrand object for this body, if there is one. diff --git a/perllib/FixMyStreet/DB/Result/Comment.pm b/perllib/FixMyStreet/DB/Result/Comment.pm index cf1ba444d..562f29693 100644 --- a/perllib/FixMyStreet/DB/Result/Comment.pm +++ b/perllib/FixMyStreet/DB/Result/Comment.pm @@ -101,6 +101,7 @@ use Moo; use namespace::clean -except => [ 'meta' ]; with 'FixMyStreet::Roles::Abuser', + 'FixMyStreet::Roles::Extra', 'FixMyStreet::Roles::PhotoSet'; my $stz = sub { @@ -128,9 +129,10 @@ sub check_for_errors { unless $self->text =~ m/\S/; # Bromley Council custom character limit - if ( $self->text && $self->problem && $self->problem->bodies_str - && $self->problem->bodies_str eq '2482' && length($self->text) > 1750 ) { - $errors{update} = sprintf( _('Updates are limited to %s characters in length. Please shorten your update'), 1750 ); + if ( $self->text && $self->problem && $self->problem->bodies_str) { + if ($self->problem->to_body_named('Bromley') && length($self->text) > 1750) { + $errors{update} = sprintf( _('Updates are limited to %s characters in length. Please shorten your update'), 1750 ); + } } return \%errors; @@ -149,6 +151,11 @@ sub confirm { $self->confirmed( \'current_timestamp' ); } +sub url { + my $self = shift; + return "/report/" . $self->problem_id . '#update_' . $self->id; +} + sub photos { my $self = shift; my $photoset = $self->get_photoset; @@ -169,22 +176,6 @@ sub photos { return \@photos; } -=head2 problem_state_display - -Returns a string suitable for display lookup in the update meta section. -Removes the '- council/user' bit from fixed states. - -=cut - -sub problem_state_display { - my $self = shift; - - my $state = $self->problem_state; - $state =~ s/ -.*$//; - - return $state; -} - =head2 latest_moderation_log_entry Return most recent ModerationLog object @@ -236,8 +227,6 @@ sub meta_line { my $meta = ''; - $c->stash->{last_state} ||= ''; - if ($self->anonymous or !$self->name) { $meta = sprintf( _( 'Posted anonymously at %s' ), Utils::prettify_dt( $self->confirmed ) ) } elsif ($self->user->from_body) { @@ -248,68 +237,54 @@ sub meta_line { } elsif ($body eq 'Royal Borough of Greenwich') { $body = "$body <img src='/cobrands/greenwich/favicon.png' alt=''>"; } - if ($c->user_exists and $c->user->has_permission_to('view_body_contribute_details', $self->problem->bodies_str_ids)) { - $meta = sprintf( _( 'Posted by <strong>%s</strong> (%s) at %s' ), $body, $user_name, Utils::prettify_dt( $self->confirmed ) ); + my $can_view_contribute = $c->user_exists && $c->user->has_permission_to('view_body_contribute_details', $self->problem->bodies_str_ids); + if ($self->text) { + if ($can_view_contribute) { + $meta = sprintf( _( 'Posted by <strong>%s</strong> (%s) at %s' ), $body, $user_name, Utils::prettify_dt( $self->confirmed ) ); + } else { + $meta = sprintf( _( 'Posted by <strong>%s</strong> at %s' ), $body, Utils::prettify_dt( $self->confirmed ) ); + } } else { - $meta = sprintf( _( 'Posted by <strong>%s</strong> at %s' ), $body, Utils::prettify_dt( $self->confirmed ) ); + if ($can_view_contribute) { + $meta = sprintf( _( 'Updated by <strong>%s</strong> (%s) at %s' ), $body, $user_name, Utils::prettify_dt( $self->confirmed ) ); + } else { + $meta = sprintf( _( 'Updated by <strong>%s</strong> at %s' ), $body, Utils::prettify_dt( $self->confirmed ) ); + } } } else { $meta = sprintf( _( 'Posted by %s at %s' ), FixMyStreet::Template::html_filter($self->name), Utils::prettify_dt( $self->confirmed ) ) } + if ($self->get_extra_metadata('defect_raised')) { + $meta .= ', ' . _( 'and a defect raised' ); + } + + return $meta; +}; + +sub problem_state_display { + my ( $self, $c ) = @_; + my $update_state = ''; + my $cobrand = $c->cobrand->moniker; if ($self->mark_fixed) { - $update_state = _( 'marked as fixed' ); + return FixMyStreet::DB->resultset("State")->display('fixed', 1); } elsif ($self->mark_open) { - $update_state = _( 'reopened' ); + return FixMyStreet::DB->resultset("State")->display('confirmed', 1); } elsif ($self->problem_state) { - my $state = $self->problem_state_display; - - if ($state eq 'confirmed') { - if ($c->stash->{last_state}) { - $update_state = _( 'reopened' ) - } - } elsif ($state eq 'investigating') { - $update_state = _( 'marked as investigating' ) - } elsif ($state eq 'planned') { - $update_state = _( 'marked as planned' ) - } elsif ($state eq 'in progress') { - $update_state = _( 'marked as in progress' ) - } elsif ($state eq 'action scheduled') { - $update_state = _( 'marked as action scheduled' ) - } elsif ($state eq 'closed') { - $update_state = _( 'marked as closed' ) - } elsif ($state eq 'fixed') { - $update_state = _( 'marked as fixed' ) - } elsif ($state eq 'unable to fix') { - $update_state = _( 'marked as no further action' ) - } elsif ($state eq 'not responsible') { - $update_state = _( "marked as not the council's responsibility" ) - } elsif ($state eq 'duplicate') { - $update_state = _( 'closed as a duplicate report' ) - } elsif ($state eq 'internal referral') { - $update_state = _( 'marked as an internal referral' ) - } - - if ($c->cobrand->moniker eq 'bromley' || ( - $self->problem->bodies_str && - $self->problem->bodies_str eq '2482' - )) { - if ($state eq 'not responsible') { - $update_state = 'marked as third party responsibility' + my $state = $self->problem_state; + if ($state eq 'not responsible') { + $update_state = _( "not the council's responsibility" ); + if ($cobrand eq 'bromley' || $self->problem->to_body_named('Bromley')) { + $update_state = 'third party responsibility'; } + } else { + $update_state = FixMyStreet::DB->resultset("State")->display($state, 1); } - - } - - if ($update_state ne $c->stash->{last_state} and $update_state) { - $meta .= ", $update_state"; } - $c->stash->{last_state} = $update_state; - - return $meta; -}; + return $update_state; +} 1; diff --git a/perllib/FixMyStreet/DB/Result/Contact.pm b/perllib/FixMyStreet/DB/Result/Contact.pm index a620b7358..f9cbf1c44 100644 --- a/perllib/FixMyStreet/DB/Result/Contact.pm +++ b/perllib/FixMyStreet/DB/Result/Contact.pm @@ -11,8 +11,17 @@ use base 'DBIx::Class::Core'; __PACKAGE__->load_components("FilterColumn", "InflateColumn::DateTime", "EncodedColumn"); __PACKAGE__->table("contacts"); __PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "contacts_id_seq", + }, "body_id", { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "category", + { data_type => "text", default_value => "Other", is_nullable => 0 }, "email", { data_type => "text", is_nullable => 0 }, "editor", @@ -21,19 +30,6 @@ __PACKAGE__->add_columns( { data_type => "timestamp", is_nullable => 0 }, "note", { data_type => "text", is_nullable => 0 }, - "confirmed", - { data_type => "boolean", is_nullable => 0 }, - "category", - { data_type => "text", default_value => "Other", is_nullable => 0 }, - "deleted", - { data_type => "boolean", is_nullable => 0 }, - "id", - { - data_type => "integer", - is_auto_increment => 1, - is_nullable => 0, - sequence => "contacts_id_seq", - }, "extra", { data_type => "text", is_nullable => 1 }, "non_public", @@ -46,6 +42,8 @@ __PACKAGE__->add_columns( { data_type => "text", default_value => "", is_nullable => 1 }, "send_method", { data_type => "text", is_nullable => 1 }, + "state", + { data_type => "text", is_nullable => 0 }, ); __PACKAGE__->set_primary_key("id"); __PACKAGE__->add_unique_constraint("contacts_body_id_category_idx", ["body_id", "category"]); @@ -75,8 +73,8 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-02-13 15:11:11 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:f9VepR/oPyr3z6PUpJ4w2A +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-07-08 20:45:04 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:t/VtPP11R8bbqPZdEVXffw __PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn"); __PACKAGE__->rabx_column('extra'); @@ -84,12 +82,18 @@ __PACKAGE__->rabx_column('extra'); use Moo; use namespace::clean -except => [ 'meta' ]; -with 'FixMyStreet::Roles::Extra'; +with 'FixMyStreet::Roles::Extra', + 'FixMyStreet::Roles::Translatable'; __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 category_display { + my $self = shift; + $self->translate_column('category'); +} + sub get_metadata_for_input { my $self = shift; my $id_field = $self->id_field; diff --git a/perllib/FixMyStreet/DB/Result/ContactsHistory.pm b/perllib/FixMyStreet/DB/Result/ContactsHistory.pm index 7126d91c9..c90bb9d66 100644 --- a/perllib/FixMyStreet/DB/Result/ContactsHistory.pm +++ b/perllib/FixMyStreet/DB/Result/ContactsHistory.pm @@ -26,22 +26,20 @@ __PACKAGE__->add_columns( { data_type => "text", default_value => "Other", is_nullable => 0 }, "email", { data_type => "text", is_nullable => 0 }, - "confirmed", - { data_type => "boolean", is_nullable => 0 }, - "deleted", - { data_type => "boolean", is_nullable => 0 }, "editor", { data_type => "text", is_nullable => 0 }, "whenedited", { data_type => "timestamp", is_nullable => 0 }, "note", { data_type => "text", is_nullable => 0 }, + "state", + { data_type => "text", is_nullable => 0 }, ); __PACKAGE__->set_primary_key("contacts_history_id"); -# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-12-12 16:37:16 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:sxflEBBn0Mn0s3MroWnWFA +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-07-08 20:45:04 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:HTt0g29yXTM/WyHKN179FA # You can replace this text with custom code or comments, and it will be preserved on regeneration diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm index 84db41490..3b622b561 100644 --- a/perllib/FixMyStreet/DB/Result/Problem.pm +++ b/perllib/FixMyStreet/DB/Result/Problem.pm @@ -206,6 +206,7 @@ my $IM = eval { with 'FixMyStreet::Roles::Abuser', 'FixMyStreet::Roles::Extra', + 'FixMyStreet::Roles::Translatable', 'FixMyStreet::Roles::PhotoSet'; =head2 @@ -219,15 +220,8 @@ HASHREF. =cut sub open_states { - my $states = { - 'confirmed' => 1, - 'investigating' => 1, - 'in progress' => 1, - 'planned' => 1, - 'action scheduled' => 1, - }; - - return wantarray ? keys %{$states} : $states; + my @states = map { $_->label } @{FixMyStreet::DB->resultset("State")->open}; + return wantarray ? @states : { map { $_ => 1 } @states }; } =head2 @@ -241,13 +235,9 @@ HASHREF. =cut sub fixed_states { - my $states = { - 'fixed' => 1, - 'fixed - user' => 1, - 'fixed - council' => 1, - }; - - return wantarray ? keys %{ $states } : $states; + my @states = map { $_->label } @{FixMyStreet::DB->resultset("State")->fixed}; + push @states, 'fixed - user', 'fixed - council' if @states; + return wantarray ? @states : { map { $_ => 1 } @states }; } =head2 @@ -261,18 +251,10 @@ HASHREF. =cut sub closed_states { - my $states = { - 'closed' => 1, - 'unable to fix' => 1, - 'not responsible' => 1, - 'duplicate' => 1, - 'internal referral' => 1, - }; - - return wantarray ? keys %{$states} : $states; + my @states = map { $_->label } @{FixMyStreet::DB->resultset("State")->closed}; + return wantarray ? @states : { map { $_ => 1 } @states }; } - =head2 @states = FixMyStreet::DB::Problem::all_states(); @@ -288,21 +270,10 @@ sub all_states { 'hidden' => 1, 'partial' => 1, 'unconfirmed' => 1, - 'confirmed' => 1, - 'investigating' => 1, - 'in progress' => 1, - 'planned' => 1, - 'action scheduled' => 1, - 'fixed' => 1, 'fixed - council' => 1, 'fixed - user' => 1, - 'unable to fix' => 1, - 'not responsible' => 1, - 'duplicate' => 1, - 'closed' => 1, - 'internal referral' => 1, }; - + map { $states->{$_->label} = 1 } @{FixMyStreet::DB->resultset("State")->states}; return wantarray ? keys %{$states} : $states; } @@ -322,75 +293,31 @@ my $hidden_states = { 'unconfirmed' => 1, }; -my $visible_states = { - map { - $hidden_states->{$_} ? () : ($_ => 1) - } all_states() -}; - ## e.g.: - # 'confirmed' => 1, - # 'investigating' => 1, - # 'in progress' => 1, - # 'planned' => 1, - # 'action scheduled' => 1, - # 'fixed' => 1, - # 'fixed - council' => 1, - # 'fixed - user' => 1, - # 'unable to fix' => 1, - # 'not responsible' => 1, - # 'duplicate' => 1, - # 'closed' => 1, - # 'internal referral' => 1, - sub hidden_states { return wantarray ? keys %{$hidden_states} : $hidden_states; } sub visible_states { - return wantarray ? keys %{$visible_states} : $visible_states; + my %visible_states = map { + $hidden_states->{$_} ? () : ($_ => 1) + } all_states(); + return wantarray ? keys %visible_states : \%visible_states; } sub visible_states_add { my ($self, @states) = @_; for my $state (@states) { delete $hidden_states->{$state}; - $visible_states->{$state} = 1; } } sub visible_states_remove { my ($self, @states) = @_; for my $state (@states) { - delete $visible_states->{$state}; $hidden_states->{$state} = 1; } } -=head2 - - @states = FixMyStreet::DB::Problem::council_states(); - -Get a list of states that are availble to council users. If called in -array context then returns an array of names, otherwise returns a -HASHREF. - -=cut -sub council_states { - my $states = { - 'confirmed' => 1, - 'investigating' => 1, - 'action scheduled' => 1, - 'in progress' => 1, - 'fixed - council' => 1, - 'unable to fix' => 1, - 'not responsible' => 1, - 'duplicate' => 1, - 'internal referral' => 1, - }; - - return wantarray ? keys %{$states} : $states; -} - my $stz = sub { my ( $orig, $self ) = ( shift, shift ); my $s = $self->$orig(@_); @@ -456,12 +383,6 @@ sub check_for_errors { $errors{category} = _('Please choose a category'); $self->category(undef); } - elsif ($self->category - && $self->category eq _('-- Pick a property type --') ) - { - $errors{category} = _('Please choose a property type'); - $self->category(undef); - } return \%errors; } @@ -489,6 +410,11 @@ sub confirm { return 1; } +sub category_display { + my $self = shift; + $self->translate_column('category'); +} + sub bodies_str_ids { my $self = shift; return [] unless $self->bodies_str; @@ -502,12 +428,36 @@ Returns a hashref of bodies to which a report was sent. =cut -sub bodies($) { +has bodies => ( + is => 'ro', + lazy => 1, + default => sub { + my $self = shift; + return {} unless $self->bodies_str; + my $cache = $self->result_source->schema->cache; + return $cache->{bodies}{$self->bodies_str} if $cache->{bodies}{$self->bodies_str}; + + my $bodies = $self->bodies_str_ids; + my @bodies = $self->result_source->schema->resultset('Body')->search( + { id => $bodies }, + { prefetch => 'body_areas' }, + )->all; + $cache->{bodies}{$self->bodies_str} = { map { $_->id => $_ } @bodies }; + return $cache->{bodies}{$self->bodies_str}; + }, +); + +sub body_names($) { my $self = shift; - return {} unless $self->bodies_str; - my $bodies = $self->bodies_str_ids; - my @bodies = $self->result_source->schema->resultset('Body')->search({ id => $bodies })->all; - return { map { $_->id => $_ } @bodies }; + my $bodies = $self->bodies; + my @names = map { $_->name } values %$bodies; + return \@names; +} + +sub to_body_named($$) { + my ($self, $re) = @_; + my $names = join(',,', @{$self->body_names}); + $names =~ /$re/; } =head2 url @@ -609,19 +559,6 @@ sub is_visible { return exists $self->visible_states->{ $self->state } ? 1 : 0; } -=head2 state_display - -Returns a string suitable for display lookup in the update meta section. -Removes the '- council/user' bit from fixed states. - -=cut - -sub state_display { - my $self = shift; - (my $state = $self->state) =~ s/ -.*$//; - return $state; -} - =head2 meta_line Returns a string to be used on a problem report page, describing some of the @@ -635,7 +572,7 @@ sub meta_line { my $date_time = Utils::prettify_dt( $problem->confirmed ); my $meta = ''; - my $category = $problem->category; + my $category = $problem->category_display; $category = $c->cobrand->call_hook(change_category_text => $category) || $category; if ( $problem->anonymous ) { @@ -787,7 +724,7 @@ sub defect_types { # Note: this only makes sense when called on a problem that has been sent! sub can_display_external_id { my $self = shift; - if ($self->external_id && $self->send_method_used && $self->bodies_str =~ /(2237|2550)/) { + if ($self->external_id && $self->send_method_used && $self->to_body_named('Oxfordshire|Angus')) { return 1; } return 0; @@ -924,6 +861,7 @@ sub as_hashref { latitude => $self->latitude, longitude => $self->longitude, postcode => $self->postcode, + areas => $self->areas, state => $self->state, state_t => _( $self->state ), used_map => $self->used_map, @@ -953,15 +891,21 @@ sub photos { my $id = $self->id; my @photos = map { my $cachebust = substr($_, 0, 8); + # Some Varnish configurations (e.g. on mySociety infra) strip cookies from + # images, which means image requests will be redirected to the login page + # if LOGIN_REQUIRED is set. To stop this happening, Varnish should be + # configured to not strip cookies if the cookie_passthrough param is + # present, which this line ensures will be if LOGIN_REQUIRED is set. + my $extra = (FixMyStreet->config('LOGIN_REQUIRED')) ? "&cookie_passthrough=1" : ""; my ($hash, $format) = split /\./, $_; { id => $hash, - url_temp => "/photo/temp.$hash.$format", - url_temp_full => "/photo/fulltemp.$hash.$format", - url => "/photo/$id.$i.$format?$cachebust", - url_full => "/photo/$id.$i.full.$format?$cachebust", - url_tn => "/photo/$id.$i.tn.$format?$cachebust", - url_fp => "/photo/$id.$i.fp.$format?$cachebust", + url_temp => "/photo/temp.$hash.$format$extra", + url_temp_full => "/photo/fulltemp.$hash.$format$extra", + url => "/photo/$id.$i.$format?$cachebust$extra", + url_full => "/photo/$id.$i.full.$format?$cachebust$extra", + url_tn => "/photo/$id.$i.tn.$format?$cachebust$extra", + url_fp => "/photo/$id.$i.fp.$format?$cachebust$extra", idx => $i++, } } $photoset->all_ids; @@ -1013,13 +957,14 @@ has get_cobrand_logged => ( sub pin_data { my ($self, $c, $page, %opts) = @_; my $colour = $c->cobrand->pin_colour($self, $page); - + my $title = $opts{private} ? $self->title : $self->title_safe; + $title = $c->cobrand->call_hook(pin_hover_title => $self, $title) || $title; { latitude => $self->latitude, longitude => $self->longitude, colour => $colour, id => $self->id, - title => $opts{private} ? $self->title : $self->title_safe, + title => $title, problem => $self, type => $opts{type}, } @@ -1082,6 +1027,7 @@ sub static_map { if ($pin) { my $im = Image::Magick->new; $im->read(FixMyStreet->path_to('web', 'i', 'pin-yellow.png')); + $im->Scale( geometry => '48x64' ); $image->Composite(image => $im, gravity => 'NorthWest', x => $pin->{px} - 24, y => $pin->{py} - 64); } @@ -1113,6 +1059,16 @@ has shortlisted_user => ( }, ); +sub set_duplicate_of { + my ($self, $other_id) = @_; + $self->set_extra_metadata( duplicate_of => $other_id ); + my $dupe = $self->result_source->schema->resultset("Problem")->find($other_id); + my $dupes_duplicates = $dupe->get_extra_metadata('duplicates') || []; + push @$dupes_duplicates, $self->id; + $dupe->set_extra_metadata( duplicates => $dupes_duplicates ); + $dupe->update; +} + has duplicate_of => ( is => 'ro', lazy => 1, @@ -1130,8 +1086,9 @@ has duplicates => ( lazy => 1, default => sub { my $self = shift; - my $rabx_id = RABX::serialise( $self->id ); - my @duplicates = $self->result_source->schema->resultset('Problem')->search({ extra => { like => "\%duplicate_of,$rabx_id%" } })->all; + my $duplicates = $self->get_extra_metadata("duplicates") || []; + return [] unless $duplicates && @$duplicates; + my @duplicates = $self->result_source->schema->resultset('Problem')->search({ id => $duplicates })->all; return \@duplicates; }, ); diff --git a/perllib/FixMyStreet/DB/Result/ReportExtraFields.pm b/perllib/FixMyStreet/DB/Result/ReportExtraFields.pm new file mode 100644 index 000000000..27a6bd2c6 --- /dev/null +++ b/perllib/FixMyStreet/DB/Result/ReportExtraFields.pm @@ -0,0 +1,45 @@ +use utf8; +package FixMyStreet::DB::Result::ReportExtraFields; + +# 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("report_extra_fields"); +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "report_extra_fields_id_seq", + }, + "name", + { data_type => "text", is_nullable => 0 }, + "cobrand", + { data_type => "text", is_nullable => 1 }, + "language", + { data_type => "text", is_nullable => 1 }, + "extra", + { data_type => "text", is_nullable => 1 }, +); +__PACKAGE__->set_primary_key("id"); + + +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-07-28 09:51:34 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:LkfbsUInnEyXowdcCEPjUQ + +__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn"); +__PACKAGE__->rabx_column('extra'); + +use Moo; +use namespace::clean -except => [ 'meta' ]; + +with 'FixMyStreet::Roles::Extra'; + +# 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/ResponsePriority.pm b/perllib/FixMyStreet/DB/Result/ResponsePriority.pm index 44635d174..df54cfa08 100644 --- a/perllib/FixMyStreet/DB/Result/ResponsePriority.pm +++ b/perllib/FixMyStreet/DB/Result/ResponsePriority.pm @@ -28,6 +28,8 @@ __PACKAGE__->add_columns( { data_type => "text", is_nullable => 1 }, "external_id", { data_type => "text", is_nullable => 1 }, + "is_default", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, ); __PACKAGE__->set_primary_key("id"); __PACKAGE__->add_unique_constraint("response_priorities_body_id_name_key", ["body_id", "name"]); @@ -51,8 +53,8 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-12-14 17:12:09 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:glsO0fLK6fNvg4TmW1DMPg +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-09-12 09:32:53 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:JBIHFnaLvXCAUjgwTSB3CQ __PACKAGE__->many_to_many( contacts => 'contact_response_priorities', 'contact' ); diff --git a/perllib/FixMyStreet/DB/Result/State.pm b/perllib/FixMyStreet/DB/Result/State.pm new file mode 100644 index 000000000..b8a35d42b --- /dev/null +++ b/perllib/FixMyStreet/DB/Result/State.pm @@ -0,0 +1,48 @@ +use utf8; +package FixMyStreet::DB::Result::State; + +# 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("state"); +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "state_id_seq", + }, + "label", + { data_type => "text", is_nullable => 0 }, + "type", + { data_type => "text", is_nullable => 0 }, + "name", + { data_type => "text", is_nullable => 0 }, +); +__PACKAGE__->set_primary_key("id"); +__PACKAGE__->add_unique_constraint("state_label_key", ["label"]); +__PACKAGE__->add_unique_constraint("state_name_key", ["name"]); + + +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-08-22 15:17:43 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:dvtAOpeYqEF9T3otHHgLqw + +use Moo; +use namespace::clean; + +with 'FixMyStreet::Roles::Translatable'; + +sub msgstr { + my $self = shift; + my $lang = $self->result_source->schema->lang; + return $self->name unless $lang && $self->translated->{name}{$lang}; + return $self->translated->{name}{$lang}{msgstr}; +} + +1; diff --git a/perllib/FixMyStreet/DB/Result/Translation.pm b/perllib/FixMyStreet/DB/Result/Translation.pm new file mode 100644 index 000000000..fafc7ccf1 --- /dev/null +++ b/perllib/FixMyStreet/DB/Result/Translation.pm @@ -0,0 +1,44 @@ +use utf8; +package FixMyStreet::DB::Result::Translation; + +# 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("translation"); +__PACKAGE__->add_columns( + "id", + { + data_type => "integer", + is_auto_increment => 1, + is_nullable => 0, + sequence => "translation_id_seq", + }, + "tbl", + { data_type => "text", is_nullable => 0 }, + "object_id", + { data_type => "integer", is_nullable => 0 }, + "col", + { data_type => "text", is_nullable => 0 }, + "lang", + { data_type => "text", is_nullable => 0 }, + "msgstr", + { data_type => "text", is_nullable => 0 }, +); +__PACKAGE__->set_primary_key("id"); +__PACKAGE__->add_unique_constraint( + "translation_tbl_object_id_col_lang_key", + ["tbl", "object_id", "col", "lang"], +); + + +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-07-14 23:24:32 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:///VNqg4BOuO29xKhnY8vw + + +# 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 cf6de9a76..19adf5d49 100644 --- a/perllib/FixMyStreet/DB/Result/User.pm +++ b/perllib/FixMyStreet/DB/Result/User.pm @@ -204,6 +204,27 @@ sub alert_for_problem { } ); } +=head2 create_alert + +Sign a user up to receive alerts on a given problem + +=cut + +sub create_alert { + my ( $self, $id, $options ) = @_; + my $alert = $self->alert_for_problem($id); + + unless ( $alert ) { + $alert = $self->alerts->create({ + %$options, + alert_type => 'new_updates', + parameter => $id, + }); + } + + $alert->confirm(); +} + sub body { my $self = shift; return '' unless $self->from_body; @@ -274,6 +295,16 @@ sub permissions { sub has_permission_to { my ($self, $permission_type, $body_ids) = @_; + # Nobody, including superusers, can have a permission which isn't available + # in the current cobrand. + my $cobrand = $self->result_source->schema->cobrand; + my $cobrand_perms = $cobrand->available_permissions; + my %available = map { %$_ } values %$cobrand_perms; + # The 'trusted' permission is never set in the cobrand's + # available_permissions (see note there in Default.pm) so include it here. + $available{trusted} = 1; + return 0 unless $available{$permission_type}; + return 1 if $self->is_superuser; return 0 if !$body_ids || (ref $body_ids && !@$body_ids); $body_ids = [ $body_ids ] unless ref $body_ids; @@ -391,9 +422,19 @@ sub active_planned_reports { $self->planned_reports->search({ removed => undef }); } +has active_user_planned_reports => ( + is => 'ro', + lazy => 1, + default => sub { + my $self = shift; + [ $self->user_planned_reports->search({ removed => undef })->all ]; + }, +); + sub is_planned_report { my ($self, $problem) = @_; - return $self->active_planned_reports->find({ id => $problem->id }); + my $id = $problem->id; + return scalar grep { $_->report_id == $id } @{$self->active_user_planned_reports}; } sub update_reputation { diff --git a/perllib/FixMyStreet/DB/ResultSet/Body.pm b/perllib/FixMyStreet/DB/ResultSet/Body.pm index 6802ed604..e79d038b1 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Body.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Body.pm @@ -14,4 +14,15 @@ sub for_areas { return $result; } +sub all_translated { + my $rs = shift; + my $schema = $rs->result_source->schema; + my @bodies = $rs->search(undef, { + '+columns' => { 'msgstr' => 'translations.msgstr' }, + join => 'translations', + bind => [ 'name', $schema->lang, 'body' ], + })->all; + return @bodies; +} + 1; diff --git a/perllib/FixMyStreet/DB/ResultSet/Contact.pm b/perllib/FixMyStreet/DB/ResultSet/Contact.pm index f402b5461..8ef6d1ac5 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Contact.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Contact.pm @@ -10,13 +10,18 @@ sub me { join('.', shift->current_source_alias, shift || q{}) } $rs = $rs->not_deleted(); -Filter down to not deleted contacts - which have C<deleted> set to false; +Filter down to not deleted contacts (so active or inactive). =cut sub not_deleted { my $rs = shift; - return $rs->search( { $rs->me('deleted') => 0 } ); + return $rs->search( { $rs->me('state') => { '!=' => 'deleted' } } ); +} + +sub active { + my $rs = shift; + $rs->search( { $rs->me('state') => [ 'unconfirmed', 'confirmed' ] } ); } sub summary_count { @@ -25,9 +30,9 @@ sub summary_count { return $rs->search( $restriction, { - group_by => ['confirmed'], - select => [ 'confirmed', { count => 'id' } ], - as => [qw/confirmed confirmed_count/] + group_by => ['state'], + select => [ 'state', { count => 'id' } ], + as => [qw/state state_count/] } ); } diff --git a/perllib/FixMyStreet/DB/ResultSet/DefectType.pm b/perllib/FixMyStreet/DB/ResultSet/DefectType.pm index a873ef252..b2ef77f7c 100644 --- a/perllib/FixMyStreet/DB/ResultSet/DefectType.pm +++ b/perllib/FixMyStreet/DB/ResultSet/DefectType.pm @@ -3,20 +3,26 @@ use base 'DBIx::Class::ResultSet'; use strict; use warnings; +use Moo; +use HTML::Entities; -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, - }); +with('FixMyStreet::Roles::ContactExtra'); + +sub join_table { + return 'contact_defect_types'; +} + +sub map_extras { + my ($rs, @ts) = @_; + return map { + my $meta = $_->get_extra_metadata(); + my %extra = map { $_ => encode_entities($meta->{$_}) } keys %$meta; + { + id => $_->id, + name => encode_entities($_->name), + extra => \%extra + } + } @ts; } 1; diff --git a/perllib/FixMyStreet/DB/ResultSet/ReportExtraFields.pm b/perllib/FixMyStreet/DB/ResultSet/ReportExtraFields.pm new file mode 100644 index 000000000..1348df3c2 --- /dev/null +++ b/perllib/FixMyStreet/DB/ResultSet/ReportExtraFields.pm @@ -0,0 +1,25 @@ +package FixMyStreet::DB::ResultSet::ReportExtraFields; +use base 'DBIx::Class::ResultSet'; + +use strict; +use warnings; + +sub for_cobrand { + my ( $rs, $cobrand ) = @_; + + my $result = $rs->search( + { cobrand => [ undef, $cobrand->moniker, '' ] } + ); + return $result; +} + +sub for_language { + my ( $rs, $language ) = @_; + + my $result = $rs->search( + { language => [ undef, $language, '' ] } + ); + return $result; +} + +1; diff --git a/perllib/FixMyStreet/DB/ResultSet/ResponsePriority.pm b/perllib/FixMyStreet/DB/ResultSet/ResponsePriority.pm index aa9c426f4..89bb4dfd7 100644 --- a/perllib/FixMyStreet/DB/ResultSet/ResponsePriority.pm +++ b/perllib/FixMyStreet/DB/ResultSet/ResponsePriority.pm @@ -3,20 +3,18 @@ use base 'DBIx::Class::ResultSet'; use strict; use warnings; +use Moo; +use HTML::Entities; -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_response_priorities' => 'contact' }, - distinct => 1, - }); +with('FixMyStreet::Roles::ContactExtra'); + +sub join_table { + return 'contact_response_priorities'; +} + +sub map_extras { + my ($rs, @ts) = @_; + return map { { id => $_->id, name => encode_entities($_->name) } } @ts; } 1; diff --git a/perllib/FixMyStreet/DB/ResultSet/ResponseTemplate.pm b/perllib/FixMyStreet/DB/ResultSet/ResponseTemplate.pm new file mode 100644 index 000000000..aa070daa3 --- /dev/null +++ b/perllib/FixMyStreet/DB/ResultSet/ResponseTemplate.pm @@ -0,0 +1,27 @@ +package FixMyStreet::DB::ResultSet::ResponseTemplate; +use base 'DBIx::Class::ResultSet'; + +use Moo; +use HTML::Entities; + +with('FixMyStreet::Roles::ContactExtra'); + +sub join_table { + return 'contact_response_templates'; +} + +sub name_column { + 'title'; +} + +sub map_extras { + my ($rs, @ts) = @_; + return map { + my $out = { id => encode_entities($_->text), name => encode_entities($_->title) }; + $out->{state} = encode_entities($_->state) if $_->state; + $out; + } @ts; +} + +1; + diff --git a/perllib/FixMyStreet/DB/ResultSet/State.pm b/perllib/FixMyStreet/DB/ResultSet/State.pm new file mode 100644 index 000000000..ac13ec2a4 --- /dev/null +++ b/perllib/FixMyStreet/DB/ResultSet/State.pm @@ -0,0 +1,84 @@ +package FixMyStreet::DB::ResultSet::State; +use base 'DBIx::Class::ResultSet'; + +use strict; +use warnings; +use Memcached; + +sub _hardcoded_states { + my $rs = shift; + # These are translated on use, not here + my $open = $rs->new({ id => -1, label => 'confirmed', type => 'open', name => "Open" }); + my $closed = $rs->new({ id => -2, label => 'closed', type => 'closed', name => "Closed" }); + return ($open, $closed); +} + +# As states will change rarely, and then only through the admin, +# we cache these in the package on first use, and clear on update. + +sub clear { + Memcached::set('states', ''); +} + +sub states { + my $rs = shift; + + my $states = Memcached::get('states'); + if ($states) { + # Need to reattach schema + $states->[0]->result_source->schema( $rs->result_source->schema ) if $states->[0]; + return $states; + } + + # Pick up and cache any translations + my $q = $rs->result_source->schema->resultset("Translation")->search({ + tbl => 'state', + col => 'name', + }); + my %trans; + $trans{$_->object_id}{$_->lang} = { id => $_->id, msgstr => $_->msgstr } foreach $q->all; + + my @states = ($rs->_hardcoded_states, $rs->search(undef, { order_by => 'label' })->all); + $_->translated->{name} = $trans{$_->id} || {} foreach @states; + $states = \@states; + Memcached::set('states', $states); + return $states; +} + +# Some functions to provide filters on the above data + +sub open { [ $_[0]->_filter(sub { $_->type eq 'open' }) ] } +sub closed { [ $_[0]->_filter(sub { $_->type eq 'closed' }) ] } +sub fixed { [ $_[0]->_filter(sub { $_->type eq 'fixed' }) ] } + +# We sometimes have only a state label to display, no associated object. +# This function can be used to return that label's display name. + +sub display { + my ($rs, $label, $single_fixed) = @_; + my $unchanging = { + unconfirmed => _("Unconfirmed"), + hidden => _("Hidden"), + partial => _("Partial"), + 'fixed - council' => _("Fixed - Council"), + 'fixed - user' => _("Fixed - User"), + }; + my $translate_now = { + confirmed => _("Open"), + closed => _("Closed"), + }; + $label = 'fixed' if $single_fixed && $label =~ /^fixed - (council|user)$/; + return $unchanging->{$label} if $unchanging->{$label}; + my ($state) = $rs->_filter(sub { $_->label eq $label }); + return $label unless $state; + $state->name($translate_now->{$label}) if $translate_now->{$label}; + return $state->msgstr; +} + +sub _filter { + my ($rs, $fn) = @_; + my $states = $rs->states; + grep &$fn, @$states; +} + +1; diff --git a/perllib/FixMyStreet/DB/ResultSet/UserPlannedReport.pm b/perllib/FixMyStreet/DB/ResultSet/UserPlannedReport.pm index 7e16e2dd3..460a4912e 100644 --- a/perllib/FixMyStreet/DB/ResultSet/UserPlannedReport.pm +++ b/perllib/FixMyStreet/DB/ResultSet/UserPlannedReport.pm @@ -6,7 +6,17 @@ use warnings; sub active { my $rs = shift; - $rs->search({ removed => undef }); + + # If we have been prefetched we can't use `active` as that'll blow away the + # cache and query the DB due to the `removed IS NULL` clause. So let's do + # the filtering here instead, if the query has been prefetched. + if ( $rs->get_cache ) { + my @users = grep { !defined($_->removed) } $rs->all; + $rs->set_cache(\@users); + $rs; + } else { + $rs->search({ removed => undef }); + } } sub for_report { diff --git a/perllib/FixMyStreet/DB/Schema.pm b/perllib/FixMyStreet/DB/Schema.pm new file mode 100644 index 000000000..be39069d8 --- /dev/null +++ b/perllib/FixMyStreet/DB/Schema.pm @@ -0,0 +1,32 @@ +use utf8; +package FixMyStreet::DB::Schema; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +use strict; +use warnings; + +use base 'DBIx::Class::Schema'; + +__PACKAGE__->load_namespaces( + result_namespace => "+FixMyStreet::DB::Result", + resultset_namespace => "+FixMyStreet::DB::ResultSet", +); + + +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-07-13 14:15:09 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:UpH30RXb6SbCqRv2FPmpkg + +use Moo; +use FixMyStreet; + +__PACKAGE__->connection(FixMyStreet->dbic_connect_info); + +has lang => ( is => 'rw' ); + +has cobrand => ( is => 'rw' ); + +has cache => ( is => 'rw', lazy => 1, default => sub { {} } ); + +1; |