diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/change_email_validator.rb | 42 | ||||
-rw-r--r-- | app/models/comment.rb | 2 | ||||
-rw-r--r-- | app/models/incoming_message.rb | 177 | ||||
-rw-r--r-- | app/models/info_request.rb | 61 | ||||
-rw-r--r-- | app/models/info_request_event.rb | 2 | ||||
-rw-r--r-- | app/models/outgoing_mailer.rb | 2 | ||||
-rw-r--r-- | app/models/outgoing_message.rb | 25 | ||||
-rw-r--r-- | app/models/post_redirect.rb | 8 | ||||
-rw-r--r-- | app/models/request_mailer.rb | 42 | ||||
-rw-r--r-- | app/models/track_mailer.rb | 30 | ||||
-rw-r--r-- | app/models/track_thing.rb | 2 | ||||
-rw-r--r-- | app/models/track_things_sent_email.rb | 6 | ||||
-rw-r--r-- | app/models/user.rb | 10 | ||||
-rw-r--r-- | app/models/user_info_request_sent_alert.rb | 1 | ||||
-rw-r--r-- | app/models/user_mailer.rb | 21 |
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 |