# == Schema Information
#
# Table name: info_request_events
#
# id :integer not null, primary key
# info_request_id :integer not null
# event_type :text not null
# params_yaml :text not null
# created_at :datetime not null
# described_state :string(255)
# calculated_state :string(255)
# last_described_at :datetime
# incoming_message_id :integer
# outgoing_message_id :integer
# comment_id :integer
#
# models/info_request_event.rb:
#
# Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved.
# Email: hello@mysociety.org; WWW: http://www.mysociety.org/
class InfoRequestEvent < ActiveRecord::Base
extend XapianQueries
belongs_to :info_request
validates_presence_of :info_request
belongs_to :outgoing_message
belongs_to :incoming_message
belongs_to :comment
has_many :user_info_request_sent_alerts
has_many :track_things_sent_emails
validates_presence_of :event_type
def self.enumerate_event_types
[
'sent',
'resent',
'followup_sent',
'followup_resent',
'edit', # title etc. edited (in admin interface)
'edit_outgoing', # outgoing message edited (in admin interface)
'edit_comment', # comment edited (in admin interface)
'destroy_incoming', # deleted an incoming message (in admin interface)
'destroy_outgoing', # deleted an outgoing message (in admin interface)
'redeliver_incoming', # redelivered an incoming message elsewhere (in admin interface)
'edit_incoming', # incoming message edited (in admin interface)
'move_request', # changed user or public body (in admin interface)
'hide', # hid a request (in admin interface)
'manual', # you did something in the db by hand
'response',
'comment',
'status_update'
]
end
validates_inclusion_of :event_type, :in => enumerate_event_types
# user described state (also update in info_request)
validate :must_be_valid_state
def must_be_valid_state
if !described_state.nil? and !InfoRequest.enumerate_states.include?(described_state)
errors.add(described_state, "is not a valid state")
end
end
# Full text search indexing
acts_as_xapian :texts => [ :search_text_main, :title ],
:values => [
[ :created_at, 0, "range_search", :date ], # for QueryParser range searches e.g. 01/01/2008..14/01/2008
[ :created_at_numeric, 1, "created_at", :number ], # for sorting
[ :described_at_numeric, 2, "described_at", :number ], # TODO: using :number for lack of :datetime support in Xapian values
[ :request, 3, "request_collapse", :string ],
[ :request_title_collapse, 4, "request_title_collapse", :string ],
],
:terms => [ [ :calculated_state, 'S', "status" ],
[ :requested_by, 'B', "requested_by" ],
[ :requested_from, 'F', "requested_from" ],
[ :commented_by, 'C', "commented_by" ],
[ :request, 'R', "request" ],
[ :variety, 'V', "variety" ],
[ :latest_variety, 'K', "latest_variety" ],
[ :latest_status, 'L', "latest_status" ],
[ :waiting_classification, 'W', "waiting_classification" ],
[ :filetype, 'T', "filetype" ],
[ :tags, 'U', "tag" ]
],
:if => :indexed_by_search?,
:eager_load => [ :outgoing_message, :comment, { :info_request => [ :user, :public_body, :censor_rules ] } ]
def requested_by
self.info_request.user_name_slug
end
def requested_from
# acts_as_xapian will detect translated fields via Globalize and add all the
# available locales to the index. But 'requested_from' is not translated directly,
# although it relies on a translated field in PublicBody. Hence, we need to
# manually add all the localized values to the index (Xapian can handle a list
# of values in a term, btw)
self.info_request.public_body.translations.map {|t| t.url_name}
end
def commented_by
if self.event_type == 'comment'
self.comment.user.url_name
else
return ''
end
end
def request
self.info_request.url_title
end
def latest_variety
for event in self.info_request.info_request_events.reverse
if !event.variety.nil? and !event.variety.empty?
return event.variety
end
end
end
def latest_status
for event in self.info_request.info_request_events.reverse
if !event.calculated_state.nil? and !event.calculated_state.empty?
return event.calculated_state
end
end
return
end
def waiting_classification
self.info_request.awaiting_description == true ? "yes" : "no"
end
def request_title_collapse
url_title = self.info_request.url_title
# remove numeric section from the end, use this to group lots
# of similar requests by
url_title = url_title.gsub(/[_0-9]+$/, "")
return url_title
end
def described_at
# For responses, people might have RSS feeds on searches for type of
# response (e.g. successful) in which case we want to date sort by
# when the responses was described as being of the type. For other
# types, just use the create at date.
return self.last_described_at || self.created_at
end
def described_at_numeric
# format it here as no datetime support in Xapian's value ranges
return self.described_at.strftime("%Y%m%d%H%M%S")
end
def created_at_numeric
# format it here as no datetime support in Xapian's value ranges
return self.created_at.strftime("%Y%m%d%H%M%S")
end
def incoming_message_selective_columns(fields)
message = IncomingMessage.select("#{ fields }, incoming_messages.info_request_id").
joins('INNER JOIN info_request_events ON incoming_messages.id = incoming_message_id').
where('info_request_events.id = ?', id)
message = message[0]
if !message.nil?
message.info_request = InfoRequest.find(message.info_request_id)
end
return message
end
def get_clipped_response_efficiently
# TODO: this ugly code is an attempt to not always load all the
# columns for an incoming message, which can be *very* large
# (due to all the cached text). We care particularly in this
# case because it's called for every search result on a page
# (to show the search snippet). Actually, we should review if we
# need all this data to be cached in the database at all, and
# then we won't need this horrid workaround.
message = self.incoming_message_selective_columns("cached_attachment_text_clipped, cached_main_body_text_folded")
clipped_body = message.cached_main_body_text_folded
clipped_attachment = message.cached_attachment_text_clipped
if clipped_body.nil? || clipped_attachment.nil?
# we're going to have to load it anyway
text = self.incoming_message.get_text_for_indexing_clipped
else
text = clipped_body.gsub("FOLDED_QUOTED_SECTION", " ").strip + "\n\n" + clipped_attachment
end
return text + "\n\n"
end
# clipped = true - means return shorter text. It is used for snippets fore
# performance reasons. Xapian will take the full text.
def search_text_main(clipped = false)
text = ''
if self.event_type == 'sent'
text = text + self.outgoing_message.get_text_for_indexing + "\n\n"
elsif self.event_type == 'followup_sent'
text = text + self.outgoing_message.get_text_for_indexing + "\n\n"
elsif self.event_type == 'response'
if clipped
text = text + self.get_clipped_response_efficiently
else
text = text + self.incoming_message.get_text_for_indexing_full + "\n\n"
end
elsif self.event_type == 'comment'
text = text + self.comment.body + "\n\n"
else
# nothing
end
return text
end
def title
if self.event_type == 'sent'
return self.info_request.title
end
return ''
end
def filetype
if self.event_type == 'response'
if self.incoming_message.nil?
raise "event type is 'response' but no incoming message for event id #{self.id}"
end
return self.incoming_message.get_present_file_extensions
end
return ''
end
def tags
# this returns an array of strings, each gets indexed as separate term by acts_as_xapian
return self.info_request.tag_array_for_search
end
def indexed_by_search?
if ['sent', 'followup_sent', 'response', 'comment'].include?(self.event_type)
if !self.info_request.indexed_by_search?
return false
end
if self.event_type == 'response' && !self.incoming_message.indexed_by_search?
return false
end
if ['sent', 'followup_sent'].include?(self.event_type) && !self.outgoing_message.indexed_by_search?
return false
end
if self.event_type == 'comment' && !self.comment.visible
return false
end
return true
else
return false
end
end
def variety
self.event_type
end
def visible
if self.event_type == 'comment'
return self.comment.visible
end
return true
end
# We store YAML version of parameters in the database
def params=(params)
# TODO: should really set these explicitly, and stop storing them in
# here, but keep it for compatibility with old way for now
if not params[:incoming_message_id].nil?
self.incoming_message_id = params[:incoming_message_id]
end
if not params[:outgoing_message_id].nil?
self.outgoing_message_id = params[:outgoing_message_id]
end
if not params[:comment_id].nil?
self.comment_id = params[:comment_id]
end
self.params_yaml = params.to_yaml
end
def params
YAML.load(self.params_yaml)
end
def params_yaml_as_html
ret = ''
# split out parameters into old/new diffs, and other ones
old_params = {}
new_params = {}
other_params = {}
for key, value in self.params
key = key.to_s
if key.match(/^old_(.*)$/)
old_params[$1] = value
elsif self.params.include?(("old_" + key).to_sym)
new_params[key] = value
else
other_params[key] = value
end
end
# loop through
for key, value in new_params
old_value = old_params[key].to_s
new_value = new_params[key].to_s
if old_value != new_value
ret = ret + "" + CGI.escapeHTML(key) + ": "
ret = ret +
CGI.escapeHTML(MySociety::Format.wrap_email_body_by_lines(old_value).strip).gsub(/\n/, '
') +
" => " +
CGI.escapeHTML(MySociety::Format.wrap_email_body_by_lines(new_value).strip).gsub(/\n/, '
')
ret = ret + "
"
end
end
for key, value in other_params
ret = ret + "" + CGI.escapeHTML(key.to_s) + ": "
ret = ret + CGI.escapeHTML(value.to_s.strip)
ret = ret + "
"
end
return ret
end
def is_incoming_message?() not self.incoming_message_selective_columns("incoming_messages.id").nil? end
def is_outgoing_message?() not self.outgoing_message.nil? end
def is_comment?() not self.comment.nil? end
# Display version of status
def display_status
if is_incoming_message?
status = self.calculated_state
return status.nil? ? _("Response") : InfoRequest.get_status_description(status)
end
if is_outgoing_message?
status = self.calculated_state
if !status.nil?
if status == 'internal_review'
return _("Internal review request")
end
if status == 'waiting_response'
return _("Clarification")
end
raise _("unknown status ") + status
end
# TRANSLATORS: "Follow up" in this context means a further
# message sent by the requester to the authority after
# the initial request
return _("Follow up")
end
raise _("display_status only works for incoming and outgoing messages right now")
end
def is_sent_sort?
['sent', 'resent'].include?(event_type)
end
def is_followup_sort?
['followup_sent', 'followup_resent'].include?(event_type)
end
def outgoing?
['sent', 'followup_sent'].include?(event_type)
end
def response?
event_type == 'response'
end
def same_email_as_previous_send?
prev_addr = self.info_request.get_previous_email_sent_to(self)
curr_addr = self.params[:email]
if prev_addr.nil? && curr_addr.nil?
return true
end
if prev_addr.nil? || curr_addr.nil?
return false
end
return MailHandler.address_from_string(prev_addr) == MailHandler.address_from_string(curr_addr)
end
def json_for_api(deep, snippet_highlight_proc = nil)
ret = {
:id => self.id,
:event_type => self.event_type,
# params_yaml has possibly sensitive data in it, don't include it
:created_at => self.created_at,
:described_state => self.described_state,
:calculated_state => self.calculated_state,
:last_described_at => self.last_described_at,
:incoming_message_id => self.incoming_message_id,
:outgoing_message_id => self.outgoing_message_id,
:comment_id => self.comment_id,
# TODO: would be nice to add links here, but alas the
# code to make them is in views only. See views/request/details.html.erb
# perhaps can call with @template somehow
}
if self.is_incoming_message? || self.is_outgoing_message?
ret[:display_status] = self.display_status
end
if !snippet_highlight_proc.nil?
ret[:snippet] = snippet_highlight_proc.call(self.search_text_main(true))
end
if deep
ret[:info_request] = self.info_request.json_for_api(false)
ret[:public_body] = self.info_request.public_body.json_for_api
ret[:user] = self.info_request.user_json_for_api
end
return ret
end
def for_admin_column
self.class.content_columns.each do |column|
yield(column.human_name, self.send(column.name), column.type.to_s, column.name)
end
end
end