aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/admin_controller.rb2
-rw-r--r--app/controllers/admin_general_controller.rb5
-rw-r--r--app/controllers/application_controller.rb11
-rw-r--r--app/controllers/public_body_controller.rb80
-rw-r--r--app/models/info_request.rb11
-rw-r--r--app/models/public_body.rb59
-rw-r--r--app/views/admin_general/admin.coffee24
-rw-r--r--app/views/admin_general/admin.js32
-rw-r--r--app/views/admin_general/admin_js.erb34
-rw-r--r--app/views/admin_request/edit_outgoing.html.erb3
-rw-r--r--app/views/layouts/admin.html.erb2
-rw-r--r--app/views/public_body/statistics.html.erb75
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! ---&gt;
- <%= 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>