diff options
| -rw-r--r-- | data/dashboard.json | 229 | ||||
| -rw-r--r-- | perllib/FixMyStreet/App/Controller/Admin.pm | 10 | ||||
| -rw-r--r-- | perllib/FixMyStreet/App/Controller/Dashboard.pm | 241 | ||||
| -rw-r--r-- | templates/web/default/dashboard/index.html | 150 | 
4 files changed, 624 insertions, 6 deletions
diff --git a/data/dashboard.json b/data/dashboard.json new file mode 100644 index 000000000..c921950be --- /dev/null +++ b/data/dashboard.json @@ -0,0 +1,229 @@ +{ +    "category_options": [ +        "Abandoned vehicles", +        "Grafitti", +        "Potholes", +        "Street lighting", +        "Trees", +        "Other" +    ], +    "counts_all": { +        "wtd": { +            "total": 10, +            "planned": 2, +            "in progress": 1, +            "investigating": 1, +            "fixed - council": 3, +            "fixed_user": 2, +            "time_to_fix": 2, +            "time_to_mark": 2, +            "not_marked": 1 +        }, +        "week": { +            "total": 21, +            "planned": 5, +            "in progress": 3, +            "investigating": 4, +            "fixed - council": 6, +            "fixed_user": 3, +            "time_to_fix": 3, +            "time_to_mark": 2, +            "not_marked": 1 +        }, +        "weeks": { +            "total": 57, +            "planned": 5, +            "in progress": 16, +            "investigating": 4, +            "fixed - council": 23, +            "fixed_user": 9, +            "time_to_fix": 2, +            "time_to_mark": 2, +            "not_marked": 1 +        }, +        "ytd": { +            "total": 171, +            "planned": 23, +            "in progress": 34, +            "investigating": 9, +            "fixed - council": 72, +            "fixed_user": 33, +            "time_to_fix": 2, +            "time_to_mark": 2, +            "not_marked": 1 +        } +    }, +    "counts_some": { +        "wtd": { +            "total": 5, +            "planned": 0, +            "in progress": 1, +            "investigating": 1, +            "fixed - council": 2, +            "fixed_user": 1, +            "time_to_fix": 2, +            "time_to_mark": 2, +            "not_marked": 0 +        }, +        "week": { +            "total": 7, +            "planned": 1, +            "in progress": 2, +            "investigating": 1, +            "fixed - council": 2, +            "fixed_user": 1, +            "time_to_fix": 3, +            "time_to_mark": 2, +            "not_marked": 0 +        }, +        "weeks": { +            "total": 57, +            "planned": 5, +            "in progress": 16, +            "investigating": 4, +            "fixed - council": 23, +            "fixed_user": 9, +            "time_to_fix": 2, +            "time_to_mark": 2, +            "not_marked": 1 +        }, +        "ytd": { +            "total": 57, +            "planned": 5, +            "in progress": 16, +            "investigating": 4, +            "fixed - council": 23, +            "fixed_user": 9, +            "time_to_fix": 2, +            "time_to_mark": 2, +            "not_marked": 1 +        } +    }, +    "lists": { +        "all": { +            "1": [ +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Fallen Tree" }, +                { "id": 0, "title": "Fly tipping" }, +                { "id": 0, "title": "Loose kerb" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Abandoned car" }, +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Uneven paving" } +            ], +            "2": [ +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Fly tipping" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Uneven paving" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Loose kerb" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Fallen Tree" }, +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Abandoned car" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Loose kerb" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Blocked drain" } +            ], +            "3": [ +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Loose kerb" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Fly tipping" }, +                { "id": 0, "title": "Uneven paving" }, +                { "id": 0, "title": "Fallen Tree" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Loose kerb" }, +                { "id": 0, "title": "Abandoned car" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Fallen Tree" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Fly tipping" }, +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Abandoned car" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Loose kerb" }, +                { "id": 0, "title": "Uneven paving" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Pothole" } +            ] +        }, +        "filtered": { +            "1": [ +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Fallen Tree" }, +                { "id": 0, "title": "Loose kerb" }, +                { "id": 0, "title": "Abandoned car" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Uneven paving" } +            ], +            "2": [ +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Loose kerb" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Fly tipping" }, +                { "id": 0, "title": "Fallen Tree" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Loose kerb" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Fallen Tree" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Fly tipping" }, +                { "id": 0, "title": "Abandoned car" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Uneven paving" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Pothole" } +            ], +            "3": [ +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Uneven paving" }, +                { "id": 0, "title": "Fallen Tree" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Loose kerb" }, +                { "id": 0, "title": "Abandoned car" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Fallen Tree" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Fly tipping" }, +                { "id": 0, "title": "Burst pipe" }, +                { "id": 0, "title": "Abandoned car" }, +                { "id": 0, "title": "Pothole" }, +                { "id": 0, "title": "Uneven paving" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Blocked drain" }, +                { "id": 0, "title": "Pothole" } +            ] +        } +    } +} diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm index 198acade6..21b820423 100644 --- a/perllib/FixMyStreet/App/Controller/Admin.pm +++ b/perllib/FixMyStreet/App/Controller/Admin.pm @@ -571,8 +571,7 @@ sub report_edit : Path('report_edit') : Args(1) {          }      )->first; -    $c->detach( '/page_error_404_not_found', -        [ _('The requested URL was not found on this server.') ] ) +    $c->detach( '/page_error_404_not_found' )        unless $problem;      $c->stash->{problem} = $problem; @@ -734,8 +733,7 @@ sub update_edit : Path('update_edit') : Args(1) {          }      )->first; -    $c->detach( '/page_error_404_not_found', -        [ _('The requested URL was not found on this server.') ] ) +    $c->detach( '/page_error_404_not_found' )        unless $update;      $c->forward('get_token'); @@ -1068,7 +1066,7 @@ sub check_token : Private {      my ( $self, $c ) = @_;      if ( !$c->req->param('token') || $c->req->param('token' ) ne $c->stash->{token} ) { -        $c->detach( '/page_error_404_not_found', [ _('The requested URL was not found on this server.') ] ); +        $c->detach( '/page_error_404_not_found' );      }      return 1; @@ -1238,7 +1236,7 @@ sub check_page_allowed : Private {      $page ||= 'summary';      if ( !grep { $_ eq $page } keys %{ $c->stash->{allowed_pages} } ) { -        $c->detach( '/page_error_404_not_found', [ _('The requested URL was not found on this server.') ] ); +        $c->detach( '/page_error_404_not_found' );      }      return 1; diff --git a/perllib/FixMyStreet/App/Controller/Dashboard.pm b/perllib/FixMyStreet/App/Controller/Dashboard.pm new file mode 100644 index 000000000..f2dddaea0 --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Dashboard.pm @@ -0,0 +1,241 @@ +package FixMyStreet::App::Controller::Dashboard; +use Moose; +use namespace::autoclean; + +use DateTime; +use File::Slurp; + +BEGIN { extends 'Catalyst::Controller'; } + +=head1 NAME + +FixMyStreet::App::Controller::Dashboard - Catalyst Controller + +=head1 DESCRIPTION + +Catalyst Controller. + +=head1 METHODS + +=cut + +sub example : Local : Args(0) { +    my ( $self, $c ) = @_; +    $c->stash->{template} = 'dashboard/index.html'; + +    $c->stash->{children} = {}; +    for my $i (1..3) { +        $c->stash->{children}{$i} = { id => $i, name => "Ward $i" }; +    } + +    # TODO Set up manual version of what the below would do +    #$c->forward( '/report/new/setup_categories_and_councils' ); + +    # See if we've had anything from the dropdowns - perhaps vary results if so +    $c->stash->{ward} = $c->req->param('ward'); +    $c->stash->{category} = $c->req->param('category'); +    $c->stash->{q_state} = $c->req->param('state'); + +    eval { +        my $data = File::Slurp::read_file( +            FixMyStreet->path_to( 'data/dashboard.json' )->stringify +        ); +        my $j = JSON->new->utf8->decode($data); +        if ( !$c->stash->{ward} && !$c->stash->{category} ) { +            $c->stash->{problems} = $j->{counts_all}; +        } else { +            $c->stash->{problems} = $j->{counts_some}; +        } +        $c->stash->{category_options} = $j->{category_options}; +        if ( lc($c->stash->{q_state}) eq 'all' or !$c->stash->{q_state} ) { +            $c->stash->{lists} = $j->{lists}->{all}; +        } else { +            $c->stash->{lists} = $j->{lists}->{filtered}; +        } +    }; +    if ($@) { +        $c->stash->{message} = _("There was a problem showing this page. Please try again later.") . ' ' . +            sprintf(_('The error was: %s'), $@); +        $c->stash->{template} = 'errors/generic.html'; +    } +} + +=head2 check_page_allowed + +Checks if we can view this page, and if not redirect to 404. + +=cut + +sub check_page_allowed : Private { +    my ( $self, $c ) = @_; + +    $c->detach( '/auth/redirect' ) unless $c->user_exists; + +    $c->detach( '/page_error_404_not_found' ) +        unless $c->user_exists && $c->user->from_council; + +    return $c->user->from_council; +} + +=head2 index + +Show the dashboard table. + +=cut + +sub index : Path : Args(0) { +    my ( $self, $c ) = @_; + +    my $council = $c->forward('check_page_allowed'); + +    # Set up the data for the dropdowns + +    my $children = mySociety::MaPit::call('area/children', $council, +        type => $mySociety::VotingArea::council_child_types, +    ); +    $c->stash->{children} = $children; + +    # XXX Hmm, this is probably the best way to go +    $c->stash->{all_councils} = { $council => { id => $council } }; +    $c->forward( '/report/new/setup_categories_and_councils' ); + +    # See if we've had anything from the dropdowns + +    $c->stash->{ward} = $c->req->param('ward'); +    $c->stash->{category} = $c->req->param('category'); + +    my %where = ( +        council => $council, # XXX This will break in a two tier council. Restriction needs looking at... +        'problem.state' => [ FixMyStreet::DB::Result::Problem->visible_states() ], +    ); +    $where{areas} = { 'like', '%,' . $c->stash->{ward} . ',%' } +        if $c->stash->{ward}; +    $where{category} = $c->stash->{category} +        if $c->stash->{category}; +    $c->stash->{where} = \%where; +    my $prob_where = { %where }; +    $prob_where->{state} = $prob_where->{'problem.state'}; +    delete $prob_where->{'problem.state'}; +    $c->stash->{prob_where} = $prob_where; + +    my %counts; +    my $t = DateTime->today; +    $counts{wtd} = $c->forward( 'updates_search', [ $t->subtract( days => $t->dow - 1 ) ] ); +    $counts{week} = $c->forward( 'updates_search', [ DateTime->now->subtract( weeks => 1 ) ] ); +    $counts{weeks} = $c->forward( 'updates_search', [ DateTime->now->subtract( weeks => 4 ) ] ); +    $counts{ytd} = $c->forward( 'updates_search', [ DateTime->today->set( day => 1, month => 1 ) ] ); + +    $c->stash->{problems} = \%counts; + +    # List of reports underneath summary table + +    $c->stash->{q_state} = $c->req->param('state') || ''; +    if ( $c->stash->{q_state} eq 'fixed' ) { +        $prob_where->{state} = [ FixMyStreet::DB::Result::Problem->fixed_states() ]; +    } elsif ( $c->stash->{q_state} ) { +        $prob_where->{state} = $c->stash->{q_state}; +    } +    my $params = { +        %$prob_where, +        'me.confirmed' => { '>=', DateTime->now->subtract( days => 30 ) }, +    }; +    my @problems = $c->cobrand->problems->search( $params )->all; +    my %problems; +    foreach (@problems) { +        if ($_->confirmed >= DateTime->now->subtract(days => 7)) { +            push @{$problems{1}}, $_; +        } elsif ($_->confirmed >= DateTime->now->subtract(days => 14)) { +            push @{$problems{2}}, $_; +        } else { +            push @{$problems{3}}, $_; +        } +    } +    $c->stash->{lists} = \%problems; +} + +sub updates_search : Private { +    my ( $self, $c, $time ) = @_; + +    my $params = { +        %{$c->stash->{where}}, +        'me.confirmed' => { '>=', $time }, +    }; + +    my $comments = $c->model('DB::Comment')->search( +        $params, +        { +            group_by => [ 'problem_state' ], +            select   => [ 'problem_state', { count => 'me.id' } ], +            as       => [ qw/state state_count/ ], +            join     => 'problem' +        } +    ); + +    my %counts = +      map { ($_->state||'-') => $_->get_column('state_count') } $comments->all; +    %counts = +      map { $_ => $counts{$_} || 0 } +      ('confirmed', 'investigating', 'in progress', 'closed', 'fixed - council', +          'fixed - user', 'fixed', 'unconfirmed', 'hidden', +          'partial', 'planned'); + +    for my $vars ( +        [ 'time_to_fix', 'fixed - council' ], +        [ 'time_to_mark', 'in progress', 'planned', 'investigating' ], +    ) { +        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( +            { %$params, +                problem_state => $vars, +                'me.id' => \"= ($substmt)", +            }, +            { +                select   => [ +                    { count => 'me.id' }, +                    { avg => { extract => "epoch from me.confirmed-problem.confirmed" } }, +                ], +                as       => [ qw/state_count time/ ], +                join     => 'problem' +            } +        )->first; +        $counts{$col} = int( ($comments->get_column('time')||0) / 60 / 60 / 24 + 0.5 ); +    } + +    $counts{fixed_user} = $c->model('DB::Comment')->search( +        { %$params, mark_fixed => 1 }, { join     => 'problem' } +    )->count; + +    $params = { +        %{$c->stash->{prob_where}}, +        'me.confirmed' => { '>=', $time }, +    }; +    $counts{total} = $c->cobrand->problems->search( $params )->count; + +    $params = { +        %{$c->stash->{prob_where}}, +        'me.confirmed' => { '>=', $time }, +        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; + +    return \%counts; +} + +=head1 AUTHOR + +Matthew Somerville + +=head1 LICENSE + +Copyright (c) 2012 UK Citizens Online Democracy. All rights reserved. +Licensed under the Affero GPL. + +=cut + +__PACKAGE__->meta->make_immutable; + +1; + diff --git a/templates/web/default/dashboard/index.html b/templates/web/default/dashboard/index.html new file mode 100644 index 000000000..f7c058a99 --- /dev/null +++ b/templates/web/default/dashboard/index.html @@ -0,0 +1,150 @@ +[%  +    INCLUDE 'header.html' +        title = loc('Dashboard') +        robots = 'noindex, nofollow' +        bodyclass = 'fullwidthpage' +%] + +<style> +    th[scope=row] { text-align: left; } +    tr.subtotal { background-color: #eee; } +    #overview tr:nth-child(2) { background-color: #fee; } +</style> + +<form> + +<p>Ward: <select name="ward"><option value=''>All</option> +    [% FOR w IN children.values.sort('name') %] +        <option value="[% w.id %]"[% ' selected' IF w.id == ward %]>[% w.name %]</option> +    [% END %] +</select> + +<p>Report category: <select name="category"><option value=''>All</option> +    [% FOR cat_op IN category_options %] +        <option value='[% cat_op | html %]'[% ' selected' IF category == cat_op %]>[% cat_op | html %]</option> +    [% END %] +    </select> + +<p><input type="submit" value="Look up"> + +<h2>Summary Statistics</h2> + +<table width="100%" id="overview"> +    <tr> +        <td> </td> +        <th scope="col">WTD</th> +        <th scope="col">Last 7 days</th> +        <th scope="col">Last 4 weeks</th> +        <th scope="col">YTD</th> +    </tr> + +    [% +    rows = { +        '0' => [ "total", "Total reports received" ] +        '1' => [ "fixed - council", "Council has marked as fixed" ] +        '2' => [ "fixed_user", "User has marked as fixed" ] +    }; +    FOR row IN rows %] +    <tr> +        <th scope="row">[% row.value.1 %]</th> +        <td>[% problems.wtd.${row.value.0} %]</td> +        <td>[% problems.week.${row.value.0} %]</td> +        <td>[% problems.weeks.${row.value.0} %]</td> +        <td>[% problems.ytd.${row.value.0} %]</td> +    </tr> +    [% END %] + +    <tr class='subtotal'> +        <th scope="row">Total marked as fixed</th> +        <td>[% problems.wtd.${"fixed - council"} + problems.wtd.fixed_user %]</td> +        <td>[% problems.week.${"fixed - council"} + problems.week.fixed_user %]</td> +        <td>[% problems.weeks.${"fixed - council"} + problems.weeks.fixed_user %]</td> +        <td>[% problems.ytd.${"fixed - council"} + problems.ytd.fixed_user %]</td> +    </tr> + +    [% +    rows = { +        '0' => [ "in progress", "Council has marked as in progress" ] +        '1' => [ "planned", "Council has marked as planned" ] +        '2' => [ "investigating", "Council has marked as investigating" ] +    }; +    wtd = 0, week = 0, weeks = 0, ytd = 0; +    FOR row IN rows %] +    <tr> +        <th scope="row">[% row.value.1 %]</th> +        <td>[% problems.wtd.${row.value.0} %]</td> +        <td>[% problems.week.${row.value.0} %]</td> +        <td>[% problems.weeks.${row.value.0} %]</td> +        <td>[% problems.ytd.${row.value.0} %]</td> +    </tr> +    [% END %] + +    <tr class='subtotal'> +        <th scope="row">Total marked</th> +        <td>[% problems.wtd.${"in progress"} + problems.wtd.planned + problems.wtd.investigating %]</td> +        <td>[% problems.week.${"in progress"} + problems.week.planned + problems.week.investigating %]</td> +        <td>[% problems.weeks.${"in progress"} + problems.weeks.planned + problems.weeks.investigating %]</td> +        <td>[% problems.ytd.${"in progress"} + problems.ytd.planned + problems.ytd.investigating %]</td> +    </tr> + +    <tr> +        <th scope="row">Average time to council marking fixed (days)</th> +        <td>[% problems.wtd.time_to_fix %]</td> +        <td>[% problems.week.time_to_fix %]</td> +        <td>[% problems.weeks.time_to_fix %]</td> +        <td>[% problems.ytd.time_to_fix %]</td> +    </tr> + +    <tr> +        <th scope="row">Average time to first council state change (days)</th> +        <td>[% problems.wtd.time_to_mark %]</td> +        <td>[% problems.week.time_to_mark %]</td> +        <td>[% problems.weeks.time_to_mark %]</td> +        <td>[% problems.ytd.time_to_mark %]</td> +    </tr> + +    <tr class='subtotal'> +        <th scope="row">Total not marked</th> +        <td>[% problems.wtd.not_marked %]</td> +        <td>[% problems.week.not_marked %]</td> +        <td>[% problems.weeks.not_marked %]</td> +        <td>[% problems.ytd.not_marked %]</td> +    </tr> + +</table> + +<h2>Reports</h2> + +                </select> +<p>Report state: <select name="state"> +<option value=''>All</option> +                [% FOREACH state IN [ ['confirmed', loc('Open')], ['investigating', +                loc('Investigating')], ['planned', loc('Planned')], ['in progress', +                loc('In Progress')], ['closed', loc('Closed')], ['fixed', loc('Fixed')] ] %] +                    <option [% 'selected ' IF state.0 == q_state %] value="[% state.0 %]">[% state.1 %]</option> +                [% END %] +</select> +<input type="submit" value="Look up"> + +<table width="100%"> +    <tr> +        <th scope="col">Less than 7 days old</th> +        <th scope="col">7-14 days old</th> +        <th scope="col">14-30 days old</th> +    </tr> +    <tr> +        <td><ul>[% INCLUDE list, list = lists.1 %]</ul></td> +        <td><ul>[% INCLUDE list, list = lists.2 %]</ul></td> +        <td><ul>[% INCLUDE list, list = lists.3 %]</ul></td> +    </tr> +</table> + +</form> + +[% INCLUDE 'footer.html' %] + +[% BLOCK list %] +[% FOR p IN list %] +<li><a href="/report/[% p.id %]">[% p.title | html %]</a></li> +[% END %] +[% END %]  | 
