aboutsummaryrefslogtreecommitdiffstats
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/info_request.rb63
-rw-r--r--app/models/public_body.rb83
2 files changed, 136 insertions, 10 deletions
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