aboutsummaryrefslogtreecommitdiffstats
path: root/app/models/info_request.rb
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/info_request.rb')
-rw-r--r--app/models/info_request.rb294
1 files changed, 139 insertions, 155 deletions
diff --git a/app/models/info_request.rb b/app/models/info_request.rb
index cee9eb959..46c247fa9 100644
--- a/app/models/info_request.rb
+++ b/app/models/info_request.rb
@@ -26,8 +26,7 @@
require 'digest/sha1'
class InfoRequest < ActiveRecord::Base
- include ActionView::Helpers::UrlHelper
- include ActionController::UrlWriter
+ include Rails.application.routes.url_helpers
strip_attributes!
@@ -51,7 +50,7 @@ class InfoRequest < ActiveRecord::Base
has_tag_string
- named_scope :visible, :conditions => {:prominence => "normal"}
+ scope :visible, :conditions => {:prominence => "normal"}
# user described state (also update in info_request_event, admin_request/edit.rhtml)
validate :must_be_valid_state
@@ -81,6 +80,11 @@ class InfoRequest < ActiveRecord::Base
'blackhole' # just dump them
]
+ # only check on create, so existing models with mixed case are allowed
+ validate :title_formatting, :on => :create
+
+ after_initialize :set_defaults
+
def self.enumerate_states
states = [
'waiting_response',
@@ -104,6 +108,12 @@ class InfoRequest < ActiveRecord::Base
states
end
+ # Possible reasons that a request could be reported for administrator attention
+ def report_reasons
+ ["Contains defamatory material", "Not a valid request", "Request for personal information",
+ "Contains personal information", "Vexatious", "Other"]
+ end
+
def must_be_valid_state
errors.add(:described_state, "is not a valid state") if
!InfoRequest.enumerate_states.include? described_state
@@ -146,9 +156,13 @@ class InfoRequest < ActiveRecord::Base
end
end
+ def user_json_for_api
+ is_external? ? { :name => user_name || _("Anonymous user") } : user.json_for_api
+ end
+
@@custom_states_loaded = false
begin
- if ENV["RAILS_ENV"] != "test"
+ if !Rails.env.test?
require 'customstates'
include InfoRequestCustomStates
@@custom_states_loaded = true
@@ -185,21 +199,6 @@ class InfoRequest < ActiveRecord::Base
self.comments.find(:all, :conditions => 'visible')
end
- # Central function to do all searches
- # (Not really the right place to put it, but everything can get it here, and it
- # does *mainly* find info requests, via their events, so hey)
- def InfoRequest.full_search(models, query, order, ascending, collapse, per_page, page)
- offset = (page - 1) * per_page
-
- return ::ActsAsXapian::Search.new(
- models, query,
- :offset => offset, :limit => per_page,
- :sort_by_prefix => order,
- :sort_by_ascending => ascending,
- :collapse_by_prefix => collapse
- )
- end
-
# If the URL name has changed, then all request: queries will break unless
# we update index for every event. Also reindex if prominence changes.
after_update :reindex_some_request_events
@@ -228,17 +227,6 @@ class InfoRequest < ActiveRecord::Base
end
end
- # For debugging
- def InfoRequest.profile_search(query)
- t = Time.now.usec
- for i in (1..10)
- t = Time.now.usec - t
- secs = t / 1000000.0
- STDOUT.write secs.to_s + " query " + i.to_s + "\n"
- results = InfoRequest.full_search([InfoRequestEvent], query, "created_at", true, nil, 25, 1).results
- end
- end
-
public
# When name is changed, also change the url name
def title=(title)
@@ -290,7 +278,7 @@ public
end
end
def email_subject_followup(incoming_message = nil)
- if incoming_message.nil? || !incoming_message.valid_to_reply_to?
+ if incoming_message.nil? || !incoming_message.valid_to_reply_to? || !incoming_message.subject
'Re: ' + self.email_subject_request
else
if incoming_message.subject.match(/^Re:/i)
@@ -347,7 +335,10 @@ public
# copying an email, and that doesn't matter)
def InfoRequest.find_by_incoming_email(incoming_email)
id, hash = InfoRequest._extract_id_hash_from_email(incoming_email)
- return self.find_by_magic_email(id, hash)
+ if hash_from_id(id) == hash
+ # Not using find(id) because we don't exception raised if nothing found
+ find_by_id(id)
+ end
end
# Return list of info requests which *might* be right given email address
@@ -456,7 +447,7 @@ public
if !allow
if self.handle_rejected_responses == 'bounce'
- RequestMailer.deliver_stopped_responses(self, email, raw_email_data) if !is_external?
+ RequestMailer.stopped_responses(self, email, raw_email_data).deliver if !is_external?
elsif self.handle_rejected_responses == 'holding_pen'
InfoRequest.holding_pen_request.receive(email, raw_email_data, false, reason)
elsif self.handle_rejected_responses == 'blackhole'
@@ -474,6 +465,17 @@ public
incoming_message = IncomingMessage.new
ActiveRecord::Base.transaction do
+
+ # To avoid a deadlock when simultaneously dealing with two
+ # incoming emails that refer to the same InfoRequest, we
+ # lock the row for update. In Rails 3.2.0 and later this
+ # can be done with info_request.with_lock or
+ # info_request.lock!, but upgrading to that version of
+ # Rails creates many other problems at the moment. In the
+ # interim, just use raw SQL to do the SELECT ... FOR UPDATE
+ raw_sql = "SELECT * FROM info_requests WHERE id = #{self.id} LIMIT 1 FOR UPDATE"
+ ActiveRecord::Base.connection.execute(raw_sql)
+
raw_email = RawEmail.new
incoming_message.raw_email = raw_email
incoming_message.info_request = self
@@ -484,13 +486,13 @@ public
self.awaiting_description = true
params = { :incoming_message_id => incoming_message.id }
if !rejected_reason.empty?
- params[:rejected_reason] = rejected_reason
+ params[:rejected_reason] = rejected_reason.to_str
end
self.log_event("response", params)
self.save!
end
self.info_request_events.each { |event| event.xapian_mark_needs_index } # for the "waiting_classification" index
- RequestMailer.deliver_new_response(self, incoming_message) if !is_external?
+ RequestMailer.new_response(self, incoming_message).deliver if !is_external?
end
@@ -548,15 +550,24 @@ public
end
def requires_admin?
- return true if InfoRequest.requires_admin_states.include?(described_state)
- return false
+ ['requires_admin', 'error_message', 'attention_requested'].include?(described_state)
+ end
+
+ # Report this request for administrator attention
+ def report!(reason, message, user)
+ ActiveRecord::Base.transaction do
+ set_described_state('attention_requested', user, "Reason: #{reason}\n\n#{message}")
+ self.attention_requested = true # tells us if attention has ever been requested
+ save!
+ end
end
# change status, including for last event for later historical purposes
- def set_described_state(new_state, set_by = nil)
+ 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.get_last_event
+ last_event = self.info_request_events.last
last_event.described_state = new_state
self.described_state = new_state
last_event.save!
@@ -568,9 +579,23 @@ public
if self.requires_admin?
# Check there is someone to send the message "from"
if !set_by.nil? || !self.user.nil?
- RequestMailer.deliver_requires_admin(self, set_by)
+ RequestMailer.requires_admin(self, set_by, message).deliver
end
end
+
+ unless set_by.nil? || is_actual_owning_user?(set_by) || described_state == 'attention_requested'
+ # Log the status change by someone other than the requester
+ event = log_event("status_update",
+ { :user_id => set_by.id,
+ :old_described_state => old_described_state,
+ :described_state => described_state,
+ })
+ # Create a classification event for league tables
+ RequestClassification.create!(:user_id => set_by.id,
+ :info_request_event_id => event.id)
+
+ RequestMailer.old_unclassified_updated(self).deliver if !is_external?
+ end
end
# Work out what the situation of the request is. In addition to values of
@@ -684,7 +709,7 @@ public
# last_event_forming_initial_request. There may be more obscure
# things, e.g. fees, not properly covered.
def date_response_required_by
- Holiday.due_date_from(self.date_initial_request_last_sent_at, Configuration::reply_late_after_days, Configuration::working_or_calendar_days)
+ Holiday.due_date_from(self.date_initial_request_last_sent_at, AlaveteliConfiguration::reply_late_after_days, AlaveteliConfiguration::working_or_calendar_days)
end
# This is a long stop - even with UK public interest test extensions, 40
# days is a very long time.
@@ -692,10 +717,10 @@ public
last_sent = last_event_forming_initial_request
if self.public_body.is_school?
# schools have 60 working days maximum (even over a long holiday)
- Holiday.due_date_from(self.date_initial_request_last_sent_at, Configuration::special_reply_very_late_after_days, Configuration::working_or_calendar_days)
+ Holiday.due_date_from(self.date_initial_request_last_sent_at, AlaveteliConfiguration::special_reply_very_late_after_days, AlaveteliConfiguration::working_or_calendar_days)
else
# public interest test ICO guidance gives 40 working maximum
- Holiday.due_date_from(self.date_initial_request_last_sent_at, Configuration::reply_very_late_after_days, Configuration::working_or_calendar_days)
+ Holiday.due_date_from(self.date_initial_request_last_sent_at, AlaveteliConfiguration::reply_very_late_after_days, AlaveteliConfiguration::working_or_calendar_days)
end
end
@@ -719,41 +744,28 @@ public
self.info_request_events.create!(:event_type => type, :params => params)
end
+ def response_events
+ self.info_request_events.select{|e| e.response?}
+ end
+
# The last response is the default one people might want to reply to
def get_last_response_event_id
- for e in self.info_request_events.reverse
- if e.event_type == 'response'
- return e.id
- end
- end
- return nil
-
+ get_last_response_event.id if get_last_response_event
end
def get_last_response_event
- for e in self.info_request_events.reverse
- if e.event_type == 'response'
- return e
- end
- end
- return nil
+ response_events.last
end
def get_last_response
- last_response_event = self.get_last_response_event
- if last_response_event.nil?
- return nil
- else
- return last_response_event.incoming_message
- end
+ get_last_response_event.incoming_message if get_last_response_event
+ end
+
+ def outgoing_events
+ info_request_events.select{|e| e.outgoing? }
end
# The last outgoing message
def get_last_outgoing_event
- for e in self.info_request_events.reverse
- if [ 'sent', 'followup_sent' ].include?(e.event_type)
- return e
- end
- end
- return nil
+ outgoing_events.last
end
# Text from the the initial request, for use in summary display
@@ -821,46 +833,31 @@ public
# Display version of status
def InfoRequest.get_status_description(status)
- if status == 'waiting_classification'
- _("Awaiting classification.")
- elsif status == 'waiting_response'
- _("Awaiting response.")
- elsif status == 'waiting_response_overdue'
- _("Delayed.")
- elsif status == 'waiting_response_very_overdue'
- _("Long overdue.")
- elsif status == 'not_held'
- _("Information not held.")
- elsif status == 'rejected'
- _("Refused.")
- elsif status == 'partially_successful'
- _("Partially successful.")
- elsif status == 'successful'
- _("Successful.")
- elsif status == 'waiting_clarification'
- _("Waiting clarification.")
- elsif status == 'gone_postal'
- _("Handled by post.")
- elsif status == 'internal_review'
- _("Awaiting internal review.")
- elsif status == 'error_message'
- _("Delivery error")
- elsif status == 'requires_admin'
- _("Unusual response.")
- elsif status == 'attention_requested'
- _("Reported for administrator attention.")
- elsif status == 'user_withdrawn'
- _("Withdrawn by the requester.")
- elsif status == 'vexatious'
- _("Considered by administrators as vexatious and hidden from site.")
- elsif status == 'not_foi'
- _("Considered by administrators as not an FOI request and hidden from site.")
+ descriptions = {
+ 'waiting_classification' => _("Awaiting classification."),
+ 'waiting_response' => _("Awaiting response."),
+ 'waiting_response_overdue' => _("Delayed."),
+ 'waiting_response_very_overdue' => _("Long overdue."),
+ 'not_held' => _("Information not held."),
+ 'rejected' => _("Refused."),
+ 'partially_successful' => _("Partially successful."),
+ 'successful' => _("Successful."),
+ 'waiting_clarification' => _("Waiting clarification."),
+ 'gone_postal' => _("Handled by post."),
+ 'internal_review' => _("Awaiting internal review."),
+ 'error_message' => _("Delivery error"),
+ 'requires_admin' => _("Unusual response."),
+ 'attention_requested' => _("Reported for administrator attention."),
+ 'user_withdrawn' => _("Withdrawn by the requester."),
+ 'vexatious' => _("Considered by administrators as vexatious and hidden from site."),
+ 'not_foi' => _("Considered by administrators as not an FOI request and hidden from site."),
+ }
+ if descriptions[status]
+ descriptions[status]
+ elsif respond_to?(:theme_display_status)
+ theme_display_status(status)
else
- begin
- return self.theme_display_status(status)
- rescue NoMethodError
- raise _("unknown status ") + status
- end
+ raise _("unknown status ") + status
end
end
@@ -895,10 +892,10 @@ public
end
def InfoRequest.magic_email_for_id(prefix_part, id)
- magic_email = Configuration::incoming_email_prefix
+ magic_email = AlaveteliConfiguration::incoming_email_prefix
magic_email += prefix_part + id.to_s
magic_email += "-" + InfoRequest.hash_from_id(id)
- magic_email += "@" + Configuration::incoming_email_domain
+ magic_email += "@" + AlaveteliConfiguration::incoming_email_domain
return magic_email
end
@@ -909,25 +906,7 @@ public
end
def InfoRequest.hash_from_id(id)
- return Digest::SHA1.hexdigest(id.to_s + Configuration::incoming_email_secret)[0,8]
- end
-
- # Called by find_by_incoming_email - and used to be called by separate
- # function for envelope from address, until we abandoned it.
- def InfoRequest.find_by_magic_email(id, hash)
- expected_hash = InfoRequest.hash_from_id(id)
- #print "expected: " + expected_hash + "\nhash: " + hash + "\n"
- if hash != expected_hash
- return nil
- else
- begin
- return self.find(id)
- rescue ActiveRecord::RecordNotFound
- # so error email is sent to admin, rather than the exception sending weird
- # error to the public body.
- return nil
- end
- end
+ return Digest::SHA1.hexdigest(id.to_s + AlaveteliConfiguration::incoming_email_secret)[0,8]
end
# Used to find when event last changed
@@ -987,13 +966,8 @@ public
end
def is_old_unclassified?
- return false if is_external?
- return false if !awaiting_description
- return false if url_title == 'holding_pen'
- last_response_event = get_last_response_event
- return false unless last_response_event
- return false if last_response_event.created_at >= Time.now - OLD_AGE_IN_DAYS
- return true
+ !is_external? && awaiting_description && url_title != 'holding_pen' && get_last_response_event &&
+ Time.now > get_last_response_event.created_at + OLD_AGE_IN_DAYS
end
# List of incoming messages to followup, by unique email
@@ -1085,25 +1059,6 @@ public
InfoRequest.update_all "allow_new_responses_from = 'nobody' where updated_at < (now() - interval '1 year') and allow_new_responses_from in ('anybody', 'authority_only') and url_title <> 'holding_pen'"
end
- # Returns a random FOI request
- def InfoRequest.random
- max_id = InfoRequest.connection.select_value('select max(id) as a from info_requests').to_i
- info_request = nil
- count = 0
- while info_request.nil?
- if count > 100
- return nil
- end
- id = rand(max_id) + 1
- begin
- count += 1
- info_request = find(id, :conditions => ["prominence = 'normal'"])
- rescue ActiveRecord::RecordNotFound
- end
- end
- return info_request
- end
-
def json_for_api(deep)
ret = {
:id => self.id,
@@ -1137,7 +1092,7 @@ public
before_save :purge_in_cache
def purge_in_cache
- if !Configuration::varnish_host.blank? && !self.id.nil?
+ if !AlaveteliConfiguration::varnish_host.blank? && !self.id.nil?
# we only do this for existing info_requests (new ones have a nil id)
path = url_for(:controller => 'request', :action => 'show', :url_title => self.url_title, :only_path => true, :locale => :none)
req = PurgeRequest.find_by_url(path)
@@ -1155,5 +1110,34 @@ public
yield(column.human_name, self.send(column.name), column.type.to_s, column.name)
end
end
+
+ private
+
+ def set_defaults
+ begin
+ if self.described_state.nil?
+ self.described_state = 'waiting_response'
+ end
+ rescue ActiveModel::MissingAttributeError
+ # this should only happen on Model.exists?() call. It can be safely ignored.
+ # See http://www.tatvartha.com/2011/03/activerecordmissingattributeerror-missing-attribute-a-bug-or-a-features/
+ end
+ # FOI or EIR?
+ if !self.public_body.nil? && self.public_body.eir_only?
+ self.law_used = 'eir'
+ end
+ end
+
+ def title_formatting
+ if !self.title.nil? && !MySociety::Validate.uses_mixed_capitals(self.title, 10)
+ errors.add(:title, _('Please write the summary using a mixture of capital and lower case letters. This makes it easier for others to read.'))
+ end
+ if !self.title.nil? && title.size > 200
+ errors.add(:title, _('Please keep the summary short, like in the subject of an email. You can use a phrase, rather than a full sentence.'))
+ end
+ if !self.title.nil? && self.title =~ /^(FOI|Freedom of Information)\s*requests?$/i
+ errors.add(:title, _('Please describe more what the request is about in the subject. There is no need to say it is an FOI request, we add that on anyway.'))
+ end
+ end
end