aboutsummaryrefslogtreecommitdiffstats
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/change_email_validator.rb42
-rw-r--r--app/models/comment.rb2
-rw-r--r--app/models/incoming_message.rb177
-rw-r--r--app/models/info_request.rb61
-rw-r--r--app/models/info_request_event.rb2
-rw-r--r--app/models/outgoing_mailer.rb2
-rw-r--r--app/models/outgoing_message.rb25
-rw-r--r--app/models/post_redirect.rb8
-rw-r--r--app/models/request_mailer.rb42
-rw-r--r--app/models/track_mailer.rb30
-rw-r--r--app/models/track_thing.rb2
-rw-r--r--app/models/track_things_sent_email.rb6
-rw-r--r--app/models/user.rb10
-rw-r--r--app/models/user_info_request_sent_alert.rb1
-rw-r--r--app/models/user_mailer.rb21
15 files changed, 335 insertions, 96 deletions
diff --git a/app/models/change_email_validator.rb b/app/models/change_email_validator.rb
new file mode 100644
index 000000000..ff7f2f931
--- /dev/null
+++ b/app/models/change_email_validator.rb
@@ -0,0 +1,42 @@
+# models/changeemail_validator.rb:
+# Validates email change form submissions.
+#
+# Copyright (c) 2010 UK Citizens Online Democracy. All rights reserved.
+# Email: francis@mysociety.org; WWW: http://www.mysociety.org/
+#
+# $Id: contact_validator.rb,v 1.32 2009-09-17 21:10:05 francis Exp $
+
+class ChangeEmailValidator < ActiveRecord::BaseWithoutTable
+ strip_attributes!
+
+ column :old_email, :string
+ column :new_email, :string
+ column :password, :string
+
+ attr_accessor :logged_in_user
+
+ validates_presence_of :old_email, :message => "^Please enter your old email address"
+ validates_presence_of :new_email, :message => "^Please enter your new email address"
+ validates_presence_of :password, :message => "^Please enter your password"
+
+ def validate
+ if !self.old_email.blank? && !MySociety::Validate.is_valid_email(self.old_email)
+ errors.add(:old_email, "doesn't look like a valid address")
+ end
+
+ if !errors[:old_email]
+ if self.old_email.downcase != self.logged_in_user.email.downcase
+ errors.add(:old_email, "address isn't the same as the address of the account you are logged in with")
+ elsif !self.logged_in_user.has_this_password?(self.password)
+ if !errors[:password]
+ errors.add(:password, "is not correct")
+ end
+ end
+ end
+
+ if !self.new_email.blank? && !MySociety::Validate.is_valid_email(self.new_email)
+ errors.add(:new_email, "doesn't look like a valid address")
+ end
+ end
+
+end
diff --git a/app/models/comment.rb b/app/models/comment.rb
index 0da6c1ce3..4e9976373 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -45,7 +45,7 @@ class Comment < ActiveRecord::Base
read_attribute(:body)
end
- # So when made invisble it vanishes
+ # So when takes changes it updates, or when made invisble it vanishes
after_save :event_xapian_update
def event_xapian_update
for event in self.info_request_events
diff --git a/app/models/incoming_message.rb b/app/models/incoming_message.rb
index 2348c17b5..316f2683a 100644
--- a/app/models/incoming_message.rb
+++ b/app/models/incoming_message.rb
@@ -29,6 +29,8 @@ require 'htmlentities'
require 'rexml/document'
require 'zip/zip'
require 'mahoro'
+require 'mapi/msg'
+require 'mapi/convert'
# Monkeypatch! Adding some extra members to store extra info in.
module TMail
@@ -51,6 +53,9 @@ $file_extension_to_mime_type = {
"xlsx" => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
"ppt" => 'application/vnd.ms-powerpoint',
"pptx" => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ "oft" => 'application/vnd.ms-outlook',
+ "msg" => 'application/vnd.ms-outlook',
+ "tnef" => 'application/ms-tnef',
"tif" => 'image/tiff',
"gif" => 'image/gif',
"jpg" => 'image/jpeg', # XXX add jpeg
@@ -303,10 +308,54 @@ class FOIAttachment
end
end
+ # Whether this type has a "View as HTML"
+ def has_body_as_html?
+ if self.content_type == 'text/plain'
+ return true
+ elsif self.content_type == 'application/vnd.ms-word'
+ return true
+ elsif self.content_type == 'application/vnd.ms-excel'
+ return true
+ elsif self.content_type == 'application/pdf'
+ return true
+ elsif self.content_type == 'application/rtf'
+ return true
+ end
+ return false
+ end
+
+ # Name of type of attachment type - only valid for things that has_body_as_html?
+ def name_of_content_type
+ if self.content_type == 'text/plain'
+ return "Text file"
+ elsif self.content_type == 'application/vnd.ms-word'
+ return "Word document"
+ elsif self.content_type == 'application/vnd.ms-excel'
+ return "Excel spreadsheet"
+ elsif self.content_type == 'application/pdf'
+ return "PDF file"
+ elsif self.content_type == 'application/rtf'
+ return "RTF file"
+ end
+ end
+
# For "View as HTML" of attachment
def body_as_html(dir)
html = nil
+ wrapper_id = "wrapper"
+
+ # simple cases, can never fail
+ if self.content_type == 'text/plain'
+ text = self.body.strip
+ text = CGI.escapeHTML(text)
+ text = MySociety::Format.make_clickable(text)
+ html = text.gsub(/\n/, '<br>')
+ return "<html><head></head><body>" + html + "</body></html>", wrapper_id
+ end
+ # the extractions will also produce image files, which go in the
+ # current directory, so change to the directory the function caller
+ # wants everything in
Dir.chdir(dir) do
tempfile = Tempfile.new('foiextract', '.')
tempfile.print self.body
@@ -317,10 +366,22 @@ class FOIAttachment
system("/usr/bin/wvHtml --charset=UTF-8 " + tempfile.path + " " + tempfile.path + ".html")
html = File.read(tempfile.path + ".html")
File.unlink(tempfile.path + ".html")
+ elsif self.content_type == 'application/vnd.ms-excel'
+ # Don't colorise, e.g. otherwise this one comes out with white
+ # text which is nasty:
+ # http://www.whatdotheyknow.com/request/30485/response/74705/attach/html/2/Empty%20premises%20Sefton.xls.html
+ IO.popen("/usr/bin/xlhtml -nc -a " + tempfile.path + "", "r") do |child|
+ html = child.read()
+ wrapper_id = "wrapper_xlhtml"
+ end
elsif self.content_type == 'application/pdf'
IO.popen("/usr/bin/pdftohtml -nodrm -zoom 1.0 -stdout -enc UTF-8 -noframes " + tempfile.path + "", "r") do |child|
html = child.read()
end
+ elsif self.content_type == 'application/rtf'
+ IO.popen("/usr/bin/unrtf --html " + tempfile.path + "", "r") do |child|
+ html = child.read()
+ end
else
raise "No HTML conversion available for type " + self.content_type
end
@@ -341,30 +402,12 @@ class FOIAttachment
body_without_tags = body.gsub(/\s+/,"").gsub(/\<[^\>]*\>/, "")
contains_images = html.match(/<img/mi) ? true : false
if !$?.success? || html.size == 0 || (body_without_tags.size == 0 && !contains_images)
- return "<html><head></head><body><p>Sorry, the conversion to HTML failed. Please use the download link at the top right.</p></body></html>"
+ return "<html><head></head><body><p>Sorry, the conversion to HTML failed. Please use the download link at the top right.</p></body></html>", wrapper_id
end
- return html
- end
-
- # Whether this type has a "View as HTML"
- def has_body_as_html?
- if self.content_type == 'application/vnd.ms-word'
- return true
- elsif self.content_type == 'application/pdf'
- return true
- end
- return false
+ return html, wrapper_id
end
- # Name of type of attachment type - only valid for things that has_body_as_html?
- def name_of_content_type
- if self.content_type == 'application/vnd.ms-word'
- return "Word document"
- elsif self.content_type == 'application/pdf'
- return "PDF file"
- end
- end
end
class IncomingMessage < ActiveRecord::Base
@@ -419,20 +462,30 @@ class IncomingMessage < ActiveRecord::Base
_count_parts_recursive(p)
end
else
- if part.content_type == 'message/rfc822'
- # An email attached as text
- # e.g. http://www.whatdotheyknow.com/request/64/response/102
- begin
+ part_filename = TMail::Mail.get_part_file_name(part)
+ begin
+ if part.content_type == 'message/rfc822'
+ # An email attached as text
+ # e.g. http://www.whatdotheyknow.com/request/64/response/102
part.rfc822_attachment = TMail::Mail.parse(part.body)
- rescue
- # If attached mail doesn't parse, treat it as text part
- part.rfc822_attachment = nil
- @count_parts_count += 1
- part.url_part_number = @count_parts_count
- else
- _count_parts_recursive(part.rfc822_attachment)
+ elsif part.content_type == 'application/vnd.ms-outlook' || part_filename && filename_to_mimetype(part_filename) == 'application/vnd.ms-outlook'
+ # An email attached as an Outlook file
+ # e.g. http://www.whatdotheyknow.com/request/chinese_names_for_british_politi
+ msg = Mapi::Msg.open(StringIO.new(part.body))
+ part.rfc822_attachment = TMail::Mail.parse(msg.to_mime.to_s)
+ elsif part.content_type == 'application/ms-tnef'
+ # A set of attachments in a TNEF file
+ part.rfc822_attachment = TNEF.as_tmail(part.body)
end
+ rescue
+ # If attached mail doesn't parse, treat it as text part
+ part.rfc822_attachment = nil
else
+ unless part.rfc822_attachment.nil?
+ _count_parts_recursive(part.rfc822_attachment)
+ end
+ end
+ if part.rfc822_attachment.nil?
@count_parts_count += 1
part.url_part_number = @count_parts_count
end
@@ -486,7 +539,7 @@ class IncomingMessage < ActiveRecord::Base
uncompressed_text = child.read()
end
# if we managed to uncompress the PDF...
- if !uncompressed_text.nil?
+ if !uncompressed_text.nil? && !uncompressed_text.empty?
# then censor stuff (making a copy so can compare again in a bit)
censored_uncompressed_text = uncompressed_text.dup
self._binary_mask_stuff_internal!(censored_uncompressed_text)
@@ -499,7 +552,7 @@ class IncomingMessage < ActiveRecord::Base
child.close_write()
recompressed_text = child.read()
end
- if !recompressed_text.nil?
+ if !recompressed_text.nil? && !recompressed_text.empty?
text[0..-1] = recompressed_text # [0..-1] makes it change the 'text' string in place
end
end
@@ -707,15 +760,22 @@ class IncomingMessage < ActiveRecord::Base
if curr_mail.sub_type == 'alternative'
# Choose best part from alternatives
best_part = nil
+ # Take the last text/plain one, or else the first one
curr_mail.parts.each do |m|
- # Take the first one, or the last text/plain one
- # XXX - could do better!
if not best_part
best_part = m
elsif m.content_type == 'text/plain'
best_part = m
end
end
+ # Take an HTML one as even higher priority. (They tend
+ # to render better than text/plain, e.g. don't wrap links here:
+ # http://www.whatdotheyknow.com/request/amount_and_cost_of_freedom_of_in#incoming-72238 )
+ curr_mail.parts.each do |m|
+ if m.content_type == 'text/html'
+ best_part = m
+ end
+ end
leaves_found += _get_attachment_leaves_recursive(best_part, within_rfc822_attachment)
else
# Add all parts
@@ -724,6 +784,11 @@ class IncomingMessage < ActiveRecord::Base
end
end
else
+ # XXX Yuck. this section alters various content_type's. That puts
+ # it into conflict with ensure_parts_counted which it has to be
+ # called both before and after. It will fail with cases of
+ # attachments of attachments etc.
+
# Don't allow nil content_types
if curr_mail.content_type.nil?
curr_mail.content_type = 'application/octet-stream'
@@ -746,9 +811,16 @@ class IncomingMessage < ActiveRecord::Base
curr_mail.content_type = 'text/plain'
end
end
+ if curr_mail.content_type == 'application/vnd.ms-outlook' || curr_mail.content_type == 'application/ms-tnef'
+ ensure_parts_counted # fills in rfc822_attachment variable
+ if curr_mail.rfc822_attachment.nil?
+ # Attached mail didn't parse, so treat as binary
+ curr_mail.content_type = 'application/octet-stream'
+ end
+ end
- # If the part is an attachment of email in text form
- if curr_mail.content_type == 'message/rfc822'
+ # If the part is an attachment of email
+ if curr_mail.content_type == 'message/rfc822' || curr_mail.content_type == 'application/vnd.ms-outlook' || curr_mail.content_type == 'application/ms-tnef'
ensure_parts_counted # fills in rfc822_attachment variable
leaves_found += _get_attachment_leaves_recursive(curr_mail.rfc822_attachment, curr_mail.rfc822_attachment)
else
@@ -863,9 +935,13 @@ class IncomingMessage < ActiveRecord::Base
def get_main_body_text_part
leaves = get_attachment_leaves
- # Find first part which is text/plain
+ # Find first part which is text/plain or text/html
+ # (We have to include HTML, as increasingly there are mail clients that
+ # include no text alternative for the main part, and we don't want to
+ # instead use the first text attachment
+ # e.g. http://www.whatdotheyknow.com/request/list_of_public_authorties)
leaves.each do |p|
- if p.content_type == 'text/plain'
+ if p.content_type == 'text/plain' or p.content_type == 'text/html'
return p
end
end
@@ -898,7 +974,7 @@ class IncomingMessage < ActiveRecord::Base
# e.g. for https://secure.mysociety.org/admin/foi/request/show_raw_email/24550
main_part = get_main_body_text_part
if main_part.nil?
- return
+ return []
end
text = main_part.body
@@ -936,10 +1012,13 @@ class IncomingMessage < ActiveRecord::Base
# Returns all attachments for use in display code
# XXX is this called multiple times and should be cached?
def get_attachments_for_display
- ensure_parts_counted
-
main_part = get_main_body_text_part
leaves = get_attachment_leaves
+
+ # XXX we have to call ensure_parts_counted after get_attachment_leaves
+ # which is really messy.
+ ensure_parts_counted
+
attachments = []
for leaf in leaves
if leaf != main_part
@@ -965,7 +1044,12 @@ class IncomingMessage < ActiveRecord::Base
headers = ""
for header in [ 'Date', 'Subject', 'From', 'To', 'Cc' ]
if leaf.within_rfc822_attachment.header.include?(header.downcase)
- headers = headers + header + ": " + leaf.within_rfc822_attachment.header[header.downcase].to_s + "\n"
+ header_value = leaf.within_rfc822_attachment.header[header.downcase]
+ # Example message which has a blank Date header:
+ # http://www.whatdotheyknow.com/request/30747/response/80253/attach/html/17/Common%20Purpose%20Advisory%20Group%20Meeting%20Tuesday%202nd%20March.txt.html
+ if !header_value.blank?
+ headers = headers + header + ": " + header_value.to_s + "\n"
+ end
end
end
# XXX call _convert_part_body_to_text here, but need to get charset somehow
@@ -1099,11 +1183,14 @@ class IncomingMessage < ActiveRecord::Base
File.unlink(tempfile.path + ".txt")
end
elsif content_type == 'application/rtf'
+ # catdoc on RTF prodcues less comments and extra bumf than --text option to unrtf
IO.popen("/usr/bin/catdoc " + tempfile.path, "r") do |child|
text += child.read() + "\n\n"
end
elsif content_type == 'text/html'
- IO.popen("/usr/bin/lynx -display_charset=UTF-8 -force_html -dump " + tempfile.path, "r") do |child|
+ # lynx wordwraps links in its output, which then don't get formatted properly
+ # by WhatDoTheyKnow. We use elinks instead, which doesn't do that.
+ IO.popen("/usr/bin/elinks -dump-charset utf-8 -force-html -dump " + tempfile.path, "r") do |child|
text += child.read() + "\n\n"
end
elsif content_type == 'application/vnd.ms-excel'
@@ -1273,7 +1360,7 @@ class IncomingMessage < ActiveRecord::Base
prefix = email
prefix =~ /^(.*)@/
prefix = $1
- if !prefix.nil? && prefix.downcase.match(/^(postmaster|mailer-daemon|auto_reply|donotreply)$/)
+ if !prefix.nil? && prefix.downcase.match(/^(postmaster|mailer-daemon|auto_reply|donotreply|no-reply)$/)
return false
end
diff --git a/app/models/info_request.rb b/app/models/info_request.rb
index 7cee3fe1c..f6eec0601 100644
--- a/app/models/info_request.rb
+++ b/app/models/info_request.rb
@@ -56,7 +56,7 @@ class InfoRequest < ActiveRecord::Base
'waiting_clarification',
'gone_postal',
'not_held',
- 'rejected',
+ 'rejected', # this is called 'refused' in UK FOI law and the user interface, but 'rejected' internally for historic reasons
'successful',
'partially_successful',
'internal_review',
@@ -83,7 +83,7 @@ class InfoRequest < ActiveRecord::Base
'authority_only', # only people from authority domains
'nobody'
]
- # what to do with rejected new responses
+ # what to do with refused new responses
validates_inclusion_of :handle_rejected_responses, :in => [
'bounce', # return them to sender
'holding_pen', # put them in the holding pen
@@ -95,6 +95,9 @@ class InfoRequest < ActiveRecord::Base
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
end
OLD_AGE_IN_DAYS = 21.days
@@ -132,7 +135,7 @@ class InfoRequest < ActiveRecord::Base
# we update index for every event. Also reindex if prominence changes.
after_update :reindex_some_request_events
def reindex_some_request_events
- if self.changes.include?('url_title') || self.changes.include?('prominence')
+ if self.changes.include?('url_title') || self.changes.include?('prominence') || self.changes.include?('user_id')
self.reindex_request_events
end
end
@@ -199,6 +202,8 @@ 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
self.law_used_full + ' request GQ - ' + self.title
@@ -206,11 +211,15 @@ public
self.law_used_full + ' request - ' + self.title
end
end
- def email_subject_followup
- if self.public_body.url_name == 'general_register_office'
- 'Re: ' + self.law_used_full + ' request GQ - ' + self.title
+ def email_subject_followup(incoming_message = nil)
+ if incoming_message.nil? || !incoming_message.valid_to_reply_to?
+ 'Re: ' + self.email_subject_request
else
- 'Re: ' + self.law_used_full + ' request - ' + self.title
+ if incoming_message.mail.subject.match(/^Re:/i)
+ incoming_message.mail.subject
+ else
+ 'Re: ' + incoming_message.mail.subject
+ end
end
end
@@ -487,10 +496,13 @@ public
# self.described_state, can take these two values:
# waiting_classification
# waiting_response_overdue
+ # waiting_response_very_overdue
def calculate_status
return 'waiting_classification' if self.awaiting_description
return described_state unless self.described_state == "waiting_response"
# Compare by date, so only overdue on next day, not if 1 second late
+ return 'waiting_response_very_overdue' if
+ Time.now.strftime("%Y-%m-%d") > self.date_very_overdue_after.strftime("%Y-%m-%d")
return 'waiting_response_overdue' if
Time.now.strftime("%Y-%m-%d") > self.date_response_required_by.strftime("%Y-%m-%d")
return 'waiting_response'
@@ -568,24 +580,35 @@ public
end
end
if last_sent.nil?
- raise "internal error, date_response_required_by gets nil for request " + self.id.to_s + " outgoing messages count " + self.outgoing_messages.size.to_s + " all events: " + self.info_request_events.to_yaml
+ raise "internal error, last_event_forming_initial_request gets nil for request " + self.id.to_s + " outgoing messages count " + self.outgoing_messages.size.to_s + " all events: " + self.info_request_events.to_yaml
end
return last_sent
end
+ # The last time that the initial request was sent/resent
+ def date_initial_request_last_sent_at
+ last_sent = last_event_forming_initial_request
+ return last_sent.outgoing_message.last_sent_at
+ end
# How do we cope with case where extra info was required from the requester
# by the public body in order to fulfill the request, as per sections 1(3)
# and 10(6b) ? For clarifications this is covered by
# last_event_forming_initial_request. There may be more obscure
# things, e.g. fees, not properly covered.
def date_response_required_by
- last_sent = last_event_forming_initial_request
- return Holiday.due_date_from(last_sent.outgoing_message.last_sent_at, 20)
+ return Holiday.due_date_from(self.date_initial_request_last_sent_at, 20)
end
-
- # Are we more than 20 working days overdue?
- def working_days_20_overdue?
- return Holiday.due_date_from(date_response_required_by.to_date, 20) <= Time.now.to_date
+ # This is a long stop - even with UK public interest test extensions, 40
+ # days is a very long time.
+ def date_very_overdue_after
+ last_sent = last_event_forming_initial_request
+ if self.public_body.is_school?
+ # schools have 60 working days maximum (even over a long holiday)
+ return Holiday.due_date_from(self.date_initial_request_last_sent_at, 60)
+ else
+ # public interest test ICO guidance gives 40 working maximum
+ return Holiday.due_date_from(self.date_initial_request_last_sent_at, 40)
+ end
end
# Where the initial request is sent to
@@ -715,11 +738,13 @@ public
elsif status == 'waiting_response'
"Awaiting response."
elsif status == 'waiting_response_overdue'
- "Response overdue."
+ "Delayed."
+ elsif status == 'waiting_response_very_overdue'
+ "Long overdue."
elsif status == 'not_held'
"Information not held."
elsif status == 'rejected'
- "Rejected."
+ "Refused."
elsif status == 'partially_successful'
"Partially successful."
elsif status == 'successful'
@@ -894,9 +919,9 @@ public
# This is called from cron regularly.
def InfoRequest.stop_new_responses_on_old_requests
# 6 months since last change to request, only allow new incoming messages from authority domains
- InfoRequest.update_all "allow_new_responses_from = 'authority_only' where updated_at < (now() - interval '6 months') and allow_new_responses_from = 'anybody'"
+ InfoRequest.update_all "allow_new_responses_from = 'authority_only' where updated_at < (now() - interval '6 months') and allow_new_responses_from = 'anybody' and url_title <> 'holding_pen'"
# 1 year since last change requests, don't allow any new incoming messages
- InfoRequest.update_all "allow_new_responses_from = 'nobody' where updated_at < (now() - interval '1 year') and allow_new_responses_from in ('anybody', 'authority_only')"
+ 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
diff --git a/app/models/info_request_event.rb b/app/models/info_request_event.rb
index 4e2a10a1f..1422431dc 100644
--- a/app/models/info_request_event.rb
+++ b/app/models/info_request_event.rb
@@ -282,7 +282,7 @@ class InfoRequestEvent < ActiveRecord::Base
elsif status == 'not_held'
return "Information not held"
elsif status == 'rejected'
- return "Rejection"
+ return "Refused"
elsif status == 'partially_successful'
return "Some information sent"
elsif status == 'successful'
diff --git a/app/models/outgoing_mailer.rb b/app/models/outgoing_mailer.rb
index ae7e86f4e..1546d3fd0 100644
--- a/app/models/outgoing_mailer.rb
+++ b/app/models/outgoing_mailer.rb
@@ -73,7 +73,7 @@ class OutgoingMailer < ApplicationMailer
if outgoing_message.what_doing == 'internal_review'
return "Internal review of " + info_request.email_subject_request
else
- return info_request.email_subject_followup
+ return info_request.email_subject_followup(outgoing_message.incoming_message_followup)
end
end
# Whether we have a valid email address for a followup
diff --git a/app/models/outgoing_message.rb b/app/models/outgoing_message.rb
index 36190ce2f..40abe0b0f 100644
--- a/app/models/outgoing_message.rb
+++ b/app/models/outgoing_message.rb
@@ -39,13 +39,23 @@ class OutgoingMessage < ActiveRecord::Base
# contact address changed
has_many :info_request_events
+ # reindex if body text is edited (e.g. by admin interface)
+ after_update :xapian_reindex_after_update
+ def xapian_reindex_after_update
+ if self.changes.include?('body')
+ for info_request_event in self.info_request_events
+ info_request_event.xapian_mark_needs_index
+ end
+ end
+ end
+
# How the default letter starts and ends
def get_salutation
ret = "Dear "
if self.message_type == 'followup' && !self.incoming_message_followup.nil? && !self.incoming_message_followup.safe_mail_from.nil? && self.incoming_message_followup.valid_to_reply_to?
ret = ret + OutgoingMailer.name_for_followup(self.info_request, self.incoming_message_followup)
else
- ret = ret + "Sir or Madam"
+ ret = ret + self.info_request.public_body.name
end
return ret + ","
end
@@ -56,6 +66,9 @@ class OutgoingMessage < ActiveRecord::Base
return "Yours faithfully,"
end
end
+ def get_internal_review_insert_here_note
+ return "GIVE DETAILS ABOUT YOUR COMPLAINT HERE"
+ end
def get_default_letter
if self.what_doing == 'internal_review'
"Please pass this on to the person who conducts Freedom of Information reviews." +
@@ -64,7 +77,7 @@ class OutgoingMessage < ActiveRecord::Base
self.info_request.public_body.name +
"'s handling of my FOI request " +
"'" + self.info_request.title + "'." +
- "\n\n" +
+ "\n\n\n\n [ " + self.get_internal_review_insert_here_note + " ] \n\n\n\n" +
"A full history of my FOI request and all correspondence is available on the Internet at this address:\n" +
"http://" + MySociety::Config.get("DOMAIN", '127.0.0.1:3000') + "/request/" + self.info_request.url_title
else
@@ -119,9 +132,13 @@ class OutgoingMessage < ActiveRecord::Base
# Check have edited letter
def validate
- if self.body.empty? || self.body =~ /\A#{get_salutation}\s+#{get_signoff}/
+ if self.body.empty? || self.body =~ /\A#{get_salutation}\s+#{get_signoff}/ || self.body =~ /#{get_internal_review_insert_here_note}/
if self.message_type == 'followup'
- errors.add(:body, "^Please enter your follow up message")
+ if self.what_doing == 'internal_review'
+ errors.add(:body, "^Please give details explaining why you want a review")
+ else
+ errors.add(:body, "^Please enter your follow up message")
+ end
elsif
errors.add(:body, "^Please enter your letter requesting information")
else
diff --git a/app/models/post_redirect.rb b/app/models/post_redirect.rb
index edd151730..a17b24d16 100644
--- a/app/models/post_redirect.rb
+++ b/app/models/post_redirect.rb
@@ -61,11 +61,7 @@ class PostRedirect < ActiveRecord::Base
# Makes a random token, suitable for using in URLs e.g confirmation messages.
def self.generate_random_token
- bits = 12 * 8
- # Make range from value to double value, so number of digits in base 36
- # encoding is quite long always.
- rand_num = rand(max = 2**(bits+1)) + 2**bits
- rand_num.to_s(base=36)
+ MySociety::Util.generate_token
end
# Make the token
@@ -90,7 +86,7 @@ class PostRedirect < ActiveRecord::Base
return post_redirects[0]
end
- # Called from cron job delete-old-post-redirects
+ # Called from cron job delete-old-things
def self.delete_old_post_redirects
PostRedirect.delete_all "updated_at < (now() - interval '6 months')"
end
diff --git a/app/models/request_mailer.rb b/app/models/request_mailer.rb
index 1bebb5181..7e2041b4f 100644
--- a/app/models/request_mailer.rb
+++ b/app/models/request_mailer.rb
@@ -85,7 +85,25 @@ class RequestMailer < ApplicationMailer
headers 'Return-Path' => blackhole_email, 'Reply-To' => @from, # not much we can do if the user's email is broken
'Auto-Submitted' => 'auto-generated' # http://tools.ietf.org/html/rfc3834
@recipients = user.name_and_email
- @subject = "You're overdue a response to your FOI request - " + info_request.title
+ @subject = "Delayed response to your FOI request - " + info_request.title
+ @body = { :info_request => info_request, :url => url }
+ end
+
+ # Tell the requester that the public body is very late in replying
+ def very_overdue_alert(info_request, user)
+ respond_url = respond_to_last_url(info_request) + "#followup"
+
+ post_redirect = PostRedirect.new(
+ :uri => respond_url,
+ :user_id => user.id)
+ post_redirect.save!
+ url = confirm_url(:email_token => post_redirect.email_token)
+
+ @from = contact_from_name_and_email
+ headers 'Return-Path' => blackhole_email, 'Reply-To' => @from, # not much we can do if the user's email is broken
+ 'Auto-Submitted' => 'auto-generated' # http://tools.ietf.org/html/rfc3834
+ @recipients = user.name_and_email
+ @subject = "You're long overdue a response to your FOI request - " + info_request.title
@body = { :info_request => info_request, :url => url }
end
@@ -209,21 +227,35 @@ class RequestMailer < ApplicationMailer
for info_request in info_requests
alert_event_id = info_request.last_event_forming_initial_request.id
# Only overdue requests
- if info_request.calculate_status == 'waiting_response_overdue'
+ if ['waiting_response_overdue', 'waiting_response_very_overdue'].include?(info_request.calculate_status)
+ if info_request.calculate_status == 'waiting_response_overdue'
+ alert_type = 'overdue_1'
+ elsif info_request.calculate_status == 'waiting_response_very_overdue'
+ alert_type = 'very_overdue_1'
+ else
+ raise "unknown request status"
+ end
+
# For now, just to the user who created the request
- sent_already = UserInfoRequestSentAlert.find(:first, :conditions => [ "alert_type = 'overdue_1' and user_id = ? and info_request_id = ? and info_request_event_id = ?", info_request.user_id, info_request.id, alert_event_id])
+ sent_already = UserInfoRequestSentAlert.find(:first, :conditions => [ "alert_type = ? and user_id = ? and info_request_id = ? and info_request_event_id = ?", alert_type, info_request.user_id, info_request.id, alert_event_id])
if sent_already.nil?
# Alert not yet sent for this user, so send it
#STDERR.puts "sending overdue alert to info_request " + info_request.id.to_s + " user " + info_request.user_id.to_s + " event " + alert_event_id
store_sent = UserInfoRequestSentAlert.new
store_sent.info_request = info_request
store_sent.user = info_request.user
- store_sent.alert_type = 'overdue_1'
+ store_sent.alert_type = alert_type
store_sent.info_request_event_id = alert_event_id
# Only send the alert if the user can act on it by making a followup
# (otherwise they are banned, and there is no point sending it)
if info_request.user.can_make_followup?
- RequestMailer.deliver_overdue_alert(info_request, info_request.user)
+ if info_request.calculate_status == 'waiting_response_overdue'
+ RequestMailer.deliver_overdue_alert(info_request, info_request.user)
+ elsif info_request.calculate_status == 'waiting_response_very_overdue'
+ RequestMailer.deliver_very_overdue_alert(info_request, info_request.user)
+ else
+ raise "unknown request status"
+ end
end
store_sent.save!
#STDERR.puts "sent " + info_request.user.email
diff --git a/app/models/track_mailer.rb b/app/models/track_mailer.rb
index 9d8e8348d..6c9d9949b 100644
--- a/app/models/track_mailer.rb
+++ b/app/models/track_mailer.rb
@@ -33,16 +33,23 @@ class TrackMailer < ApplicationMailer
now = Time.now()
users = User.find(:all, :conditions => [ "last_daily_track_email < ?", now - 1.day ])
for user in users
- #STDERR.puts "user " + user.url_name
+ #STDERR.puts Time.now.to_s + " user " + user.url_name
email_about_things = []
track_things = TrackThing.find(:all, :conditions => [ "tracking_user_id = ? and track_medium = ?", user.id, 'email_daily' ])
for track_thing in track_things
- #STDERR.puts " track " + track_thing.track_query
+ #STDERR.puts Time.now.to_s + " track " + track_thing.track_query
# What have we alerted on already?
+ #
+ # We only use track_things_sent_emails records which are less than 14 days old.
+ # In the search query loop below, we also only use items described in last 7 days.
+ # An item described that recently definitely can't appear in track_things_sent_emails
+ # earlier, so this is safe (with a week long margin of error). If the alerts break
+ # for a whole week, then they will miss some items. Tough.
done_info_request_events = {}
- for t in track_thing.track_things_sent_emails
+ tt_sent = track_thing.track_things_sent_emails.find(:all, :conditions => ['created_at > ?', now - 14.days])
+ for t in tt_sent
if not t.info_request_event_id.nil?
done_info_request_events[t.info_request_event_id] = 1
end
@@ -51,19 +58,20 @@ class TrackMailer < ApplicationMailer
# Query for things in this track. We use described_at for the
# ordering, so we catch anything new (before described), or
# anything whose new status has been described.
- xapian_object = InfoRequest.full_search([InfoRequestEvent], track_thing.track_query, 'described_at', true, nil, 200, 1)
-
+ xapian_object = InfoRequest.full_search([InfoRequestEvent], track_thing.track_query, 'described_at', true, nil, 100, 1)
# Go through looking for unalerted things
alert_results = []
for result in xapian_object.results
- if result[:model].class.to_s == "InfoRequestEvent"
- if not done_info_request_events.include?(result[:model].id) and track_thing.created_at < result[:model].described_at
- # OK alert this one
- alert_results.push(result)
- end
- else
+ if result[:model].class.to_s != "InfoRequestEvent"
raise "need to add other types to TrackMailer.alert_tracks (unalerted)"
end
+
+ next if track_thing.created_at >= result[:model].described_at # made before the track was created
+ next if result[:model].described_at < now - 7.days # older than 1 week (see 14 days / 7 days in comment above)
+ next if done_info_request_events.include?(result[:model].id) # definitely already done
+
+ # OK alert this one
+ alert_results.push(result)
end
# If there were more alerts for this track, then store them
if alert_results.size > 0
diff --git a/app/models/track_thing.rb b/app/models/track_thing.rb
index 3392a4210..c2036118f 100644
--- a/app/models/track_thing.rb
+++ b/app/models/track_thing.rb
@@ -215,7 +215,7 @@ class TrackThing < ActiveRecord::Base
# RSS sorting - XXX hmmm, we don't really know which to use
# here for sorting. Might be a query term (e.g. 'cctv'), in
# which case newest is good, or might be something like
- # all rejected requests in which case want to sort by
+ # all refused requests in which case want to sort by
# described (when we discover criteria is met). Rather
# conservatively am picking described, as that will make
# things appear in feed more than the should, rather than less.
diff --git a/app/models/track_things_sent_email.rb b/app/models/track_things_sent_email.rb
index 35229371c..b39dad932 100644
--- a/app/models/track_things_sent_email.rb
+++ b/app/models/track_things_sent_email.rb
@@ -25,6 +25,12 @@ class TrackThingsSentEmail < ActiveRecord::Base
belongs_to :user
belongs_to :public_body
belongs_to :track_thing
+
+ # Called from cron job delete-old-things
+ def self.delete_old_track_things_sent_email
+ TrackThingsSentEmail.delete_all "updated_at < (now() - interval '1 month')"
+ end
+
end
diff --git a/app/models/user.rb b/app/models/user.rb
index bcad6229f..eb8089cf1 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -134,8 +134,7 @@ class User < ActiveRecord::Base
user = self.find_user_by_email(params[:email])
if user
# There is user with email, check password
- expected_password = encrypted_password(params[:password], user.salt)
- if user.hashed_password != expected_password
+ if !user.has_this_password?(params[:password])
user.errors.add_to_base(auth_fail_message)
end
else
@@ -184,7 +183,12 @@ class User < ActiveRecord::Base
self.hashed_password = User.encrypted_password(self.password, self.salt)
end
- # For use in to/from in email messages
+ def has_this_password?(password)
+ expected_password = User.encrypted_password(password, self.salt)
+ return self.hashed_password == expected_password
+ end
+
+# For use in to/from in email messages
def name_and_email
return TMail::Address.address_from_name_and_email(self.name, self.email).to_s
end
diff --git a/app/models/user_info_request_sent_alert.rb b/app/models/user_info_request_sent_alert.rb
index 309466792..dde6fd339 100644
--- a/app/models/user_info_request_sent_alert.rb
+++ b/app/models/user_info_request_sent_alert.rb
@@ -25,6 +25,7 @@ class UserInfoRequestSentAlert < ActiveRecord::Base
validates_inclusion_of :alert_type, :in => [
'overdue_1', # tell user that info request has become overdue
+ 'very_overdue_1', # tell user that info request has become very overdue
'new_response_reminder_1', # reminder user to classify the recent response
'new_response_reminder_2', # repeat reminder user to classify the recent response
'new_response_reminder_3', # repeat reminder user to classify the recent response
diff --git a/app/models/user_mailer.rb b/app/models/user_mailer.rb
index 06a115c5b..70ca42675 100644
--- a/app/models/user_mailer.rb
+++ b/app/models/user_mailer.rb
@@ -27,5 +27,26 @@ class UserMailer < ApplicationMailer
@body[:url] = url
end
+ def changeemail_confirm(user, new_email, url)
+ @from = contact_from_name_and_email
+ headers 'Return-Path' => blackhole_email, 'Reply-To' => @from # we don't care about bounces when people are fiddling with their account
+ @recipients = new_email
+ @subject = "Confirm your new email address on WhatDoTheyKnow.com"
+ @body[:name] = user.name
+ @body[:url] = url
+ @body[:old_email] = user.email
+ @body[:new_email] = new_email
+ end
+
+ def changeemail_already_used(old_email, new_email)
+ @from = contact_from_name_and_email
+ headers 'Return-Path' => blackhole_email, 'Reply-To' => @from # we don't care about bounces when people are fiddling with their account
+ @recipients = new_email
+ @subject = "Unable to change email address on WhatDoTheyKnow.com"
+ @body[:old_email] = old_email
+ @body[:new_email] = new_email
+ end
+
+
end