diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/controllers/admin_controller.rb | 2 | ||||
-rw-r--r-- | app/controllers/admin_general_controller.rb | 5 | ||||
-rw-r--r-- | app/controllers/application_controller.rb | 11 | ||||
-rw-r--r-- | app/controllers/public_body_controller.rb | 80 | ||||
-rw-r--r-- | app/models/info_request.rb | 11 | ||||
-rw-r--r-- | app/models/public_body.rb | 59 | ||||
-rw-r--r-- | app/views/admin_general/admin.coffee | 24 | ||||
-rw-r--r-- | app/views/admin_general/admin.js | 32 | ||||
-rw-r--r-- | app/views/admin_general/admin_js.erb | 34 | ||||
-rw-r--r-- | app/views/admin_request/edit_outgoing.html.erb | 3 | ||||
-rw-r--r-- | app/views/layouts/admin.html.erb | 2 | ||||
-rw-r--r-- | app/views/public_body/statistics.html.erb | 75 |
12 files changed, 231 insertions, 107 deletions
diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 0bccd3358..f5191504e 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -17,7 +17,7 @@ class AdminController < ApplicationController end # Always give full stack trace for admin interface - def local_request? + def show_rails_exceptions? true end diff --git a/app/controllers/admin_general_controller.rb b/app/controllers/admin_general_controller.rb index ec5f95eda..196616ed6 100644 --- a/app/controllers/admin_general_controller.rb +++ b/app/controllers/admin_general_controller.rb @@ -5,7 +5,6 @@ # Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class AdminGeneralController < AdminController - skip_before_filter :authenticate, :only => :admin_js def index # ensure we have a trailing slash @@ -142,9 +141,5 @@ class AdminGeneralController < AdminController @request_env = request.env end - # TODO: Remove this when support for proxy admin interface is removed - def admin_js - render :layout => false, :content_type => "application/javascript" - end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 88b107861..2ce44011f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -119,12 +119,9 @@ class ApplicationController < ActionController::Base end def render_exception(exception) - - # In development, or the admin interface, or for a local request, let Rails handle the exception - # with its stack trace templates. Local requests in testing are a special case so that we can - # test this method - there we use consider_all_requests_local to control behaviour. - if Rails.application.config.consider_all_requests_local || local_request? || - (request.local? && !Rails.env.test?) + # In development or the admin interface let Rails handle the exception + # with its stack trace templates + if Rails.application.config.consider_all_requests_local || show_rails_exceptions? raise exception end @@ -150,7 +147,7 @@ class ApplicationController < ActionController::Base end end - def local_request? + def show_rails_exceptions? false end diff --git a/app/controllers/public_body_controller.rb b/app/controllers/public_body_controller.rb index 374866eda..9f692c5ba 100644 --- a/app/controllers/public_body_controller.rb +++ b/app/controllers/public_body_controller.rb @@ -6,6 +6,7 @@ # Email: hello@mysociety.org; WWW: http://www.mysociety.org/ require 'fastercsv' +require 'confidence_intervals' class PublicBodyController < ApplicationController # XXX tidy this up with better error messages, and a more standard infrastructure for the redirect to canonical URL @@ -149,6 +150,84 @@ class PublicBodyController < ApplicationController :disposition =>'attachment', :encoding => 'utf8') end + def statistics + unless AlaveteliConfiguration::public_body_statistics_page + raise ActiveRecord::RecordNotFound.new("Page not enabled") + end + + per_graph = 8 + minimum_requests = AlaveteliConfiguration::minimum_requests_for_statistics + # Make sure minimum_requests is > 0 to avoid division-by-zero + minimum_requests = [minimum_requests, 1].max + total_column = 'info_requests_count' + + @graph_list = [] + + [[total_column, + [{ + :title => 'Public bodies with the most requests', + :y_axis => 'Number of requests', + :highest => true}]], + ['info_requests_successful_count', + [{ + :title => 'Public bodies with the most successful requests', + :y_axis => 'Percentage of total requests', + :highest => true}, + { + :title => 'Public bodies with the fewest successful requests', + :y_axis => 'Percentage of total requests', + :highest => false}]], + ['info_requests_overdue_count', + [{ + :title => 'Public bodies with most overdue requests', + :y_axis => 'Percentage of requests that are overdue', + :highest => true}]], + ['info_requests_not_held_count', + [{ + :title => 'Public bodies that most frequently replied with "Not Held"', + :y_axis => 'Percentage of total requests', + :highest => true}]]].each do |column, graphs_properties| + + graphs_properties.each do |graph_properties| + + percentages = (column != total_column) + highest = graph_properties[:highest] + + data = nil + if percentages + data = PublicBody.get_request_percentages(column, + per_graph, + highest, + minimum_requests) + else + data = PublicBody.get_request_totals(per_graph, + highest, + minimum_requests) + end + + data_to_draw = { + 'id' => "#{column}-#{highest ? 'highest' : 'lowest'}", + 'x_axis' => 'Public Bodies', + 'y_axis' => graph_properties[:y_axis], + 'errorbars' => percentages, + 'title' => graph_properties[:title]} + + if data + data_to_draw.update(data) + data_to_draw['x_values'] = data['public_bodies'].each_with_index.map { |pb, i| i } + data_to_draw['x_ticks'] = data['public_bodies'].each_with_index.map { |pb, i| [i, pb.name] } + end + + @graph_list.push data_to_draw + end + end + + respond_to do |format| + format.html { render :template => "public_body/statistics" } + format.json { render :json => @graph_list } + end + end + # Type ahead search def search_typeahead # Since acts_as_xapian doesn't support the Partial match flag, we work around it @@ -158,4 +237,3 @@ class PublicBodyController < ApplicationController render :partial => "public_body/search_ahead" end end - diff --git a/app/models/info_request.rb b/app/models/info_request.rb index 9bce2ca88..91bd37d9f 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -1125,6 +1125,17 @@ public end end + after_save :update_counter_cache + after_destroy :update_counter_cache + def update_counter_cache + self.public_body.info_requests_not_held_count = InfoRequest.where( + :public_body_id => self.public_body.id, + :described_state => 'not_held').count + self.public_body.info_requests_successful_count = InfoRequest.where( + :public_body_id => self.public_body.id, + :described_state => ['successful', 'partially_successful']).count + end + def for_admin_column self.class.content_columns.map{|c| c unless %w(title url_title).include?(c.name) }.compact.each do |column| yield(column.human_name, self.send(column.name), column.type.to_s, column.name) diff --git a/app/models/public_body.rb b/app/models/public_body.rb index a76aeb189..300fdb956 100644 --- a/app/models/public_body.rb +++ b/app/models/public_body.rb @@ -632,6 +632,65 @@ class PublicBody < ActiveRecord::Base end end + # Return data for the 'n' public bodies with the highest (or + # lowest) number of requests, but only returning data for those + # with at least 'minimum_requests' requests. + def self.get_request_totals(n, highest, minimum_requests) + ordering = "info_requests_count" + ordering += " DESC" if highest + where_clause = "info_requests_count >= #{minimum_requests}" + public_bodies = PublicBody.order(ordering).where(where_clause).limit(n) + public_bodies.reverse! if highest + y_values = public_bodies.map { |pb| pb.info_requests_count } + return { + 'public_bodies' => public_bodies, + 'y_values' => y_values, + 'y_max' => y_values.max} + end + + # Return data for the 'n' public bodies with the highest (or + # lowest) score according to the metric of the value in 'column' + # divided by the total number of requests, expressed as a + # percentage. This only returns data for those public bodies with + # at least 'minimum_requests' requests. + def self.get_request_percentages(column, n, highest, minimum_requests) + total_column = "info_requests_count" + ordering = "y_value" + ordering += " DESC" if highest + y_value_column = "(cast(#{column} as float) / #{total_column})" + where_clause = "#{total_column} >= #{minimum_requests} AND #{column} IS NOT NULL" + public_bodies = PublicBody.select("*, #{y_value_column} AS y_value").order(ordering).where(where_clause).limit(n) + public_bodies.reverse! if highest + y_values = public_bodies.map { |pb| pb.y_value.to_f } + + original_values = public_bodies.map { |pb| pb.send(column) } + # If these are all nil, then probably the values have never + # been set; some have to be set by a rake task. In that case, + # just return nil: + return nil unless original_values.any? { |ov| !ov.nil? } + + original_totals = public_bodies.map { |pb| pb.send(total_column) } + # Calculate confidence intervals, as offsets from the proportion: + cis_below = [] + cis_above = [] + original_totals.each_with_index.map { |total, i| + lower_ci, higher_ci = ci_bounds original_values[i], total, 0.05 + cis_below.push(y_values[i] - lower_ci) + cis_above.push(higher_ci - y_values[i]) + } + # Turn the y values and confidence interval offsets into + # percentages: + [y_values, cis_below, cis_above].each { |l| + l.map! { |v| 100 * v } + } + return { + 'public_bodies' => public_bodies, + 'y_values' => y_values, + 'cis_below' => cis_below, + 'cis_above' => cis_above, + 'y_max' => 100} + end + private def request_email_if_requestable diff --git a/app/views/admin_general/admin.coffee b/app/views/admin_general/admin.coffee deleted file mode 100644 index 3d39369a4..000000000 --- a/app/views/admin_general/admin.coffee +++ /dev/null @@ -1,24 +0,0 @@ -jQuery -> - $('.locales a:first').tab('show') - $('.accordion-body').on('hidden', -> - $(@).prev().find('i').first().removeClass().addClass('icon-chevron-right') - ) - $('.accordion-body').on('shown', -> - $(@).prev().find('i').first().removeClass().addClass('icon-chevron-down')) - $('.toggle-hidden').live('click', -> - $(@).parents('td').find('div:hidden').show() - false) - $('#request_hidden_user_explanation_reasons input').live('click', -> - $('#request_hidden_user_subject, #request_hidden_user_explanation, #request_hide_button').show() - info_request_id = $('#hide_request_form').attr('data-info-request-id') - reason = $(this).val() - $('#request_hidden_user_explanation_field').attr("value", "[loading default text...]") - $.ajax "/hidden_user_explanation?reason=" + reason + "&info_request_id=" + info_request_id, - type: "GET" - dataType: "text" - error: (data, textStatus, jqXHR) -> - $('#request_hidden_user_explanation_field').attr("value", "Error: #{textStatus}") - success: (data, textStatus, jqXHR) -> - $('#request_hidden_user_explanation_field').attr("value", data) - ) - diff --git a/app/views/admin_general/admin.js b/app/views/admin_general/admin.js deleted file mode 100644 index 9daa51459..000000000 --- a/app/views/admin_general/admin.js +++ /dev/null @@ -1,32 +0,0 @@ -(function() { - jQuery(function() { - $('.locales a:first').tab('show'); - $('.accordion-body').on('hidden', function() { - return $(this).prev().find('i').first().removeClass().addClass('icon-chevron-right'); - }); - $('.accordion-body').on('shown', function() { - return $(this).prev().find('i').first().removeClass().addClass('icon-chevron-down'); - }); - $('.toggle-hidden').live('click', function() { - $(this).parents('td').find('div:hidden').show(); - return false; - }); - return $('#request_hidden_user_explanation_reasons input').live('click', function() { - var info_request_id, reason; - $('#request_hidden_user_subject, #request_hidden_user_explanation, #request_hide_button').show(); - info_request_id = $('#hide_request_form').attr('data-info-request-id'); - reason = $(this).val(); - $('#request_hidden_user_explanation_field').attr("value", "[loading default text...]"); - return $.ajax("/hidden_user_explanation?reason=" + reason + "&info_request_id=" + info_request_id, { - type: "GET", - dataType: "text", - error: function(data, textStatus, jqXHR) { - return $('#request_hidden_user_explanation_field').attr("value", "Error: " + textStatus); - }, - success: function(data, textStatus, jqXHR) { - return $('#request_hidden_user_explanation_field').attr("value", data); - } - }); - }); - }); -}).call(this); diff --git a/app/views/admin_general/admin_js.erb b/app/views/admin_general/admin_js.erb deleted file mode 100644 index c8788a452..000000000 --- a/app/views/admin_general/admin_js.erb +++ /dev/null @@ -1,34 +0,0 @@ -(function() { - - jQuery(function() { - $('.locales a:first').tab('show'); - $('.accordion-body').on('hidden', function() { - return $(this).prev().find('i').first().removeClass().addClass('icon-chevron-right'); - }); - $('.accordion-body').on('shown', function() { - return $(this).prev().find('i').first().removeClass().addClass('icon-chevron-down'); - }); - $('.toggle-hidden').live('click', function() { - $(this).parents('td').find('div:hidden').show(); - return false; - }); - return $('#request_hidden_user_explanation_reasons input').live('click', function() { - var info_request_id, reason; - $('#request_hidden_user_subject, #request_hidden_user_explanation, #request_hide_button').show(); - info_request_id = $('#hide_request_form').attr('data-info-request-id'); - reason = $(this).val(); - $('#request_hidden_user_explanation_field').attr("value", "[loading default text...]"); - return $.ajax("/hidden_user_explanation?reason=" + reason + "&info_request_id=" + info_request_id, { - type: "GET", - dataType: "text", - error: function(data, textStatus, jqXHR) { - return $('#request_hidden_user_explanation_field').attr("value", "Error: " + textStatus); - }, - success: function(data, textStatus, jqXHR) { - return $('#request_hidden_user_explanation_field').attr("value", data); - } - }); - }); - }); - -}).call(this); diff --git a/app/views/admin_request/edit_outgoing.html.erb b/app/views/admin_request/edit_outgoing.html.erb index a0c60520a..9fe8a4497 100644 --- a/app/views/admin_request/edit_outgoing.html.erb +++ b/app/views/admin_request/edit_outgoing.html.erb @@ -25,8 +25,7 @@ <%= form_tag admin_request_destroy_outgoing_path do %> <div> <%= hidden_field_tag 'outgoing_message_id', @outgoing_message.id %> - Warning, this is permanent! ---> - <%= submit_tag "Destroy outgoing message" %> + <%= submit_tag "Destroy outgoing message", :class => "btn btn-danger", :confirm => "This is permanent! Are you sure?" %> </div> <% end %> diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index a58913892..7722efad4 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -4,7 +4,7 @@ <meta http-equiv="content-type" content="text/html;charset=UTF-8" > <title><%= site_name %> admin<%= @title ? ":" : "" %> <%=@title%></title> - <%= javascript_include_tag '/javascripts/jquery.js', '/admin/javascripts/jquery-ui.min.js', '/admin/javascripts/bootstrap-collapse', '/admin/javascripts/bootstrap-tab', '/admin/javascripts/admin' %> + <%= javascript_include_tag '/javascripts/jquery.js', '/admin/javascripts/jquery-ui.min.js', '/admin/javascripts/bootstrap-collapse', '/admin/javascripts/bootstrap-tab', '/admin/javascripts/admin', '/javascripts/jquery_ujs' %> <%= stylesheet_link_tag 'admin-theme/jquery-ui-1.8.15.custom.css', :rel => 'stylesheet'%> <%= stylesheet_link_tag "/admin/stylesheets/admin", :title => "Main", :rel => "stylesheet" %> diff --git a/app/views/public_body/statistics.html.erb b/app/views/public_body/statistics.html.erb new file mode 100644 index 000000000..840af0c10 --- /dev/null +++ b/app/views/public_body/statistics.html.erb @@ -0,0 +1,75 @@ +<% @title = _("Public Body Statistics") %> +<div id="main_content"> + <h1>Public Body Statistics</h1> + + <p><%= _("This page of public body statistics is currently \ +experimental, so there are some caveats that should be borne \ +in mind:") %></p> + + <ul> + + <li><%= _("The percentages are calculated with respect to \ +the total number of requests, which includes invalid \ +requests; this is a known problem that will be fixed in a \ +later release.") %></li> + + <li><%= _("The classification of requests (e.g. to say \ +whether they were successful or not) is done manually by users \ +and administrators of the site, which means that they are \ +subject to error.") %></li> + + <li><%= _("Requests are considered successful if they were \ +classified as either 'Successful' or 'Partially Successful'.") %></li> + + <li><%= _("Requests are considered overdue if they are in \ +the 'Overdue' or 'Very Overdue' states.") %></li> + + <li><%= _("The error bars shown are 95% confidence intervals \ +for the hypothesized underlying proportion (i.e. that which \ +you would obtain by making an infinite number of requests \ +through this site to that authority). In other words, the \ +population being sampled is all the current and future \ +requests to the authority through this site, rather than, \ +say, all requests that have been made to the public body by \ +any means.") %></li> + + </ul> + + <p><%= _("These graphs were partly inspired by \ +<a href=\"http://mark.goodge.co.uk/2011/08/number-crunching-whatdotheyknow/\">some \ +statistics that Mark Goodge produced for WhatDoTheyKnow</a>, so thanks \ +are due to him.") %></p> + + <% @graph_list.each do |graph_data| %> + <h3 class="public-body-ranking-title"><%= graph_data['title']%></h3> + <div class="public-body-ranking" id="<%= graph_data['id'] %>"> + <% if graph_data['x_values'] %> + <table border=0> + <thead> + <tr> + <th>Public Body</th> + <th><%= graph_data['y_axis'] %></th> + </tr> + </thead> + <tbody> + <% graph_data['x_ticks'].each_with_index do |pb_and_index, i| %> + <tr> + <td><%= pb_and_index[1] %></td> + <td class="statistic"><%= graph_data['y_values'][i].round %></td> + </tr> + <% end %> + </tbody> + </table> + <% else %> + <%= _("There was no data calculated for this graph yet.") %> + <% end %> + </div> + <% end %> + +<script type="text/javascript"> + var graphs_data = <%= @graph_list.to_json.html_safe %>; +</script> +<!--[if lte IE 8]><%= javascript_include_tag 'excanvas.min.js' %><![endif]--> +<%= javascript_include_tag 'jquery.flot.min.js', 'jquery.flot.errorbars.min.js', 'jquery.flot.axislabels.js', 'stats-graphs.js' %> + +</div> |