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.rb606
1 files changed, 403 insertions, 203 deletions
diff --git a/app/models/info_request.rb b/app/models/info_request.rb
index cee9eb959..47ad435cb 100644
--- a/app/models/info_request.rb
+++ b/app/models/info_request.rb
@@ -1,44 +1,49 @@
+# encoding: utf-8
# == Schema Information
-# Schema version: 20120919140404
+# Schema version: 20131024114346
#
# Table name: info_requests
#
-# id :integer not null, primary key
-# title :text not null
+# id :integer not null, primary key
+# title :text not null
# user_id :integer
-# public_body_id :integer not null
-# created_at :datetime not null
-# updated_at :datetime not null
-# described_state :string(255) not null
-# awaiting_description :boolean default(FALSE), not null
-# prominence :string(255) default("normal"), not null
-# url_title :text not null
-# law_used :string(255) default("foi"), not null
-# allow_new_responses_from :string(255) default("anybody"), not null
-# handle_rejected_responses :string(255) default("bounce"), not null
-# idhash :string(255) not null
+# public_body_id :integer not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# described_state :string(255) not null
+# awaiting_description :boolean default(FALSE), not null
+# prominence :string(255) default("normal"), not null
+# url_title :text not null
+# law_used :string(255) default("foi"), not null
+# allow_new_responses_from :string(255) default("anybody"), not null
+# handle_rejected_responses :string(255) default("bounce"), not null
+# idhash :string(255) not null
# external_user_name :string(255)
# external_url :string(255)
-# attention_requested :boolean default(FALSE)
-# comments_allowed :boolean default(TRUE), not null
+# attention_requested :boolean default(FALSE)
+# comments_allowed :boolean default(TRUE), not null
+# info_request_batch_id :integer
#
require 'digest/sha1'
class InfoRequest < ActiveRecord::Base
- include ActionView::Helpers::UrlHelper
- include ActionController::UrlWriter
+ include Rails.application.routes.url_helpers
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
belongs_to :public_body, :counter_cache => true
- validates_presence_of :public_body_id
+ belongs_to :info_request_batch
+ validates_presence_of :public_body_id, :unless => Proc.new { |info_request| info_request.is_batch_request_template? }
has_many :outgoing_messages, :order => 'created_at'
has_many :incoming_messages, :order => 'created_at'
@@ -48,10 +53,11 @@ class InfoRequest < ActiveRecord::Base
has_many :comments, :order => 'created_at'
has_many :censor_rules, :order => 'created_at desc'
has_many :mail_server_logs, :order => 'mail_server_log_done_id'
+ attr_accessor :is_batch_request_template
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 +87,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,11 +115,25 @@ 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
end
+ def is_batch_request_template?
+ is_batch_request_template == true
+ end
+
# The request must either be internal, in which case it has
# a foreign key reference to a User object and no external_url or external_user_name,
# or else be external in which case it has no user_id but does have an external_url,
@@ -146,9 +171,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 +214,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 +242,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)
@@ -280,17 +283,11 @@ public
# Subject lines for emails about the request
def email_subject_request
- # XXX pull out this general_register_office specialisation
- # into some sort of separate jurisdiction dependent file
- if self.public_body.url_name == 'general_register_office'
- # without GQ in the subject, you just get an auto response
- _('{{law_used_full}} request GQ - {{title}}',:law_used_full=>self.law_used_full,:title=>self.title.html_safe)
- else
- _('{{law_used_full}} request - {{title}}',:law_used_full=>self.law_used_full,:title=>self.title.html_safe)
- end
+ _('{{law_used_full}} request - {{title}}',:law_used_full=>self.law_used_full,:title=>self.title.html_safe)
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 +344,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
@@ -391,7 +391,7 @@ public
# repeated requests, say once a quarter for time information, then might need to do that.
# XXX this *should* also check outgoing message joined to is an initial
# request (rather than follow up)
- def InfoRequest.find_by_existing_request(title, public_body_id, body)
+ def InfoRequest.find_existing(title, public_body_id, body)
return InfoRequest.find(:first, :conditions => [ "title = ? and public_body_id = ? and outgoing_messages.body = ?", title, public_body_id, body ], :include => [ :outgoing_messages ] )
end
@@ -456,7 +456,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 +474,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 +495,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,16 +559,28 @@ 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)
+ # 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.get_last_event
+ last_event = self.info_request_events.last
last_event.described_state = new_state
+
self.described_state = new_state
last_event.save!
self.save!
@@ -568,16 +591,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'
+ 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
- # 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
@@ -596,10 +626,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
@@ -633,10 +675,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
@@ -684,7 +738,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 +746,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 +773,30 @@ public
self.info_request_events.create!(:event_type => type, :params => params)
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
+ def public_response_events
+ self.info_request_events.select{|e| e.response? && e.incoming_message.all_can_view? }
+ end
+ # The last public response is the default one people might want to reply to
+ def get_last_public_response_event_id
+ get_last_public_response_event.id if get_last_public_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
+
+ def get_last_public_response_event
+ public_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
+
+ def get_last_public_response
+ get_last_public_response_event.incoming_message if get_last_public_response_event
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
+ def public_outgoing_events
+ info_request_events.select{|e| e.outgoing? && e.outgoing_message.all_can_view? }
+ end
+
+ # The last public outgoing message
+ def get_last_public_outgoing_event
+ public_outgoing_events.last
end
# Text from the the initial request, for use in summary display
@@ -804,6 +847,10 @@ public
end
end
+ def last_update_hash
+ Digest::SHA1.hexdigest(info_request_events.last.created_at.to_i.to_s + updated_at.to_i.to_s)
+ end
+
# Get previous email sent to
def get_previous_email_sent_to(info_request_event)
last_email = nil
@@ -821,46 +868,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 +927,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
@@ -908,42 +940,48 @@ public
self.idhash = InfoRequest.hash_from_id(self.id)
end
- def InfoRequest.hash_from_id(id)
- return Digest::SHA1.hexdigest(id.to_s + Configuration::incoming_email_secret)[0,8]
+ def InfoRequest.create_from_attributes(info_request_atts, outgoing_message_atts, user=nil)
+ info_request = new(info_request_atts)
+ default_message_params = {
+ :status => 'ready',
+ :message_type => 'initial_request',
+ :what_doing => 'normal_sort'
+ }
+ outgoing_message = OutgoingMessage.new(outgoing_message_atts.merge(default_message_params))
+ info_request.outgoing_messages << outgoing_message
+ outgoing_message.info_request = info_request
+ info_request.user = user
+ info_request
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
+ def InfoRequest.hash_from_id(id)
+ return Digest::SHA1.hexdigest(id.to_s + AlaveteliConfiguration::incoming_email_secret)[0,8]
end
# Used to find when event last changed
- def InfoRequest.last_event_time_clause(event_type=nil)
+ def InfoRequest.last_event_time_clause(event_type=nil, join_table=nil, join_clause=nil)
event_type_clause = ''
event_type_clause = " AND info_request_events.event_type = '#{event_type}'" if event_type
- "(SELECT created_at
- FROM info_request_events
+ tables = ['info_request_events']
+ tables << join_table if join_table
+ join_clause = "AND #{join_clause}" if join_clause
+ "(SELECT info_request_events.created_at
+ FROM #{tables.join(', ')}
WHERE info_request_events.info_request_id = info_requests.id
#{event_type_clause}
+ #{join_clause}
ORDER BY created_at desc
LIMIT 1)"
end
+ def InfoRequest.last_public_response_clause()
+ join_clause = "incoming_messages.id = info_request_events.incoming_message_id
+ AND incoming_messages.prominence = 'normal'"
+ last_event_time_clause('response', 'incoming_messages', join_clause)
+ end
+
def InfoRequest.old_unclassified_params(extra_params, include_last_response_time=false)
- last_response_created_at = last_event_time_clause('response')
+ last_response_created_at = last_public_response_clause()
age = extra_params[:age_in_days] ? extra_params[:age_in_days].days : OLD_AGE_IN_DAYS
params = { :conditions => ["awaiting_description = ?
AND #{last_response_created_at} < ?
@@ -959,11 +997,21 @@ public
def InfoRequest.count_old_unclassified(extra_params={})
params = old_unclassified_params(extra_params)
+ if extra_params[:conditions]
+ condition_string = extra_params[:conditions].shift
+ params[:conditions][0] += " AND #{condition_string}"
+ params[:conditions] += extra_params[:conditions]
+ end
count(:all, params)
end
- def InfoRequest.get_random_old_unclassified(limit)
+ def InfoRequest.get_random_old_unclassified(limit, extra_params)
params = old_unclassified_params({})
+ if extra_params[:conditions]
+ condition_string = extra_params[:conditions].shift
+ params[:conditions][0] += " AND #{condition_string}"
+ params[:conditions] += extra_params[:conditions]
+ end
params[:limit] = limit
params[:order] = "random()"
find(:all, params)
@@ -986,14 +1034,39 @@ public
find(:all, params)
end
+ def InfoRequest.download_zip_dir()
+ File.join(Rails.root, "cache", "zips", "#{Rails.env}")
+ end
+
+ def request_dirs
+ first_three_digits = id.to_s()[0..2]
+ File.join(first_three_digits.to_s, id.to_s)
+ end
+
+ def download_zip_dir
+ File.join(InfoRequest.download_zip_dir, "download", request_dirs)
+ end
+
+ def make_zip_cache_path(user)
+ cache_file_dir = File.join(InfoRequest.download_zip_dir(),
+ "download",
+ request_dirs,
+ last_update_hash)
+ cache_file_suffix = if all_can_view_all_correspondence?
+ ""
+ elsif Ability.can_view_with_prominence?('hidden', self, user)
+ "_hidden"
+ elsif Ability.can_view_with_prominence?('requester_only', self, user)
+ "_requester_only"
+ else
+ ""
+ end
+ File.join(cache_file_dir, "#{url_title}#{cache_file_suffix}.zip")
+ 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_public_response_event &&
+ Time.now > get_last_public_response_event.created_at + OLD_AGE_IN_DAYS
end
# List of incoming messages to followup, by unique email
@@ -1006,6 +1079,8 @@ public
end
incoming_message.safe_mail_from
+ next if ! incoming_message.all_can_view?
+
email = OutgoingMailer.email_for_followup(self, incoming_message)
name = OutgoingMailer.name_for_followup(self, incoming_message)
@@ -1025,7 +1100,10 @@ public
# Get the list of censor rules that apply to this request
def applicable_censor_rules
- applicable_rules = [self.censor_rules, self.public_body.censor_rules, CensorRule.global.all]
+ applicable_rules = [self.censor_rules, CensorRule.global.all]
+ unless is_batch_request_template?
+ applicable_rules << self.public_body.censor_rules
+ end
if self.user && !self.user.censor_rules.empty?
applicable_rules << self.user.censor_rules
end
@@ -1055,13 +1133,7 @@ public
end
def user_can_view?(user)
- if self.prominence == 'hidden'
- return User.view_hidden_requests?(user)
- end
- if self.prominence == 'requester_only'
- return self.is_owning_user?(user)
- end
- return true
+ Ability.can_view_with_prominence?(self.prominence, self, user)
end
# Is this request visible to everyone?
@@ -1070,6 +1142,12 @@ public
return false
end
+ def all_can_view_all_correspondence?
+ all_can_view? &&
+ incoming_messages.all?{ |message| message.all_can_view? } &&
+ outgoing_messages.all?{ |message| message.all_can_view? }
+ end
+
def indexed_by_search?
if self.prominence == 'backpage' || self.prominence == 'hidden' || self.prominence == 'requester_only'
return false
@@ -1085,25 +1163,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 +1196,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)
@@ -1150,10 +1209,151 @@ public
end
end
+ after_save :update_counter_cache
+ after_destroy :update_counter_cache
+ # This method updates the count columns of the PublicBody that
+ # store the number of "not held", "to some extent successful" and
+ # "both visible and classified" requests when saving or destroying
+ # an InfoRequest associated with the body:
+ def update_counter_cache
+ PublicBody.skip_callback(:save, :after, :purge_in_cache)
+ basic_params = {
+ :public_body_id => self.public_body_id,
+ :awaiting_description => false,
+ :prominence => 'normal'
+ }
+ [['info_requests_not_held_count', {:described_state => 'not_held'}],
+ ['info_requests_successful_count', {:described_state => ['successful', 'partially_successful']}],
+ ['info_requests_visible_classified_count', {}]].each do |column, extra_params|
+ params = basic_params.clone.update extra_params
+ self.public_body.send "#{column}=", InfoRequest.where(params).count
+ end
+ 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)
end
end
+
+
+ # Get requests that have similar important terms
+ def similar_requests(limit=10)
+ xapian_similar = nil
+ xapian_similar_more = false
+ begin
+ xapian_similar = ActsAsXapian::Similar.new([InfoRequestEvent],
+ info_request_events,
+ :limit => limit,
+ :collapse_by_prefix => 'request_collapse')
+ xapian_similar_more = (xapian_similar.matches_estimated > limit)
+ rescue
+ end
+ return [xapian_similar, xapian_similar_more]
+ end
+
+ def InfoRequest.request_list(filters, page, per_page, max_results)
+ xapian_object = ActsAsXapian::Search.new([InfoRequestEvent],
+ InfoRequestEvent.make_query_from_params(filters),
+ :offset => (page - 1) * per_page,
+ :limit => 25,
+ :sort_by_prefix => 'created_at',
+ :sort_by_ascending => true,
+ :collapse_by_prefix => 'request_collapse'
+ )
+ list_results = xapian_object.results.map { |r| r[:model] }
+ matches_estimated = xapian_object.matches_estimated
+ show_no_more_than = [matches_estimated, max_results].min
+ return { :results => list_results,
+ :matches_estimated => matches_estimated,
+ :show_no_more_than => show_no_more_than }
+ end
+
+ def InfoRequest.recent_requests
+ request_events = []
+ request_events_all_successful = false
+ # Get some successful requests
+ begin
+ query = 'variety:response (status:successful OR status:partially_successful)'
+ sortby = "newest"
+ max_count = 5
+
+ xapian_object = ActsAsXapian::Search.new([InfoRequestEvent],
+ query,
+ :offset => 0,
+ :limit => 5,
+ :sort_by_prefix => 'created_at',
+ :sort_by_ascending => true,
+ :collapse_by_prefix => 'request_title_collapse'
+ )
+ xapian_object.results
+ request_events = xapian_object.results.map { |r| r[:model] }
+
+ # If there are not yet enough successful requests, fill out the list with
+ # other requests
+ if request_events.count < max_count
+ query = 'variety:sent'
+ xapian_object = ActsAsXapian::Search.new([InfoRequestEvent],
+ query,
+ :offset => 0,
+ :limit => max_count-request_events.count,
+ :sort_by_prefix => 'created_at',
+ :sort_by_ascending => true,
+ :collapse_by_prefix => 'request_title_collapse'
+ )
+ xapian_object.results
+ more_events = xapian_object.results.map { |r| r[:model] }
+ request_events += more_events
+ # Overall we still want the list sorted with the newest first
+ request_events.sort!{|e1,e2| e2.created_at <=> e1.created_at}
+ else
+ request_events_all_successful = true
+ end
+ rescue
+ request_events = []
+ end
+
+ return [request_events, request_events_all_successful]
+ end
+
+ def InfoRequest.find_in_state(state)
+ find(:all, :select => '*, ' + last_event_time_clause + ' as last_event_time',
+ :conditions => ["described_state = ?", state],
+ :order => "last_event_time")
+ 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