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/admin_public_body_controller.rb7
-rw-r--r--app/controllers/admin_request_controller.rb8
-rw-r--r--app/controllers/api_controller.rb2
-rw-r--r--app/controllers/application_controller.rb11
-rw-r--r--app/controllers/public_body_controller.rb182
-rw-r--r--app/controllers/request_controller.rb2
-rw-r--r--app/models/info_request.rb63
-rw-r--r--app/models/public_body.rb83
-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_general/timeline.html.erb2
-rw-r--r--app/views/admin_outgoing_message/edit.html.erb3
-rw-r--r--app/views/admin_public_body/import_csv.html.erb4
-rw-r--r--app/views/layouts/admin.html.erb2
-rw-r--r--app/views/public_body/statistics.html.erb75
18 files changed, 392 insertions, 149 deletions
diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb
index 0d83c9251..8b606ea85 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/admin_public_body_controller.rb b/app/controllers/admin_public_body_controller.rb
index 078af12f4..e0da234b0 100644
--- a/app/controllers/admin_public_body_controller.rb
+++ b/app/controllers/admin_public_body_controller.rb
@@ -14,6 +14,7 @@ class AdminPublicBodyController < AdminController
def _lookup_query_internal
@locale = self.locale_from_params()
+ underscore_locale = @locale.gsub '-', '_'
I18n.with_locale(@locale) do
@query = params[:query]
if @query == ""
@@ -23,10 +24,10 @@ class AdminPublicBodyController < AdminController
if @page == ""
@page = nil
end
- @public_bodies = PublicBody.joins(:translations).where(@query.nil? ? "public_body_translations.locale = '#{@locale}'" :
+ @public_bodies = PublicBody.joins(:translations).where(@query.nil? ? "public_body_translations.locale = '#{underscore_locale}'" :
["(lower(public_body_translations.name) like lower('%'||?||'%') or
lower(public_body_translations.short_name) like lower('%'||?||'%') or
- lower(public_body_translations.request_email) like lower('%'||?||'%' )) AND (public_body_translations.locale = '#{@locale}')", @query, @query, @query]).paginate :order => "public_body_translations.name", :page => @page, :per_page => 100
+ lower(public_body_translations.request_email) like lower('%'||?||'%' )) AND (public_body_translations.locale = '#{underscore_locale}')", @query, @query, @query]).paginate :order => "public_body_translations.name", :page => @page, :per_page => 100
end
@public_bodies_by_tag = PublicBody.find_by_tag(@query)
end
@@ -146,12 +147,12 @@ class AdminPublicBodyController < AdminController
if params[:csv_file]
csv_contents = params[:csv_file].read
@original_csv_file = params[:csv_file].original_filename
+ csv_contents = normalize_string_to_utf8(csv_contents)
# or from previous dry-run temporary file
elsif params[:temporary_csv_file] && params[:original_csv_file]
csv_contents = retrieve_csv_data(params[:temporary_csv_file])
@original_csv_file = params[:original_csv_file]
end
-
if !csv_contents.nil?
# Try with dry run first
errors, notes = PublicBody.import_csv(csv_contents,
diff --git a/app/controllers/admin_request_controller.rb b/app/controllers/admin_request_controller.rb
index 62d7a0b39..4d45ced8b 100644
--- a/app/controllers/admin_request_controller.rb
+++ b/app/controllers/admin_request_controller.rb
@@ -62,9 +62,6 @@ class AdminRequestController < AdminController
@info_request.title = params[:info_request][:title]
@info_request.prominence = params[:info_request][:prominence]
- if @info_request.described_state != params[:info_request][:described_state]
- @info_request.set_described_state(params[:info_request][:described_state])
- end
@info_request.awaiting_description = params[:info_request][:awaiting_description] == "true" ? true : false
@info_request.allow_new_responses_from = params[:info_request][:allow_new_responses_from]
@info_request.handle_rejected_responses = params[:info_request][:handle_rejected_responses]
@@ -77,13 +74,16 @@ class AdminRequestController < AdminController
{ :editor => admin_current_user(),
:old_title => old_title, :title => @info_request.title,
:old_prominence => old_prominence, :prominence => @info_request.prominence,
- :old_described_state => old_described_state, :described_state => @info_request.described_state,
+ :old_described_state => old_described_state, :described_state => params[:info_request][:described_state],
:old_awaiting_description => old_awaiting_description, :awaiting_description => @info_request.awaiting_description,
:old_allow_new_responses_from => old_allow_new_responses_from, :allow_new_responses_from => @info_request.allow_new_responses_from,
:old_handle_rejected_responses => old_handle_rejected_responses, :handle_rejected_responses => @info_request.handle_rejected_responses,
:old_tag_string => old_tag_string, :tag_string => @info_request.tag_string,
:old_comments_allowed => old_comments_allowed, :comments_allowed => @info_request.comments_allowed
})
+ if @info_request.described_state != params[:info_request][:described_state]
+ @info_request.set_described_state(params[:info_request][:described_state])
+ end
# expire cached files
expire_for_request(@info_request)
flash[:notice] = 'Request successfully updated.'
diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb
index 49b226e4b..e7bea67ef 100644
--- a/app/controllers/api_controller.rb
+++ b/app/controllers/api_controller.rb
@@ -63,6 +63,8 @@ class ApiController < ApplicationController
:smtp_message_id => nil
)
+ request.set_described_state('waiting_response')
+
# Return the URL and ID number.
render :json => {
'url' => make_url("request", request.url_title),
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 902b43215..cbdffc441 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..02f0ceb19 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
@@ -85,34 +86,45 @@ class PublicBodyController < ApplicationController
def list
long_cache
# XXX move some of these tag SQL queries into has_tag_string.rb
- @query = "%#{params[:public_body_query].nil? ? "" : params[:public_body_query]}%"
+
+ like_query = params[:public_body_query]
+ like_query = "" if like_query.nil?
+ like_query = "%#{like_query}%"
+
@tag = params[:tag]
- @locale = self.locale_from_params()
- default_locale = I18n.default_locale.to_s
- locale_condition = "(upper(public_body_translations.name) LIKE upper(?)
- OR upper(public_body_translations.notes) LIKE upper (?))
- AND public_body_translations.locale = ?
- AND public_bodies.id <> #{PublicBody.internal_admin_body.id}"
+
+ @locale = self.locale_from_params
+ underscore_locale = @locale.gsub '-', '_'
+ underscore_default_locale = I18n.default_locale.to_s.gsub '-', '_'
+
+ where_condition = "public_bodies.id <> #{PublicBody.internal_admin_body.id}"
+ where_parameters = []
+
+ first_letter = false
+
+ base_tag_condition = " AND (SELECT count(*) FROM has_tag_string_tags" \
+ " WHERE has_tag_string_tags.model_id = public_bodies.id" \
+ " AND has_tag_string_tags.model = 'PublicBody'"
+
+ # Restrict the public bodies shown according to the tag
+ # parameter supplied in the URL:
if @tag.nil? or @tag == "all"
@tag = "all"
- conditions = [locale_condition, @query, @query, default_locale]
elsif @tag == 'other'
category_list = PublicBodyCategories::get().tags().map{|c| "'"+c+"'"}.join(",")
- conditions = [locale_condition + ' AND (select count(*) from has_tag_string_tags where has_tag_string_tags.model_id = public_bodies.id
- and has_tag_string_tags.model = \'PublicBody\'
- and has_tag_string_tags.name in (' + category_list + ')) = 0', @query, @query, default_locale]
+ where_condition += base_tag_condition + " AND has_tag_string_tags.name in (#{category_list})) = 0"
elsif @tag.size == 1
@tag.upcase!
- conditions = [locale_condition + ' AND public_body_translations.first_letter = ?', @query, @query, default_locale, @tag]
+ # The first letter queries have to be done on
+ # translations, so just indicate to add that later:
+ first_letter = true
elsif @tag.include?(":")
name, value = HasTagString::HasTagStringTag.split_tag_into_name_value(@tag)
- conditions = [locale_condition + ' AND (select count(*) from has_tag_string_tags where has_tag_string_tags.model_id = public_bodies.id
- and has_tag_string_tags.model = \'PublicBody\'
- and has_tag_string_tags.name = ? and has_tag_string_tags.value = ?) > 0', @query, @query, default_locale, name, value]
+ where_condition += base_tag_condition + " AND has_tag_string_tags.name = ? AND has_tag_string_tags.value = ?) > 0"
+ where_parameters.concat [name, value]
else
- conditions = [locale_condition + ' AND (select count(*) from has_tag_string_tags where has_tag_string_tags.model_id = public_bodies.id
- and has_tag_string_tags.model = \'PublicBody\'
- and has_tag_string_tags.name = ?) > 0', @query, @query, default_locale, @tag]
+ where_condition += base_tag_condition + " AND has_tag_string_tags.name = ?) > 0"
+ where_parameters.concat [@tag]
end
if @tag == "all"
@@ -127,10 +139,45 @@ class PublicBodyController < ApplicationController
@description = _("in the category ‘{{category_name}}’", :category_name=>category_name)
end
end
+
I18n.with_locale(@locale) do
- @public_bodies = PublicBody.where(conditions).joins(:translations).order("public_body_translations.name").paginate(
- :page => params[:page], :per_page => 100
- )
+
+ if AlaveteliConfiguration::public_body_list_fallback_to_default_locale
+ # Unfortunately, when we might fall back to the
+ # default locale, this is a rather complex query:
+ query = %Q{
+ SELECT public_bodies.*, COALESCE(current_locale.name, default_locale.name) AS display_name
+ FROM public_bodies
+ LEFT OUTER JOIN public_body_translations as current_locale
+ ON (public_bodies.id = current_locale.public_body_id
+ AND current_locale.locale = ? AND #{get_public_body_list_translated_condition 'current_locale', first_letter})
+ LEFT OUTER JOIN public_body_translations as default_locale
+ ON (public_bodies.id = default_locale.public_body_id
+ AND default_locale.locale = ? AND #{get_public_body_list_translated_condition 'default_locale', first_letter})
+ WHERE #{where_condition} AND COALESCE(current_locale.name, default_locale.name) IS NOT NULL
+ ORDER BY display_name}
+ sql = [query, underscore_locale, like_query, like_query]
+ sql.push @tag if first_letter
+ sql += [underscore_default_locale, like_query, like_query]
+ sql.push @tag if first_letter
+ sql += where_parameters
+ @public_bodies = PublicBody.paginate_by_sql(
+ sql,
+ :page => params[:page],
+ :per_page => 100)
+ else
+ # The simpler case where we're just searching in the current locale:
+ where_condition = get_public_body_list_translated_condition('public_body_translations', first_letter, true) +
+ ' AND ' + where_condition
+ where_sql = [where_condition, like_query, like_query]
+ where_sql.push @tag if first_letter
+ where_sql += [underscore_locale] + where_parameters
+ @public_bodies = PublicBody.where(where_sql) \
+ .joins(:translations) \
+ .order("public_body_translations.name") \
+ .paginate(:page => params[:page], :per_page => 100)
+ end
+
respond_to do |format|
format.html { render :template => "public_body/list" }
end
@@ -149,6 +196,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
@@ -157,5 +282,18 @@ class PublicBodyController < ApplicationController
@xapian_requests = perform_search_typeahead(query, PublicBody)
render :partial => "public_body/search_ahead"
end
-end
+ private
+ def get_public_body_list_translated_condition(table, first_letter=false, locale=nil)
+ result = "(upper(#{table}.name) LIKE upper(?)" \
+ " OR upper(#{table}.notes) LIKE upper (?))"
+ if first_letter
+ result += " AND #{table}.first_letter = ?"
+ end
+ if locale
+ result += " AND #{table}.locale = ?"
+ end
+ result
+ end
+
+end
diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb
index cd416a0c4..82697b792 100644
--- a/app/controllers/request_controller.rb
+++ b/app/controllers/request_controller.rb
@@ -586,7 +586,7 @@ class RequestController < ApplicationController
@outgoing_message.set_signature_name(@user.name) if !@user.nil?
if (not @incoming_message.nil?) and @info_request != @incoming_message.info_request
- raise sprintf("Incoming message %d does not belong to request %d", @incoming_message.info_request_id, @info_request.id)
+ raise ActiveRecord::RecordNotFound.new("Incoming message #{@incoming_message.id} does not belong to request #{@info_request.id}")
end
# Test for hidden requests
diff --git a/app/models/info_request.rb b/app/models/info_request.rb
index 44593295d..86cc98371 100644
--- a/app/models/info_request.rb
+++ b/app/models/info_request.rb
@@ -1,3 +1,4 @@
+# encoding: utf-8
# == Schema Information
#
# Table name: info_requests
@@ -30,7 +31,10 @@ class InfoRequest < ActiveRecord::Base
strip_attributes!
validates_presence_of :title, :message => N_("Please enter a summary of your request")
- validates_format_of :title, :with => /[a-zA-Z]/, :message => N_("Please write a summary with some text in it"), :if => Proc.new { |info_request| !info_request.title.nil? && !info_request.title.empty? }
+ # TODO: When we no longer support Ruby 1.8, this can be done with /[[:alpha:]]/
+ validates_format_of :title, :with => /[ёЁа-яА-Яa-zA-Zà-üÀ-Ü]/iu,
+ :message => N_("Please write a summary with some text in it"),
+ :if => Proc.new { |info_request| !info_request.title.nil? && !info_request.title.empty? }
belongs_to :user
validate :must_be_internal_or_external
@@ -562,12 +566,15 @@ public
end
# change status, including for last event for later historical purposes
+ # described_state should always indicate the current state of the request, as described
+ # by the request owner (or, in some other cases an admin or other user)
def set_described_state(new_state, set_by = nil, message = "")
old_described_state = described_state
ActiveRecord::Base.transaction do
self.awaiting_description = false
last_event = self.info_request_events.last
last_event.described_state = new_state
+
self.described_state = new_state
last_event.save!
self.save!
@@ -587,11 +594,14 @@ public
end
end
- # Work out what the situation of the request is. In addition to values of
- # self.described_state, can take these two values:
+ # Work out what state to display for the request on the site. In addition to values of
+ # self.described_state, can take these values:
# waiting_classification
# waiting_response_overdue
# waiting_response_very_overdue
+ # (this method adds an assessment of overdueness with respect to the current time to 'waiting_response'
+ # states, and will return 'waiting_classification' instead of the described_state if the
+ # awaiting_description flag is set on the request).
def calculate_status(cached_value_ok=false)
if cached_value_ok && @cached_calculated_status
return @cached_calculated_status
@@ -610,10 +620,22 @@ public
return 'waiting_response'
end
+
+ # 'described_state' can be populated on any info_request_event but is only
+ # ever used in the process populating calculated_state on the
+ # info_request_event (if it represents a response, outgoing message, edit
+ # or status update), or previous response or outgoing message events for
+ # the same request.
+
# Fill in any missing event states for first response before a description
# was made. i.e. We take the last described state in between two responses
# (inclusive of earlier), and set it as calculated value for the earlier
- # response.
+ # response. Also set the calculated state for any initial outgoing message,
+ # follow up, edit or status_update to the described state of that event.
+
+ # Note that the calculated state of the latest info_request_event will
+ # be used in latest_status based searches and should match the described_state
+ # of the info_request.
def calculate_event_states
curr_state = nil
for event in self.info_request_events.reverse
@@ -635,7 +657,7 @@ public
event.save!
end
curr_state = nil
- elsif !curr_state.nil? && (event.event_type == 'followup_sent' || event.event_type == 'sent' || event.event_type == "status_update")
+ elsif !curr_state.nil? && (event.event_type == 'followup_sent' || event.event_type == 'sent') && !event.described_state.nil? && (event.described_state == 'waiting_response' || event.described_state == 'internal_review')
# Followups can set the status to waiting response / internal
# review. Initial requests ('sent') set the status to waiting response.
@@ -647,10 +669,22 @@ public
event.save!
end
- # And we don't want to propogate it to the response itself,
+ # And we don't want to propagate it to the response itself,
# as that might already be set to waiting_clarification / a
# success status, which we want to know about.
curr_state = nil
+ elsif !curr_state.nil? && (['edit', 'status_update'].include? event.event_type)
+ # A status update or edit event should get the same calculated state as described state
+ # so that the described state is always indexed (and will be the latest_status
+ # for the request immediately after it has been described, regardless of what
+ # other request events precede it). This means that request should be correctly included
+ # in status searches for that status. These events allow the described state to propagate in
+ # case there is a preceding response that the described state should be applied to.
+ if event.calculated_state != event.described_state
+ event.calculated_state = event.described_state
+ event.last_described_at = Time.now()
+ event.save!
+ end
end
end
end
@@ -1142,6 +1176,23 @@ public
end
end
+ after_save :update_counter_cache
+ after_destroy :update_counter_cache
+ def update_counter_cache
+ PublicBody.skip_callback(:save, :after, :purge_in_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
+ self.public_body.without_revision do
+ public_body.no_xapian_reindex = true
+ public_body.save
+ end
+ PublicBody.set_callback(:save, :after, :purge_in_cache)
+ 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 4ae889906..828e8c94a 100644
--- a/app/models/public_body.rb
+++ b/app/models/public_body.rb
@@ -40,6 +40,7 @@ class PublicBody < ActiveRecord::Base
has_many :info_requests, :order => 'created_at desc'
has_many :track_things, :order => 'created_at desc'
has_many :censor_rules, :order => 'created_at desc'
+ attr_accessor :no_xapian_reindex
has_tag_string
before_save :set_api_key, :set_default_publication_scheme
@@ -60,12 +61,23 @@ class PublicBody < ActiveRecord::Base
# XXX - Don't like repeating this!
def calculate_cached_fields(t)
- t.first_letter = t.name.scan(/^./mu)[0].upcase unless t.name.nil? or t.name.empty?
+ PublicBody.set_first_letter(t)
short_long_name = t.name
short_long_name = t.short_name if t.short_name and !t.short_name.empty?
t.url_name = MySociety::Format.simplify_url_part(short_long_name, 'body')
end
+ # Set the first letter on a public body or translation
+ def PublicBody.set_first_letter(instance)
+ unless instance.name.nil? or instance.name.empty?
+ # we use a regex to ensure it works with utf-8/multi-byte
+ first_letter = instance.name.scan(/^./mu)[0].upcase
+ if first_letter != instance.first_letter
+ instance.first_letter = first_letter
+ end
+ end
+ end
+
def translated_versions
translations
end
@@ -130,8 +142,7 @@ class PublicBody < ActiveRecord::Base
# Set the first letter, which is used for faster queries
before_save(:set_first_letter)
def set_first_letter
- # we use a regex to ensure it works with utf-8/multi-byte
- self.first_letter = self.name.scan(/./mu)[0].upcase
+ PublicBody.set_first_letter(self)
end
# If tagged "not_apply", then FOI/EIR no longer applies to authority at all
@@ -177,7 +188,11 @@ class PublicBody < ActiveRecord::Base
end
acts_as_versioned
- self.non_versioned_columns << 'created_at' << 'updated_at' << 'first_letter' << 'api_key' << 'info_requests_count'
+ self.non_versioned_columns << 'created_at' << 'updated_at' << 'first_letter' << 'api_key'
+ self.non_versioned_columns << 'info_requests_count' << 'info_requests_successful_count'
+ self.non_versioned_columns << 'info_requests_not_held_count' << 'info_requests_overdue'
+ self.non_versioned_columns << 'info_requests_overdue_count'
+
class Version
attr_accessor :created_at
@@ -231,6 +246,7 @@ class PublicBody < ActiveRecord::Base
def reindex_requested_from
if self.changes.include?('url_name')
for info_request in self.info_requests
+
for info_request_event in info_request.info_request_events
info_request_event.xapian_mark_needs_index
end
@@ -632,6 +648,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_general/timeline.html.erb b/app/views/admin_general/timeline.html.erb
index fe2221294..c4ea4849b 100644
--- a/app/views/admin_general/timeline.html.erb
+++ b/app/views/admin_general/timeline.html.erb
@@ -88,7 +88,7 @@
<% elsif event.event_type == 'comment' %>
had an annotation posted by <%=h event.comment.user.name %>.
<% elsif event.event_type == 'status_update' %>
- had its status updated by <%=h User.find(event.params[:user_id]).name %> from '<%= h event.params[:old_described_state] %>' to '<%= h event.params[:described_state] %>'.
+ had its status updated by <%= event.params[:user_id] ? User.find(event.params[:user_id]).name : event.params[:script] %> from '<%= h event.params[:old_described_state] %>' to '<%= h event.params[:described_state] %>'.
<% else %>
had '<%=event.event_type%>' done to it, parameters <%=h event.params_yaml%>.
<% end %>
diff --git a/app/views/admin_outgoing_message/edit.html.erb b/app/views/admin_outgoing_message/edit.html.erb
index d40ea03ef..d5f5f43bf 100644
--- a/app/views/admin_outgoing_message/edit.html.erb
+++ b/app/views/admin_outgoing_message/edit.html.erb
@@ -43,8 +43,7 @@
<%= form_tag admin_outgoing_destroy_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/admin_public_body/import_csv.html.erb b/app/views/admin_public_body/import_csv.html.erb
index afda5a468..18341ecf1 100644
--- a/app/views/admin_public_body/import_csv.html.erb
+++ b/app/views/admin_public_body/import_csv.html.erb
@@ -31,8 +31,8 @@
<p>
<label for="tag_behaviour">What to do with existing tags?</label>
<%= select_tag 'tag_behaviour',
- "<option value='add' selected>Add new tags to existing ones</option>
- <option value='replace'>Replace existing tags with new ones</option>"
+ raw("<option value='add' selected>Add new tags to existing ones</option>
+ <option value='replace'>Replace existing tags with new ones</option>")
%>
</p>
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>