use utf8; package FixMyStreet::DB::Result::Problem; # 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("problem"); __PACKAGE__->add_columns( "id", { data_type => "integer", is_auto_increment => 1, is_nullable => 0, sequence => "problem_id_seq", }, "postcode", { data_type => "text", is_nullable => 0 }, "latitude", { data_type => "double precision", is_nullable => 0 }, "longitude", { data_type => "double precision", is_nullable => 0 }, "council", { data_type => "text", is_nullable => 1 }, "areas", { data_type => "text", is_nullable => 0 }, "category", { data_type => "text", default_value => "Other", is_nullable => 0 }, "title", { data_type => "text", is_nullable => 0 }, "detail", { data_type => "text", is_nullable => 0 }, "photo", { data_type => "bytea", is_nullable => 1 }, "used_map", { data_type => "boolean", is_nullable => 0 }, "user_id", { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, "name", { data_type => "text", is_nullable => 0 }, "anonymous", { data_type => "boolean", is_nullable => 0 }, "external_id", { data_type => "text", is_nullable => 1 }, "external_body", { data_type => "text", is_nullable => 1 }, "external_team", { data_type => "text", is_nullable => 1 }, "created", { data_type => "timestamp", default_value => \"ms_current_timestamp()", is_nullable => 0, }, "confirmed", { data_type => "timestamp", is_nullable => 1 }, "state", { data_type => "text", is_nullable => 0 }, "lang", { data_type => "text", default_value => "en-gb", is_nullable => 0 }, "service", { data_type => "text", default_value => "", is_nullable => 0 }, "cobrand", { data_type => "text", default_value => "", is_nullable => 0 }, "cobrand_data", { data_type => "text", default_value => "", is_nullable => 0 }, "lastupdate", { data_type => "timestamp", default_value => \"ms_current_timestamp()", is_nullable => 0, }, "whensent", { data_type => "timestamp", is_nullable => 1 }, "send_questionnaire", { data_type => "boolean", default_value => \"true", is_nullable => 0 }, "extra", { data_type => "text", is_nullable => 1 }, "flagged", { data_type => "boolean", default_value => \"false", is_nullable => 0 }, "geocode", { data_type => "bytea", is_nullable => 1 }, "send_fail_count", { data_type => "integer", default_value => 0, is_nullable => 0 }, "send_fail_reason", { data_type => "text", is_nullable => 1 }, "send_fail_timestamp", { data_type => "timestamp", is_nullable => 1 }, "send_method_used", { data_type => "text", is_nullable => 1 }, "non_public", { data_type => "boolean", default_value => \"false", is_nullable => 1 }, "external_source", { data_type => "text", is_nullable => 1 }, "external_source_id", { data_type => "text", is_nullable => 1 }, "interest_count", { data_type => "integer", is_nullable => 1 }, "subcategory", { data_type => "text", is_nullable => 1 }, ); __PACKAGE__->set_primary_key("id"); __PACKAGE__->has_many( "comments", "FixMyStreet::DB::Result::Comment", { "foreign.problem_id" => "self.id" }, { cascade_copy => 0, cascade_delete => 0 }, ); __PACKAGE__->has_many( "questionnaires", "FixMyStreet::DB::Result::Questionnaire", { "foreign.problem_id" => "self.id" }, { cascade_copy => 0, cascade_delete => 0 }, ); __PACKAGE__->belongs_to( "user", "FixMyStreet::DB::Result::User", { id => "user_id" }, { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, ); # Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-12-03 17:48:10 # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:xN/RB8Vx50CwyOeBjvJezQ # Add fake relationship to stored procedure table __PACKAGE__->has_one( "nearby", "FixMyStreet::DB::Result::Nearby", { "foreign.problem_id" => "self.id" }, { cascade_copy => 0, cascade_delete => 0 }, ); __PACKAGE__->filter_column( extra => { filter_from_storage => sub { my $self = shift; my $ser = shift; return undef unless defined $ser; my $h = new IO::String($ser); return RABX::wire_rd($h); }, filter_to_storage => sub { my $self = shift; my $data = shift; my $ser = ''; my $h = new IO::String($ser); RABX::wire_wr( $data, $h ); return $ser; }, } ); __PACKAGE__->filter_column( geocode => { filter_from_storage => sub { my $self = shift; my $ser = shift; return undef unless defined $ser; my $h = new IO::String($ser); return RABX::wire_rd($h); }, filter_to_storage => sub { my $self = shift; my $data = shift; my $ser = ''; my $h = new IO::String($ser); RABX::wire_wr( $data, $h ); return $ser; }, } ); use DateTime::TimeZone; use Image::Size; use Moose; use namespace::clean -except => [ 'meta' ]; use Utils; use RABX; with 'FixMyStreet::Roles::Abuser'; =head2 @states = FixMyStreet::DB::Problem::open_states(); Get a list or states that are regarded as open. If called in array context then returns an array of names, otherwise returns a HASHREF. =cut sub open_states { my $states = { 'confirmed' => 1, 'investigating' => 1, 'in progress' => 1, 'planned' => 1, 'action scheduled' => 1, }; return wantarray ? keys %{$states} : $states; } =head2 @states = FixMyStreet::DB::Problem::fixed_states(); Get a list or states that should be regarded as fixed. If called in array context then returns an array of names, otherwise returns a HASHREF. =cut sub fixed_states { my $states = { 'fixed' => 1, 'fixed - user' => 1, 'fixed - council' => 1, }; return wantarray ? keys %{ $states } : $states; } =head2 @states = FixMyStreet::DB::Problem::closed_states(); Get a list or states that should be regarded as closed. If called in array context then returns an array of names, otherwise returns a HASHREF. =cut sub closed_states { my $states = { 'closed' => 1, 'unable to fix' => 1, 'not responsible' => 1, 'duplicate' => 1, }; return wantarray ? keys %{$states} : $states; } =head2 @states = FixMyStreet::DB::Problem::visible_states(); Get a list of states that should be visible on the site. If called in array context then returns an array of names, otherwise returns a HASHREF. =cut sub visible_states { my $states = { '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, }; return wantarray ? keys %{$states} : $states; } =head2 @states = FixMyStreet::DB::Problem::all_states(); Get a list of all states that a problem can have. If called in array context then returns an array of names, otherwise returns a HASHREF. =cut sub all_states { my $states = { 'hidden' => 1, 'partial' => 1, 'unconfirmed' => 1, 'confirmed' => 1, 'investigating' => 1, 'in progress' => 1, 'action scheduled' => 1, 'fixed' => 1, 'fixed - council' => 1, 'fixed - user' => 1, 'unable to fix' => 1, 'not responsible' => 1, 'duplicate' => 1, 'closed' => 1, }; return wantarray ? keys %{$states} : $states; } =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, }; return wantarray ? keys %{$states} : $states; } my $tz = DateTime::TimeZone->new( name => "local" ); sub confirmed_local { my $self = shift; return $self->confirmed ? $self->confirmed->set_time_zone($tz) : $self->confirmed; } sub created_local { my $self = shift; return $self->created ? $self->created->set_time_zone($tz) : $self->created; } sub whensent_local { my $self = shift; return $self->whensent ? $self->whensent->set_time_zone($tz) : $self->whensent; } sub lastupdate_local { my $self = shift; return $self->lastupdate ? $self->lastupdate->set_time_zone($tz) : $self->lastupdate; } around service => sub { my ( $orig, $self ) = ( shift, shift ); my $s = $self->$orig(@_); $s =~ s/_/ /g; return $s; }; =head2 check_for_errors $error_hashref = $problem->check_for_errors(); Look at all the fields and return a hashref with all errors found, keyed on the field name. This is intended to be passed back to the form to display the errors. TODO - ideally we'd pass back error codes which would be humanised in the templates (eg: 'missing','email_not_valid', etc). =cut sub check_for_errors { my $self = shift; my %errors = (); $errors{title} = _('Please enter a subject') unless $self->title =~ m/\S/; $errors{detail} = _('Please enter some details') unless $self->detail =~ m/\S/; $errors{council} = _('No council selected') unless $self->council && $self->council =~ m/^(?:-1|[\d,]+(?:\|[\d,]+)?)$/; if ( !$self->name || $self->name !~ m/\S/ ) { $errors{name} = _('Please enter your name'); } elsif (length( $self->name ) < 5 || $self->name !~ m/\s/ || $self->name =~ m/\ba\s*n+on+((y|o)mo?u?s)?(ly)?\b/i ) { $errors{name} = _( 'Please enter your full name, councils need this information – if you do not wish your name to be shown on the site, untick the box below' ); } if ( $self->category && $self->category eq _('-- Pick a category --') ) { $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); } if ( $self->council && $self->detail && $self->council eq '2482' && length($self->detail) > 2000 ) { $errors{detail} = _('Reports are limited to 2000 characters in length. Please shorten your report'); } return \%errors; } =head2 confirm $bool = $problem->confirm( ); $problem->update; Set the state to 'confirmed' and put current time into 'confirmed' field. This is a no-op if the report is already confirmed. NOTE - does not update storage - call update or insert to do that. =cut sub confirm { my $self = shift; return if $self->state eq 'confirmed'; $self->state('confirmed'); $self->confirmed( \'ms_current_timestamp()' ); return 1; } =head2 councils Returns an arrayref of councils to which a report was sent. =cut sub councils { my $self = shift; return [] unless $self->council; (my $council = $self->council) =~ s/\|.*$//; my @council = split( /,/, $council ); return \@council; } =head2 url Returns a URL for this problem report. =cut sub url { my $self = shift; return "/report/" . $self->id; } =head2 get_photo_params Returns a hashref of details of any attached photo for use in templates. =cut sub get_photo_params { my $self = shift; return FixMyStreet::App::get_photo_params($self, 'id'); } =head2 is_open Returns 1 if the problem is in a open state otherwise 0. =cut sub is_open { my $self = shift; return exists $self->open_states->{ $self->state } ? 1 : 0; } =head2 is_fixed Returns 1 if the problem is in a fixed state otherwise 0. =cut sub is_fixed { my $self = shift; return exists $self->fixed_states->{ $self->state } ? 1 : 0; } =head2 is_closed Returns 1 if the problem is in a closed state otherwise 0. =cut sub is_closed { my $self = shift; return exists $self->closed_states->{ $self->state } ? 1 : 0; } =head2 is_visible Returns 1 if the problem should be displayed on the site otherwise 0. =cut sub is_visible { my $self = shift; return exists $self->visible_states->{ $self->state } ? 1 : 0; } =head2 meta_line Returns a string to be used on a problem report page, describing some of the meta data about the report. =cut sub meta_line { my ( $problem, $c ) = @_; my $date_time = Utils::prettify_epoch( $problem->confirmed_local->epoch ); my $meta = ''; # FIXME Should be in cobrand if ($c->cobrand->moniker eq 'emptyhomes') { my $category = _($problem->category); utf8::decode($category); if ($problem->anonymous) { $meta = sprintf(_('%s, reported anonymously at %s'), $category, $date_time); } else { $meta = sprintf(_('%s, reported by %s at %s'), $category, $problem->name, $date_time); } } else { if ( $problem->anonymous ) { if ( $problem->service and $problem->category && $problem->category ne _('Other') ) { $meta = sprintf( _('Reported by %s in the %s category anonymously at %s'), $problem->service, $problem->category, $date_time ); } elsif ( $problem->service ) { $meta = sprintf( _('Reported by %s anonymously at %s'), $problem->service, $date_time ); } elsif ( $problem->category and $problem->category ne _('Other') ) { $meta = sprintf( _('Reported in the %s category anonymously at %s'), $problem->category, $date_time ); } else { $meta = sprintf( _('Reported anonymously at %s'), $date_time ); } } else { if ( $problem->service and $problem->category && $problem->category ne _('Other') ) { $meta = sprintf( _('Reported by %s in the %s category by %s at %s'), $problem->service, $problem->category, $problem->name, $date_time ); } elsif ( $problem->service ) { $meta = sprintf( _('Reported by %s by %s at %s'), $problem->service, $problem->name, $date_time ); } elsif ( $problem->category and $problem->category ne _('Other') ) { $meta = sprintf( _('Reported in the %s category by %s at %s'), $problem->category, $problem->name, $date_time ); } else { $meta = sprintf( _('Reported by %s at %s'), $problem->name, $date_time ); } } } return $meta; } sub body { my ( $problem, $c ) = @_; my $body; if ($problem->external_body) { $body = $problem->external_body; } else { my $councils = $problem->councils; my $areas_info = mySociety::MaPit::call('areas', $councils); $body = join( _(' and '), map { my $name = $areas_info->{$_}->{name}; if (mySociety::Config::get('AREA_LINKS_FROM_PROBLEMS')) { '' . $name . ''; } else { $name; } } @$councils ); } return $body; } # returns true if the external id is the council's ref, i.e., useful to publish it # (by way of an example, the barnet send method returns a useful reference when # it succeeds, so that is the ref we should show on the problem report page). # Future: this is installation-dependent so maybe should be using the contact # data to determine if the external id is public on a council-by-council basis. # 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->send_method_used eq 'barnet' || $self->council =~ /2237/)) { return 1; } return 0; } # TODO Some/much of this could be moved to the template # either: # "sent to council 3 mins later" # "[Council name] ref: XYZ" # or # "sent to council 3 mins later, their ref: XYZ" # # Note: some silliness with pronouns and the adjacent comma mean this is # being presented as a single string rather than two sub processed_summary_string { my ( $problem, $c ) = @_; my ($duration_clause, $external_ref_clause); if ($problem->whensent) { $duration_clause = $problem->duration_string($c); } if ($problem->can_display_external_id) { if ($duration_clause) { $external_ref_clause = sprintf(_('council ref: %s'), $problem->external_id); } else { $external_ref_clause = sprintf(_('%s ref: %s'), $problem->external_body, $problem->external_id); } } if ($duration_clause and $external_ref_clause) { return "$duration_clause, $external_ref_clause" } else { return $duration_clause || $external_ref_clause } } sub duration_string { my ( $problem, $c ) = @_; my $body = $problem->body( $c ); return sprintf(_('Sent to %s %s later'), $body, Utils::prettify_duration($problem->whensent_local->epoch - $problem->confirmed_local->epoch, 'minute') ); } =head2 update_from_open311_service_request $p->update_from_open311_service_request( $request, $council_details, $system_user ); Updates the problem based on information in the passed in open311 request. If the request has an older update time than the problem's lastupdate time then nothing happens. Otherwise a comment will be created if there is status update text in the open311 request. If the open311 request has a state of closed then the problem will be marked as fixed. NB: a comment will always be created if the problem is being marked as fixed. Fixed problems will not be re-opened by this method. =cut sub update_from_open311_service_request { my ( $self, $request, $council_details, $system_user ) = @_; my ( $updated, $status_notes ); if ( ! ref $request->{updated_datetime} ) { $updated = $request->{updated_datetime}; } if ( ! ref $request->{status_notes} ) { $status_notes = $request->{status_notes}; } my $update = FixMyStreet::App->model('DB::Comment')->new( { problem_id => $self->id, state => 'confirmed', created => $updated || \'ms_current_timestamp()', confirmed => \'ms_current_timestamp()', text => $status_notes, mark_open => 0, mark_fixed => 0, user => $system_user, anonymous => 0, name => $council_details->{name}, } ); my $w3c = DateTime::Format::W3CDTF->new; my $req_time = $w3c->parse_datetime( $request->{updated_datetime} ); # set a timezone here as the $req_time will have one and if we don't # use a timezone then the date comparisons are invalid. # of course if local timezone is not the one that went into the data # base then we're also in trouble my $lastupdate = $self->lastupdate; $lastupdate->set_time_zone( DateTime::TimeZone->new( name => 'local' ) ); # update from open311 is older so skip if ( $req_time < $lastupdate ) { return 0; } if ( $request->{status} eq 'closed' ) { if ( $self->state ne 'fixed' ) { $self->state('fixed'); $update->mark_fixed(1); if ( !$status_notes ) { # FIXME - better text here $status_notes = _('Closed by council'); } } } if ( $status_notes ) { $update->text( $status_notes ); $self->lastupdate( $req_time ); $self->update; $update->insert; } return 1; } sub update_send_failed { my $self = shift; my $msg = shift; $self->update( { send_fail_count => $self->send_fail_count + 1, send_fail_timestamp => \'ms_current_timestamp()', send_fail_reason => $msg } ); } # we need the inline_constructor bit as we don't inherit from Moose __PACKAGE__->meta->make_immutable( inline_constructor => 0 ); 1;