aboutsummaryrefslogtreecommitdiffstats
path: root/perllib/FixMyStreet
diff options
context:
space:
mode:
Diffstat (limited to 'perllib/FixMyStreet')
-rw-r--r--perllib/FixMyStreet/App.pm90
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin.pm222
-rw-r--r--perllib/FixMyStreet/App/Controller/Around.pm7
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth.pm13
-rw-r--r--perllib/FixMyStreet/App/Controller/Contact.pm15
-rw-r--r--perllib/FixMyStreet/App/Controller/Dashboard.pm16
-rw-r--r--perllib/FixMyStreet/App/Controller/Moderate.pm4
-rw-r--r--perllib/FixMyStreet/App/Controller/My.pm14
-rw-r--r--perllib/FixMyStreet/App/Controller/Photo.pm163
-rwxr-xr-xperllib/FixMyStreet/App/Controller/Questionnaire.pm14
-rw-r--r--perllib/FixMyStreet/App/Controller/Report.pm19
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/New.pm45
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/Update.pm2
-rw-r--r--perllib/FixMyStreet/App/Controller/Reports.pm46
-rwxr-xr-xperllib/FixMyStreet/App/Controller/Rss.pm6
-rwxr-xr-xperllib/FixMyStreet/App/Controller/Status.pm70
-rw-r--r--perllib/FixMyStreet/App/Controller/Tokens.pm8
-rw-r--r--perllib/FixMyStreet/App/Model/PhotoSet.pm306
-rw-r--r--perllib/FixMyStreet/App/View/Web.pm20
-rw-r--r--perllib/FixMyStreet/Cobrand/Bromley.pm24
-rw-r--r--perllib/FixMyStreet/Cobrand/Default.pm68
-rw-r--r--perllib/FixMyStreet/Cobrand/EastSussex.pm15
-rw-r--r--perllib/FixMyStreet/Cobrand/Greenwich.pm5
-rw-r--r--perllib/FixMyStreet/Cobrand/Oxfordshire.pm5
-rw-r--r--perllib/FixMyStreet/Cobrand/SeeSomething.pm9
-rw-r--r--perllib/FixMyStreet/Cobrand/Stevenage.pm5
-rw-r--r--perllib/FixMyStreet/Cobrand/UKCouncils.pm32
-rw-r--r--perllib/FixMyStreet/Cobrand/Zurich.pm574
-rw-r--r--perllib/FixMyStreet/DB/Result/AdminLog.pm9
-rw-r--r--perllib/FixMyStreet/DB/Result/Alert.pm9
-rw-r--r--perllib/FixMyStreet/DB/Result/AlertSent.pm7
-rw-r--r--perllib/FixMyStreet/DB/Result/Body.pm22
-rw-r--r--perllib/FixMyStreet/DB/Result/Comment.pm9
-rw-r--r--perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm11
-rw-r--r--perllib/FixMyStreet/DB/Result/Problem.pm92
-rw-r--r--perllib/FixMyStreet/DB/Result/ResponseTemplate.pm49
-rw-r--r--perllib/FixMyStreet/DB/Result/Token.pm12
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/Alert.pm4
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/AlertType.pm32
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/Comment.pm34
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/Contact.pm4
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/Nearby.pm18
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/Problem.pm57
-rw-r--r--perllib/FixMyStreet/DB/ResultSet/Questionnaire.pm46
-rw-r--r--perllib/FixMyStreet/Email.pm12
-rw-r--r--perllib/FixMyStreet/Geocode/Google.pm28
-rw-r--r--perllib/FixMyStreet/Geocode/Zurich.pm11
-rw-r--r--perllib/FixMyStreet/Map.pm10
-rw-r--r--perllib/FixMyStreet/Map/Zurich.pm174
-rw-r--r--perllib/FixMyStreet/SendReport/Barnet.pm208
-rw-r--r--perllib/FixMyStreet/SendReport/Email.pm14
-rw-r--r--perllib/FixMyStreet/SendReport/Zurich.pm13
-rw-r--r--perllib/FixMyStreet/TestMech.pm34
53 files changed, 1939 insertions, 797 deletions
diff --git a/perllib/FixMyStreet/App.pm b/perllib/FixMyStreet/App.pm
index 787755a05..c9286b177 100644
--- a/perllib/FixMyStreet/App.pm
+++ b/perllib/FixMyStreet/App.pm
@@ -191,6 +191,8 @@ sub setup_request {
$c->log->debug( sprintf "Set lang to '%s' and cobrand to '%s'",
$set_lang, $cobrand->moniker );
+ $c->stash->{site_name} = Utils::trim_text($c->render_fragment('site-name.html'));
+
$c->model('DB::Problem')->set_restriction( $cobrand->site_key() );
Memcached::set_namespace( FixMyStreet->config('FMS_DB_NAME') . ":" );
@@ -205,7 +207,8 @@ sub setup_request {
# XXX Put in cobrand / do properly
if ($c->cobrand->moniker eq 'zurich') {
- FixMyStreet::DB::Result::Problem->visible_states_add_unconfirmed();
+ FixMyStreet::DB::Result::Problem->visible_states_add('unconfirmed');
+ FixMyStreet::DB::Result::Problem->visible_states_remove('investigating');
}
if (FixMyStreet->test_mode) {
@@ -316,15 +319,15 @@ sub send_email {
]
};
+ return if $c->is_abuser($vars->{to});
+
# render the template
my $content = $c->view('Email')->render( $c, $template, $vars );
# create an email - will parse headers out of content
my $email = Email::Simple->new($content);
- $email->header_set( ucfirst($_), $vars->{$_} )
- for grep { $vars->{$_} } qw( to from subject);
-
- return if $c->is_abuser( $email->header('To') );
+ $email->header_set( 'Subject', $vars->{subject} ) if $vars->{subject};
+ $email->header_set( 'Reply-To', $vars->{'Reply-To'} ) if $vars->{'Reply-To'};
$email->header_set( 'Message-ID', sprintf('<fms-%s-%s@%s>',
time(), unpack('h*', random_bytes(5, 1)), $c->config->{EMAIL_DOMAIN}
@@ -337,10 +340,16 @@ sub send_email {
_template_ => $email->body, # will get line wrapped
_parameters_ => {},
_line_indent => '',
+ From => $vars->{from},
+ To => $vars->{to},
$email->header_pairs
}
) };
+ if (my $attachments = $extra_stash_values->{attachments}) {
+ $email_text = munge_attachments($email_text, $attachments);
+ }
+
# send the email
$c->model('EmailSend')->send($email_text);
@@ -350,17 +359,14 @@ sub send_email {
sub send_email_cron {
my ( $c, $params, $env_from, $nomail, $cobrand, $lang_code ) = @_;
- my $first_to;
- if (ref($params->{To}) eq 'ARRAY') {
- if (ref($params->{To}[0]) eq 'ARRAY') {
- $first_to = $params->{To}[0][0];
- } else {
- $first_to = $params->{To}[0];
- }
- } else {
- $first_to = $params->{To};
+ my $sender = $c->config->{DO_NOT_REPLY_EMAIL};
+ $env_from ||= $sender;
+ if (!$params->{From}) {
+ my $sender_name = $cobrand->contact_name;
+ $params->{From} = [ $sender, _($sender_name) ];
}
- return 1 if $c->is_abuser($first_to);
+
+ return 1 if $c->is_abuser($params->{To});
$params->{'Message-ID'} = sprintf('<fms-cron-%s-%s@%s>', time(),
unpack('h*', random_bytes(5, 1)), FixMyStreet->config('EMAIL_DOMAIN')
@@ -394,8 +400,12 @@ sub send_email_cron {
$params->{_parameters_}->{site_name} = $site_name;
$params->{_line_indent} = '';
+ my $attachments = delete $params->{attachments};
+
my $email = mySociety::Locale::in_gb_locale { mySociety::Email::construct_email($params) };
+ $email = munge_attachments($email, $attachments) if $attachments;
+
if ($nomail) {
print $email;
return 1; # Failure
@@ -409,6 +419,44 @@ sub send_email_cron {
}
}
+sub munge_attachments {
+ my ($message, $attachments) = @_;
+ # $attachments should be an array_ref of things that can be parsed to Email::MIME,
+ # for example
+ # [
+ # body => $binary_data,
+ # attributes => {
+ # content_type => 'image/jpeg',
+ # encoding => 'base64',
+ # filename => '1234.1.jpeg',
+ # name => '1234.1.jpeg',
+ # },
+ # ...
+ # ]
+ #
+ # XXX: mySociety::Email::construct_email isn't using a MIME library and
+ # requires more analysis to refactor, so for now, we'll simply parse the
+ # generated MIME and add attachments.
+ #
+ # (Yes, this means that the email is constructed by Email::Simple, munged
+ # manually by custom code, turned back into Email::Simple, and then munged
+ # with Email::MIME. What's your point?)
+
+ require Email::MIME;
+ my $mime = Email::MIME->new($message);
+ $mime->parts_add([ map { Email::MIME->create(%$_)} @$attachments ]);
+ my $data = $mime->as_string;
+
+ # unsure why Email::MIME adds \r\n. Possibly mail client should handle
+ # gracefully, BUT perhaps as the segment constructed by
+ # mySociety::Email::construct_email strips to \n, they seem not to.
+ # So we re-run the same regexp here to the added part.
+ $data =~ s/\r\n/\n/gs;
+
+ return $data;
+}
+
+
=head2 uri_with
$uri = $c->uri_with( ... );
@@ -524,7 +572,17 @@ sub get_photo_params {
}
sub is_abuser {
- my ($c, $email) = @_;
+ my ($c, $to) = @_;
+ my $email;
+ if (ref($to) eq 'ARRAY') {
+ if (ref($to->[0]) eq 'ARRAY') {
+ $email = $to->[0][0];
+ } else {
+ $email = $to->[0];
+ }
+ } else {
+ $email = $to;
+ }
my ($domain) = $email =~ m{ @ (.*) \z }x;
return $c->model('DB::Abuse')->search( { email => [ $email, $domain ] } )->first;
}
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm
index 6145a6eb0..a61032988 100644
--- a/perllib/FixMyStreet/App/Controller/Admin.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin.pm
@@ -10,6 +10,7 @@ use Digest::SHA qw(sha1_hex);
use mySociety::EmailUtil qw(is_valid_email);
use if !$ENV{TRAVIS}, 'Image::Magick';
use DateTime::Format::Strptime;
+use List::Util 'first';
use FixMyStreet::SendReport;
@@ -70,8 +71,6 @@ sub index : Path : Args(0) {
return $c->cobrand->admin();
}
- my $site_restriction = $c->cobrand->site_restriction();
-
my $problems = $c->cobrand->problems->summary_count;
my %prob_counts =
@@ -85,7 +84,7 @@ sub index : Path : Args(0) {
for ( FixMyStreet::DB::Result::Problem->visible_states() );
$c->stash->{total_problems_users} = $c->cobrand->problems->unique_users;
- my $comments = $c->model('DB::Comment')->summary_count( $site_restriction );
+ my $comments = $c->model('DB::Comment')->summary_count( $c->cobrand->body_restriction );
my %comment_counts =
map { $_->state => $_->get_column('state_count') } $comments->all;
@@ -130,7 +129,9 @@ sub index : Path : Args(0) {
: _('n/a');
$c->stash->{questionnaires} = \%questionnaire_counts;
- $c->stash->{categories} = $c->cobrand->problems->categories_summary();
+ if ($c->get_param('show_categories')) {
+ $c->stash->{categories} = $c->cobrand->problems->categories_summary();
+ }
$c->stash->{total_bodies} = $c->model('DB::Body')->count();
@@ -150,7 +151,6 @@ sub config_page : Path( 'config' ) : Args(0) {
sub timeline : Path( 'timeline' ) : Args(0) {
my ($self, $c) = @_;
- my $site_restriction = $c->cobrand->site_restriction();
my %time;
$c->model('DB')->schema->storage->sql_maker->quote_char( '"' );
@@ -171,7 +171,7 @@ sub timeline : Path( 'timeline' ) : Args(0) {
push @{$time{$_->whenanswered->epoch}}, { type => 'quesAnswered', date => $_->whenanswered, obj => $_ } if $_->whenanswered;
}
- my $updates = $c->model('DB::Comment')->timeline( $site_restriction );
+ my $updates = $c->model('DB::Comment')->timeline( $c->cobrand->body_restriction );
foreach ($updates->all) {
push @{$time{$_->created->epoch}}, { type => 'update', date => $_->created, obj => $_} ;
@@ -359,13 +359,21 @@ sub update_contacts : Private {
$contact->deleted( $c->get_param('deleted') ? 1 : 0 );
$contact->non_public( $c->get_param('non_public') ? 1 : 0 );
$contact->note( $c->get_param('note') );
- $contact->whenedited( \'ms_current_timestamp()' );
+ $contact->whenedited( \'current_timestamp' );
$contact->editor( $editor );
$contact->endpoint( $c->get_param('endpoint') );
$contact->jurisdiction( $c->get_param('jurisdiction') );
$contact->api_key( $c->get_param('api_key') );
$contact->send_method( $c->get_param('send_method') );
+ # Set the photo_required flag in extra to the appropriate value
+ if ( $c->get_param('photo_required') ) {
+ $contact->set_extra_metadata_if_undefined( photo_required => 1 );
+ }
+ else {
+ $contact->unset_extra_metadata( 'photo_required' );
+ }
+
if ( %errors ) {
$c->stash->{updated} = _('Please correct the errors below');
$c->stash->{contact} = $contact;
@@ -395,7 +403,7 @@ sub update_contacts : Private {
$contacts->update(
{
confirmed => 1,
- whenedited => \'ms_current_timestamp()',
+ whenedited => \'current_timestamp',
note => 'Confirmed',
editor => $editor,
}
@@ -538,8 +546,6 @@ sub reports : Path('reports') {
if (my $search = $c->get_param('search')) {
$c->stash->{searched} = $search;
- my $site_restriction = $c->cobrand->site_restriction;
-
my $search_n = 0;
$search_n = int($search) if $search =~ /^\d+$/;
@@ -616,9 +622,10 @@ sub reports : Path('reports') {
}
if (@$query) {
- my $updates = $c->model('DB::Comment')->search(
+ my $updates = $c->model('DB::Comment')
+ ->to_body($c->cobrand->body_restriction)
+ ->search(
{
- %{ $site_restriction },
-or => $query,
},
{
@@ -650,8 +657,6 @@ sub reports : Path('reports') {
sub report_edit : Path('report_edit') : Args(1) {
my ( $self, $c, $id ) = @_;
- my $site_restriction = $c->cobrand->site_restriction;
-
my $problem = $c->cobrand->problems->search( { id => $id } )->first;
$c->detach( '/page_error_404_not_found' )
@@ -675,12 +680,21 @@ sub report_edit : Path('report_edit') : Args(1) {
type => 'big',
} ]
: [],
+ print_report => 1,
);
}
- if ( $c->get_param('rotate_photo') ) {
- $c->forward('rotate_photo');
- return 1;
+ if (my $rotate_photo_param = $self->_get_rotate_photo_param($c)) {
+ $self->rotate_photo($c, @$rotate_photo_param);
+ if ( $c->cobrand->moniker eq 'zurich' ) {
+ # Clicking the photo rotation buttons should do nothing
+ # except for rotating the photo, so return the user
+ # to the report screen now.
+ $c->res->redirect( $c->uri_for( 'report_edit', $problem->id ) );
+ return;
+ } else {
+ return 1;
+ }
}
if ( $c->cobrand->moniker eq 'zurich' ) {
@@ -707,7 +721,7 @@ sub report_edit : Path('report_edit') : Args(1) {
}
elsif ( $c->get_param('mark_sent') ) {
$c->forward('check_token');
- $problem->whensent(\'ms_current_timestamp()');
+ $problem->whensent(\'current_timestamp');
$problem->update();
$c->stash->{status_message} = '<p><em>' . _('That problem has been marked as sent.') . '</em></p>';
$c->forward( 'log_edit', [ $id, 'problem', 'marked sent' ] );
@@ -787,14 +801,14 @@ sub report_edit : Path('report_edit') : Args(1) {
}
if ( $problem->is_visible() and $old_state eq 'unconfirmed' ) {
- $problem->confirmed( \'ms_current_timestamp()' );
+ $problem->confirmed( \'current_timestamp' );
}
if ($done) {
$problem->discard_changes;
}
else {
- $problem->lastupdate( \'ms_current_timestamp()' ) if $edited || $new_state ne $old_state;
+ $problem->lastupdate( \'current_timestamp' ) if $edited || $new_state ne $old_state;
$problem->update;
if ( $new_state ne $old_state ) {
@@ -816,6 +830,85 @@ sub report_edit : Path('report_edit') : Args(1) {
return 1;
}
+sub templates : Path('templates') : Args(0) {
+ my ( $self, $c ) = @_;
+
+ $c->detach( '/page_error_404_not_found' )
+ unless $c->cobrand->moniker eq 'zurich';
+
+ my $user = $c->user;
+
+ $self->templates_for_body($c, $user->from_body );
+}
+
+sub templates_view : Path('templates') : Args(1) {
+ my ($self, $c, $body_id) = @_;
+
+ $c->detach( '/page_error_404_not_found' )
+ unless $c->cobrand->moniker eq 'zurich';
+
+ # e.g. for admin
+
+ my $body = $c->model('DB::Body')->find($body_id)
+ or $c->detach( '/page_error_404_not_found' );
+
+ $self->templates_for_body($c, $body);
+}
+
+sub template_edit : Path('templates') : Args(2) {
+ my ( $self, $c, $body_id, $template_id ) = @_;
+
+ $c->detach( '/page_error_404_not_found' )
+ unless $c->cobrand->moniker eq 'zurich';
+
+ my $body = $c->model('DB::Body')->find($body_id)
+ or $c->detach( '/page_error_404_not_found' );
+ $c->stash->{body} = $body;
+
+ my $template;
+ if ($template_id eq 'new') {
+ $template = $body->response_templates->new({});
+ }
+ else {
+ $template = $body->response_templates->find( $template_id )
+ or $c->detach( '/page_error_404_not_found' );
+ }
+
+ if ($c->req->method eq 'POST') {
+ if ($c->get_param('delete_template') eq _("Delete template")) {
+ $template->delete;
+ } else {
+ $template->title( $c->get_param('title') );
+ $template->text ( $c->get_param('text') );
+ $template->update_or_insert;
+ }
+
+ $c->res->redirect( $c->uri_for( 'templates', $body->id ) );
+ }
+
+ $c->stash->{response_template} = $template;
+
+ $c->stash->{template} = 'admin/template_edit.html';
+}
+
+
+sub templates_for_body {
+ my ( $self, $c, $body ) = @_;
+
+ $c->stash->{body} = $body;
+
+ my @templates = $body->response_templates->search(
+ undef,
+ {
+ order_by => 'title'
+ }
+ );
+
+ $c->stash->{response_templates} = \@templates;
+
+ $c->stash->{template} = 'admin/templates.html';
+}
+
sub users: Path('users') : Args(0) {
my ( $self, $c ) = @_;
@@ -874,13 +967,9 @@ sub users: Path('users') : Args(0) {
sub update_edit : Path('update_edit') : Args(1) {
my ( $self, $c, $id ) = @_;
- my $site_restriction = $c->cobrand->site_restriction;
- my $update = $c->model('DB::Comment')->search(
- {
- id => $id,
- %{$site_restriction},
- }
- )->first;
+ my $update = $c->model('DB::Comment')
+ ->to_body($c->cobrand->body_restriction)
+ ->search({ id => $id })->first;
$c->detach( '/page_error_404_not_found' )
unless $update;
@@ -943,10 +1032,10 @@ sub update_edit : Path('update_edit') : Args(1) {
}
if ( $new_state eq 'confirmed' and $old_state eq 'unconfirmed' ) {
- $update->confirmed( \'ms_current_timestamp()' );
+ $update->confirmed( \'current_timestamp' );
if ( $update->problem_state && $update->created > $update->problem->lastupdate ) {
$update->problem->state( $update->problem_state );
- $update->problem->lastupdate( \'ms_current_timestamp()' );
+ $update->problem->lastupdate( \'current_timestamp' );
$update->problem->update;
}
}
@@ -1064,7 +1153,7 @@ sub user_edit : Path('user_edit') : Args(1) {
sub flagged : Path('flagged') : Args(0) {
my ( $self, $c ) = @_;
- my $problems = $c->model('DB::Problem')->search( { flagged => 1 } );
+ my $problems = $c->cobrand->problems->search( { flagged => 1 } );
# pass in as array ref as using same template as search_reports
# which has to use an array ref for sql quoting reasons
@@ -1121,9 +1210,6 @@ sub stats : Path('stats') : Args(0) {
my $bymonth = $c->get_param('bymonth');
$c->stash->{bymonth} = $bymonth;
- my ( %body, %dates );
- $body{bodies_str} = { like => $c->get_param('body') }
- if $c->get_param('body');
$c->stash->{selected_body} = $c->get_param('body');
@@ -1154,14 +1240,12 @@ sub stats : Path('stats') : Args(0) {
);
}
- my $p = $c->model('DB::Problem')->search(
+ my $p = $c->cobrand->problems->to_body($c->get_param('body'))->search(
{
-AND => [
$field => { '>=', $start_date},
$field => { '<=', $end_date + $one_day },
],
- %body,
- %dates,
},
\%select,
);
@@ -1266,13 +1350,24 @@ Adds an entry into the admin_log table using the current user.
=cut
sub log_edit : Private {
- my ( $self, $c, $id, $object_type, $action ) = @_;
+ my ( $self, $c, $id, $object_type, $action, $time_spent ) = @_;
+
+ $time_spent //= 0;
+ $time_spent = 0 if $time_spent < 0;
+
+ my $user_object = do {
+ my $auth_user = $c->user;
+ $auth_user ? $auth_user->get_object : undef;
+ };
+
$c->model('DB::AdminLog')->create(
{
admin_user => $c->forward('get_user'),
+ $user_object ? ( user => $user_object ) : (), # as (rel => undef) doesn't work
object_type => $object_type,
action => $action,
object_id => $id,
+ time_spent => $time_spent,
}
)->insert();
}
@@ -1385,36 +1480,27 @@ Rotate a photo 90 degrees left or right
=cut
+# returns index of photo to rotate, if any
+sub _get_rotate_photo_param {
+ my ($self, $c) = @_;
+ my $key = first { /^rotate_photo/ } keys %{ $c->req->params } or return;
+ my ($index) = $key =~ /(\d+)$/;
+ my $direction = $c->get_param($key);
+ return [ $index || 0, $key, $direction ];
+}
+
sub rotate_photo : Private {
- my ( $self, $c ) =@_;
+ my ( $self, $c, $index, $key, $direction ) = @_;
- my $direction = $c->get_param('rotate_photo');
return unless $direction eq _('Rotate Left') or $direction eq _('Rotate Right');
- my $photo = $c->stash->{problem}->photo;
- my $file;
+ my $problem = $c->stash->{problem};
+ my $fileid = $problem->get_photoset($c)->rotate_image(
+ $index,
+ $direction eq _('Rotate Left') ? -90 : 90
+ ) or return;
- # If photo field contains a hash
- if ( length($photo) == 40 ) {
- $file = file( $c->config->{UPLOAD_DIR}, "$photo.jpeg" );
- $photo = $file->slurp;
- }
-
- $photo = _rotate_image( $photo, $direction eq _('Rotate Left') ? -90 : 90 );
- return unless $photo;
-
- # Write out to new location
- my $fileid = sha1_hex($photo);
- $file = file( $c->config->{UPLOAD_DIR}, "$fileid.jpeg" );
-
- my $fh = $file->open('w');
- print $fh $photo;
- close $fh;
-
- unlink glob FixMyStreet->path_to( 'web', 'photo', $c->stash->{problem}->id . '.*' );
-
- $c->stash->{problem}->photo( $fileid );
- $c->stash->{problem}->update();
+ $problem->update({ photo => $fileid });
return 1;
}
@@ -1464,18 +1550,6 @@ sub trim {
return $e;
}
-sub _rotate_image {
- my ($photo, $direction) = @_;
- my $image = Image::Magick->new;
- $image->BlobToImage($photo);
- my $err = $image->Rotate($direction);
- return 0 if $err;
- my @blobs = $image->ImageToBlob();
- undef $image;
- return $blobs[0];
-}
-
-
=head1 AUTHOR
Struan Donald
diff --git a/perllib/FixMyStreet/App/Controller/Around.pm b/perllib/FixMyStreet/App/Controller/Around.pm
index 723684793..4aa695ae5 100644
--- a/perllib/FixMyStreet/App/Controller/Around.pm
+++ b/perllib/FixMyStreet/App/Controller/Around.pm
@@ -28,7 +28,7 @@ If no search redirect back to the homepage.
=cut
-sub around_index : Path : Args(0) {
+sub index : Path : Args(0) {
my ( $self, $c ) = @_;
# handle old coord systems
@@ -302,15 +302,10 @@ sub ajax : Path('/ajax') {
'around/on_map_list_items.html',
{ on_map => $on_map, around_map => $around_map }
);
- my $around_map_list_html = $c->render_fragment(
- 'around/around_map_list_items.html',
- { on_map => $on_map, around_map => $around_map }
- );
# JSON encode the response
my $json = { pins => $pins };
$json->{current} = $on_map_list_html if $on_map_list_html;
- $json->{current_near} = $around_map_list_html if $around_map_list_html;
my $body = JSON->new->utf8(1)->encode($json);
$c->res->body($body);
}
diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm
index 63bf91ff5..6de416c53 100644
--- a/perllib/FixMyStreet/App/Controller/Auth.pm
+++ b/perllib/FixMyStreet/App/Controller/Auth.pm
@@ -36,16 +36,23 @@ sub general : Path : Args(0) {
return unless $c->req->method eq 'POST';
# decide which action to take
- my $has_password = $c->get_param('sign_in') || $c->get_param('password_sign_in');
- my $has_email = $c->get_param('email_sign_in') || $c->get_param('name') || $c->get_param('password_register');
+ my $clicked_password = $c->get_param('sign_in');
+ my $clicked_email = $c->get_param('email_sign_in');
+ my $data_password = $c->get_param('password_sign_in');
+ my $data_email = $c->get_param('name') || $c->get_param('password_register');
- $c->detach('email_sign_in') if $has_email && !$has_password;
+ $c->detach('email_sign_in') if $clicked_email || ($data_email && !$data_password);
$c->forward( 'sign_in' )
&& $c->detach( 'redirect_on_signin', [ $c->get_param('r') ] );
}
+sub general_test : Path('_test_') : Args(0) {
+ my ( $self, $c ) = @_;
+ $c->stash->{template} = 'auth/token.html';
+}
+
=head2 sign_in
Allow the user to sign in with a username and a password.
diff --git a/perllib/FixMyStreet/App/Controller/Contact.pm b/perllib/FixMyStreet/App/Controller/Contact.pm
index 912224649..115f4e3d2 100644
--- a/perllib/FixMyStreet/App/Controller/Contact.pm
+++ b/perllib/FixMyStreet/App/Controller/Contact.pm
@@ -5,6 +5,7 @@ use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller'; }
use mySociety::EmailUtil;
+use FixMyStreet::Email;
=head1 NAME
@@ -239,11 +240,19 @@ sub send_email : Private {
? ' ( forwarded from ' . $c->req->header('X-Forwarded-For') . ' )'
: '';
- $c->send_email( 'contact.txt', {
+ my $from = [ $c->stash->{em}, $c->stash->{form_name} ];
+ my $params = {
to => [ [ $recipient, _($recipient_name) ] ],
- from => [ $c->stash->{em}, $c->stash->{form_name} ],
subject => 'FMS message: ' . $c->stash->{subject},
- });
+ };
+ if (FixMyStreet::Email::test_dmarc($c->stash->{em})) {
+ $params->{'Reply-To'} = [ $from ];
+ $params->{from} = [ $recipient, $c->stash->{form_name} ];
+ } else {
+ $params->{from} = $from;
+ }
+
+ $c->send_email('contact.txt', $params);
# above is always succesful :(
$c->stash->{success} = 1;
diff --git a/perllib/FixMyStreet/App/Controller/Dashboard.pm b/perllib/FixMyStreet/App/Controller/Dashboard.pm
index c3aa35008..faddaa89e 100644
--- a/perllib/FixMyStreet/App/Controller/Dashboard.pm
+++ b/perllib/FixMyStreet/App/Controller/Dashboard.pm
@@ -89,6 +89,7 @@ sub index : Path : Args(0) {
my ( $self, $c ) = @_;
my $body = $c->forward('check_page_allowed');
+ $c->stash->{body} = $body;
# Set up the data for the dropdowns
@@ -112,7 +113,6 @@ sub index : Path : Args(0) {
$c->stash->{category} = $c->get_param('category');
my %where = (
- bodies_str => $body->id, # XXX Does this break in a two tier council? Restriction needs looking at...
'problem.state' => [ FixMyStreet::DB::Result::Problem->visible_states() ],
);
$where{areas} = { 'like', '%,' . $c->stash->{ward} . ',%' }
@@ -155,7 +155,7 @@ sub index : Path : Args(0) {
%$prob_where,
'me.confirmed' => { '>=', $dtf->format_datetime( $now->clone->subtract( days => 30 ) ) },
};
- my $problems_rs = $c->cobrand->problems->search( $params );
+ my $problems_rs = $c->cobrand->problems->to_body($body)->search( $params );
my @problems = $problems_rs->all;
my %problems;
@@ -270,12 +270,14 @@ sub export_as_csv {
sub updates_search : Private {
my ( $self, $c, $time ) = @_;
+ my $body = $c->stash->{body};
+
my $params = {
%{$c->stash->{where}},
'me.confirmed' => { '>=', $time },
};
- my $comments = $c->model('DB::Comment')->search(
+ my $comments = $c->model('DB::Comment')->to_body($body)->search(
$params,
{
group_by => [ 'problem_state' ],
@@ -302,7 +304,7 @@ sub updates_search : Private {
my $col = shift @$vars;
my $substmt = "select min(id) from comment where me.problem_id=comment.problem_id and problem_state in ('"
. join("','", @$vars) . "')";
- $comments = $c->model('DB::Comment')->search(
+ $comments = $c->model('DB::Comment')->to_body($body)->search(
{ %$params,
problem_state => $vars,
'me.id' => \"= ($substmt)",
@@ -319,7 +321,7 @@ sub updates_search : Private {
$counts{$col} = int( ($comments->get_column('time')||0) / 60 / 60 / 24 + 0.5 );
}
- $counts{fixed_user} = $c->model('DB::Comment')->search(
+ $counts{fixed_user} = $c->model('DB::Comment')->to_body($body)->search(
{ %$params, mark_fixed => 1, problem_state => undef }, { join => 'problem' }
)->count;
@@ -327,7 +329,7 @@ sub updates_search : Private {
%{$c->stash->{prob_where}},
'me.confirmed' => { '>=', $time },
};
- $counts{total} = $c->cobrand->problems->search( $params )->count;
+ $counts{total} = $c->cobrand->problems->to_body($body)->search( $params )->count;
$params = {
%{$c->stash->{prob_where}},
@@ -335,7 +337,7 @@ sub updates_search : Private {
state => 'confirmed',
'(select min(id) from comment where me.id=problem_id and problem_state is not null)' => undef,
};
- $counts{not_marked} = $c->cobrand->problems->search( $params )->count;
+ $counts{not_marked} = $c->cobrand->problems->to_body($body)->search( $params )->count;
return \%counts;
}
diff --git a/perllib/FixMyStreet/App/Controller/Moderate.pm b/perllib/FixMyStreet/App/Controller/Moderate.pm
index 08c4280a1..77a3346dc 100644
--- a/perllib/FixMyStreet/App/Controller/Moderate.pm
+++ b/perllib/FixMyStreet/App/Controller/Moderate.pm
@@ -102,9 +102,6 @@ sub report_moderate_audit : Private {
my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($problem->cobrand)->new();
- my $sender = FixMyStreet->config('DO_NOT_REPLY_EMAIL');
- my $sender_name = _($cobrand->contact_name);
-
my $token = $c->model("DB::Token")->create({
scope => 'moderation',
data => { id => $problem->id }
@@ -113,7 +110,6 @@ sub report_moderate_audit : Private {
$c->send_email( 'problem-moderated.txt', {
to => [ [ $user->email, $user->name ] ],
- from => [ $sender, $sender_name ],
types => $types_csv,
user => $user,
problem => $problem,
diff --git a/perllib/FixMyStreet/App/Controller/My.pm b/perllib/FixMyStreet/App/Controller/My.pm
index 83d5f7adb..3c4ce2cf7 100644
--- a/perllib/FixMyStreet/App/Controller/My.pm
+++ b/perllib/FixMyStreet/App/Controller/My.pm
@@ -31,16 +31,12 @@ sub my : Path : Args(0) {
$c->forward( '/reports/stash_report_filter_status' );
my $pins = [];
- my $problems = {};
+ my $problems = [];
my $states = $c->stash->{filter_problem_states};
my $params = {
state => [ keys %$states ],
};
- $params = {
- %{ $c->cobrand->problems_clause },
- %$params
- } if $c->cobrand->problems_clause;
my $category = $c->get_param('filter_category');
if ( $category ) {
@@ -48,7 +44,9 @@ sub my : Path : Args(0) {
$c->stash->{filter_category} = $category;
}
- my $rs = $c->user->problems->search( $params, {
+ my $rs = $c->user->problems
+ ->to_body($c->cobrand->body_restriction)
+ ->search( $params, {
order_by => { -desc => 'confirmed' },
rows => 50
} )->page( $p_page );
@@ -62,9 +60,7 @@ sub my : Path : Args(0) {
id => $problem->id,
title => $problem->title,
};
- my $state = $problem->is_fixed ? 'fixed' : $problem->is_closed ? 'closed' : 'confirmed';
- push @{ $problems->{$state} }, $problem;
- push @{ $problems->{all} }, $problem;
+ push @$problems, $problem;
}
$c->stash->{problems_pager} = $rs->pager;
$c->stash->{problems} = $problems;
diff --git a/perllib/FixMyStreet/App/Controller/Photo.pm b/perllib/FixMyStreet/App/Controller/Photo.pm
index a2ec7d4c8..bc72f4bfb 100644
--- a/perllib/FixMyStreet/App/Controller/Photo.pm
+++ b/perllib/FixMyStreet/App/Controller/Photo.pm
@@ -8,8 +8,8 @@ use DateTime::Format::HTTP;
use Digest::SHA qw(sha1_hex);
use File::Path;
use File::Slurp;
-use Image::Size;
use Path::Class;
+use FixMyStreet::App::Model::PhotoSet;
use if !$ENV{TRAVIS}, 'Image::Magick';
=head1 NAME
@@ -49,13 +49,13 @@ sub during :LocalRegex('^([0-9a-f]{40})\.(temp|fulltemp)\.jpeg$') {
$c->forward( 'output', [ $photo ] );
}
-sub index :LocalRegex('^(c/)?(\d+)(?:\.(full|tn|fp))?\.jpeg$') {
+sub index :LocalRegex('^(c/)?(\d+)(?:\.(\d+))?(?:\.(full|tn|fp))?\.jpeg$') {
my ( $self, $c ) = @_;
- my ( $is_update, $id, $size ) = @{ $c->req->captures };
+ my ( $is_update, $id, $photo_number, $size ) = @{ $c->req->captures };
- my @photo;
+ my $item;
if ( $is_update ) {
- @photo = $c->model('DB::Comment')->search( {
+ ($item) = $c->model('DB::Comment')->search( {
id => $id,
state => 'confirmed',
photo => { '!=', undef },
@@ -67,34 +67,40 @@ sub index :LocalRegex('^(c/)?(\d+)(?:\.(full|tn|fp))?\.jpeg$') {
}
$c->detach( 'no_photo' ) if $id =~ /\D/;
- @photo = $c->cobrand->problems->search( {
+ ($item) = $c->cobrand->problems->search( {
id => $id,
state => [ FixMyStreet::DB::Result::Problem->visible_states(), 'partial' ],
photo => { '!=', undef },
} );
}
- $c->detach( 'no_photo' ) unless @photo;
+ $c->detach( 'no_photo' ) unless $item;
- my $item = $photo[0];
$c->detach( 'no_photo' ) unless $c->cobrand->allow_photo_display($item); # Should only be for reports, not updates
- my $photo = $item->photo;
- # If photo field contains a hash
- if (length($photo) == 40) {
- my $file = file( $c->config->{UPLOAD_DIR}, "$photo.jpeg" );
- $photo = $file->slurp;
- }
-
- if ( $size eq 'tn' ) {
- $photo = _shrink( $photo, 'x100' );
- } elsif ( $size eq 'fp' ) {
- $photo = _crop( $photo );
- } elsif ( $size eq 'full' ) {
- } elsif ( $c->cobrand->default_photo_resize ) {
- $photo = _shrink( $photo, $c->cobrand->default_photo_resize );
+ my $photo;
+ if ($item->can('get_photoset')) {
+ $photo = $item->get_photoset( $c )
+ ->get_image_data( num => $photo_number, size => $size )
+ or $c->detach( 'no_photo' );
} else {
- $photo = _shrink( $photo, '250x250' );
+ $photo = $item->photo;
+ # If photo field contains a hash
+ if (length($photo) == 40) {
+ my $file = file( $c->config->{UPLOAD_DIR}, "$photo.jpeg" );
+ $photo = $file->slurp;
+ }
+
+ if ( $size eq 'tn' ) {
+ $photo = _shrink( $photo, 'x100' );
+ } elsif ( $size eq 'fp' ) {
+ $photo = _crop( $photo );
+ } elsif ( $size eq 'full' ) {
+ } elsif ( $c->cobrand->default_photo_resize ) {
+ $photo = _shrink( $photo, $c->cobrand->default_photo_resize );
+ } else {
+ $photo = _shrink( $photo, '250x250' );
+ }
}
$c->forward( 'output', [ $photo ] );
@@ -156,84 +162,67 @@ sub process_photo : Private {
my ( $self, $c ) = @_;
return
- $c->forward('process_photo_upload')
- || $c->forward('process_photo_cache')
+ $c->forward('process_photo_upload_or_cache')
+ || $c->forward('process_photo_required')
|| 1; # always return true
}
-sub process_photo_upload : Private {
+sub process_photo_upload_or_cache : Private {
my ( $self, $c ) = @_;
-
- # check for upload or return
- my $upload = $c->req->upload('photo')
- || return;
-
- # check that the photo is a jpeg
- my $ct = $upload->type;
- $ct =~ s/x-citrix-//; # Thanks, Citrix
- # Had a report of a JPEG from an Android 2.1 coming through as a byte stream
- unless ( $ct eq 'image/jpeg' || $ct eq 'image/pjpeg' || $ct eq 'application/octet-stream' ) {
- $c->log->info('Bad photo tried to upload, type=' . $ct);
- $c->stash->{photo_error} = _('Please upload a JPEG image only');
- return;
- }
-
- # get the photo into a variable
- my $photo_blob = eval {
- my $filename = $upload->tempname;
- my $out = `jhead -se -autorot $filename 2>&1`;
- unless (defined $out) {
- my ($w, $h, $err) = Image::Size::imgsize($filename);
- die _("Please upload a JPEG image only") . "\n" if !defined $w || $err ne 'JPG';
- }
- die _("Please upload a JPEG image only") . "\n" if $out && $out =~ /Not JPEG:/;
- my $photo = $upload->slurp;
- return $photo;
- };
- if ( my $error = $@ ) {
- my $format = _(
-"That image doesn't appear to have uploaded correctly (%s), please try again."
- );
- $c->stash->{photo_error} = sprintf( $format, $error );
- return;
- }
-
- # we have an image we can use - save it to the upload dir for storage
- my $cache_dir = dir( $c->config->{UPLOAD_DIR} );
- $cache_dir->mkpath;
- unless ( -d $cache_dir && -w $cache_dir ) {
- warn "Can't find/write to photo cache directory '$cache_dir'";
- return;
- }
-
- my $fileid = sha1_hex($photo_blob);
- $upload->copy_to( file($cache_dir, $fileid . '.jpeg') );
-
- # stick the hash on the stash, so don't have to reupload in case of error
- $c->stash->{upload_fileid} = $fileid;
-
+ my @items = (
+ ( map {
+ /^photo/ ? # photo, photo1, photo2 etc.
+ ($c->req->upload($_)) : ()
+ } sort $c->req->upload),
+ split /,/, ($c->get_param('upload_fileid') || '')
+ );
+
+ my $photoset = FixMyStreet::App::Model::PhotoSet->new({
+ c => $c,
+ data_items => \@items,
+ });
+
+ my $fileid = $photoset->data;
+
+ $c->stash->{upload_fileid} = $fileid or return;
return 1;
}
-=head2 process_photo_cache
+=head2 process_photo_required
+
+Checks that a report has a photo attached if any of its Contacts
+require it (by setting extra->photo_required == 1). Puts an error in
+photo_error on the stash if it's required and missing, otherwise returns
+true.
-Look for the upload_fileid parameter and check it matches a file on disk. If it
-does return true and put fileid on stash, otherwise false.
+(Note that as we have reached this action, we *know* that the photo
+is missing, otherwise it would have already been handled.)
=cut
-sub process_photo_cache : Private {
+sub process_photo_required : Private {
my ( $self, $c ) = @_;
- # get the fileid and make sure it is just a hex number
- my $fileid = $c->get_param('upload_fileid') || '';
- $fileid =~ s{[^0-9a-f]}{}gi;
- return unless $fileid;
-
- my $file = file( $c->config->{UPLOAD_DIR}, "$fileid.jpeg" );
- return unless -e $file;
+ # load the report
+ my $report = $c->stash->{report} or return 1; # don't check photo for updates
+ my $bodies = $c->stash->{bodies};
+
+ my @contacts = $c-> #
+ model('DB::Contact') #
+ ->not_deleted #
+ ->search(
+ {
+ body_id => [ keys %$bodies ],
+ category => $report->category
+ }
+ )->all;
+ foreach my $contact ( @contacts ) {
+ if ( $contact->get_extra_metadata('photo_required') ) {
+ $c->stash->{photo_error} = _("Photo is required.");
+ return;
+ }
+ }
- $c->stash->{upload_fileid} = $fileid;
return 1;
}
diff --git a/perllib/FixMyStreet/App/Controller/Questionnaire.pm b/perllib/FixMyStreet/App/Controller/Questionnaire.pm
index f9a08e408..8fe2514c0 100755
--- a/perllib/FixMyStreet/App/Controller/Questionnaire.pm
+++ b/perllib/FixMyStreet/App/Controller/Questionnaire.pm
@@ -142,8 +142,8 @@ sub submit_creator_fixed : Private {
$questionnaire->ever_reported( $c->stash->{reported} eq 'Yes' ? 1 : 0 );
$questionnaire->old_state( $old_state );
- $questionnaire->whensent( \'ms_current_timestamp()' );
- $questionnaire->whenanswered( \'ms_current_timestamp()' );
+ $questionnaire->whensent( \'current_timestamp' );
+ $questionnaire->whenanswered( \'current_timestamp' );
$questionnaire->insert;
}
@@ -173,13 +173,13 @@ sub submit_standard : Private {
# Record state change, if there was one
if ( $new_state ) {
$problem->state( $new_state );
- $problem->lastupdate( \'ms_current_timestamp()' );
+ $problem->lastupdate( \'current_timestamp' );
}
# If it's not fixed and they say it's still not been fixed, record time update
if ( $c->stash->{been_fixed} eq 'No' &&
FixMyStreet::DB::Result::Problem->open_states->{$old_state} ) {
- $problem->lastupdate( \'ms_current_timestamp()' );
+ $problem->lastupdate( \'current_timestamp' );
}
# Record questionnaire response
@@ -189,7 +189,7 @@ sub submit_standard : Private {
my $q = $c->stash->{questionnaire};
$q->update( {
- whenanswered => \'ms_current_timestamp()',
+ whenanswered => \'current_timestamp',
ever_reported => $reported,
old_state => $old_state,
new_state => $c->stash->{been_fixed} eq 'Unknown' ? 'unknown' : ($new_state || $old_state),
@@ -210,7 +210,7 @@ sub submit_standard : Private {
lang => $c->stash->{lang_code},
cobrand => $c->cobrand->moniker,
cobrand_data => '',
- confirmed => \'ms_current_timestamp()',
+ confirmed => \'current_timestamp',
anonymous => $problem->anonymous,
}
);
@@ -234,6 +234,8 @@ sub process_questionnaire : Private {
map { $c->stash->{$_} = $c->get_param($_) || '' } qw(been_fixed reported another update);
+ $c->stash->{update} = Utils::cleanup_text($c->stash->{update}, { allow_multiline => 1 });
+
# EHA questionnaires done for you
if ($c->cobrand->moniker eq 'emptyhomes') {
$c->stash->{another} = $c->stash->{num_questionnaire}==1 ? 'Yes' : 'No';
diff --git a/perllib/FixMyStreet/App/Controller/Report.pm b/perllib/FixMyStreet/App/Controller/Report.pm
index 7b001ee4c..d7cae05d4 100644
--- a/perllib/FixMyStreet/App/Controller/Report.pm
+++ b/perllib/FixMyStreet/App/Controller/Report.pm
@@ -99,23 +99,30 @@ sub load_problem_or_display_error : Private {
my $problem
= ( !$id || $id =~ m{\D} ) # is id non-numeric?
? undef # ...don't even search
- : $c->cobrand->problems->find( { id => $id } );
+ : $c->cobrand->problems->find( { id => $id } )
+ or $c->detach( '/page_error_404_not_found', [ _('Unknown problem ID') ] );
# check that the problem is suitable to show.
- if ( !$problem || ($problem->state eq 'unconfirmed' && !$c->cobrand->show_unconfirmed_reports) || $problem->state eq 'partial' ) {
+ # hidden_states includes partial and unconfirmed, but they have specific handling,
+ # so we check for them first.
+ if ( $problem->state eq 'partial' ) {
$c->detach( '/page_error_404_not_found', [ _('Unknown problem ID') ] );
}
- elsif ( $problem->state eq 'hidden' ) {
+ elsif ( $problem->state eq 'unconfirmed' ) {
+ $c->detach( '/page_error_404_not_found', [ _('Unknown problem ID') ] )
+ unless $c->cobrand->show_unconfirmed_reports ;
+ }
+ elsif ( $problem->hidden_states->{ $problem->state } or
+ (($problem->get_extra_metadata('closure_status')||'') eq 'hidden')) {
$c->detach(
'/page_error_410_gone',
[ _('That report has been removed from FixMyStreet.') ] #
);
} elsif ( $problem->non_public ) {
if ( !$c->user || $c->user->id != $problem->user->id ) {
- my $site_name = Utils::trim_text($c->render_fragment('site-name.html'));
$c->detach(
'/page_error_403_access_denied',
- [ sprintf(_('That report cannot be viewed on %s.'), $site_name) ] #
+ [ sprintf(_('That report cannot be viewed on %s.'), $c->stash->{site_name}) ]
);
}
}
@@ -242,7 +249,7 @@ sub delete :Local :Args(1) {
return $c->res->redirect($uri) unless $p->bodies->{$body->id};
$p->state('hidden');
- $p->lastupdate( \'ms_current_timestamp()' );
+ $p->lastupdate( \'current_timestamp' );
$p->update;
$c->model('DB::AdminLog')->create( {
diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm
index b540a1961..246facbee 100644
--- a/perllib/FixMyStreet/App/Controller/Report/New.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/New.pm
@@ -93,6 +93,7 @@ sub report_new : Path : Args(0) {
$c->forward('check_for_category');
# deal with the user and report and check both are happy
+
return unless $c->forward('check_form_submitted');
$c->forward('process_user');
$c->forward('process_report');
@@ -106,6 +107,12 @@ sub report_new : Path : Args(0) {
# report_new but there's a few workflow differences as we only ever want
# to sent JSON back here
+sub report_new_test : Path('_test_') : Args(0) {
+ my ( $self, $c ) = @_;
+ $c->stash->{template} = 'email_sent.html';
+ $c->stash->{email_type} = $c->get_param('email_type');
+}
+
sub report_new_ajax : Path('mobile') : Args(0) {
my ( $self, $c ) = @_;
@@ -284,7 +291,7 @@ sub report_import : Path('/import') {
}
# handle the photo upload
- $c->forward( '/photo/process_photo_upload' );
+ $c->forward( '/photo/process_photo' );
my $fileid = $c->stash->{upload_fileid};
if ( my $error = $c->stash->{photo_error} ) {
push @errors, $error;
@@ -883,13 +890,29 @@ sub process_report : Private {
$report->bodies_str(-1);
} else {
# construct the bodies string:
- # 'x,x' - x are body IDs that have this category
- # 'x,x|y' - x are body IDs that have this category, y body IDs with *no* contact
- my $body_string = join( ',', map { $_->body_id } @contacts );
- $body_string .=
- '|' . join( ',', map { $_->id } @{ $c->stash->{missing_details_bodies} } )
- if $body_string && @{ $c->stash->{missing_details_bodies} };
+ my $body_string = do {
+ if ( $c->cobrand->can('singleton_bodies_str') && $c->cobrand->singleton_bodies_str ) {
+ # Cobrands like Zurich can only ever have a single body: 'x', because some functionality
+ # relies on string comparison against bodies_str.
+ if (@contacts) {
+ $contacts[0]->body_id;
+ }
+ else {
+ '';
+ }
+ }
+ else {
+ # 'x,x' - x are body IDs that have this category
+ my $bs = join( ',', map { $_->body_id } @contacts );
+ $bs;
+ };
+ };
$report->bodies_str($body_string);
+ # Record any body IDs which might have meant to match, but had no contact
+ if ($body_string && @{ $c->stash->{missing_details_bodies} }) {
+ my $missing = join( ',', map { $_->id } @{ $c->stash->{missing_details_bodies} } );
+ $report->bodies_missing($missing);
+ }
}
my @extra;
@@ -1142,6 +1165,9 @@ sub redirect_or_confirm_creation : Private {
return 1;
}
+ my $template = 'problem-confirm.txt';
+ $template = 'problem-confirm-not-sending.txt' unless $report->bodies_str;
+
# otherwise create a confirm token and email it to them.
my $data = $c->stash->{token_data} || {};
my $token = $c->model("DB::Token")->create( {
@@ -1152,7 +1178,10 @@ sub redirect_or_confirm_creation : Private {
}
} );
$c->stash->{token_url} = $c->uri_for_email( '/P', $token->token );
- $c->send_email( 'problem-confirm.txt', {
+ if ($c->cobrand->can('problem_confirm_email_extras')) {
+ $c->cobrand->problem_confirm_email_extras($report);
+ }
+ $c->send_email( $template, {
to => [ $report->name ? [ $report->user->email, $report->name ] : $report->user->email ],
} );
diff --git a/perllib/FixMyStreet/App/Controller/Report/Update.pm b/perllib/FixMyStreet/App/Controller/Report/Update.pm
index 17aec2113..445723fec 100644
--- a/perllib/FixMyStreet/App/Controller/Report/Update.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/Update.pm
@@ -80,7 +80,7 @@ sub update_problem : Private {
$problem->interest_count( \'interest_count + 1' );
}
- $problem->lastupdate( \'ms_current_timestamp()' );
+ $problem->lastupdate( \'current_timestamp' );
$problem->update;
$c->stash->{problem_id} = $problem->id;
diff --git a/perllib/FixMyStreet/App/Controller/Reports.pm b/perllib/FixMyStreet/App/Controller/Reports.pm
index 6b0d516a6..407fc625e 100644
--- a/perllib/FixMyStreet/App/Controller/Reports.pm
+++ b/perllib/FixMyStreet/App/Controller/Reports.pm
@@ -33,6 +33,7 @@ sub index : Path : Args(0) {
# Zurich goes straight to map page, with all reports
if ( $c->cobrand->moniker eq 'zurich' ) {
+ $c->forward( 'stash_report_filter_status' );
$c->forward( 'load_and_group_problems' );
my $pins = $c->stash->{pins};
$c->stash->{page} = 'reports';
@@ -121,7 +122,7 @@ sub ward : Path : Args(2) {
$c->stash->{stats} = $c->cobrand->get_report_stats();
- my @categories = $c->stash->{body}->contacts->search( undef, {
+ my @categories = $c->stash->{body}->contacts->not_deleted->search( undef, {
columns => [ 'category' ],
distinct => 1,
order_by => [ 'category' ],
@@ -269,9 +270,7 @@ sub rss_ward : Path('/rss/reports') : Args(2) {
# Problems sent to a council
$c->stash->{type} = 'council_problems';
$c->stash->{title_params} = { COUNCIL => $c->stash->{body}->name };
- # XXX This looks up in both bodies_str and areas, but is only using body ID.
- # This will not work properly in any install where body IDs are not === area IDs.
- $c->stash->{db_params} = [ $c->stash->{body}->id, $c->stash->{body}->id ];
+ $c->stash->{db_params} = [ $c->stash->{body}->id ];
}
# Send on to the RSS generation
@@ -396,20 +395,20 @@ sub load_and_group_problems : Private {
my $not_open = [ FixMyStreet::DB::Result::Problem::fixed_states(), FixMyStreet::DB::Result::Problem::closed_states() ];
if ( $type eq 'new' ) {
- $where->{confirmed} = { '>', \"ms_current_timestamp() - INTERVAL '4 week'" };
+ $where->{confirmed} = { '>', \"current_timestamp - INTERVAL '4 week'" };
$where->{state} = { 'IN', [ FixMyStreet::DB::Result::Problem::open_states() ] };
} elsif ( $type eq 'older' ) {
- $where->{confirmed} = { '<', \"ms_current_timestamp() - INTERVAL '4 week'" };
- $where->{lastupdate} = { '>', \"ms_current_timestamp() - INTERVAL '8 week'" };
+ $where->{confirmed} = { '<', \"current_timestamp - INTERVAL '4 week'" };
+ $where->{lastupdate} = { '>', \"current_timestamp - INTERVAL '8 week'" };
$where->{state} = { 'IN', [ FixMyStreet::DB::Result::Problem::open_states() ] };
} elsif ( $type eq 'unknown' ) {
- $where->{lastupdate} = { '<', \"ms_current_timestamp() - INTERVAL '8 week'" };
+ $where->{lastupdate} = { '<', \"current_timestamp - INTERVAL '8 week'" };
$where->{state} = { 'IN', [ FixMyStreet::DB::Result::Problem::open_states() ] };
} elsif ( $type eq 'fixed' ) {
- $where->{lastupdate} = { '>', \"ms_current_timestamp() - INTERVAL '8 week'" };
+ $where->{lastupdate} = { '>', \"current_timestamp - INTERVAL '8 week'" };
$where->{state} = $not_open;
} elsif ( $type eq 'older_fixed' ) {
- $where->{lastupdate} = { '<', \"ms_current_timestamp() - INTERVAL '8 week'" };
+ $where->{lastupdate} = { '<', \"current_timestamp - INTERVAL '8 week'" };
$where->{state} = $not_open;
}
@@ -417,29 +416,16 @@ sub load_and_group_problems : Private {
$where->{category} = $category;
}
+ my $problems = $c->cobrand->problems;
+
if ($c->stash->{ward}) {
$where->{areas} = { 'like', '%,' . $c->stash->{ward}->{id} . ',%' };
- $where->{bodies_str} = [
- undef,
- $c->stash->{body}->id,
- { 'like', $c->stash->{body}->id . ',%' },
- { 'like', '%,' . $c->stash->{body}->id },
- ];
+ $problems = $problems->to_body($c->stash->{body});
} elsif ($c->stash->{body}) {
- # XXX FixMyStreet used to have the following line so that reports not
- # currently sent anywhere could still be listed in the appropriate
- # (body/area), as they were the same. Now they're not, not sure if
- # there's a way to do this easily.
- #$where->{areas} = { 'like', '%,' . $c->stash->{body}->id . ',%' };
- $where->{bodies_str} = [
- # undef,
- $c->stash->{body}->id,
- { 'like', $c->stash->{body}->id . ',%' },
- { 'like', '%,' . $c->stash->{body}->id },
- ];
+ $problems = $problems->to_body($c->stash->{body});
}
- my $problems = $c->cobrand->problems->search(
+ $problems = $problems->search(
$where,
{
order_by => $c->cobrand->reports_ordering,
@@ -463,7 +449,6 @@ sub load_and_group_problems : Private {
}
} else {
# Add to bodies it was sent to
- # XXX Assumes body ID matches "council ID"
my $bodies = $problem->bodies_str_ids;
foreach ( @$bodies ) {
next if $_ != $c->stash->{body}->id;
@@ -507,6 +492,9 @@ sub stash_report_filter_status : Private {
} elsif ( $status eq 'open' ) {
$c->stash->{filter_status} = 'open';
$c->stash->{filter_problem_states} = FixMyStreet::DB::Result::Problem->open_states();
+ } elsif ( $status eq 'closed' ) {
+ $c->stash->{filter_status} = 'closed';
+ $c->stash->{filter_problem_states} = FixMyStreet::DB::Result::Problem->closed_states();
} elsif ( $status eq 'fixed' ) {
$c->stash->{filter_status} = 'fixed';
$c->stash->{filter_problem_states} = FixMyStreet::DB::Result::Problem->fixed_states();
diff --git a/perllib/FixMyStreet/App/Controller/Rss.pm b/perllib/FixMyStreet/App/Controller/Rss.pm
index 7aafc99ff..b817fe326 100755
--- a/perllib/FixMyStreet/App/Controller/Rss.pm
+++ b/perllib/FixMyStreet/App/Controller/Rss.pm
@@ -264,11 +264,7 @@ sub add_row : Private {
(my $link = $alert_type->item_link) =~ s/{{(.*?)}}/$row->{$1}/g;
(my $desc = _($alert_type->item_description)) =~ s/{{(.*?)}}/$row->{$1}/g;
- my $hashref_restriction = $c->cobrand->site_restriction;
- my $base_url = $c->cobrand->base_url;
- if ( $hashref_restriction && $hashref_restriction->{bodies_str} && $row->{bodies_str} && $row->{bodies_str} ne $hashref_restriction->{bodies_str} ) {
- $base_url = $c->config->{BASE_URL};
- }
+ my $base_url = $c->cobrand->base_url_for_report($row);
my $url = $base_url . $link;
my %item = (
diff --git a/perllib/FixMyStreet/App/Controller/Status.pm b/perllib/FixMyStreet/App/Controller/Status.pm
new file mode 100755
index 000000000..907fe5456
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Status.pm
@@ -0,0 +1,70 @@
+package FixMyStreet::App::Controller::Status;
+use Moose;
+use namespace::autoclean;
+
+use HTTP::Negotiate;
+use JSON;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+=head1 NAME
+
+FixMyStreet::App::Controller::Status - Catalyst Controller
+
+=head1 DESCRIPTION
+
+Status page Catalyst Controller.
+
+=head1 METHODS
+
+=cut
+
+sub index_json : Path('/status.json') : Args(0) {
+ my ($self, $c) = @_;
+ $c->forward('index', [ 'json' ]);
+}
+
+sub index : Path : Args(0) {
+ my ($self, $c, $format) = @_;
+
+ # Fetch summary stats from admin front page
+ $c->forward('/admin/index');
+
+ # Fetch git version
+ $c->forward('/admin/config_page');
+
+ my $chosen = $format;
+ unless ($chosen) {
+ my $variants = [
+ ['html', undef, 'text/html', undef, undef, undef, undef],
+ ['json', undef, 'application/json', undef, undef, undef, undef],
+ ];
+ $chosen = HTTP::Negotiate::choose($variants, $c->req->headers);
+ $chosen = 'html' unless $chosen;
+ }
+
+ # TODO Perform health checks here
+
+ if ($chosen eq 'json') {
+ $c->res->content_type('application/json; charset=utf-8');
+ my $data = {
+ version => $c->stash->{git_version},
+ reports => $c->stash->{total_problems_live},
+ updates => $c->stash->{comments}{confirmed},
+ alerts_confirmed => $c->stash->{alerts}{1},
+ alerts_unconfirmed => $c->stash->{alerts}{0},
+ questionnaires_sent => $c->stash->{questionnaires}{total},
+ questionnaires_answered => $c->stash->{questionnaires}{1},
+ bodies => $c->stash->{total_bodies},
+ contacts => $c->stash->{contacts}{total},
+ };
+ my $body = JSON->new->utf8(1)->pretty->encode($data);
+ $c->res->body($body);
+ }
+
+ return 1;
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Tokens.pm b/perllib/FixMyStreet/App/Controller/Tokens.pm
index 21c269502..ba15162ce 100644
--- a/perllib/FixMyStreet/App/Controller/Tokens.pm
+++ b/perllib/FixMyStreet/App/Controller/Tokens.pm
@@ -58,7 +58,7 @@ sub confirm_problem : Path('/P') {
# check that this email or domain are not the cause of abuse. If so hide it.
if ( $problem->is_from_abuser ) {
$problem->update(
- { state => 'hidden', lastupdate => \'ms_current_timestamp()' } );
+ { state => 'hidden', lastupdate => \'current_timestamp' } );
$c->stash->{template} = 'tokens/abuse.html';
return;
}
@@ -68,7 +68,7 @@ sub confirm_problem : Path('/P') {
if ($c->cobrand->moniker eq 'zurich') {
$problem->set_extra_metadata( email_confirmed => 1 );
$problem->update( {
- confirmed => \'ms_current_timestamp()',
+ confirmed => \'current_timestamp',
} );
if ( $data->{name} || $data->{password} ) {
@@ -90,8 +90,8 @@ sub confirm_problem : Path('/P') {
$problem->update(
{
state => 'confirmed',
- confirmed => \'ms_current_timestamp()',
- lastupdate => \'ms_current_timestamp()',
+ confirmed => \'current_timestamp',
+ lastupdate => \'current_timestamp',
}
);
diff --git a/perllib/FixMyStreet/App/Model/PhotoSet.pm b/perllib/FixMyStreet/App/Model/PhotoSet.pm
new file mode 100644
index 000000000..b18460821
--- /dev/null
+++ b/perllib/FixMyStreet/App/Model/PhotoSet.pm
@@ -0,0 +1,306 @@
+package FixMyStreet::App::Model::PhotoSet;
+
+# TODO this isn't a Cat model, rename to something else
+
+use Moose;
+use Path::Tiny 'path';
+use if !$ENV{TRAVIS}, 'Image::Magick';
+use Scalar::Util 'openhandle', 'blessed';
+use Digest::SHA qw(sha1_hex);
+use Image::Size;
+use MIME::Base64;
+
+has c => (
+ is => 'ro',
+);
+
+has object => (
+ is => 'ro',
+);
+
+has data => ( # generic data from DB field
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ # yes, this is a little circular: data -> data_items -> items -> data
+ # e.g. if not provided, then we're presumably uploading/etc., so calculate from
+ # the stored cached fileids
+ # (obviously if you provide none of these, then you'll get an infinite loop)
+ my $self = shift;
+ my $data = join ',', map { $_->[0] } $self->all_images;
+ return $data;
+ }
+);
+
+has data_items => ( # either a) split from data or b) provided by photo upload
+ isa => 'ArrayRef',
+ is => 'rw',
+ traits => ['Array'],
+ lazy => 1,
+ handles => {
+ map_data_items => 'map',
+ },
+ default => sub {
+ my $self = shift;
+ my $data = $self->data
+ or return [];
+
+ return [$data] if (_jpeg_magic($data));
+
+ return [ split ',' => $data ];
+ },
+);
+
+has upload_dir => (
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ my $cache_dir = path( $self->c->config->{UPLOAD_DIR} );
+ $cache_dir->mkpath;
+ unless ( -d $cache_dir && -w $cache_dir ) {
+ warn "Can't find/write to photo cache directory '$cache_dir'";
+ return;
+ }
+ $cache_dir;
+ },
+);
+
+sub _jpeg_magic {
+ $_[0] =~ /^\x{ff}\x{d8}/; # JPEG
+ # NB: should we also handle \x{89}\x{50} (PNG, 15 results in live DB) ?
+ # and \x{49}\x{49} (Tiff, 3 results in live DB) ?
+}
+
+=head2 C<images>, C<num_images>, C<get_raw_image_data>, C<all_images>
+
+C<$photoset-E<GT>images> is an AoA containing the filed and the binary image data.
+
+ [
+ [ $fileid1, $binary_data ],
+ [ $fileid2, $binary_data ],
+ ...
+ ]
+
+Various accessors are provided onto it:
+
+ num_images: count
+ get_raw_image_data ($index): return the [$fileid, $binary_data] tuple
+ all_images: return AoA as an array (e.g. rather than arrayref)
+
+=cut
+
+has images => ( # AoA of [$fileid, $binary_data] tuples
+ isa => 'ArrayRef',
+ is => 'rw',
+ traits => ['Array'],
+ lazy => 1,
+ handles => {
+ num_images => 'count',
+ get_raw_image_data => 'get',
+ all_images => 'elements',
+ },
+ default => sub {
+ my $self = shift;
+ my @photos = $self->map_data_items( sub {
+ my $part = $_;
+
+ if (blessed $part and $part->isa('Catalyst::Request::Upload')) {
+ # check that the photo is a jpeg
+ my $upload = $part;
+ my $ct = $upload->type;
+ $ct =~ s/x-citrix-//; # Thanks, Citrix
+ # Had a report of a JPEG from an Android 2.1 coming through as a byte stream
+ unless ( $ct eq 'image/jpeg' || $ct eq 'image/pjpeg' || $ct eq 'application/octet-stream' ) {
+ my $c = $self->c;
+ $c->log->info('Bad photo tried to upload, type=' . $ct);
+ $c->stash->{photo_error} = _('Please upload a JPEG image only');
+ return ();
+ }
+
+ # base64 decode the file if it's encoded that way
+ # Catalyst::Request::Upload doesn't do this automatically
+ # unfortunately.
+ my $transfer_encoding = $upload->headers->header('Content-Transfer-Encoding');
+ if (defined $transfer_encoding && $transfer_encoding eq 'base64') {
+ my $decoded = decode_base64($upload->slurp);
+ if (open my $fh, '>', $upload->tempname) {
+ binmode $fh;
+ print $fh $decoded;
+ close $fh
+ } else {
+ my $c = $self->c;
+ $c->log->info('Couldn\'t open temp file to save base64 decoded image: ' . $!);
+ $c->stash->{photo_error} = _("Sorry, we couldn't save your image(s), please try again.");
+ return ();
+ }
+ }
+
+ # get the photo into a variable
+ my $photo_blob = eval {
+ my $filename = $upload->tempname;
+ my $out = `jhead -se -autorot $filename 2>&1`;
+ unless (defined $out) {
+ my ($w, $h, $err) = Image::Size::imgsize($filename);
+ die _("Please upload a JPEG image only") . "\n" if !defined $w || $err ne 'JPG';
+ }
+ die _("Please upload a JPEG image only") . "\n" if $out && $out =~ /Not JPEG:/;
+ my $photo = $upload->slurp;
+ };
+ if ( my $error = $@ ) {
+ my $format = _(
+ "That image doesn't appear to have uploaded correctly (%s), please try again."
+ );
+ $self->c->stash->{photo_error} = sprintf( $format, $error );
+ return ();
+ }
+
+ # we have an image we can use - save it to the upload dir for storage
+ my $fileid = $self->get_fileid($photo_blob);
+ my $file = $self->get_file($fileid);
+ $upload->copy_to( $file );
+ return [$fileid, $photo_blob];
+
+ }
+ if (_jpeg_magic($part)) {
+ my $photo_blob = $part;
+ my $fileid = $self->get_fileid($photo_blob);
+ my $file = $self->get_file($fileid);
+ $file->spew_raw($photo_blob);
+ return [$fileid, $photo_blob];
+ }
+ if (length($part) == 40) {
+ my $fileid = $part;
+ my $file = $self->get_file($fileid);
+ if ($file->exists) {
+ my $photo = $file->slurp_raw;
+ [$fileid, $photo];
+ }
+ else {
+ warn "File $fileid doesn't exist";
+ ();
+ }
+ }
+ else {
+ warn sprintf "Received bad photo hash of length %d", length($part);
+ ();
+ }
+ });
+ return \@photos;
+ },
+);
+
+sub get_fileid {
+ my ($self, $photo_blob) = @_;
+ return sha1_hex($photo_blob);
+}
+
+sub get_file {
+ my ($self, $fileid) = @_;
+ my $cache_dir = $self->upload_dir;
+ return path( $cache_dir, "$fileid.jpeg" );
+}
+
+sub get_image_data {
+ my ($self, %args) = @_;
+ my $num = $args{num} || 0;
+
+ my $data = $self->get_raw_image_data( $num )
+ or return;
+
+ my ($fileid, $photo) = @$data;
+
+ my $size = $args{size};
+ if ( $size eq 'tn' ) {
+ $photo = _shrink( $photo, 'x100' );
+ } elsif ( $size eq 'fp' ) {
+ $photo = _crop( $photo );
+ } elsif ( $size eq 'full' ) {
+ # do nothing
+ } else {
+ $photo = _shrink( $photo, $self->c->cobrand->default_photo_resize || '250x250' );
+ }
+
+ return $photo;
+}
+
+sub delete_cached {
+ my ($self) = @_;
+ my $object = $self->object or return;
+
+ unlink glob FixMyStreet->path_to(
+ 'web',
+ 'photo',
+ $object->id . '.*'
+ );
+}
+
+sub rotate_image {
+ my ($self, $index, $direction) = @_;
+
+ my @images = $self->all_images;
+ return if $index > $#images;
+
+ my @items = map $_->[0], @images;
+ $items[$index] = _rotate_image( $images[$index][1], $direction );
+
+ my $new_set = (ref $self)->new({
+ data_items => \@items,
+ c => $self->c,
+ object => $self->object,
+ });
+
+ $self->delete_cached();
+
+ return $new_set->data; # e.g. new comma-separated fileid
+}
+
+sub _rotate_image {
+ my ($photo, $direction) = @_;
+ return $photo unless $Image::Magick::VERSION;
+ my $image = Image::Magick->new;
+ $image->BlobToImage($photo);
+ my $err = $image->Rotate($direction);
+ return 0 if $err;
+ my @blobs = $image->ImageToBlob();
+ undef $image;
+ return $blobs[0];
+}
+
+
+
+
+
+# NB: These 2 subs stolen from A::C::Photo, should be purged from there!
+#
+# Shrinks a picture to the specified size, but keeping in proportion.
+sub _shrink {
+ my ($photo, $size) = @_;
+ return $photo unless $Image::Magick::VERSION;
+ my $image = Image::Magick->new;
+ $image->BlobToImage($photo);
+ my $err = $image->Scale(geometry => "$size>");
+ throw Error::Simple("resize failed: $err") if "$err";
+ $image->Strip();
+ my @blobs = $image->ImageToBlob();
+ undef $image;
+ return $blobs[0];
+}
+
+# Shrinks a picture to 90x60, cropping so that it is exactly that.
+sub _crop {
+ my ($photo) = @_;
+ return $photo unless $Image::Magick::VERSION;
+ my $image = Image::Magick->new;
+ $image->BlobToImage($photo);
+ my $err = $image->Resize( geometry => "90x60^" );
+ throw Error::Simple("resize failed: $err") if "$err";
+ $err = $image->Extent( geometry => '90x60', gravity => 'Center' );
+ throw Error::Simple("resize failed: $err") if "$err";
+ $image->Strip();
+ my @blobs = $image->ImageToBlob();
+ undef $image;
+ return $blobs[0];
+}
+
+1;
diff --git a/perllib/FixMyStreet/App/View/Web.pm b/perllib/FixMyStreet/App/View/Web.pm
index da549ece8..a92021f0c 100644
--- a/perllib/FixMyStreet/App/View/Web.pm
+++ b/perllib/FixMyStreet/App/View/Web.pm
@@ -11,7 +11,7 @@ use Utils;
__PACKAGE__->config(
TEMPLATE_EXTENSION => '.html',
- INCLUDE_PATH => [ #
+ INCLUDE_PATH => [
FixMyStreet->path_to( 'templates', 'web', 'base' ),
],
ENCODING => 'utf8',
@@ -38,17 +38,26 @@ TT View for FixMyStreet::App.
=cut
+# Override parent function so that errors are only logged once.
+sub _rendering_error {
+ my ($self, $c, $err) = @_;
+ my $error = qq/Couldn't render template "$err"/;
+ # $c->log->error($error);
+ $c->error($error);
+ return 0;
+}
+
=head2 loc
- [% loc('Some text to localize') %]
+ [% loc('Some text to localize', 'Optional comment for translator') %]
Passes the text to the localisation engine for translations.
=cut
sub loc {
- my ( $self, $c, @args ) = @_;
- return _(@args);
+ my ( $self, $c, $msgid ) = @_;
+ return _($msgid);
}
=head2 nget
@@ -133,6 +142,9 @@ sub escape_js {
'>' => 'u003e',
);
$text =~ s/([\\"'<>])/\\$lookup{$1}/g;
+
+ $text =~ s/(?:\r\n|\n|\r)/\\n/g; # replace newlines
+
return $text;
}
diff --git a/perllib/FixMyStreet/Cobrand/Bromley.pm b/perllib/FixMyStreet/Cobrand/Bromley.pm
index 9bee45128..687843a2a 100644
--- a/perllib/FixMyStreet/Cobrand/Bromley.pm
+++ b/perllib/FixMyStreet/Cobrand/Bromley.pm
@@ -20,15 +20,7 @@ sub disambiguate_location {
my $town = 'Bromley';
- # Bing turns High St Bromley into Bromley High St which is in
- # Bromley by Bow.
- $town .= ', BR1' if $string =~ /^high\s+st(reet)?$/i;
-
- # Disambiguations required for BR5
- $town .= ', BR5' if $string =~ /^kelsey\s+r(?:oa)?d$/i;
- $town = 'BR5 Bromley' if $string =~ /^leith\s+hill$/i; # doesn't like appended BR5 for some reason
-
- # There has also been a road name change for a section of Ramsden Road
+ # There has been a road name change for a section of Ramsden Road
# (BR5) between Church Hill and Court Road has changed to 'Old Priory
# Avenue' - presently entering Old Priory Avenue simply takes the user to
# a different Priory Avenue in Petts Wood
@@ -37,18 +29,10 @@ sub disambiguate_location {
$string = 'Ramsden Road';
$town = ', BR6 0PL';
}
- $town .= ', BR5' if $string =~ /^meadway/i;
-
- # and BR6
- $town .= ', BR6' if $string =~ /^berrylands/i;
# White Horse Hill is on boundary with Greenwich, so need a
# specific postcode
- $town = 'chislehurst, BR7 6DH' if $string =~ /^white\s+horse/i;
-
- # Mottingham Lane is 90% inside Bromley, but goes outside too and Bing
- # defaults to the top end of it.
- $town = 'Mottingham Lane, SE9 4RW' if $string =~ /^mottingham\s+lane/i;
+ $string = 'BR7 6DH' if $string =~ /^white\s+horse/i;
$town = '' if $string =~ /orpington/i;
@@ -61,6 +45,10 @@ sub disambiguate_location {
};
}
+sub get_geocoder {
+ return 'OSM'; # default of Bing gives poor results, let's try overriding.
+}
+
sub example_places {
return ( 'BR1 3UH', 'Glebe Rd, Bromley' );
}
diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm
index c3185ea05..9541f2601 100644
--- a/perllib/FixMyStreet/Cobrand/Default.pm
+++ b/perllib/FixMyStreet/Cobrand/Default.pm
@@ -45,15 +45,6 @@ sub country {
return '';
}
-=head1 problems_clause
-
-Returns a hash for a query to be used by problems (and elsewhere in joined
-queries) to restrict results for a cobrand.
-
-=cut
-
-sub problems_clause {}
-
=head1 problems
Returns a ResultSet of Problems, restricted to a subset if we're on a cobrand
@@ -66,20 +57,20 @@ sub problems {
return $self->{c}->model('DB::Problem');
}
-=head1 site_restriction
+=head1 body_restriction
-Return a site key and a hash of extra query parameters if the cobrand uses a
-subset of the FixMyStreet data. Parameter is any extra data the cobrand needs.
-Returns a site key of 0 and an empty hash if the cobrand uses all the data.
+Return an extra query parameter to restrict reports to those sent to a
+particular body.
=cut
-sub site_restriction { return {}; }
+sub body_restriction {}
+
sub site_key { return 0; }
=head2 restriction
-Return a restriction to pull out data saved while using the cobrand site.
+Return a restriction to data saved while using this specific cobrand site.
=cut
@@ -117,7 +108,8 @@ sub base_url { FixMyStreet->config('BASE_URL') }
=head2 base_url_for_report
Return the base url for a report (might be different in a two-tier county, but
-most of the time will be same as base_url).
+most of the time will be same as base_url). Report may be an object, or a
+hashref.
=cut
@@ -921,4 +913,48 @@ sub jurisdiction_id_example {
return $self->moniker;
}
+=head2 body_details_data
+
+Returns a list of bodies to create with ensure_body. These
+are mostly just passed to ->find_or_create, but there is some
+pre-processing so that you can enter:
+
+ area_id => 123,
+ parent => 'Big Town',
+
+instead of
+
+ body_areas => [ { area_id => 123 } ],
+ parent => { name => 'Big Town' },
+
+For example:
+
+ return (
+ {
+ name => 'Big Town',
+ },
+ {
+ name => 'Small town',
+ parent => 'Big Town',
+ area_id => 1234,
+ },
+
+
+=cut
+
+sub body_details_data {
+ return ();
+}
+
+=head2 contact_details_data
+
+Returns a list of contact_data to create with setup_contacts.
+See Zurich for an example.
+
+=cut
+
+sub contact_details_data {
+ return ()
+}
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/EastSussex.pm b/perllib/FixMyStreet/Cobrand/EastSussex.pm
index 5aabae449..2ba3a4f70 100644
--- a/perllib/FixMyStreet/Cobrand/EastSussex.pm
+++ b/perllib/FixMyStreet/Cobrand/EastSussex.pm
@@ -48,16 +48,16 @@ Can run with a script or command line like:
=cut
use constant POTHOLE_SIZES => [
- {'key' => ['Blank'], 'name' => ['--']},
- {'key' => ['golf'], 'name' => ['Golf ball sized']},
- {'key' => ['tennis'], 'name' => ['Tennis ball sized']},
+ {'key' => ['Blank'], 'name' => ['--']},
+ {'key' => ['golf'], 'name' => ['Golf ball sized']},
+ {'key' => ['tennis'], 'name' => ['Tennis ball sized']},
{'key' => ['football'], 'name' => ['Football sized']},
{'key' => ['larger'], 'name' => ['Larger']}
];
use constant POTHOLE_DICT => {
map {
- @{ $_->{key} },
+ @{ $_->{key} },
@{ $_->{name} },
} @{ POTHOLE_SIZES() },
};
@@ -86,7 +86,7 @@ sub temp_update_potholes_contact {
'description' => 'Size of the pothole?',
'required' => 'true',
'datatype' => 'singlevaluelist',
- 'datatype_description' => {},
+ 'datatype_description' => {},
'values' => {
'value' => $self->POTHOLE_SIZES,
},
@@ -123,5 +123,10 @@ sub send_questionnaires {
return 0;
}
+sub contact_email {
+ my $self = shift;
+ return join( '@', 'highways', 'eastsussex.gov.uk' );
+}
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/Greenwich.pm b/perllib/FixMyStreet/Cobrand/Greenwich.pm
index 18b0c2f5e..7535a34bf 100644
--- a/perllib/FixMyStreet/Cobrand/Greenwich.pm
+++ b/perllib/FixMyStreet/Cobrand/Greenwich.pm
@@ -44,4 +44,9 @@ sub pin_colour {
return 'yellow';
}
+sub contact_email {
+ my $self = shift;
+ return join( '@', 'fixmystreet', 'royalgreenwich.gov.uk' );
+}
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
index b9d48a95c..c78ae5e09 100644
--- a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
+++ b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm
@@ -103,4 +103,9 @@ sub pin_colour {
sub on_map_default_status { return 'open'; }
+sub contact_email {
+ my $self = shift;
+ return join( '@', 'highway.enquiries', 'oxfordshire.gov.uk' );
+}
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/SeeSomething.pm b/perllib/FixMyStreet/Cobrand/SeeSomething.pm
index 75f1c32e8..9ae15fd8d 100644
--- a/perllib/FixMyStreet/Cobrand/SeeSomething.pm
+++ b/perllib/FixMyStreet/Cobrand/SeeSomething.pm
@@ -10,14 +10,9 @@ sub council_name { return 'See Something Say Something'; }
sub council_url { return 'seesomething'; }
sub area_types { [ 'MTD' ] }
-sub site_restriction {
+sub body_restriction {
my $self = shift;
- return { bodies_str => { IN => $self->council_id } };
-}
-
-sub problems_clause {
- my $self = shift;
- return { bodies_str => { IN => $self->council_id } };
+ return $self->council_id;
}
sub area_check {
diff --git a/perllib/FixMyStreet/Cobrand/Stevenage.pm b/perllib/FixMyStreet/Cobrand/Stevenage.pm
index 1de44a50d..5e2f07341 100644
--- a/perllib/FixMyStreet/Cobrand/Stevenage.pm
+++ b/perllib/FixMyStreet/Cobrand/Stevenage.pm
@@ -29,5 +29,10 @@ sub default_map_zoom { return 3; }
sub users_can_hide { return 1; }
+sub contact_email {
+ my $self = shift;
+ return join( '@', 'csc', 'stevenage.gov.uk' );
+}
+
1;
diff --git a/perllib/FixMyStreet/Cobrand/UKCouncils.pm b/perllib/FixMyStreet/Cobrand/UKCouncils.pm
index e653ae522..074da0915 100644
--- a/perllib/FixMyStreet/Cobrand/UKCouncils.pm
+++ b/perllib/FixMyStreet/Cobrand/UKCouncils.pm
@@ -22,10 +22,11 @@ sub path_to_web_templates {
];
}
-sub site_restriction {
+sub body_restriction {
my $self = shift;
- return { bodies_str => sprintf('%d', $self->council_id) };
+ return $self->council_id;
}
+
sub site_key {
my $self = shift;
return $self->council_url;
@@ -35,23 +36,9 @@ sub restriction {
return { cobrand => shift->moniker };
}
-# Different function to site_restriction due to two-tier use
-sub problems_clause {
- my $self = shift;
-
- if ($self->is_two_tier) {
- return { bodies_str => {
- like => ('%' . $self->council_id . '%')
- }};
- }
- else {
- return { bodies_str => sprintf('%d', $self->council_id) };
- }
-}
-
sub problems {
my $self = shift;
- return $self->{c}->model('DB::Problem')->search( $self->problems_clause );
+ return $self->{c}->model('DB::Problem')->to_body($self->council_id);
}
sub base_url {
@@ -122,8 +109,15 @@ sub recent_photos {
# Returns true if the cobrand owns the problem.
sub owns_problem {
my ($self, $report) = @_;
- my $bodies = $report->bodies;
- my %areas = map { %{$_->areas} } values %$bodies;
+ my @bodies;
+ if (ref $report eq 'HASH') {
+ return unless $report->{bodies_str};
+ @bodies = split /,/, $report->{bodies_str};
+ @bodies = FixMyStreet::App->model('DB::Body')->search({ id => \@bodies })->all;
+ } else { # Object
+ @bodies = values %{$report->bodies};
+ }
+ my %areas = map { %{$_->areas} } @bodies;
return $areas{$self->council_id} ? 1 : undef;
}
diff --git a/perllib/FixMyStreet/Cobrand/Zurich.pm b/perllib/FixMyStreet/Cobrand/Zurich.pm
index c64fe1177..6da3e566c 100644
--- a/perllib/FixMyStreet/Cobrand/Zurich.pm
+++ b/perllib/FixMyStreet/Cobrand/Zurich.pm
@@ -5,9 +5,12 @@ use DateTime;
use POSIX qw(strcoll);
use RABX;
use Scalar::Util 'blessed';
+use DateTime::Format::Pg;
use strict;
use warnings;
+use feature 'say';
+use utf8;
=head1 NAME
@@ -105,8 +108,11 @@ sub prettify_dt {
sub zurich_closed_states {
my $states = {
'fixed - council' => 1,
- 'closed' => 1,
+ 'closed' => 1, # extern
'hidden' => 1,
+ 'investigating' => 1, # wish
+ 'unable to fix' => 1, # jurisdiction unknown
+ 'partial' => 1, # not contactable
};
return wantarray ? keys %{ $states } : $states;
@@ -117,6 +123,37 @@ sub problem_is_closed {
return exists $self->zurich_closed_states->{ $problem->state } ? 1 : 0;
}
+sub zurich_public_response_states {
+ my $states = {
+ 'fixed - council' => 1,
+ 'closed' => 1, # extern
+ 'unable to fix' => 1, # jurisdiction unknown
+ };
+
+ return wantarray ? keys %{ $states } : $states;
+}
+
+sub zurich_user_response_states {
+ my $states = {
+ 'hidden' => 1,
+ 'investigating' => 1, # wish
+ 'partial' => 1, # not contactable
+ };
+
+ return wantarray ? keys %{ $states } : $states;
+}
+
+sub problem_has_public_response {
+ my ($self, $problem) = @_;
+ return exists $self->zurich_public_response_states->{ $problem->state } ? 1 : 0;
+}
+
+sub problem_has_user_response {
+ my ($self, $problem) = @_;
+ my $state_matches = exists $self->zurich_user_response_states->{ $problem->state } ? 1 : 0;
+ return $state_matches && $problem->get_extra_metadata('public_response');
+}
+
sub problem_as_hashref {
my $self = shift;
my $problem = shift;
@@ -132,16 +169,35 @@ sub problem_as_hashref {
$hashref->{title} = _('This report is awaiting moderation.');
$hashref->{state} = 'submitted';
$hashref->{state_t} = _('Submitted');
+ $hashref->{banner_id} = 'closed';
} else {
if ( $problem->state eq 'confirmed' ) {
$hashref->{state} = 'open';
$hashref->{state_t} = _('Open');
+ $hashref->{banner_id} = 'closed';
+ } elsif ( $problem->state eq 'closed' ) {
+ $hashref->{state} = 'extern'; # is this correct?
+ $hashref->{banner_id} = 'closed';
+ $hashref->{state_t} = _('Extern');
+ } elsif ( $problem->state eq 'unable to fix' ) {
+ $hashref->{state} = 'jurisdiction unknown'; # is this correct?
+ $hashref->{state_t} = _('Jurisdiction Unknown');
+ $hashref->{banner_id} = 'fixed'; # green
+ } elsif ( $problem->state eq 'partial' ) {
+ $hashref->{state} = 'not contactable'; # is this correct?
+ $hashref->{state_t} = _('Not contactable');
+ # no banner_id as hidden
+ } elsif ( $problem->state eq 'investigating' ) {
+ $hashref->{state} = 'wish'; # is this correct?
+ $hashref->{state_t} = _('Wish');
} elsif ( $problem->is_fixed ) {
$hashref->{state} = 'closed';
+ $hashref->{banner_id} = 'fixed';
$hashref->{state_t} = _('Closed');
} elsif ( $problem->state eq 'in progress' || $problem->state eq 'planned' ) {
$hashref->{state} = 'in progress';
$hashref->{state_t} = _('In progress');
+ $hashref->{banner_id} = 'progress';
}
}
@@ -275,13 +331,46 @@ sub get_or_check_overdue {
return $self->overdue($problem);
}
+=head1 C<set_problem_state>
+
+If the state has changed, sets the state and calls C::Admin's C<log_edit> action.
+If the state hasn't changed, defers to update_admin_log (to update time_spent if any).
+
+Returns either undef or the AdminLog entry created.
+
+=cut
+
sub set_problem_state {
my ($self, $c, $problem, $new_state) = @_;
- return if $new_state eq $problem->state;
+ return $self->update_admin_log($c, $problem) if $new_state eq $problem->state;
$problem->state( $new_state );
$c->forward( 'log_edit', [ $problem->id, 'problem', "state change to $new_state" ] );
}
+=head1 C<update_admin_log>
+
+Calls C::Admin's C<log_edit> if either a) text is provided, or b) there has
+been time_spent on the task. As set_problem_state will already call log_edit
+if required, don't call this as well.
+
+Returns either undef or the AdminLog entry created.
+
+=cut
+
+sub update_admin_log {
+ my ($self, $c, $problem, $text) = @_;
+
+ my $time_spent = ( ($c->get_param('time_spent') // 0) + 0 );
+ $c->set_param('time_spent' => 0); # explicitly zero this to avoid duplicates
+
+ if (!$text) {
+ return unless $time_spent;
+ $text = "Logging time_spent";
+ }
+
+ $c->forward( 'log_edit', [ $problem->id, 'problem', $text, $time_spent ] );
+}
+
# Specific administrative displays
sub admin_pages {
@@ -289,23 +378,25 @@ sub admin_pages {
my $c = $self->{c};
my $type = $c->stash->{admin_type};
+
my $pages = {
'summary' => [_('Summary'), 0],
'reports' => [_('Reports'), 2],
'report_edit' => [undef, undef],
'update_edit' => [undef, undef],
+ 'stats' => [_('Stats'), 4],
};
return $pages if $type eq 'sdm';
$pages = { %$pages,
'bodies' => [_('Bodies'), 1],
'body' => [undef, undef],
+ 'templates' => [_('Templates'), 2],
};
return $pages if $type eq 'dm';
$pages = { %$pages,
'users' => [_('Users'), 3],
- 'stats' => [_('Stats'), 4],
'user_edit' => [undef, undef],
};
return $pages if $type eq 'super';
@@ -448,7 +539,7 @@ sub admin_report_edit {
}
- # If super or sdm check that the token is correct before proceeding
+ # If super or dm check that the token is correct before proceeding
if ( ($type eq 'super' || $type eq 'dm') && $c->get_param('submit') ) {
$c->forward('check_token');
}
@@ -468,6 +559,7 @@ sub admin_report_edit {
}
}
+
# Problem updates upon submission
if ( ($type eq 'super' || $type eq 'dm') && $c->get_param('submit') ) {
$problem->set_extra_metadata('publish_photo' => $c->get_param('publish_photo') || 0 );
@@ -488,10 +580,43 @@ sub admin_report_edit {
my $internal_note_text = "";
# Workflow things
+ #
+ # Note that 2 types of email may be sent
+ # 1) _admin_send_email() sends an email to the *user*, if their email is confirmed
+ #
+ # 2) setting $problem->whensent(undef) may make it eligible for generating an email
+ # to the body (internal or external). See DBRS::Problem->send_reports for Zurich-
+ # specific categories which are eligible for this.
+ #
+ # It looks like both of these will do:
+ # a) TT processing of [% ... %] directives, in FMS::App->send_email(_cron)
+ # b) pseudo-PHP substitution of <?=$values['name']?> which is done as
+ # naive substitution
+ # commonlib mySociety::Email
+ #
+ # So it makes sense to add new parameters as the more powerful TT (a).
+
my $redirect = 0;
- my $new_cat = $c->get_param('category');
- if ( $new_cat && $new_cat ne $problem->category ) {
- my $cat = $c->model('DB::Contact')->search( { category => $c->get_param('category') } )->first;
+ my $new_cat = $c->get_param('category') || '';
+ my $state = $c->get_param('state') || '';
+ my $oldstate = $problem->state;
+
+ my $closure_states = $self->zurich_closed_states;
+ delete $closure_states->{'fixed - council'}; # may not be needed?
+
+ my $old_closure_state = $problem->get_extra_metadata('closure_status');
+
+ # update the public update from DM
+ if (my $update = $c->get_param('status_update')) {
+ $problem->set_extra_metadata(public_response => $update);
+ }
+
+ if (
+ ($state eq 'confirmed')
+ && $new_cat
+ && $new_cat ne $problem->category
+ ) {
+ my $cat = $c->model('DB::Contact')->search({ category => $c->get_param('category') } )->first;
my $old_cat = $problem->category;
$problem->category( $new_cat );
$problem->external_body( undef );
@@ -499,7 +624,25 @@ sub admin_report_edit {
$problem->whensent( undef );
$problem->set_extra_metadata(changed_category => 1);
$internal_note_text = "Weitergeleitet von $old_cat an $new_cat";
+ $self->update_admin_log($c, $problem, "Changed category from $old_cat to $new_cat");
$redirect = 1 if $cat->body_id ne $body->id;
+ } elsif ( $closure_states->{$state} and
+ ( $oldstate ne 'planned' )
+ || (($old_closure_state ||'') ne $state))
+ {
+ # for these states
+ # - closed (Extern)
+ # - investigating (Wish)
+ # - hidden
+ # - partial (Not contactable)
+ # - unable to fix (Jurisdiction unknown)
+ # we divert to planned (Rueckmeldung ausstehend) and set closure_status to the requested state
+ # From here, the DM can reply to the user, triggering the setting of problem to correct state
+ $problem->set_extra_metadata( closure_status => $state );
+ $self->set_problem_state($c, $problem, 'planned');
+ $state = 'planned';
+ $problem->set_extra_metadata_if_undefined( moderated_overdue => $self->overdue( $problem ) );
+
} elsif ( my $subdiv = $c->get_param('body_subdivision') ) {
$problem->set_extra_metadata_if_undefined( moderated_overdue => $self->overdue( $problem ) );
$self->set_problem_state($c, $problem, 'in progress');
@@ -507,31 +650,82 @@ sub admin_report_edit {
$problem->bodies_str( $subdiv );
$problem->whensent( undef );
$redirect = 1;
- } elsif ( my $external = $c->get_param('body_external') ) {
- $problem->set_extra_metadata_if_undefined( moderated_overdue => $self->overdue( $problem ) );
- $self->set_problem_state($c, $problem, 'closed');
- $problem->set_extra_metadata_if_undefined( closed_overdue => $self->overdue( $problem ) );
- $problem->external_body( $external );
- $problem->whensent( undef );
- _admin_send_email( $c, 'problem-external.txt', $problem );
- $redirect = 1;
} else {
- if (my $state = $c->get_param('state')) {
+ if ($state) {
- if ($problem->state eq 'unconfirmed' and $state ne 'unconfirmed') {
+ if ($oldstate eq 'unconfirmed' and $state ne 'unconfirmed') {
# only set this for the first state change
$problem->set_extra_metadata_if_undefined( moderated_overdue => $self->overdue( $problem ) );
}
- $self->set_problem_state($c, $problem, $state);
+ $self->set_problem_state($c, $problem, $state)
+ unless $closure_states->{$state};
+ # we'll defer to 'planned' clause below to change the state
+ }
+ }
+
+ if ($problem->state eq 'planned') {
+ # Rueckmeldung ausstehend
+ # override $state from the metadata set above
+ $state = $problem->get_extra_metadata('closure_status') || '';
+ my ($moderated, $closed) = (0, 0);
- if ($self->problem_is_closed($problem)) {
- $problem->set_extra_metadata_if_undefined( closed_overdue => $self->overdue( $problem ) );
+ if ($state eq 'hidden' && $c->get_param('publish_response') ) {
+ _admin_send_email( $c, 'problem-rejected.txt', $problem );
+
+ $self->set_problem_state($c, $problem, $state);
+ $moderated++;
+ $closed++;
+ }
+ elsif ($state =~/^(closed|investigating)$/) { # Extern | Wish
+ $moderated++;
+ # Nested if instead of `and` because in these cases, we *don't*
+ # want to close unless we have body_external (so we don't want
+ # the final elsif clause below to kick in on publish_response)
+ if (my $external = $c->get_param('body_external')) {
+ my $external_body = $c->model('DB::Body')->find($external)
+ or die "Body $external not found";
+ $problem->external_body( $external );
+ }
+ if ($problem->external_body && $c->get_param('publish_response')) {
+ $problem->whensent( undef );
+ $self->set_problem_state($c, $problem, $state);
+ my $template = ($state eq 'investigating') ? 'problem-wish.txt' : 'problem-external.txt';
+ _admin_send_email( $c, $template, $problem );
+ $redirect = 0;
+ $closed++;
}
- if ( $state eq 'hidden' && $c->get_param('send_rejected_email') ) {
- _admin_send_email( $c, 'problem-rejected.txt', $problem );
+ # set the external_message in extra, so that it can be edited again
+ if ( my $external_message = $c->get_param('external_message') ) {
+ $problem->set_extra_metadata( external_message => $external_message );
}
+ # else should really return a message here
+ }
+ elsif ($c->get_param('publish_response')) {
+ # otherwise we only set the state if publish_response is set
+ #
+
+ # if $state wasn't set, then we are simply closing the message as fixed
+ $state ||= 'fixed - council';
+ _admin_send_email( $c, 'problem-closed.txt', $problem );
+ $redirect = 0;
+ $moderated++;
+ $closed++;
+ }
+
+ if ($moderated) {
+ $problem->set_extra_metadata_if_undefined( moderated_overdue => $self->overdue( $problem ) );
}
+
+ if ($closed) {
+ # set to either the closure_status from metadata or 'fixed - council' as above
+ $self->set_problem_state($c, $problem, $state);
+ $problem->set_extra_metadata_if_undefined( closed_overdue => $self->overdue( $problem ) );
+ $problem->unset_extra_metadata('closure_status');
+ }
+ }
+ else {
+ $problem->unset_extra_metadata('closure_status');
}
$problem->title( $c->get_param('title') ) if $c->get_param('title');
@@ -539,21 +733,42 @@ sub admin_report_edit {
$problem->latitude( $c->get_param('latitude') );
$problem->longitude( $c->get_param('longitude') );
- # Final, public, Update from DM
- if (my $update = $c->get_param('status_update')) {
- $problem->set_extra_metadata(public_response => $update);
- if ($c->get_param('publish_response')) {
- $self->set_problem_state($c, $problem, 'fixed - council');
- $problem->set_extra_metadata( closed_overdue => $self->overdue( $problem ) );
- _admin_send_email( $c, 'problem-closed.txt', $problem );
- }
+ # send external_message if provided and state is *now* Wish|Extern
+ # e.g. was already, or was set in the Rueckmeldung ausstehend clause above.
+ if ( my $external_message = $c->get_param('external_message')
+ and $problem->state =~ /^(closed|investigating)$/)
+ {
+ my $external = $problem->external_body;
+ my $external_body = $c->model('DB::Body')->find($external)
+ or die "Body $external not found";
+
+ $problem->set_extra_metadata_if_undefined( moderated_overdue => $self->overdue( $problem ) );
+ # Create a Comment on this Problem with the content of the external message.
+ # NB this isn't directly shown anywhere, but is for logging purposes.
+ $problem->add_to_comments( {
+ text => (
+ sprintf '(%s %s) %s',
+ $state eq 'closed' ?
+ _('Forwarded to external body') :
+ _('Forwarded wish to external body'),
+ $external_body->name,
+ $external_message,
+ ),
+ user => $c->user->obj,
+ state => 'hidden', # seems best fit, should not be shown publicly
+ mark_fixed => 0,
+ anonymous => 1,
+ extra => { is_internal_note => 1, is_external_message => 1 },
+ } );
+ # set the external_message in extra, so that it will be picked up
+ # later by send-reports
+ $problem->set_extra_metadata( external_message => $external_message );
}
- $problem->lastupdate( \'ms_current_timestamp()' );
+ $problem->lastupdate( \'current_timestamp' );
$problem->update;
- $c->stash->{status_message} =
- '<p><em>' . _('Updated!') . '</em></p>';
+ $c->stash->{status_message} = '<p class="message-updated">' . _('Updated!') . '</p>';
# do this here otherwise lastupdate and confirmed times
# do not display correctly (reloads problem from database, including
@@ -572,14 +787,21 @@ sub admin_report_edit {
} );
}
- if ( $redirect ) {
- $c->detach('index');
+ # Just update if time_spent still hasn't been logged
+ # (this will only happen if no other update_admin_log has already been called)
+ $self->update_admin_log($c, $problem);
+
+ if ( $redirect and $type eq 'dm' ) {
+ # only redirect for DM
+ $c->stash->{status_message} ||= '<p class="message-updated">' . _('Updated!') . '</p>';
+ $c->go('index');
}
$c->stash->{updates} = [ $c->model('DB::Comment')
->search( { problem_id => $problem->id }, { order_by => 'created' } )
->all ];
+ $self->stash_states($problem);
return 1;
}
@@ -588,15 +810,32 @@ sub admin_report_edit {
# Has cut-down edit template for adding update and sending back up only
$c->stash->{template} = 'admin/report_edit-sdm.html';
- if ($c->get_param('send_back')) {
+ if ($c->get_param('send_back') or $c->get_param('not_contactable')) {
+ # SDM can send back a report either to be assigned to a different
+ # subdivision, or because the customer was not contactable.
+ # We handle these in the same way but with different statuses.
+
$c->forward('check_token');
+ my $not_contactable = $c->get_param('not_contactable');
+
$problem->bodies_str( $body->parent->id );
- $self->set_problem_state($c, $problem, 'confirmed');
+ if ($not_contactable) {
+ # we can't directly set state, but mark the closure_status for DM to confirm.
+ $self->set_problem_state($c, $problem, 'planned');
+ $problem->set_extra_metadata( closure_status => 'partial');
+ }
+ else {
+ $self->set_problem_state($c, $problem, 'confirmed');
+ }
$problem->update;
- # log here
+ $c->forward( 'log_edit', [ $problem->id, 'problem',
+ $not_contactable ?
+ _('Customer not contactable')
+ : _('Sent report back') ] );
+ # Make sure the problem's time_spent is updated
+ $self->update_admin_log($c, $problem);
$c->res->redirect( '/admin/summary' );
-
} elsif ($c->get_param('submit')) {
$c->forward('check_token');
@@ -621,8 +860,10 @@ sub admin_report_edit {
anonymous => 1,
} );
}
+ # Make sure the problem's time_spent is updated
+ $self->update_admin_log($c, $problem);
- $c->stash->{status_message} = '<p><em>' . _('Updated!') . '</em></p>';
+ $c->stash->{status_message} = '<p class="message-updated">' . _('Updated!') . '</p>';
# If they clicked the no more updates button, we're done.
if ($c->get_param('no_more_updates')) {
@@ -639,14 +880,113 @@ sub admin_report_edit {
->search( { problem_id => $problem->id }, { order_by => 'created' } )
->all ];
+ $self->stash_states($problem);
return 1;
}
+ $self->stash_states($problem);
return 0;
}
+sub stash_states {
+ my ($self, $problem) = @_;
+ my $c = $self->{c};
+
+ # current problem state affects which states are visible in dropdowns
+ my @states = (
+ {
+ # Erfasst
+ state => 'unconfirmed',
+ trans => _('Submitted'),
+ unconfirmed => 1,
+ hidden => 1,
+ },
+ {
+ # Aufgenommen
+ state => 'confirmed',
+ trans => _('Open'),
+ unconfirmed => 1,
+ },
+ {
+ # Unsichtbar (hidden)
+ state => 'hidden',
+ trans => _('Hidden'),
+ unconfirmed => 1,
+ hidden => 1,
+ },
+ {
+ # Extern
+ state => 'closed',
+ trans => _('Extern'),
+ },
+ {
+ # Zustaendigkeit unbekannt
+ state => 'unable to fix',
+ trans => _('Jurisdiction unknown'),
+ },
+ {
+ # Wunsch (hidden)
+ state => 'investigating',
+ trans => _('Wish'),
+ },
+ {
+ # Nicht kontaktierbar (hidden)
+ state => 'partial',
+ trans => _('Not contactable'),
+ },
+ );
+ my %state_trans = map { $_->{state} => $_->{trans} } @states;
+
+ my $state = $problem->state;
+
+ # Rueckmeldung ausstehend may also indicate the status it's working towards.
+ push @states, do {
+ if ($state eq 'planned' and my $closure_status = $problem->get_extra_metadata('closure_status')) {
+ {
+ state => $closure_status,
+ trans => sprintf '%s (%s)', _('Planned'), $state_trans{$closure_status},
+ };
+ }
+ else {
+ {
+ state => 'planned',
+ trans => _('Planned'),
+ };
+ }
+ };
+
+ if ($state eq 'in progress') {
+ push @states, {
+ state => 'in progress',
+ trans => _('In progress'),
+ };
+ }
+ elsif ($state eq 'fixed - council') {
+ push @states, {
+ state => 'fixed - council',
+ trans => _('Closed'),
+ };
+ }
+ elsif ($state =~/^(hidden|unconfirmed)$/) {
+ @states = grep { $_->{$state} } @states;
+ }
+ $c->stash->{states} = \@states;
+ $c->stash->{states_trans} = { map { $_->{state} => $_->{trans} } @states }; # [% states_trans.${problem.state} %]
+
+ # stash details about the public response
+ $c->stash->{default_public_response} = "\nFreundliche Grüsse\n\nIhre Stadt Zürich\n";
+ $c->stash->{show_publish_response} =
+ ($problem->state eq 'planned');
+}
+
+=head2 _admin_send_email
+
+Send an email to the B<user> who logged the problem, if their email address is confirmed.
+
+=cut
+
sub _admin_send_email {
my ( $c, $template, $problem ) = @_;
@@ -665,9 +1005,36 @@ sub _admin_send_email {
to => [ $to ],
url => $c->uri_for_email( $problem->url ),
from => [ $sender, $sender_name ],
+ problem => $problem,
} );
}
+sub munge_sendreport_params {
+ my ($self, $c, $row, $h, $params) = @_;
+ if ($row->state =~ /^(closed|investigating)$/ && $row->get_extra_metadata('publish_photo')) {
+ # we attach images to reports sent to external bodies
+ my $photoset = $row->get_photoset($c);
+ my @images = $photoset->all_images
+ or return;
+ my $index = 0;
+ my $id = $row->id;
+ my @attachments = map {
+ my $i = $index++;
+ {
+ body => $_->[1],
+ attributes => {
+ filename => "$id.$i.jpeg",
+ content_type => 'image/jpeg',
+ encoding => 'base64',
+ # quoted-printable ends up with newlines corrupting binary data
+ name => "$id.$i.jpeg",
+ },
+ }
+ } @images;
+ $params->{attachments} = \@attachments;
+ }
+}
+
sub admin_fetch_all_bodies {
my ( $self, @bodies ) = @_;
@@ -718,7 +1085,10 @@ sub admin_stats {
if ($y && $m) {
$c->stash->{start_date} = DateTime->new( year => $y, month => $m, day => 1 );
$c->stash->{end_date} = $c->stash->{start_date} + DateTime::Duration->new( months => 1 );
- $date_params{created} = { '>=', $c->stash->{start_date}, '<', $c->stash->{end_date} };
+ $date_params{created} = {
+ '>=', DateTime::Format::Pg->format_datetime($c->stash->{start_date}),
+ '<', DateTime::Format::Pg->format_datetime($c->stash->{end_date}),
+ };
}
my %params = (
@@ -730,6 +1100,8 @@ sub admin_stats {
my $problems = $c->model('DB::Problem')->search(
{%date_params},
{
+ join => 'admin_log_entries',
+ distinct => 1,
columns => [
'id', 'created',
'latitude', 'longitude',
@@ -741,10 +1113,11 @@ sub admin_stats {
'whensent', 'lastupdate',
'service',
'extra',
- ],
+ { sum_time_spent => { sum => 'admin_log_entries.time_spent' } },
+ ]
}
);
- my $body = "Report ID,Created,Sent to Agency,Last Updated,E,N,Category,Status,UserID,External Body,Title,Detail,Media URL,Interface Used,Council Response\n";
+ my $body = "Report ID,Created,Sent to Agency,Last Updated,E,N,Category,Status,UserID,External Body,Time Spent,Title,Detail,Media URL,Interface Used,Council Response\n";
require Text::CSV;
my $csv = Text::CSV->new({ binary => 1 });
while ( my $report = $problems->next ) {
@@ -759,7 +1132,10 @@ sub admin_stats {
# replace newlines with HTML <br/> element
$detail =~ s{\r?\n}{ <br/> }g;
- $public_response =~ s{\r?\n}{ <br/> }g;
+ $public_response =~ s{\r?\n}{ <br/> }g if $public_response;
+
+ # Assemble photo URL, if report has a photo
+ my $media_url = $report->get_photo_params->{url} ? ($c->cobrand->base_url . $report->get_photo_params->{url}) : '';
my @columns = (
$report->id,
@@ -769,9 +1145,10 @@ sub admin_stats {
$report->local_coords, $report->category,
$report->state, $report->user_id,
$body_name,
+ $report->get_column('sum_time_spent') || 0,
$report->title,
$detail,
- $c->cobrand->base_url . $report->get_photo_params->{url},
+ $media_url,
$report->service || 'Web interface',
$public_response,
);
@@ -850,4 +1227,111 @@ sub admin_stats {
return 1;
}
+sub problem_confirm_email_extras {
+ my ($self, $report) = @_;
+ my $confirmed_reports = $report->user->problems->search({
+ extra => { like => '%email_confirmed%' },
+ })->count;
+
+ $self->{c}->stash->{email_confirmed} = $confirmed_reports;
+}
+
+sub body_details_data {
+ return (
+ {
+ name => 'Stadt Zurich'
+ },
+ {
+ name => 'Elektrizitäwerk Stadt Zürich',
+ parent => 'Stadt Zurich',
+ area_id => 423017,
+ },
+ {
+ name => 'ERZ Entsorgung + Recycling Zürich',
+ parent => 'Stadt Zurich',
+ area_id => 423017,
+ },
+ {
+ name => 'Fachstelle Graffiti',
+ parent => 'Stadt Zurich',
+ area_id => 423017,
+ },
+ {
+ name => 'Grün Stadt Zürich',
+ parent => 'Stadt Zurich',
+ area_id => 423017,
+ },
+ {
+ name => 'Tiefbauamt Stadt Zürich',
+ parent => 'Stadt Zurich',
+ area_id => 423017,
+ },
+ {
+ name => 'Dienstabteilung Verkehr',
+ parent => 'Stadt Zurich',
+ area_id => 423017,
+ },
+ );
+}
+
+sub contact_details_data {
+ return (
+ {
+ category => 'Beleuchtung/Uhren',
+ body_name => 'Elektrizitätswerk Stadt Zürich',
+ fields => [
+ {
+ code => 'strasse',
+ description => 'Strasse',
+ datatype => 'string',
+ required => 'yes',
+ },
+ {
+ code => 'haus_nr',
+ description => 'Haus-Nr.',
+ datatype => 'string',
+ },
+ {
+ code => 'mast_nr',
+ description => 'Mast-Nr.',
+ datatype => 'string',
+ }
+ ],
+ },
+ {
+ category => 'Brunnen/Hydranten',
+ # body_name ???
+ fields => [
+ {
+ code => 'hydranten_nr',
+ description => 'Hydranten-Nr.',
+ datatype => 'string',
+ },
+ ],
+ },
+ {
+ category => "Grünflächen/Spielplätze",
+ body_name => 'Grün Stadt Zürich',
+ rename_from => "Tiere/Grünflächen",
+ },
+ {
+ category => 'Spielplatz/Sitzbank',
+ body_name => 'Grün Stadt Zürich',
+ delete => 1,
+ },
+ );
+}
+
+sub contact_details_data_body_default {
+ my ($self) = @_;
+ # temporary measure to assign un-bodied contacts to parent
+ # (this isn't at all how things will be setup in live, but is
+ # handy during dev.)
+ return $self->{c}->model('DB::Body')->find({ name => 'Stadt Zurich' });
+}
+
+sub reports_per_page { return 20; }
+
+sub singleton_bodies_str { 1 }
+
1;
diff --git a/perllib/FixMyStreet/DB/Result/AdminLog.pm b/perllib/FixMyStreet/DB/Result/AdminLog.pm
index 41bc3100a..d60915cfc 100644
--- a/perllib/FixMyStreet/DB/Result/AdminLog.pm
+++ b/perllib/FixMyStreet/DB/Result/AdminLog.pm
@@ -29,13 +29,16 @@ __PACKAGE__->add_columns(
"whenedited",
{
data_type => "timestamp",
- default_value => \"ms_current_timestamp()",
+ default_value => \"current_timestamp",
is_nullable => 0,
+ original => { default_value => \"now()" },
},
"user_id",
{ data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"reason",
{ data_type => "text", default_value => "", is_nullable => 0 },
+ "time_spent",
+ { data_type => "integer", default_value => "0", is_nullable => 0 },
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->belongs_to(
@@ -51,7 +54,7 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2014-07-31 15:58:54
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:okGiaKaVYaTrlz0LCV01vA
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:RCi1FEwb9T2MZ2X+QOTTUA
1;
diff --git a/perllib/FixMyStreet/DB/Result/Alert.pm b/perllib/FixMyStreet/DB/Result/Alert.pm
index c64cb2ff4..35cce8368 100644
--- a/perllib/FixMyStreet/DB/Result/Alert.pm
+++ b/perllib/FixMyStreet/DB/Result/Alert.pm
@@ -37,8 +37,9 @@ __PACKAGE__->add_columns(
"whensubscribed",
{
data_type => "timestamp",
- default_value => \"ms_current_timestamp()",
+ default_value => \"current_timestamp",
is_nullable => 0,
+ original => { default_value => \"now()" },
},
"whendisabled",
{ data_type => "timestamp", is_nullable => 1 },
@@ -64,8 +65,8 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2013-09-10 17:11:54
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:d9yIFiTGtbtFaULXZNKstQ
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:5RNyB430T8PqtFlmGV/MUg
# You can replace this text with custom code or comments, and it will be preserved on regeneration
@@ -106,7 +107,7 @@ sub confirm {
sub disable {
my $self = shift;
- $self->whendisabled( \'ms_current_timestamp()' );
+ $self->whendisabled( \'current_timestamp' );
$self->update;
return 1;
diff --git a/perllib/FixMyStreet/DB/Result/AlertSent.pm b/perllib/FixMyStreet/DB/Result/AlertSent.pm
index 422e010a9..83043a33b 100644
--- a/perllib/FixMyStreet/DB/Result/AlertSent.pm
+++ b/perllib/FixMyStreet/DB/Result/AlertSent.pm
@@ -18,8 +18,9 @@ __PACKAGE__->add_columns(
"whenqueued",
{
data_type => "timestamp",
- default_value => \"ms_current_timestamp()",
+ default_value => \"current_timestamp",
is_nullable => 0,
+ original => { default_value => \"now()" },
},
);
__PACKAGE__->belongs_to(
@@ -30,8 +31,8 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2013-09-10 17:11:54
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:COwsprqRSNZS1IxJrPYgMQ
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:/+Vodu8VJxJ0EY9P3Qjjjw
# You can replace this text with custom code or comments, and it will be preserved on regeneration
diff --git a/perllib/FixMyStreet/DB/Result/Body.pm b/perllib/FixMyStreet/DB/Result/Body.pm
index 0c1046cd8..a2e004c6a 100644
--- a/perllib/FixMyStreet/DB/Result/Body.pm
+++ b/perllib/FixMyStreet/DB/Result/Body.pm
@@ -18,12 +18,6 @@ __PACKAGE__->add_columns(
is_nullable => 0,
sequence => "body_id_seq",
},
- "name",
- { data_type => "text", is_nullable => 0 },
- "external_url",
- { data_type => "text", is_nullable => 1 },
- "parent",
- { data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"endpoint",
{ data_type => "text", is_nullable => 1 },
"jurisdiction",
@@ -42,8 +36,14 @@ __PACKAGE__->add_columns(
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
"send_extended_statuses",
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
+ "name",
+ { data_type => "text", is_nullable => 0 },
+ "parent",
+ { data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"deleted",
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
+ "external_url",
+ { data_type => "text", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->has_many(
@@ -87,6 +87,12 @@ __PACKAGE__->belongs_to(
},
);
__PACKAGE__->has_many(
+ "response_templates",
+ "FixMyStreet::DB::Result::ResponseTemplate",
+ { "foreign.body_id" => "self.id" },
+ { cascade_copy => 0, cascade_delete => 0 },
+);
+__PACKAGE__->has_many(
"user_body_permissions",
"FixMyStreet::DB::Result::UserBodyPermission",
{ "foreign.body_id" => "self.id" },
@@ -100,8 +106,8 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2014-07-29 13:54:07
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:PhUeFDRLSQVMk7Sts5K6MQ
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-02-19 16:13:43
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:d6GuQm8vrNmCc4NWw58srA
sub url {
my ( $self, $c, $args ) = @_;
diff --git a/perllib/FixMyStreet/DB/Result/Comment.pm b/perllib/FixMyStreet/DB/Result/Comment.pm
index 836462ed5..0caaa8968 100644
--- a/perllib/FixMyStreet/DB/Result/Comment.pm
+++ b/perllib/FixMyStreet/DB/Result/Comment.pm
@@ -31,8 +31,9 @@ __PACKAGE__->add_columns(
"created",
{
data_type => "timestamp",
- default_value => \"ms_current_timestamp()",
+ default_value => \"current_timestamp",
is_nullable => 0,
+ original => { default_value => \"now()" },
},
"confirmed",
{ data_type => "timestamp", is_nullable => 1 },
@@ -88,8 +89,8 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2014-07-31 15:59:43
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:08AtJ6CZFyUe7qKMF50MHg
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ZR+YNA1Jej3s+8mr52iq6Q
#
__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn");
@@ -144,7 +145,7 @@ sub confirm {
my $self = shift;
$self->state( 'confirmed' );
- $self->confirmed( \'ms_current_timestamp()' );
+ $self->confirmed( \'current_timestamp' );
}
=head2 get_photo_params
diff --git a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
index 08d03f94b..d7240cd5d 100644
--- a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
+++ b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
@@ -33,8 +33,9 @@ __PACKAGE__->add_columns(
"created",
{
data_type => "timestamp",
- default_value => \"ms_current_timestamp()",
+ default_value => \"current_timestamp",
is_nullable => 0,
+ original => { default_value => \"now()" },
},
);
__PACKAGE__->set_primary_key("id");
@@ -46,7 +47,7 @@ __PACKAGE__->belongs_to(
{
is_deferrable => 0,
join_type => "LEFT",
- on_delete => "NO ACTION",
+ on_delete => "CASCADE,",
on_update => "NO ACTION",
},
);
@@ -54,12 +55,12 @@ __PACKAGE__->belongs_to(
"problem",
"FixMyStreet::DB::Result::Problem",
{ id => "problem_id" },
- { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
+ { is_deferrable => 0, on_delete => "CASCADE,", on_update => "NO ACTION" },
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2014-07-31 15:59:43
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:yR1Vi7cJQrX67dFwAcJW6w
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:DBtGjCJykDtLnGtkj638eA
# 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 bed2f160a..3b7f8bcfd 100644
--- a/perllib/FixMyStreet/DB/Result/Problem.pm
+++ b/perllib/FixMyStreet/DB/Result/Problem.pm
@@ -53,8 +53,9 @@ __PACKAGE__->add_columns(
"created",
{
data_type => "timestamp",
- default_value => \"ms_current_timestamp()",
+ default_value => \"current_timestamp",
is_nullable => 0,
+ original => { default_value => \"now()" },
},
"confirmed",
{ data_type => "timestamp", is_nullable => 1 },
@@ -71,8 +72,9 @@ __PACKAGE__->add_columns(
"lastupdate",
{
data_type => "timestamp",
- default_value => \"ms_current_timestamp()",
+ default_value => \"current_timestamp",
is_nullable => 0,
+ original => { default_value => \"now()" },
},
"whensent",
{ data_type => "timestamp", is_nullable => 1 },
@@ -102,6 +104,8 @@ __PACKAGE__->add_columns(
{ data_type => "integer", default_value => 0, is_nullable => 1 },
"subcategory",
{ data_type => "text", is_nullable => 1 },
+ "bodies_missing",
+ { data_type => "text", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->has_many(
@@ -130,8 +134,8 @@ __PACKAGE__->belongs_to(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2014-07-31 15:57:02
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:EvD4sS1mdJJyI1muZ4TrCw
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Go+T9oFRfwQ1Ag89qPpF/g
# Add fake relationship to stored procedure table
__PACKAGE__->has_one(
@@ -319,10 +323,6 @@ sub visible_states_remove {
}
}
-sub visible_states_add_unconfirmed {
- $_[0]->visible_states_add('unconfirmed')
-}
-
=head2
@states = FixMyStreet::DB::Problem::council_states();
@@ -400,7 +400,7 @@ sub check_for_errors {
$errors{bodies} = _('No council selected')
unless $self->bodies_str
- && $self->bodies_str =~ m/^(?:-1|[\d,]+(?:\|[\d,]+)?)$/;
+ && $self->bodies_str =~ m/^(?:-1|[\d,]+)$/;
if ( !$self->name || $self->name !~ m/\S/ ) {
$errors{name} = _('Please enter your name');
@@ -441,15 +441,14 @@ sub confirm {
return if $self->state eq 'confirmed';
$self->state('confirmed');
- $self->confirmed( \'ms_current_timestamp()' );
+ $self->confirmed( \'current_timestamp' );
return 1;
}
sub bodies_str_ids {
my $self = shift;
return unless $self->bodies_str;
- (my $bodies = $self->bodies_str) =~ s/\|.*$//;
- my @bodies = split( /,/, $bodies );
+ my @bodies = split( /,/, $self->bodies_str );
return \@bodies;
}
@@ -480,11 +479,17 @@ sub url {
=head2 get_photo_params
-Returns a hashref of details of any attached photo for use in templates.
+Returns a hashref of details of the attached photo, if any, for use in templates.
+
+NB: this method doesn't currently support multiple photos gracefully.
+
+Use get_photoset($c) instead to do the right thing with reports with 0, 1, or more photos.
=cut
sub get_photo_params {
+ # use Carp 'cluck';
+ # cluck "get_photo_params called"; # TEMPORARY die to make sure I've done right thing with Zurich templates
my $self = shift;
return FixMyStreet::App::get_photo_params($self, 'id');
}
@@ -634,6 +639,25 @@ sub body {
return $body;
}
+=head2 response_templates
+
+Returns all ResponseTemplates attached to this problem's bodies, in alphabetical
+order of title.
+
+=cut
+
+sub response_templates {
+ my $problem = shift;
+ return FixMyStreet::App->model('DB::ResponseTemplate')->search(
+ {
+ body_id => $problem->bodies_str_ids
+ },
+ {
+ order_by => 'title'
+ }
+ );
+}
+
# 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).
@@ -642,8 +666,7 @@ sub body {
# 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->bodies_str =~ /2237/)) {
+ if ($self->external_id && $self->send_method_used && $self->bodies_str =~ /2237/) {
return 1;
}
return 0;
@@ -735,8 +758,8 @@ sub update_from_open311_service_request {
{
problem_id => $self->id,
state => 'confirmed',
- created => $updated || \'ms_current_timestamp()',
- confirmed => \'ms_current_timestamp()',
+ created => $updated || \'current_timestamp',
+ confirmed => \'current_timestamp',
text => $status_notes,
mark_open => 0,
mark_fixed => 0,
@@ -789,7 +812,7 @@ sub update_send_failed {
$self->update( {
send_fail_count => $self->send_fail_count + 1,
- send_fail_timestamp => \'ms_current_timestamp()',
+ send_fail_timestamp => \'current_timestamp',
send_fail_reason => $msg
} );
}
@@ -828,6 +851,27 @@ sub latest_moderation_log_entry {
return $self->admin_log_entries->search({ action => 'moderation' }, { order_by => 'id desc' })->first;
}
+=head2 get_photoset
+
+Return a PhotoSet object for all photos attached to this field
+
+ my $photoset = $obj->get_photoset( $c );
+ print $photoset->num_images;
+ return $photoset->get_image_data(num => 0, size => 'full');
+
+=cut
+
+sub get_photoset {
+ my ($self, $c) = @_;
+ my $class = 'FixMyStreet::App::Model::PhotoSet';
+ eval "use $class";
+ return $class->new({
+ c => $c,
+ data => $self->photo,
+ object => $self,
+ });
+}
+
__PACKAGE__->has_many(
"admin_log_entries",
"FixMyStreet::DB::Result::AdminLog",
@@ -838,6 +882,18 @@ __PACKAGE__->has_many(
}
);
+sub get_time_spent {
+ my $self = shift;
+ my $admin_logs = $self->admin_log_entries->search({},
+ {
+ group_by => 'object_id',
+ columns => [
+ { sum_time_spent => { sum => 'time_spent' } },
+ ]
+ })->single;
+ return $admin_logs ? $admin_logs->get_column('sum_time_spent') : 0;
+}
+
# we need the inline_constructor bit as we don't inherit from Moose
__PACKAGE__->meta->make_immutable( inline_constructor => 0 );
diff --git a/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm b/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm
new file mode 100644
index 000000000..48a1ab3ae
--- /dev/null
+++ b/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm
@@ -0,0 +1,49 @@
+use utf8;
+package FixMyStreet::DB::Result::ResponseTemplate;
+
+# 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("response_templates");
+__PACKAGE__->add_columns(
+ "id",
+ {
+ data_type => "integer",
+ is_auto_increment => 1,
+ is_nullable => 0,
+ sequence => "response_templates_id_seq",
+ },
+ "body_id",
+ { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
+ "title",
+ { data_type => "text", is_nullable => 0 },
+ "text",
+ { data_type => "text", is_nullable => 0 },
+ "created",
+ {
+ data_type => "timestamp",
+ default_value => \"current_timestamp",
+ is_nullable => 0,
+ },
+);
+__PACKAGE__->set_primary_key("id");
+__PACKAGE__->add_unique_constraint("response_templates_body_id_title_key", ["body_id", "title"]);
+__PACKAGE__->belongs_to(
+ "body",
+ "FixMyStreet::DB::Result::Body",
+ { id => "body_id" },
+ { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" },
+);
+
+
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-02-19 16:13:43
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:xzhmxtu0taAnBMZN0HBocw
+
+
+# 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/Token.pm b/perllib/FixMyStreet/DB/Result/Token.pm
index 0156af137..a60e23839 100644
--- a/perllib/FixMyStreet/DB/Result/Token.pm
+++ b/perllib/FixMyStreet/DB/Result/Token.pm
@@ -20,15 +20,16 @@ __PACKAGE__->add_columns(
"created",
{
data_type => "timestamp",
- default_value => \"ms_current_timestamp()",
+ default_value => \"current_timestamp",
is_nullable => 0,
+ original => { default_value => \"now()" },
},
);
__PACKAGE__->set_primary_key("scope", "token");
-# Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-03-08 17:19:55
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:+LLZ8P5GXqPetuGyrra2vw
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2015-08-13 16:33:38
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:HkvzOY5STjOdXN64hxg5NA
use mySociety::AuthToken;
@@ -42,8 +43,7 @@ Representation of mySociety::AuthToken in the DBIx::Class world.
The 'data' value is automatically inflated and deflated in the same way that the
AuthToken would do it. 'token' is set to a new random value by default and the
-'created' timestamp is achieved using the database function
-ms_current_timestamp.
+'created' timestamp is achieved using the database function current_timestamp.
=cut
@@ -55,7 +55,7 @@ sub new {
my ( $class, $attrs ) = @_;
$attrs->{token} ||= mySociety::AuthToken::random_token();
- $attrs->{created} ||= \'ms_current_timestamp()';
+ $attrs->{created} ||= \'current_timestamp';
my $new = $class->next::method($attrs);
return $new;
diff --git a/perllib/FixMyStreet/DB/ResultSet/Alert.pm b/perllib/FixMyStreet/DB/ResultSet/Alert.pm
index 5848265f1..bb1c61141 100644
--- a/perllib/FixMyStreet/DB/ResultSet/Alert.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/Alert.pm
@@ -14,7 +14,7 @@ sub timeline_created {
return $rs->search(
{
- whensubscribed => { '>=', \"ms_current_timestamp()-'7 days'::interval" },
+ whensubscribed => { '>=', \"current_timestamp-'7 days'::interval" },
confirmed => 1,
%{ $restriction },
},
@@ -29,7 +29,7 @@ sub timeline_disabled {
return $rs->search(
{
- whendisabled => { '>=', \"ms_current_timestamp()-'7 days'::interval" },
+ whendisabled => { '>=', \"current_timestamp-'7 days'::interval" },
%{ $restriction },
},
);
diff --git a/perllib/FixMyStreet/DB/ResultSet/AlertType.pm b/perllib/FixMyStreet/DB/ResultSet/AlertType.pm
index 0b430008a..25c727e25 100644
--- a/perllib/FixMyStreet/DB/ResultSet/AlertType.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/AlertType.pm
@@ -30,18 +30,19 @@ sub email_alerts ($) {
$item_table.name as item_name, $item_table.anonymous as item_anonymous,
$item_table.confirmed as item_confirmed,
$head_table.*
- from alert
- inner join $item_table on alert.parameter::integer = $item_table.${head_table}_id
- inner join $head_table on alert.parameter::integer = $head_table.id
+ from alert, $item_table, $head_table
+ where alert.parameter::integer = $head_table.id
+ and $item_table.${head_table}_id = $head_table.id
";
} else {
$query .= " $item_table.*,
$item_table.id as item_id
- from alert, $item_table";
+ from alert, $item_table
+ where 1 = 1";
}
$query .= "
- where alert_type='$ref' and whendisabled is null and $item_table.confirmed >= whensubscribed
- and $item_table.confirmed >= ms_current_timestamp() - '7 days'::interval
+ and alert_type='$ref' and whendisabled is null and $item_table.confirmed >= whensubscribed
+ and $item_table.confirmed >= current_timestamp - '7 days'::interval
and (select whenqueued from alert_sent where alert_sent.alert_id = alert.id and alert_sent.parameter::integer = $item_table.id) is null
and $item_table.user_id <> alert.user_id
and " . $alert_type->item_where . "
@@ -69,8 +70,6 @@ sub email_alerts ($) {
# this is for the new_updates alerts
next if $row->{non_public} and $row->{user_id} != $row->{alert_user_id};
- my $hashref_restriction = $cobrand->site_restriction( $row->{cobrand_data} );
-
FixMyStreet::App->model('DB::AlertSent')->create( {
alert_id => $row->{alert_id},
parameter => $row->{item_id},
@@ -89,10 +88,7 @@ sub email_alerts ($) {
$data{state_message} = _("This report is currently marked as open.");
}
- my $url = $cobrand->base_url( $row->{alert_cobrand_data} );
- if ( $hashref_restriction && $hashref_restriction->{bodies_str} && $row->{bodies_str} ne $hashref_restriction->{bodies_str} ) {
- $url = mySociety::Config::get('BASE_URL');
- }
+ my $url = $cobrand->base_url_for_report($row);
# this is currently only for new_updates
if ($row->{item_text}) {
if ( $cobrand->moniker ne 'zurich' && $row->{alert_user_id} == $row->{user_id} ) {
@@ -171,7 +167,6 @@ sub email_alerts ($) {
my $longitude = $alert->parameter;
my $latitude = $alert->parameter2;
- my $hashref_restriction = $cobrand->site_restriction( $alert->cobrand_data );
my $d = mySociety::Gaze::get_radius_containing_population($latitude, $longitude, 200000);
# Convert integer to GB locale string (with a ".")
$d = mySociety::Locale::in_gb_locale {
@@ -184,7 +179,7 @@ sub email_alerts ($) {
and problem.user_id = users.id
and problem.state in ($states)
and problem.non_public = 'f'
- and problem.confirmed >= ? and problem.confirmed >= ms_current_timestamp() - '7 days'::interval
+ and problem.confirmed >= ? and problem.confirmed >= current_timestamp - '7 days'::interval
and (select whenqueued from alert_sent where alert_sent.alert_id = ? and alert_sent.parameter::integer = problem.id) is null
and users.email <> ?
order by confirmed desc";
@@ -195,10 +190,7 @@ sub email_alerts ($) {
alert_id => $alert->id,
parameter => $row->{id},
} );
- my $url = $cobrand->base_url( $alert->cobrand_data );
- if ( $hashref_restriction && $hashref_restriction->{bodies_str} && $row->{bodies_str} ne $hashref_restriction->{bodies_str} ) {
- $url = mySociety::Config::get('BASE_URL');
- }
+ my $url = $cobrand->base_url_for_report($row);
$data{data} .= $url . "/report/" . $row->{id} . " - $row->{title}\n\n";
if ( exists $row->{geocode} && $row->{geocode} ) {
my $nearest_st = _get_address_from_gecode( $row->{geocode} );
@@ -241,15 +233,13 @@ sub _send_aggregated_alert_email(%) {
my $template = FixMyStreet->get_email_template($cobrand->moniker, $data{lang}, "$data{template}.txt");
- my $sender = FixMyStreet->config('DO_NOT_REPLY_EMAIL');
my $result = FixMyStreet::App->send_email_cron(
{
_template_ => $template,
_parameters_ => \%data,
- From => [ $sender, _($cobrand->contact_name) ],
To => $data{alert_email},
},
- $sender,
+ undef,
0,
$cobrand,
$data{lang}
diff --git a/perllib/FixMyStreet/DB/ResultSet/Comment.pm b/perllib/FixMyStreet/DB/ResultSet/Comment.pm
index 70f8027aa..1b6afb819 100644
--- a/perllib/FixMyStreet/DB/ResultSet/Comment.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/Comment.pm
@@ -4,19 +4,24 @@ use base 'DBIx::Class::ResultSet';
use strict;
use warnings;
+sub to_body {
+ my ($rs, $body_restriction) = @_;
+ return FixMyStreet::DB::ResultSet::Problem::to_body($rs, $body_restriction);
+}
+
+
sub timeline {
- my ( $rs, $restriction ) = @_;
+ my ( $rs, $body_restriction ) = @_;
my $prefetch =
FixMyStreet::App->model('DB')->schema->storage->sql_maker->quote_char ?
[ qw/user/ ] :
[];
- return $rs->search(
+ return $rs->to_body($body_restriction)->search(
{
state => 'confirmed',
- created => { '>=', \"ms_current_timestamp()-'7 days'::interval" },
- %{ $restriction },
+ created => { '>=', \"current_timestamp-'7 days'::interval" },
},
{
prefetch => $prefetch,
@@ -25,17 +30,18 @@ sub timeline {
}
sub summary_count {
- my ( $rs, $restriction ) = @_;
+ my ( $rs, $body_restriction ) = @_;
- return $rs->search(
- $restriction,
- {
- group_by => ['me.state'],
- select => [ 'me.state', { count => 'me.id' } ],
- as => [qw/state state_count/],
- join => 'problem'
- }
- );
+ my $params = {
+ group_by => ['me.state'],
+ select => [ 'me.state', { count => 'me.id' } ],
+ as => [qw/state state_count/],
+ };
+ if ($body_restriction) {
+ $rs = $rs->to_body($body_restriction);
+ $params->{join} = 'problem';
+ }
+ return $rs->search(undef, $params);
}
1;
diff --git a/perllib/FixMyStreet/DB/ResultSet/Contact.pm b/perllib/FixMyStreet/DB/ResultSet/Contact.pm
index 6fa6a03a0..f402b5461 100644
--- a/perllib/FixMyStreet/DB/ResultSet/Contact.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/Contact.pm
@@ -4,6 +4,8 @@ use base 'DBIx::Class::ResultSet';
use strict;
use warnings;
+sub me { join('.', shift->current_source_alias, shift || q{}) }
+
=head2 not_deleted
$rs = $rs->not_deleted();
@@ -14,7 +16,7 @@ Filter down to not deleted contacts - which have C<deleted> set to false;
sub not_deleted {
my $rs = shift;
- return $rs->search( { deleted => 0 } );
+ return $rs->search( { $rs->me('deleted') => 0 } );
}
sub summary_count {
diff --git a/perllib/FixMyStreet/DB/ResultSet/Nearby.pm b/perllib/FixMyStreet/DB/ResultSet/Nearby.pm
index a0ccb8a6d..a6b00ef7b 100644
--- a/perllib/FixMyStreet/DB/ResultSet/Nearby.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/Nearby.pm
@@ -19,12 +19,10 @@ sub nearby {
if $interval;
$params->{id} = { -not_in => $ids }
if $ids;
- $params = {
- %{ $c->cobrand->problems_clause },
- %$params
- } if $c->cobrand->problems_clause;
$params->{category} = $category if $category;
+ $rs = FixMyStreet::DB::ResultSet::Problem::to_body($rs, $c->cobrand->body_restriction);
+
my $attrs = {
prefetch => 'problem',
bind => [ $mid_lat, $mid_lon, $dist ],
@@ -36,16 +34,4 @@ sub nearby {
return \@problems;
}
-# XXX Not currently used, so not migrating at present.
-#sub fixed_nearby {
-# my ($dist, $mid_lat, $mid_lon) = @_;
-# mySociety::Locale::in_gb_locale { select_all(
-# "select id, title, latitude, longitude, distance
-# from problem_find_nearby(?, ?, $dist) as nearby, problem
-# where nearby.problem_id = problem.id and state='fixed'
-# site_restriction
-# order by lastupdate desc", $mid_lat, $mid_lon);
-# }
-#}
-
1;
diff --git a/perllib/FixMyStreet/DB/ResultSet/Problem.pm b/perllib/FixMyStreet/DB/ResultSet/Problem.pm
index 7885c28b3..e9f5d0f8e 100644
--- a/perllib/FixMyStreet/DB/ResultSet/Problem.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/Problem.pm
@@ -20,6 +20,18 @@ sub set_restriction {
$site_key = $key;
}
+sub to_body {
+ my ($rs, $bodies) = @_;
+ return $rs unless $bodies;
+ unless (ref $bodies eq 'ARRAY') {
+ $bodies = [ map { ref $_ ? $_->id : $_ } $bodies ];
+ }
+ $rs = $rs->search(
+ \[ "regexp_split_to_array(bodies_str, ',') && ?", [ {} => $bodies ] ]
+ );
+ return $rs;
+}
+
# Front page statistics
sub recent_fixed {
@@ -168,9 +180,9 @@ sub timeline {
return $rs->search(
{
-or => {
- created => { '>=', \"ms_current_timestamp()-'7 days'::interval" },
- confirmed => { '>=', \"ms_current_timestamp()-'7 days'::interval" },
- whensent => { '>=', \"ms_current_timestamp()-'7 days'::interval" },
+ created => { '>=', \"current_timestamp-'7 days'::interval" },
+ confirmed => { '>=', \"current_timestamp-'7 days'::interval" },
+ whensent => { '>=', \"current_timestamp-'7 days'::interval" },
}
},
{
@@ -198,7 +210,10 @@ sub unique_users {
return $rs->search( {
state => [ FixMyStreet::DB::Result::Problem->visible_states() ],
}, {
- select => [ { count => { distinct => 'user_id' } } ],
+ select => [ { distinct => 'user_id' } ],
+ as => [ 'user_id' ]
+ } )->as_subselect_rs->search( undef, {
+ select => [ { count => 'user_id' } ],
as => [ 'count' ]
} )->first->get_column('count');
}
@@ -235,8 +250,8 @@ sub send_reports {
my $site = $site_override || CronFns::site($base_url);
my $states = [ 'confirmed', 'fixed' ];
- $states = [ 'unconfirmed', 'confirmed', 'in progress', 'planned', 'closed' ] if $site eq 'zurich';
- my $unsent = FixMyStreet::App->model("DB::Problem")->search( {
+ $states = [ 'unconfirmed', 'confirmed', 'in progress', 'planned', 'closed', 'investigating' ] if $site eq 'zurich';
+ my $unsent = $rs->search( {
state => $states,
whensent => undef,
bodies_str => { '!=', undef },
@@ -320,15 +335,19 @@ sub send_reports {
$cobrand->process_additional_metadata_for_email($row, \%h);
}
- # XXX Needs locks!
- # XXX Only copes with at most one missing body
- my ($bodies, $missing) = $row->bodies_str =~ /^([\d,]+)(?:\|(\d+))?/;
- my @bodies = split(/,/, $bodies);
- $bodies = FixMyStreet::App->model("DB::Body")->search(
- { id => \@bodies },
+ my $bodies = FixMyStreet::App->model("DB::Body")->search(
+ { id => $row->bodies_str_ids },
{ order_by => 'name' },
);
- $missing = FixMyStreet::App->model("DB::Body")->find($missing) if $missing;
+
+ my $missing;
+ if ($row->bodies_missing) {
+ my @missing = FixMyStreet::App->model("DB::Body")->search(
+ { id => [ split /,/, $row->bodies_missing ] },
+ { order_by => 'name' }
+ )->get_column('name')->all;
+ $missing = join(' / ', @missing) if @missing;
+ }
my @dear;
my %reporters = ();
@@ -337,7 +356,7 @@ sub send_reports {
my $sender = "FixMyStreet::SendReport::" . $sender_info->{method};
if ( ! exists $senders->{ $sender } ) {
- warn "No such sender [ $sender ] for body $body->name ( $body->id )";
+ warn sprintf "No such sender [ $sender ] for body %s ( %d )", $body->name, $body->id;
next;
}
$reporters{ $sender } ||= $sender->new();
@@ -400,7 +419,7 @@ sub send_reports {
$h{missing} = '';
if ($missing) {
$h{missing} = '[ '
- . sprintf(_('We realise this problem might be the responsibility of %s; however, we don\'t currently have any contact details for them. If you know of an appropriate contact address, please do get in touch.'), $missing->name)
+ . sprintf(_('We realise this problem might be the responsibility of %s; however, we don\'t currently have any contact details for them. If you know of an appropriate contact address, please do get in touch.'), $missing)
. " ]\n\n";
}
@@ -433,8 +452,8 @@ sub send_reports {
unless ($result) {
$row->update( {
- whensent => \'ms_current_timestamp()',
- lastupdate => \'ms_current_timestamp()',
+ whensent => \'current_timestamp',
+ lastupdate => \'current_timestamp',
} );
if ( $cobrand->report_sent_confirmation_email && !$h{anonymous_report}) {
_send_report_sent_email( $row, \%h, $nomail, $cobrand );
@@ -468,7 +487,7 @@ sub send_reports {
}
}
my $sending_errors = '';
- my $unsent = FixMyStreet::App->model("DB::Problem")->search( {
+ my $unsent = $rs->search( {
state => [ 'confirmed', 'fixed' ],
whensent => undef,
bodies_str => { '!=', undef },
@@ -499,7 +518,7 @@ sub _send_report_sent_email {
_template_ => $template,
_parameters_ => $h,
To => $row->user->email,
- From => mySociety::Config::get('CONTACT_EMAIL'),
+ From => [ mySociety::Config::get('CONTACT_EMAIL'), $cobrand->contact_name ],
},
mySociety::Config::get('CONTACT_EMAIL'),
$nomail,
diff --git a/perllib/FixMyStreet/DB/ResultSet/Questionnaire.pm b/perllib/FixMyStreet/DB/ResultSet/Questionnaire.pm
index 63a91697d..bf1c68c49 100644
--- a/perllib/FixMyStreet/DB/ResultSet/Questionnaire.pm
+++ b/perllib/FixMyStreet/DB/ResultSet/Questionnaire.pm
@@ -22,7 +22,7 @@ sub send_questionnaires_period {
whensent => [
'-and',
{ '!=', undef },
- { '<', \"ms_current_timestamp() - '$period'::interval" },
+ { '<', \"current_timestamp - '$period'::interval" },
],
send_questionnaire => 1,
};
@@ -34,7 +34,7 @@ sub send_questionnaires_period {
} else {
$q_params->{'-or'} = [
'(select max(whensent) from questionnaire where me.id=problem_id)' => undef,
- '(select max(whenanswered) from questionnaire where me.id=problem_id)' => { '<', \"ms_current_timestamp() - '$period'::interval" }
+ '(select max(whenanswered) from questionnaire where me.id=problem_id)' => { '<', \"current_timestamp - '$period'::interval" }
];
}
@@ -70,7 +70,7 @@ sub send_questionnaires_period {
my $questionnaire = FixMyStreet::App->model('DB::Questionnaire')->create( {
problem_id => $row->id,
- whensent => \'ms_current_timestamp()',
+ whensent => \'current_timestamp',
} );
# We won't send another questionnaire unless they ask for it (or it was
@@ -84,9 +84,6 @@ sub send_questionnaires_period {
} );
$h{url} = $cobrand->base_url($row->cobrand_data) . '/Q/' . $token->token;
- my $sender = FixMyStreet->config('DO_NOT_REPLY_EMAIL');
- my $sender_name = _($cobrand->contact_name);
-
print "Sending questionnaire " . $questionnaire->id . ", problem "
. $row->id . ", token " . $token->token . " to "
. $row->user->email . "\n"
@@ -97,9 +94,8 @@ sub send_questionnaires_period {
_template_ => $template,
_parameters_ => \%h,
To => [ [ $row->user->email, $row->name ] ],
- From => [ $sender, $sender_name ],
},
- $sender,
+ undef,
$params->{nomail},
$cobrand
);
@@ -117,32 +113,36 @@ sub send_questionnaires_period {
sub timeline {
my ( $rs, $restriction ) = @_;
+ my $attrs;
+ if (%$restriction) {
+ $attrs = {
+ -select => [qw/me.*/],
+ prefetch => [qw/problem/],
+ }
+ }
return $rs->search(
{
-or => {
- whenanswered => { '>=', \"ms_current_timestamp()-'7 days'::interval" },
- 'me.whensent' => { '>=', \"ms_current_timestamp()-'7 days'::interval" },
+ whenanswered => { '>=', \"current_timestamp-'7 days'::interval" },
+ 'me.whensent' => { '>=', \"current_timestamp-'7 days'::interval" },
},
%{ $restriction },
},
- {
- -select => [qw/me.*/],
- prefetch => [qw/problem/],
- }
+ $attrs
);
}
sub summary_count {
my ( $rs, $restriction ) = @_;
- return $rs->search(
- $restriction,
- {
- group_by => [ \'whenanswered is not null' ],
- select => [ \'(whenanswered is not null)', { count => 'me.id' } ],
- as => [qw/answered questionnaire_count/],
- join => 'problem'
- }
- );
+ my $params = {
+ group_by => [ \'whenanswered is not null' ],
+ select => [ \'(whenanswered is not null)', { count => 'me.id' } ],
+ as => [qw/answered questionnaire_count/],
+ };
+ if (%$restriction) {
+ $params->{join} = 'problem';
+ }
+ return $rs->search($restriction, $params);
}
1;
diff --git a/perllib/FixMyStreet/Email.pm b/perllib/FixMyStreet/Email.pm
new file mode 100644
index 000000000..4a2784787
--- /dev/null
+++ b/perllib/FixMyStreet/Email.pm
@@ -0,0 +1,12 @@
+package FixMyStreet::Email;
+
+use Utils::Email;
+use FixMyStreet;
+
+sub test_dmarc {
+ my $email = shift;
+ return if FixMyStreet->test_mode;
+ return Utils::Email::test_dmarc($email);
+}
+
+1;
diff --git a/perllib/FixMyStreet/Geocode/Google.pm b/perllib/FixMyStreet/Geocode/Google.pm
index 35fcec36f..5261bb7e4 100644
--- a/perllib/FixMyStreet/Geocode/Google.pm
+++ b/perllib/FixMyStreet/Geocode/Google.pm
@@ -28,34 +28,32 @@ sub string {
$s = FixMyStreet::Geocode::escape($s);
- my $url = 'http://maps.google.com/maps/geo?q=' . $s;
- $url .= '&ll=' . $params->{centre} if $params->{centre};
- $url .= '&spn=' . $params->{span} if $params->{span};
+ my $url = 'https://maps.googleapis.com/maps/api/geocode/json?address=' . $s;
+ $url .= '&bounds=' . $params->{bounds}[0] . ',' . $params->{bounds}[1]
+ . '|' . $params->{bounds}[2] . ',' . $params->{bounds}[3]
+ if $params->{bounds};
if ($params->{google_country}) {
- $url .= '&gl=' . $params->{google_country};
+ $url .= '&region=' . $params->{google_country};
} elsif ($params->{country}) {
- $url .= '&gl=' . $params->{country};
+ $url .= '&region=' . $params->{country};
}
- $url .= '&hl=' . $params->{lang} if $params->{lang};
+ $url .= '&language=' . $params->{lang} if $params->{lang};
- my $args = 'sensor=false&key=' . FixMyStreet->config('GOOGLE_MAPS_API_KEY');
- my $js = FixMyStreet::Geocode::cache('google', $url, $args, qr/"code":6[12]0/);
+ my $args = 'key=' . FixMyStreet->config('GOOGLE_MAPS_API_KEY');
+ my $js = FixMyStreet::Geocode::cache('google', $url, $args, qr/"status"\s*:\s*"(OVER_QUERY_LIMIT|REQUEST_DENIED|INVALID_REQUEST|UNKNOWN_ERROR)"/);
if (!$js) {
return { error => _('Sorry, we could not parse that location. Please try again.') };
}
- if ($js->{Status}->{code} ne '200') {
- return { error => _('Sorry, we could not find that location.') };
- }
+ return unless $js->{status} eq 'OK';
- my $results = $js->{Placemark};
+ my $results = $js->{results};
my ( $error, @valid_locations, $latitude, $longitude );
foreach (@$results) {
- next unless $_->{AddressDetails}->{Accuracy} >= 4;
- my $address = $_->{address};
+ my $address = $_->{formatted_address};
next unless $c->cobrand->geocoded_string_check( $address );
( $longitude, $latitude ) =
map { Utils::truncate_coordinate($_) }
- @{ $_->{Point}->{coordinates} };
+ ($_->{geometry}{location}{lat}, $_->{geometry}{location}{lng});
push (@$error, {
address => $address,
latitude => $latitude,
diff --git a/perllib/FixMyStreet/Geocode/Zurich.pm b/perllib/FixMyStreet/Geocode/Zurich.pm
index aad918b0e..50a7c355e 100644
--- a/perllib/FixMyStreet/Geocode/Zurich.pm
+++ b/perllib/FixMyStreet/Geocode/Zurich.pm
@@ -31,6 +31,7 @@ sub setup_soap {
my $action = "$attr/IFixMyZuerich/";
require SOAP::Lite;
+ # SOAP::Lite->import( +trace => [transport => \&log_message ] );
# Set up the SOAP handler
$security = SOAP::Header->name("Security")->attr({
@@ -109,5 +110,15 @@ sub string {
return { error => $error };
}
+sub log_message {
+ my ($in) = @_;
+ eval {
+ printf "log_message [$in]: %s\n\n", $in->content; # ...for example
+ };
+ if ($@) {
+ print "log_message [$in]: ???? \n\n";
+ }
+}
+
1;
diff --git a/perllib/FixMyStreet/Map.pm b/perllib/FixMyStreet/Map.pm
index b050592ba..81b81f656 100644
--- a/perllib/FixMyStreet/Map.pm
+++ b/perllib/FixMyStreet/Map.pm
@@ -35,6 +35,16 @@ sub allowed_maps {
return grep { $avail{$_} } @allowed;
}
+=head2 reload_allowed_maps
+
+Allows tests to override MAP_TYPE at run time.
+
+=cut
+
+sub reload_allowed_maps {
+ @ALL_MAP_CLASSES = allowed_maps();
+}
+
=head2 map_class
Set and return the appropriate class given a query parameter string.
diff --git a/perllib/FixMyStreet/Map/Zurich.pm b/perllib/FixMyStreet/Map/Zurich.pm
index 9b01f2978..3e198f820 100644
--- a/perllib/FixMyStreet/Map/Zurich.pm
+++ b/perllib/FixMyStreet/Map/Zurich.pm
@@ -11,25 +11,53 @@ use Geo::Coordinates::CH1903;
use Math::Trig;
use Utils;
-use constant ZOOM_LEVELS => 8;
+use constant ZOOM_LEVELS => 9;
use constant DEFAULT_ZOOM => 5;
use constant MIN_ZOOM_LEVEL => 0;
use constant ID_OFFSET => 2;
+use constant TILE_SIZE => 512;
sub map_tiles {
- my ( $self, %params ) = @_;
- my ( $col, $row, $z ) = ( $params{x_tile}, $params{y_tile}, $params{matrix_id} );
+ my ($self, %params) = @_;
+ my ($left_col, $top_row, $z) = @params{'x_left_tile', 'y_top_tile', 'matrix_id'};
my $tile_url = $self->base_tile_url();
+ my $cols = $params{cols};
+ my $rows = $params{rows};
+
+ my @col_offsets = (0.. ($cols-1) );
+ my @row_offsets = (0.. ($rows-1) );
+
return [
- "$tile_url/$z/" . ($row - 1) . "/" . ($col - 1) . ".jpg",
- "$tile_url/$z/" . ($row - 1) . "/$col.jpg",
- "$tile_url/$z/$row/" . ($col - 1) . ".jpg",
- "$tile_url/$z/$row/$col.jpg",
+ map {
+ my $row_offset = $_;
+ [
+ map {
+ my $col_offset = $_;
+ my $row = $top_row + $row_offset;
+ my $col = $left_col + $col_offset;
+ my $src = sprintf '%s/%d/%d/%d.jpg',
+ $tile_url, $z, $row, $col;
+ my $dotted_id = sprintf '%d.%d', $col, $row;
+
+ # return the data structure for the cell
+ +{
+ src => $src,
+ row_offset => $row_offset,
+ col_offset => $col_offset,
+ dotted_id => $dotted_id,
+ alt => "Map tile $dotted_id", # TODO "NW map tile"?
+ }
+ }
+ @col_offsets
+ ]
+ }
+ @row_offsets
];
}
sub base_tile_url {
- return '/maps/Hybrid/1.0.0/Hybrid/default/nativeTileMatrixSet';
+ # use the new 512px maps as used by Javascript
+ return 'http://www.gis.stadt-zuerich.ch/maps/rest/services/tiled/LuftbildHybrid/MapServer/WMTS/tile/1.0.0/tiled_LuftbildHybrid/default/default028mm';
}
sub copyright {
@@ -50,29 +78,77 @@ sub display_map {
$params{longitude} = Utils::truncate_coordinate($c->get_param('lon') + 0)
if defined $c->get_param('lon');
- my $zoom = defined $c->get_param('zoom')
- ? $c->get_param('zoom') + 0
- : $c->stash->{page} eq 'report'
- ? DEFAULT_ZOOM+1
- : DEFAULT_ZOOM;
- $zoom = ZOOM_LEVELS - 1 if $zoom >= ZOOM_LEVELS;
- $zoom = 0 if $zoom < 0;
+ $params{rows} //= 2; # 2x2 square is default
+ $params{cols} //= 2;
+
+ $params{zoom} = do {
+ my $zoom = defined $c->get_param('zoom')
+ ? $c->get_param('zoom') + 0
+ : $c->stash->{page} eq 'report'
+ ? DEFAULT_ZOOM+1
+ : DEFAULT_ZOOM;
+ $zoom = ZOOM_LEVELS - 1 if $zoom >= ZOOM_LEVELS;
+ $zoom = 0 if $zoom < 0;
+ $zoom;
+ };
+
+ $c->stash->{map} = $self->get_map_hash( %params );
- ($params{x_tile}, $params{y_tile}, $params{matrix_id}) = latlon_to_tile_with_adjust($params{latitude}, $params{longitude}, $zoom);
+ if ($params{print_report}) {
+ $params{zoom}++ unless $params{zoom} >= ZOOM_LEVELS;
+ $c->stash->{print_report_map}
+ = $self->get_map_hash(
+ %params,
+ img_type => 'img',
+ cols => 4, rows => 4,
+ );
+ # NB: we can passthrough img_type as literal here, as only designed for print
- foreach my $pin (@{$params{pins}}) {
- ($pin->{px}, $pin->{py}) = latlon_to_px($pin->{latitude}, $pin->{longitude}, $params{x_tile}, $params{y_tile}, $zoom);
+ # NB we can do arbitrary size, including non-squares, however we'd have
+ # to modify .square-map style with padding-bottom percentage calculated in
+ # an inline style:
+ # <zarino> in which case, the only change that'd be required is
+ # removing { padding-bottom: 100% } from .square-map__outer, putting
+ # the percentage into an inline style on the element itself, and then
+ # probably renaming .square-map__* to .fixed-aspect-map__* or something
+ # since it's no longer necessarily square
}
+}
+
+sub get_map_hash {
+ my ($self, %params) = @_;
- $c->stash->{map} = {
+ @params{'x_centre_tile', 'y_centre_tile', 'matrix_id'}
+ = latlon_to_tile_with_adjust(
+ @params{'latitude', 'longitude', 'zoom', 'rows', 'cols'});
+
+ # centre_(row|col) is either in middle, or just to right.
+ # e.g. if centre is the number in parens:
+ # 1 (2) 3 => 2 - int( 3/2 ) = 1
+ # 1 2 (3) 4 => 3 - int( 4/2 ) = 1
+ $params{x_left_tile} = $params{x_centre_tile} - int($params{cols} / 2);
+ $params{y_top_tile} = $params{y_centre_tile} - int($params{rows} / 2);
+
+ $params{pins} = [
+ map {
+ my $pin = { %$_ }; # shallow clone
+ ($pin->{px}, $pin->{py})
+ = latlon_to_px($pin->{latitude}, $pin->{longitude},
+ @params{'x_left_tile', 'y_top_tile', 'zoom'});
+ $pin;
+ } @{ $params{pins} }
+ ];
+
+ return {
%params,
type => 'zurich',
map_type => 'OpenLayers.Layer.WMTS',
tiles => $self->map_tiles( %params ),
copyright => $self->copyright(),
- zoom => $zoom,
+ zoom => $params{zoom},,
zoomOffset => MIN_ZOOM_LEVEL,
numZoomLevels => ZOOM_LEVELS,
+ tile_size => TILE_SIZE,
};
}
@@ -83,29 +159,46 @@ sub latlon_to_tile($$$) {
my ($x, $y) = Geo::Coordinates::CH1903::from_latlon($lat, $lon);
my $matrix_id = $zoom + ID_OFFSET;
- my @scales = ( '250000', '125000', '64000', '32000', '16000', '8000', '4000', '2000', '1000', '500' );
+ my @scales = (
+ '250000', '125000',
+ '64000', '32000',
+ '16000', '8000',
+ '4000', '2000',
+ '1000', '500',
+ '250'
+ );
my $tileOrigin = { lat => 30814423, lon => -29386322 };
- my $tileSize = 256;
- my $res = $scales[$matrix_id] / (39.3701 * 96); # OpenLayers.INCHES_PER_UNIT[units] * OpenLayers.DOTS_PER_INCH
+ my $res = $scales[$matrix_id] / (39.3701 * 96);
+ # OpenLayers.INCHES_PER_UNIT[units] * OpenLayers.DOTS_PER_INCH
- my $fx = ( $x - $tileOrigin->{lon} ) / ($res * $tileSize);
- my $fy = ( $tileOrigin->{lat} - $y ) / ($res * $tileSize);
+ my $fx = ( $x - $tileOrigin->{lon} ) / ($res * TILE_SIZE);
+ my $fy = ( $tileOrigin->{lat} - $y ) / ($res * TILE_SIZE);
return ( $fx, $fy, $matrix_id );
}
# Given a lat/lon, convert it to OSM tile co-ordinates (nearest actual tile,
# adjusted so the point will be near the centre of a 2x2 tiled map).
-sub latlon_to_tile_with_adjust($$$) {
- my ($lat, $lon, $zoom) = @_;
- my ($x_tile, $y_tile, $matrix_id) = latlon_to_tile($lat, $lon, $zoom);
+#
+# Takes parameter for rows/cols. For even sizes (2x2, 4x4 etc.) will
+# do adjustment, but simply returns actual for odd sizes.
+#
+sub latlon_to_tile_with_adjust {
+ my ($lat, $lon, $zoom, $rows, $cols) = @_;
+ my ($x_tile, $y_tile, $matrix_id)
+ = my @ret
+ = latlon_to_tile($lat, $lon, $zoom);
- # Try and have point near centre of map
- if ($x_tile - int($x_tile) > 0.5) {
- $x_tile += 1;
+ # Try and have point near centre of map, passing through if odd
+ unless ($cols % 2) {
+ if ($x_tile - int($x_tile) > 0.5) {
+ $x_tile += 1;
+ }
}
- if ($y_tile - int($y_tile) > 0.5) {
- $y_tile += 1;
+ unless ($rows % 2) {
+ if ($y_tile - int($y_tile) > 0.5) {
+ $y_tile += 1;
+ }
}
return ( int($x_tile), int($y_tile), $matrix_id );
@@ -115,13 +208,12 @@ sub tile_to_latlon {
my ($fx, $fy, $zoom) = @_;
my $matrix_id = $zoom + ID_OFFSET;
- my @scales = ( '250000', '125000', '64000', '32000', '16000', '8000', '4000', '2000', '1000', '500' );
+ my @scales = ( '250000', '125000', '64000', '32000', '16000', '8000', '4000', '2000', '1000', '500', '250' );
my $tileOrigin = { lat => 30814423, lon => -29386322 };
- my $tileSize = 256;
my $res = $scales[$matrix_id] / (39.3701 * 96); # OpenLayers.INCHES_PER_UNIT[units] * OpenLayers.DOTS_PER_INCH
- my $x = $fx * $res * $tileSize + $tileOrigin->{lon};
- my $y = $tileOrigin->{lat} - $fy * $res * $tileSize;
+ my $x = $fx * $res * TILE_SIZE + $tileOrigin->{lon};
+ my $y = $tileOrigin->{lat} - $fy * $res * TILE_SIZE;
my ($lat, $lon) = Geo::Coordinates::CH1903::to_latlon($x, $y);
@@ -141,16 +233,16 @@ sub latlon_to_px($$$$$) {
# C is centre tile reference of displayed map
sub tile_to_px {
my ($p, $c) = @_;
- $p = 256 * ($p - $c + 1);
+ $p = TILE_SIZE * ($p - $c);
$p = int($p + .5 * ($p <=> 0));
return $p;
}
sub click_to_tile {
my ($pin_tile, $pin) = @_;
- $pin -= 256 while $pin > 256;
- $pin += 256 while $pin < 0;
- return $pin_tile + $pin / 256;
+ $pin -= TILE_SIZE while $pin > TILE_SIZE;
+ $pin += TILE_SIZE while $pin < 0;
+ return $pin_tile + $pin / TILE_SIZE;
}
# Given some click co-ords (the tile they were on, and where in the
diff --git a/perllib/FixMyStreet/SendReport/Barnet.pm b/perllib/FixMyStreet/SendReport/Barnet.pm
deleted file mode 100644
index 07adb4c33..000000000
--- a/perllib/FixMyStreet/SendReport/Barnet.pm
+++ /dev/null
@@ -1,208 +0,0 @@
-package FixMyStreet::SendReport::Barnet;
-
-use Moose;
-
-BEGIN { extends 'FixMyStreet::SendReport'; }
-
-use Encode;
-use Utils;
-use mySociety::Config;
-use mySociety::Web qw(ent);
-
-# specific council numbers
-use constant COUNCIL_ID_BARNET => 2489;
-use constant MAX_LINE_LENGTH => 132;
-
-sub construct_message {
- my %h = @_;
- my $message = <<EOF;
-Subject: $h{title}
-
-Details: $h{detail}
-
-$h{fuzzy}, or to provide an update on the problem, please visit the following link:
-
-$h{url}
-
-$h{closest_address}
-EOF
-}
-
-
-sub send {
- my ( $self, $row, $h ) = @_;
-
- my %h = %$h;
-
- $h{message} = construct_message(%h);
-
- my $return = 1;
- my $err_msg = "";
-
- my $default_kbid = 14; # This is the default, "Street Scene"
- my $kbid = sprintf( "%050d", Utils::barnet_categories()->{$h{category}} || $default_kbid);
-
- my $geo_code = "$h{easting} $h{northing}";
-
- require BarnetInterfaces::service::ZLBB_SERVICE_ORDER;
- my $interface = BarnetInterfaces::service::ZLBB_SERVICE_ORDER->new();
-
- my ($nearest_postcode, $nearest_street) = ('', '');
- for ($h{closest_address}) {
- $nearest_postcode = sprintf("%-10s", $1) if /Nearest postcode [^:]+: ((\w{1,4}\s?\w+|\w+))/;
- # use partial postcode or comma as delimiter, strip leading number (possible letter 221B) off too
- # "99 Foo Street, London N11 1XX" becomes Foo Street
- # "99 Foo Street N11 1XX" becomes Foo Street
- $nearest_street = $1 if /Nearest road [^:]+: (?:\d+\w? )?(.*?)(\b[A-Z]+\d|,|$)/m;
- }
- my $postcode = mySociety::PostcodeUtil::is_valid_postcode($h{query})
- ? $h{query} : $nearest_postcode; # use given postcode if available
-
- # note: endpoint can be of form 'https://username:password@url'
- my $body = FixMyStreet::App->model("DB::Body")->search( { 'body_areas.area_id' => COUNCIL_ID_BARNET }, { join => "body_areas" } )->first;
- if ($body and $body->endpoint) {
- $interface->set_proxy($body->endpoint);
- # Barnet web service doesn't like namespaces in the elements so use a prefix
- $interface->set_prefix('urn');
- # uncomment these lines to print XML that will be sent rather
- # than connecting to the endpoint
- #$interface->outputxml(1);
- #$interface->no_dispatch(1);
- } else {
- die "Barnet webservice FAIL: looks like you're missing some config data: no endpoint (URL) found for area ID " . COUNCIL_ID_BARNET;
- }
-
- eval {
- my $result = $interface->Z_CRM_SERVICE_ORDER_CREATE( {
- ET_RETURN => { # ignored by server
- item => {
- TYPE => "", ID => "", NUMBER => "", MESSAGE => "", LOG_NO => "", LOG_MSG_NO => "",
- MESSAGE_V1 => "", MESSAGE_V2 => "", MESSAGE_V3 => "", MESSAGE_V4 => "", PARAMETER => "",
- ROW => "", FIELD => "", SYSTEM => "",
- },
- },
- IT_PROBLEM_DESC => { # MyTypes::TABLE_OF_CRMT_SERVICE_REQUEST_TEXT
- item => [ # MyTypes::CRMT_SERVICE_REQUEST_TEXT
- map { { TEXT_LINE => $_ } } split_text_with_entities(ent(encode_utf8($h{message})), 132) # char132
- ],
- },
- IV_CUST_EMAIL => truncate_string_with_entities(ent(encode_utf8($h{email})), 241), # char241
- IV_CUST_NAME => truncate_string_with_entities(ent(encode_utf8($h{name})), 50), # char50
- IV_KBID => $kbid, # char50
- IV_PROBLEM_ID => $h{id}, # char35
- IV_PROBLEM_LOC => { # MyTypes::BAPI_TTET_ADDRESS_COM
- COUNTRY2 => 'GB', # char2
- REGION => "", # char3
- COUNTY => "", # char30
- CITY => "", # char30
- POSTALCODE => $postcode, # char10
- STREET => truncate_string_with_entities(ent(encode_utf8($nearest_street)), 30), # char30
- STREETNUMBER => "", # char5
- GEOCODE => $geo_code, # char32
- },
- IV_PROBLEM_SUB => truncate_string_with_entities(ent(encode_utf8($h{title})), 40), # char40
- },
- );
- if ($result) {
- # currently not using this: get_EV_ORDER_GUID (maybe that's the customer number in the CRM)
- if (my $barnet_id = $result->get_EV_ORDER_NO()) {
- $row->external_id( $barnet_id );
- $row->external_body( 'Barnet Borough Council' ); # better to use $row->body()?
- $row->send_method_used('barnet');
- $return = 0;
- } else {
- my @returned_items = split /<item[^>]*>/, $result->get_ET_RETURN;
- my @messages = ();
- foreach my $item (@returned_items) {
- if ($item=~/<MESSAGE [^>]*>\s*(\S.*?)<\/MESSAGE>/) { # if there's a non-null MESSAGE in there, grab it
- push @messages, $1; # best stab at extracting useful error message back from convoluted response
- }
- }
- push @messages, "service returned no external id" unless @messages;
- $err_msg = "Failed (problem id $h{id}): " . join(" \n ", @messages);
- }
- } else {
- my %fault = (
- 'code' => $result->get_faultcode(),
- 'actor' => $result->get_faultactor(),
- 'string' => $result->get_faultstring(),
- 'detail' => $result->get_detail(), # possibly only contains debug info
- );
- foreach (keys %fault) {
- $fault{$_}="" unless defined $fault{$_};
- $fault{$_}=~s/^\s*|\s*$//g;
- }
- $fault{actor}&&=" (actor: $fault{actor})";
- $fault{'detail'} &&= "\n" . $fault{'detail'};
- $err_msg = "Failed (problem id $h{id}): Fault $fault{code}$fault{actor}\n$fault{string}$fault{detail}";
- }
-
- };
- if ($err_msg) {
- # for timeouts, we can tidy the message a wee bit (i.e. strip the 'error deserializing...' message)
- $err_msg=~s/(?:Error deserializing message:.*)(Can't connect to [a-zA-Z0-9.:]+\s*\(Connection timed out\)).*/$1/s;
- print "$err_msg\n";
- }
- if ($@) {
- my $e = shift;
- print "Caught an error: $@\n";
- }
- if ( $return ) {
- $self->error( "Error sending to Barnet: $err_msg" );
- }
- $self->success( !$return );
- return $return;
-}
-
-# for barnet webservice: max-length fields require truncate and split
-
-# truncate_string_with_entities
-# args: text to truncate
-# max number of chars
-# returns: string truncated
-# Note: must not partially truncate an entity (e.g., &amp;)
-sub truncate_string_with_entities {
- my ($str, $max_len) = @_;
- my $retVal = "";
- foreach my $chunk (split /(\&(?:\#\d+|\w+);)/, $str) {
- if ($chunk=~/^\&(\#\d+|\w+);$/){
- my $next = $retVal.$chunk;
- last if length $next > $max_len;
- $retVal=$next
- } else {
- $retVal.=$chunk;
- if (length $retVal > $max_len) {
- $retVal = substr($retVal, 0, $max_len);
- last
- }
- }
- }
- return $retVal
-}
-
-# split_text_with_entities into lines
-# args: text to be broken into lines
-# max length (option: uses constant MAX_LINE_LENGTH)
-# returns: array of lines
-# Must not to split an entity (e.g., &amp;)
-# Not worrying about hyphenating here, since a word is only ever split if
-# it's longer than the whole line, which is uncommon in genuine problem reports
-sub split_text_with_entities {
- my ($text, $max_line_length) = @_;
- $max_line_length ||= MAX_LINE_LENGTH;
- my @lines;
- foreach my $line (split "\n", $text) {
- while (length $line > $max_line_length) {
- if (! ($line =~ s/^(.{1,$max_line_length})\s// # break on a space
- or $line =~ s/^(.{1,$max_line_length})(\&(\#\d+|\w+);)/$2/ # break before an entity
- or $line =~ s/(.{$max_line_length})//)) { # break the word ruthlessly
- $line =~ s/(.*)//; # otherwise gobble whole line (which is now shorter than max length)
- }
- push @lines, $1;
- }
- push @lines, $line;
- }
- return @lines;
-}
-
-1;
diff --git a/perllib/FixMyStreet/SendReport/Email.pm b/perllib/FixMyStreet/SendReport/Email.pm
index fa4d437fb..bac408510 100644
--- a/perllib/FixMyStreet/SendReport/Email.pm
+++ b/perllib/FixMyStreet/SendReport/Email.pm
@@ -1,6 +1,7 @@
package FixMyStreet::SendReport::Email;
use Moose;
+use FixMyStreet::Email;
BEGIN { extends 'FixMyStreet::SendReport'; }
@@ -92,8 +93,19 @@ sub send {
To => $self->to,
From => $self->send_from( $row ),
};
+
+ my $app = FixMyStreet::App->new( cobrand => $cobrand );
+
+ $cobrand->munge_sendreport_params($app, $row, $h, $params) if $cobrand->can('munge_sendreport_params');
+
$params->{Bcc} = $self->bcc if @{$self->bcc};
- my $result = FixMyStreet::App->send_email_cron(
+
+ if (FixMyStreet::Email::test_dmarc($params->{From}[0])) {
+ $params->{'Reply-To'} = [ $params->{From} ];
+ $params->{From} = [ mySociety::Config::get('CONTACT_EMAIL'), $params->{From}[1] ];
+ }
+
+ my $result = $app->send_email_cron(
$params,
mySociety::Config::get('CONTACT_EMAIL'),
$nomail,
diff --git a/perllib/FixMyStreet/SendReport/Zurich.pm b/perllib/FixMyStreet/SendReport/Zurich.pm
index 40417b41e..2838440cb 100644
--- a/perllib/FixMyStreet/SendReport/Zurich.pm
+++ b/perllib/FixMyStreet/SendReport/Zurich.pm
@@ -9,10 +9,21 @@ sub build_recipient_list {
# Only one body ever, most of the time with an email endpoint
my $body = @{ $self->bodies }[0];
+
+ # we set external_message (but default to '' in case of race condition e.g.
+ # Wunsch set, but external_message hasn't yet been filled in. TODO should
+ # we instead be holding off sending?)
if ( $row->external_body ) {
$body = FixMyStreet::App->model("DB::Body")->find( { id => $row->external_body } );
$h->{bodies_name} = $body->name;
+ $h->{external_message} = $row->get_extra_metadata('external_message') || '';
}
+ $h->{external_message} //= '';
+
+ my ($west, $nord) = $row->local_coords;
+ $h->{west} = $west;
+ $h->{nord} = $nord;
+
my $body_email = $body->endpoint;
my $parent = $body->parent;
@@ -39,6 +50,8 @@ sub get_template {
$template = 'submit-in-progress.txt';
} elsif ( $row->state eq 'planned' ) {
$template = 'submit-feedback-pending.txt';
+ } elsif ( $row->state eq 'investigating' ) {
+ $template = 'submit-external-wish.txt';
} elsif ( $row->state eq 'closed' ) {
$template = 'submit-external.txt';
if ( $row->extra->{third_personal} ) {
diff --git a/perllib/FixMyStreet/TestMech.pm b/perllib/FixMyStreet/TestMech.pm
index bd2ca4096..cc5f9dd71 100644
--- a/perllib/FixMyStreet/TestMech.pm
+++ b/perllib/FixMyStreet/TestMech.pm
@@ -165,6 +165,7 @@ sub delete_user {
$a->delete;
}
$_->delete for $user->comments;
+ $_->delete for $user->admin_logs;
$user->delete;
return 1;
@@ -221,6 +222,24 @@ sub get_email {
return $emails[0];
}
+=head2 get_first_email
+
+ $email = $mech->get_first_email(@emails);
+
+Returns first email in queue as a string and fails a test if the mail doesn't have a date and epoch-containing Message-ID header.
+
+=cut
+
+sub get_first_email {
+ my $mech = shift;
+ my $email = shift or do { fail 'No email retrieved'; return };
+ my $email_as_string = $email->as_string;
+ ok $email_as_string =~ s{\s+Date:\s+\S.*?$}{}xmsg, "Found and stripped out date";
+ ok $email_as_string =~ s{\s+Message-ID:\s+\S.*?$}{}xmsg, "Found and stripped out message ID (contains epoch)";
+ return $email_as_string;
+}
+
+
=head2 page_errors
my $arrayref = $mech->page_errors;
@@ -383,7 +402,7 @@ sub extract_update_metas {
my $result = scraper {
process 'div#updates div.problem-update p em', 'meta[]', 'TEXT';
- process '.update-text .meta-2', 'meta[]', 'TEXT';
+ process '.item-list__update-text .meta-2', 'meta[]', 'TEXT';
}
->scrape( $mech->response );
@@ -404,7 +423,7 @@ sub extract_problem_list {
my $mech = shift;
my $result = scraper {
- process 'ul.issue-list-a li a h4', 'problems[]', 'TEXT';
+ process 'ul.item-list--reports li a h4', 'problems[]', 'TEXT';
}->scrape( $mech->response );
return $result->{ problems } || [];
@@ -631,7 +650,7 @@ sub create_problems_for_body {
latitude => '51.5016605453401',
longitude => '-0.142497580865087',
user_id => $user->id,
- photo => 1,
+ photo => $mech->get_photo_data,
};
my %report_params = ( %$default_params, %$params );
@@ -646,4 +665,13 @@ sub create_problems_for_body {
return @problems;
}
+sub get_photo_data {
+ my $mech = shift;
+ return $mech->{sample_photo} ||= do {
+ my $sample_file = FixMyStreet->path_to( 't/app/controller/sample.jpg' );
+ $mech->builder->ok( -f "$sample_file", "sample file $sample_file exists" );
+ $sample_file->slurp(iomode => '<:raw');
+ };
+}
+
1;