diff options
Diffstat (limited to 'app/models')
31 files changed, 1268 insertions, 1622 deletions
diff --git a/app/models/about_me_validator.rb b/app/models/about_me_validator.rb index 8ee505ac8..7df70fb61 100644 --- a/app/models/about_me_validator.rb +++ b/app/models/about_me_validator.rb @@ -1,25 +1,23 @@ -# == Schema Information -# Schema version: 114 -# -# Table name: about_me_validators -# -# about_me :text default("I..."), not null -# - # models/about_me_validator.rb: # Validates editing about me text on user profile pages. # # Copyright (c) 2010 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ -class AboutMeValidator < ActiveRecord::BaseWithoutTable - strip_attributes! +class AboutMeValidator + include ActiveModel::Validations - column :about_me, :text, "I...", false + attr_accessor :about_me # TODO: Switch to built in validations validate :length_of_about_me + def initialize(attributes = {}) + attributes.each do |name, value| + send("#{name}=", value) + end + end + private def length_of_about_me diff --git a/app/models/application_mailer.rb b/app/models/application_mailer.rb deleted file mode 100644 index 1a97a4bf9..000000000 --- a/app/models/application_mailer.rb +++ /dev/null @@ -1,164 +0,0 @@ -# models/application_mailer.rb: -# Shared code between different mailers. -# -# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ - -require 'action_mailer/version' -class ApplicationMailer < ActionMailer::Base - # Include all the functions views get, as emails call similar things. - helper :application - include MailerHelper - - # This really should be the default - otherwise you lose any information - # about the errors, and have to do error checking on return codes. - self.raise_delivery_errors = true - - def blackhole_email - Configuration::blackhole_prefix+"@"+Configuration::incoming_email_domain - end - - # URL generating functions are needed by all controllers (for redirects), - # views (for links) and mailers (for use in emails), so include them into - # all of all. - include LinkToHelper - - # Site-wide access to configuration settings - include ConfigHelper - - # Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer - # will be initialized according to the named method. If not, the mailer will - # remain uninitialized (useful when you only need to invoke the "receive" - # method, for instance). - def initialize(method_name=nil, *parameters) #:nodoc: - create!(method_name, *parameters) if method_name - end - - # For each multipart template (e.g. "the_template_file.text.html.erb") available, - # add the one from the view path with the highest priority as a part to the mail - def render_multipart_templates - added_content_types = {} - self.view_paths.each do |view_path| - Dir.glob("#{view_path}/#{mailer_name}/#{@template}.*").each do |path| - template = view_path["#{mailer_name}/#{File.basename(path)}"] - - # Skip unless template has a multipart format - next unless template && template.multipart? - next if added_content_types[template.content_type] == true - @parts << Part.new( - :content_type => template.content_type, - :disposition => "inline", - :charset => charset, - :body => render_message(template, @body) - ) - added_content_types[template.content_type] = true - end - end - end - - # Look for the current template in each element of view_paths in order, - # return the first - def find_template - self.view_paths.each do |view_path| - if template = view_path["#{mailer_name}/#{@template}"] - return template - end - end - return nil - end - - if ActionMailer::VERSION::MAJOR == 2 - - # This method is a customised version of ActionMailer::Base.create! - # modified to allow templates to be selected correctly for multipart - # mails when themes have added to the view_paths. The problem from our - # point of view with ActionMailer::Base is that it sets template_root to - # the first element of the view_paths array and then uses only that (directly - # and via template_path, which is created from it) in the create! method when - # looking for templates. Our modified version looks for templates in the view_paths - # in order. - - # It also has a line converting the mail subject to a string. This is because we - # use translated strings in the subject lines, sometimes in conjunction with - # user input, like request titles. The _() function used for translation - # returns an instance of SafeBuffer, which doesn't handle gsub calls in the block form - # with $ variables - https://github.com/rails/rails/issues/1555. - # Unfortunately ActionMailer uses that form in quoted_printable(), which will be - # called if any part of the subject requires quoting. So we convert the subject - # back to a string via to_str() before passing in to create_mail. There is a test - # for this in spec/models/request_mailer_spec.rb - - # Changed lines marked with *** - - # Initialize the mailer via the given +method_name+. The body will be - # rendered and a new TMail::Mail object created. - def create!(method_name, *parameters) #:nodoc: - initialize_defaults(method_name) - __send__(method_name, *parameters) - - # If an explicit, textual body has not been set, we check assumptions. - unless String === @body - # First, we look to see if there are any likely templates that match, - # which include the content-type in their file name (i.e., - # "the_template_file.text.html.erb", etc.). Only do this if parts - # have not already been specified manually. - if @parts.empty? - # *** render_multipart_templates replaces the following code - # Dir.glob("#{template_path}/#{@template}.*").each do |path| - # template = template_root["#{mailer_name}/#{File.basename(path)}"] - # - # # Skip unless template has a multipart format - # next unless template && template.multipart? - # - # @parts << Part.new( - # :content_type => template.content_type, - # :disposition => "inline", - # :charset => charset, - # :body => render_message(template, @body) - # ) - # end - render_multipart_templates - - unless @parts.empty? - @content_type = "multipart/alternative" if @content_type !~ /^multipart/ - @parts = sort_parts(@parts, @implicit_parts_order) - end - end - - # Then, if there were such templates, we check to see if we ought to - # also render a "normal" template (without the content type). If a - # normal template exists (or if there were no implicit parts) we render - # it. - template_exists = @parts.empty? - - # *** find_template replaces template_root call - # template_exists ||= template_root["#{mailer_name}/#{@template}"] - template_exists ||= find_template - - @body = render_message(@template, @body) if template_exists - - # Finally, if there are other message parts and a textual body exists, - # we shift it onto the front of the parts and set the body to nil (so - # that create_mail doesn't try to render it in addition to the parts). - if !@parts.empty? && String === @body - @parts.unshift ActionMailer::Part.new(:charset => charset, :body => @body) - @body = nil - end - end - - # If this is a multipart e-mail add the mime_version if it is not - # already set. - @mime_version ||= "1.0" if !@parts.empty? - - # *** Convert into a string - @subject = @subject.to_str if @subject - - # build the mail object itself - @mail = create_mail - end - else - raise "ApplicationMailer.create! is obsolete - find another way to ensure that themes can override mail templates for multipart mails" - end - -end - diff --git a/app/models/censor_rule.rb b/app/models/censor_rule.rb index f40ab6fbb..3c5c77563 100644 --- a/app/models/censor_rule.rb +++ b/app/models/censor_rule.rb @@ -1,18 +1,17 @@ # == Schema Information -# Schema version: 20120919140404 # # Table name: censor_rules # -# id :integer not null, primary key +# id :integer not null, primary key # info_request_id :integer # user_id :integer # public_body_id :integer -# text :text not null -# replacement :text not null -# last_edit_editor :string(255) not null -# last_edit_comment :text not null -# created_at :datetime not null -# updated_at :datetime not null +# text :text not null +# replacement :text not null +# last_edit_editor :string(255) not null +# last_edit_comment :text not null +# created_at :datetime not null +# updated_at :datetime not null # regexp :boolean # @@ -20,7 +19,7 @@ # Stores alterations to remove specific data from requests. # # Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class CensorRule < ActiveRecord::Base belongs_to :info_request @@ -33,13 +32,15 @@ class CensorRule < ActiveRecord::Base validate :require_valid_regexp, :if => proc{ |rule| rule.regexp? == true } validates_presence_of :text - named_scope :global, {:conditions => {:info_request_id => nil, - :user_id => nil, - :public_body_id => nil}} + scope :global, {:conditions => {:info_request_id => nil, + :user_id => nil, + :public_body_id => nil}} def require_user_request_or_public_body if self.info_request.nil? && self.user.nil? && self.public_body.nil? - errors.add("Censor must apply to an info request a user or a body; ") + [:info_request, :user, :public_body].each do |a| + errors.add(a, "Rule must apply to an info request, a user or a body") + end end end diff --git a/app/models/change_email_validator.rb b/app/models/change_email_validator.rb index 2ddebb177..5cc13d4c2 100644 --- a/app/models/change_email_validator.rb +++ b/app/models/change_email_validator.rb @@ -1,36 +1,27 @@ -# == Schema Information -# Schema version: 114 -# -# Table name: change_email_validators -# -# old_email :string -# new_email :string -# password :string -# user_circumstance :string -# - # 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/ - -class ChangeEmailValidator < ActiveRecord::BaseWithoutTable - strip_attributes! +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ - column :old_email, :string - column :new_email, :string - column :password, :string - column :user_circumstance, :string +class ChangeEmailValidator + include ActiveModel::Validations - attr_accessor :logged_in_user + attr_accessor :old_email, :new_email, :password, :user_circumstance, :logged_in_user validates_presence_of :old_email, :message => N_("Please enter your old email address") validates_presence_of :new_email, :message => N_("Please enter your new email address") validates_presence_of :password, :message => N_("Please enter your password"), :unless => :changing_email validate :password_and_format_of_email - def changing_email() + def initialize(attributes = {}) + attributes.each do |name, value| + send("#{name}=", value) + end + end + + + def changing_email self.user_circumstance == 'change_email' end @@ -41,11 +32,11 @@ class ChangeEmailValidator < ActiveRecord::BaseWithoutTable errors.add(:old_email, _("Old email doesn't look like a valid address")) end - if !errors[:old_email] + if errors[:old_email].blank? if self.old_email.downcase != self.logged_in_user.email.downcase errors.add(:old_email, _("Old email address isn't the same as the address of the account you are logged in with")) elsif (!self.changing_email) && (!self.logged_in_user.has_this_password?(self.password)) - if !errors[:password] + if errors[:password].blank? errors.add(:password, _("Password is not correct")) end end @@ -55,5 +46,4 @@ class ChangeEmailValidator < ActiveRecord::BaseWithoutTable errors.add(:new_email, _("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 70f3ba00d..b4c099123 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,24 +1,23 @@ # == Schema Information -# Schema version: 114 # # Table name: comments # -# id :integer not null, primary key -# user_id :integer not null -# comment_type :string(255) default("internal_error"), not null +# id :integer not null, primary key +# user_id :integer not null +# comment_type :string(255) default("internal_error"), not null # info_request_id :integer -# body :text not null -# visible :boolean default(TRUE), not null -# created_at :datetime not null -# updated_at :datetime not null -# locale :text default(""), not null +# body :text not null +# visible :boolean default(TRUE), not null +# created_at :datetime not null +# updated_at :datetime not null +# locale :text default(""), not null # # models/comments.rb: # A comment by a user upon something. # # Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class Comment < ActiveRecord::Base strip_attributes! @@ -63,7 +62,7 @@ class Comment < ActiveRecord::Base end # When posting a new comment, use this to check user hasn't double submitted. - def Comment.find_by_existing_comment(info_request_id, body) + def Comment.find_existing(info_request_id, body) # XXX can add other databases here which have regexp_replace if ActiveRecord::Base.connection.adapter_name == "PostgreSQL" # Exclude spaces from the body comparison using regexp_replace diff --git a/app/models/contact_mailer.rb b/app/models/contact_mailer.rb deleted file mode 100644 index abde64928..000000000 --- a/app/models/contact_mailer.rb +++ /dev/null @@ -1,56 +0,0 @@ -# models/contact_mailer.rb: -# Sends contact form mails. -# -# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ - -class ContactMailer < ApplicationMailer - - # Send message to administrator - def to_admin_message(name, email, subject, message, logged_in_user, last_request, last_body) - @from = name + " <" + email + ">" - @recipients = contact_from_name_and_email - @subject = subject - @body = { :message => message, - :logged_in_user => logged_in_user , - :last_request => last_request, - :last_body => last_body - } - end - - # We always set Reply-To when we set Return-Path to be different from From, - # since some email clients seem to erroneously use the envelope from when - # they shouldn't, and this might help. (Have had mysterious cases of a - # reply coming in duplicate from a public body to both From and envelope - # from) - - # Send message to another user - def user_message(from_user, recipient_user, from_user_url, subject, message) - @from = from_user.name_and_email - # Do not set envelope from address to the from_user, so they can't get - # someone's email addresses from transitory bounce messages. - headers 'Return-Path' => blackhole_email, 'Reply-To' => @from - @recipients = recipient_user.name_and_email - @subject = subject - @body = { - :message => message, - :from_user => from_user, - :recipient_user => recipient_user, - :from_user_url => from_user_url - } - end - - # Send message to a user from the administrator - def from_admin_message(recipient_user, subject, message) - @from = contact_from_name_and_email - @recipients = recipient_user.name_and_email - @subject = subject - @body = { - :message => message, - :from_user => @from, - :recipient_user => recipient_user, - } - bcc Configuration::contact_email - end - -end diff --git a/app/models/contact_validator.rb b/app/models/contact_validator.rb index d277161f9..65e539669 100644 --- a/app/models/contact_validator.rb +++ b/app/models/contact_validator.rb @@ -1,35 +1,29 @@ -# == Schema Information -# Schema version: 114 -# -# Table name: contact_validators -# -# name :string -# email :string -# subject :text -# message :text -# - # models/contact_validator.rb: # Validates contact form submissions. # # Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ -class ContactValidator < ActiveRecord::BaseWithoutTable - strip_attributes! +class ContactValidator + include ActiveModel::Validations - column :name, :string - column :email, :string - column :subject, :text - column :message, :text + attr_accessor :name, :email, :subject, :message validates_presence_of :name, :message => N_("Please enter your name") validates_presence_of :email, :message => N_("Please enter your email address") validates_presence_of :subject, :message => N_("Please enter a subject") validates_presence_of :message, :message => N_("Please enter the message you want to send") + validate :email_format - def validate - errors.add(:email, _("Email doesn't look like a valid address")) unless MySociety::Validate.is_valid_email(self.email) + def initialize(attributes = {}) + attributes.each do |name, value| + send("#{name}=", value) + end end + private + + def email_format + errors.add(:email, _("Email doesn't look like a valid address")) unless MySociety::Validate.is_valid_email(self.email) + end end diff --git a/app/models/foi_attachment.rb b/app/models/foi_attachment.rb index 723bc4abb..ecd4a1872 100644 --- a/app/models/foi_attachment.rb +++ b/app/models/foi_attachment.rb @@ -1,11 +1,9 @@ # encoding: UTF-8 - # == Schema Information -# Schema version: 114 # # Table name: foi_attachments # -# id :integer not null, primary key +# id :integer not null, primary key # content_type :text # filename :text # charset :text @@ -20,7 +18,7 @@ # An attachment to an email (IncomingMessage) # # Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ # This is the type which is used to send data about attachments to the view require 'digest' @@ -38,11 +36,7 @@ class FoiAttachment < ActiveRecord::Base BODY_MAX_DELAY = 5 def directory - rails_env = Rails.env - if rails_env.nil? || rails_env.empty? - raise "$RAILS_ENV is not set" - end - base_dir = File.expand_path(File.join(File.dirname(__FILE__), "../../cache", "attachments_#{rails_env}")) + base_dir = File.expand_path(File.join(File.dirname(__FILE__), "../../cache", "attachments_#{Rails.env}")) return File.join(base_dir, self.hexdigest[0..2]) end @@ -67,28 +61,20 @@ class FoiAttachment < ActiveRecord::Base file.write d } update_display_size! - encode_cached_body! @cached_body = d end - # If the original mail part had a charset, it's some kind of string, so assume that - # it should be handled as a string in the stated charset, not a bytearray, and then - # convert it our default encoding. For ruby 1.8 this is a noop. - def encode_cached_body! - if RUBY_VERSION.to_f >= 1.9 - if charset - @cached_body.force_encoding(charset) - @cached_body = @cached_body.encode(Encoding.default_internal, charset) - end - end - end - def body if @cached_body.nil? tries = 0 delay = 1 begin - @cached_body = File.open(self.filepath, "rb" ).read + binary_data = File.open(self.filepath, "rb" ){ |file| file.read } + if self.content_type =~ /^text/ + @cached_body = convert_string_to_utf8_or_binary(binary_data, 'UTF-8') + else + @cached_body = binary_data + end rescue Errno::ENOENT # we've lost our cached attachments for some reason. Reparse them. if tries > BODY_MAX_TRIES @@ -103,7 +89,6 @@ class FoiAttachment < ActiveRecord::Base self.incoming_message.parse_raw_email!(force) retry end - encode_cached_body! end return @cached_body end @@ -317,8 +302,7 @@ class FoiAttachment < ActiveRecord::Base text = CGI.escapeHTML(text) text = MySociety::Format.make_clickable(text) html = text.gsub(/\n/, '<br>') - return '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" - "http://www.w3.org/TR/html4/loose.dtd"><html><head><title></title></head><body>' + html + "</body></html>", wrapper_id + return '<!DOCTYPE html><html><head><title></title></head><body>' + html + "</body></html>", wrapper_id end # the extractions will also produce image files, which go in the @@ -375,7 +359,8 @@ class FoiAttachment < ActiveRecord::Base ret = "<html><head></head><body>"; if self.has_google_docs_viewer? wrapper_id = "wrapper_google_embed" - ret = ret + "<iframe src='http://docs.google.com/viewer?url=<attachment-url-here>&embedded=true' width='100%' height='100%' style='border: none;'></iframe>"; + protocol = AlaveteliConfiguration::force_ssl ? 'https' : 'http' + ret = ret + "<iframe src='#{protocol}://docs.google.com/viewer?url=<attachment-url-here>&embedded=true' width='100%' height='100%' style='border: none;'></iframe>"; else ret = ret + "<p>Sorry, we were unable to convert this file to HTML. Please use the download link at the top right.</p>" end diff --git a/app/models/holiday.rb b/app/models/holiday.rb index 13258396a..3076cc0fd 100644 --- a/app/models/holiday.rb +++ b/app/models/holiday.rb @@ -1,9 +1,8 @@ # == Schema Information -# Schema version: 114 # # Table name: holidays # -# id :integer not null, primary key +# id :integer not null, primary key # day :date # description :text # @@ -19,7 +18,7 @@ # -- Freedom of Information Act 2000 section 10 # # Copyright (c) 2009 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class Holiday < ActiveRecord::Base diff --git a/app/models/incoming_message.rb b/app/models/incoming_message.rb index 5c845ead3..6db145348 100644 --- a/app/models/incoming_message.rb +++ b/app/models/incoming_message.rb @@ -1,15 +1,13 @@ # coding: utf-8 - # == Schema Information -# Schema version: 114 # # Table name: incoming_messages # -# id :integer not null, primary key -# info_request_id :integer not null -# created_at :datetime not null -# updated_at :datetime not null -# raw_email_id :integer not null +# id :integer not null, primary key +# info_request_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# raw_email_id :integer not null # cached_attachment_text_clipped :text # cached_main_body_text_folded :text # cached_main_body_text_unfolded :text @@ -19,27 +17,28 @@ # last_parsed :datetime # mail_from :text # sent_at :datetime +# prominence :string(255) default("normal"), not null +# prominence_reason :text +# # models/incoming_message.rb: # An (email) message from really anybody to be logged with a request. e.g. A # response from the public body. # # Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ # TODO # Move some of the (e.g. quoting) functions here into rblib, as they feel # general not specific to IncomingMessage. -require 'alaveteli_file_types' require 'htmlentities' require 'rexml/document' require 'zip/zip' -require 'mapi/msg' -require 'mapi/convert' - +require 'iconv' unless String.method_defined?(:encode) class IncomingMessage < ActiveRecord::Base + extend MessageProminence belongs_to :info_request validates_presence_of :info_request @@ -51,6 +50,8 @@ class IncomingMessage < ActiveRecord::Base belongs_to :raw_email + has_prominence + # See binary_mask_stuff function below. It just test for inclusion # in this hash, not the value of the right hand side. DoNotBinaryMask = { @@ -62,6 +63,12 @@ class IncomingMessage < ActiveRecord::Base 'application/zip' => 1, } + # Given that there are in theory many info request events, a convenience method for + # getting the response event + def response_event + self.info_request_events.detect{ |e| e.event_type == 'response' } + end + # Return a cached structured mail object def mail(force = nil) if (!force.nil? || @mail.nil?) && !self.raw_email.nil? @@ -122,16 +129,17 @@ class IncomingMessage < ActiveRecord::Base if (!force.nil? || self.last_parsed.nil?) ActiveRecord::Base.transaction do self.extract_attachments! - self.sent_at = self.mail.date || self.created_at - self.subject = self.mail.subject - self.mail_from = MailHandler.get_from_name(self.mail) + write_attribute(:sent_at, self.mail.date || self.created_at) + write_attribute(:subject, self.mail.subject) + write_attribute(:mail_from, MailHandler.get_from_name(self.mail)) if self.from_email self.mail_from_domain = PublicBody.extract_domain_from_email(self.from_email) else self.mail_from_domain = "" end - self.valid_to_reply_to = self._calculate_valid_to_reply_to + write_attribute(:valid_to_reply_to, self._calculate_valid_to_reply_to) self.last_parsed = Time.now + self.foi_attachments reload=true self.save! end end @@ -153,14 +161,17 @@ class IncomingMessage < ActiveRecord::Base parse_raw_email! super end + def subject parse_raw_email! super end + def mail_from parse_raw_email! super end + def safe_mail_from if !self.mail_from.nil? mail_from = self.mail_from.dup @@ -168,20 +179,43 @@ class IncomingMessage < ActiveRecord::Base return mail_from end end + + def specific_from_name? + !safe_mail_from.nil? && safe_mail_from.strip != info_request.public_body.name.strip + end + + def from_public_body? + safe_mail_from.nil? || (mail_from_domain == info_request.public_body.request_email_domain) + end + def mail_from_domain parse_raw_email! super end - # And look up by URL part number to get an attachment + # And look up by URL part number and display filename to get an attachment # XXX relies on extract_attachments calling MailHandler.ensure_parts_counted - def self.get_attachment_by_url_part_number(attachments, found_url_part_number) - attachments.each do |a| - if a.url_part_number == found_url_part_number - return a + # The filename here is passed from the URL parameter, so it's the + # display_filename rather than the real filename. + def self.get_attachment_by_url_part_number_and_filename(attachments, found_url_part_number, display_filename) + attachment_by_part_number = attachments.detect { |a| a.url_part_number == found_url_part_number } + if attachment_by_part_number && attachment_by_part_number.display_filename == display_filename + # Then the filename matches, which is fine: + attachment_by_part_number + else + # Otherwise if the URL part number and filename don't + # match - this is probably due to a reparsing of the + # email. In that case, try to find a unique matching + # filename from any attachment. + attachments_by_filename = attachments.select { |a| + a.display_filename == display_filename + } + if attachments_by_filename.length == 1 + attachments_by_filename[0] + else + nil end end - return nil end # Converts email addresses we know about into textual descriptions of them @@ -193,7 +227,7 @@ class IncomingMessage < ActiveRecord::Base text.gsub!(self.info_request.public_body.request_email, _("[{{public_body}} request email]", :public_body => self.info_request.public_body.short_or_long_name)) end text.gsub!(self.info_request.incoming_email, _('[FOI #{{request}} email]', :request => self.info_request.id.to_s) ) - text.gsub!(Configuration::contact_email, _("[{{site_name}} contact email]", :site_name => Configuration::site_name) ) + text.gsub!(AlaveteliConfiguration::contact_email, _("[{{site_name}} contact email]", :site_name => AlaveteliConfiguration::site_name) ) end # Replaces all email addresses in (possibly binary data) with equal length alternative ones. @@ -219,7 +253,7 @@ class IncomingMessage < ActiveRecord::Base if censored_uncompressed_text != uncompressed_text # then use the altered file (recompressed) recompressed_text = nil - if Configuration::use_ghostscript_compression == true + if AlaveteliConfiguration::use_ghostscript_compression == true command = ["gs", "-sDEVICE=pdfwrite", "-dCompatibilityLevel=1.4", "-dPDFSETTINGS=/screen", "-dNOPAUSE", "-dQUIET", "-dBATCH", "-sOutputFile=-", "-"] else command = ["pdftk", "-", "output", "-", "compress"] @@ -246,7 +280,7 @@ class IncomingMessage < ActiveRecord::Base # Used by binary_mask_stuff - replace text in place def _binary_mask_stuff_internal!(text) # Keep original size, so can check haven't resized it - orig_size = text.size + orig_size = text.mb_chars.size # Replace ASCII email addresses... text.gsub!(MySociety::Validate.email_find_regexp) do |email| @@ -258,11 +292,21 @@ class IncomingMessage < ActiveRecord::Base # equivalents to the UCS-2 ascii_chars = text.gsub(/\0/, "") emails = ascii_chars.scan(MySociety::Validate.email_find_regexp) + # Convert back to UCS-2, making a mask at the same time - emails.map! {|email| [ - Iconv.conv('ucs-2le', 'ascii', email[0]), - Iconv.conv('ucs-2le', 'ascii', email[0].gsub(/[^@.]/, 'x')) - ] } + if String.method_defined?(:encode) + emails.map! do |email| + # We want the ASCII representation of UCS-2 + [email[0].encode('UTF-16LE').force_encoding('US-ASCII'), + email[0].gsub(/[^@.]/, 'x').encode('UTF-16LE').force_encoding('US-ASCII')] + end + else + emails.map! {|email| [ + Iconv.conv('ucs-2le', 'ascii', email[0]), + Iconv.conv('ucs-2le', 'ascii', email[0].gsub(/[^@.]/, 'x')) + ] } + end + # Now search and replace the UCS-2 email with the UCS-2 mask for email, mask in emails text.gsub!(email, mask) @@ -271,7 +315,7 @@ class IncomingMessage < ActiveRecord::Base # Replace censor items self.info_request.apply_censor_rules_to_binary!(text) - raise "internal error in binary_mask_stuff" if text.size != orig_size + raise "internal error in binary_mask_stuff" if text.mb_chars.size != orig_size return text end @@ -316,7 +360,7 @@ class IncomingMessage < ActiveRecord::Base text.gsub!(/(Mobile|Mob)([\s\/]*(Fax|Tel))*\s*:?[\s\d]*\d/, "[mobile number]") # Remove WhatDoTheyKnow signup links - text.gsub!(/http:\/\/#{Configuration::domain}\/c\/[^\s]+/, "[WDTK login link]") + text.gsub!(/http:\/\/#{AlaveteliConfiguration::domain}\/c\/[^\s]+/, "[WDTK login link]") # Remove things from censor rules self.info_request.apply_censor_rules_to_text!(text) @@ -341,6 +385,10 @@ class IncomingMessage < ActiveRecord::Base multiline_original_message = '(' + '''>>>.* \d\d/\d\d/\d\d\d\d\s+\d\d:\d\d(?::\d\d)?\s*>>>''' + ')' text.gsub!(/^(#{multiline_original_message}\n.*)$/m, replacement) + # On Thu, Nov 28, 2013 at 9:08 AM, A User + # <[1]request-7-skm40s2ls@xxx.xxxx> wrote: + text.gsub!(/^( On [^\n]+\n\s*\<[^>\n]+\> (wrote|said):\s*\n.*)$/m, replacement) + # Single line sections text.gsub!(/^(>.*\n)/, replacement) text.gsub!(/^(On .+ (wrote|said):\n)/, replacement) @@ -472,7 +520,7 @@ class IncomingMessage < ActiveRecord::Base # should instead tell elinks to respect the source # charset use_charset = "utf-8" - if RUBY_VERSION.to_f >= 1.9 + if String.method_defined?(:encode) begin text.encode('utf-8') rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError @@ -503,7 +551,7 @@ class IncomingMessage < ActiveRecord::Base end def _sanitize_text(text) - if RUBY_VERSION.to_f >= 1.9 + if String.method_defined?(:encode) begin # Test if it's good UTF-8 text.encode('utf-8') @@ -534,7 +582,7 @@ class IncomingMessage < ActiveRecord::Base source_charset = 'utf-8' if source_charset.nil? text = Iconv.conv('utf-8//IGNORE', source_charset, text) + _("\n\n[ {{site_name}} note: The above text was badly encoded, and has had strange characters removed. ]", - :site_name => Configuration::site_name) + :site_name => AlaveteliConfiguration::site_name) rescue Iconv::InvalidEncoding, Iconv::IllegalSequence, Iconv::InvalidCharacter if source_charset != "utf-8" source_charset = "utf-8" @@ -546,9 +594,11 @@ class IncomingMessage < ActiveRecord::Base text end - # Returns part which contains main body text, or nil if there isn't one - def get_main_body_text_part - leaves = self.foi_attachments + # Returns part which contains main body text, or nil if there isn't one, + # from a set of foi_attachments. If the leaves parameter is empty or not + # supplied, uses its own foi_attachments. + def get_main_body_text_part(leaves=[]) + leaves = self.foi_attachments if leaves.empty? # Find first part which is text/plain or text/html # (We have to include HTML, as increasingly there are mail clients that @@ -582,6 +632,7 @@ class IncomingMessage < ActiveRecord::Base # nil in this case) return p end + # Returns attachments that are uuencoded in main body part def _uudecode_and_save_attachments(text) # Find any uudecoded things buried in it, yeuchly @@ -605,7 +656,7 @@ class IncomingMessage < ActiveRecord::Base content_type = 'application/octet-stream' end hexdigest = Digest::MD5.hexdigest(content) - attachment = self.foi_attachments.find_or_create_by_hexdigest(:hexdigest => hexdigest) + attachment = self.foi_attachments.find_or_create_by_hexdigest(hexdigest) attachment.update_attributes(:filename => filename, :content_type => content_type, :body => content, @@ -632,15 +683,19 @@ class IncomingMessage < ActiveRecord::Base attachment_attributes = MailHandler.get_attachment_attributes(self.mail(force)) attachments = [] attachment_attributes.each do |attrs| - attachment = self.foi_attachments.find_or_create_by_hexdigest(:hexdigest => attrs[:hexdigest]) - body = attrs.delete(:body) + attachment = self.foi_attachments.find_or_create_by_hexdigest(attrs[:hexdigest]) attachment.update_attributes(attrs) - # Set the body separately as its handling can depend on the value of charset - attachment.body = body attachment.save! - attachments << attachment.id + attachments << attachment end - main_part = get_main_body_text_part + + # Reload to refresh newly created foi_attachments + self.reload + + # get the main body part from the set of attachments we just created, + # not from the self.foi_attachments association - some of the total set of + # self.foi_attachments may now be obsolete + main_part = get_main_body_text_part(attachments) # we don't use get_main_body_text_internal, as we want to avoid charset # conversions, since /usr/bin/uudecode needs to deal with those. # e.g. for https://secure.mysociety.org/admin/foi/request/show_raw_email/24550 @@ -651,12 +706,14 @@ class IncomingMessage < ActiveRecord::Base c += 1 uudecode_attachment.url_part_number = c uudecode_attachment.save! - attachments << uudecode_attachment.id + attachments << uudecode_attachment end end + attachment_ids = attachments.map{ |attachment| attachment.id } # now get rid of any attachments we no longer have - FoiAttachment.destroy_all("id NOT IN (#{attachments.join(',')}) AND incoming_message_id = #{self.id}") + FoiAttachment.destroy_all(["id NOT IN (?) AND incoming_message_id = ?", + attachment_ids, self.id]) end # Returns body text as HTML with quotes flattened, and emails removed. @@ -682,7 +739,7 @@ class IncomingMessage < ActiveRecord::Base text.strip! # if there is nothing but quoted stuff, then show the subject if text == "FOLDED_QUOTED_SECTION" - text = "[Subject only] " + CGI.escapeHTML(self.subject) + text + text = "[Subject only] " + CGI.escapeHTML(self.subject || '') + text end # and display link for quoted stuff text = text.gsub(/FOLDED_QUOTED_SECTION/, "\n\n" + '<span class="unfold_link"><a href="?unfold=1#incoming-'+self.id.to_s+'">'+_("show quoted sections")+'</a></span>' + "\n\n") @@ -739,20 +796,27 @@ class IncomingMessage < ActiveRecord::Base return self.cached_attachment_text_clipped end - def _get_attachment_text_internal + def _extract_text # Extract text from each attachment - text = '' - attachments = self.get_attachments_for_display - for attachment in attachments - text += MailHandler.get_attachment_text_one_file(attachment.content_type, + self.get_attachments_for_display.reduce(''){ |memo, attachment| + memo += MailHandler.get_attachment_text_one_file(attachment.content_type, attachment.body, attachment.charset) - end - # Remove any bad characters - text = Iconv.conv('utf-8//IGNORE', 'utf-8', text) - return text + } end + def _get_attachment_text_internal + text = self._extract_text + + # Remove any bad characters + if String.method_defined?(:encode) + # handle "problematic" encoding + text.encode!('UTF-16', 'UTF-8', :invalid => :replace, :undef => :replace, :replace => '') + text.encode('UTF-8', 'UTF-16') + else + Iconv.conv('utf-8//IGNORE', 'utf-8', text) + end + end # Returns text for indexing def get_text_for_indexing_full 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 diff --git a/app/models/info_request_batch.rb b/app/models/info_request_batch.rb new file mode 100644 index 000000000..498ab4951 --- /dev/null +++ b/app/models/info_request_batch.rb @@ -0,0 +1,73 @@ +# == Schema Information +# Schema version: 20131024114346 +# +# Table name: info_request_batches +# +# id :integer not null, primary key +# title :text not null +# user_id :integer not null +# created_at :datetime +# updated_at :datetime +# + +class InfoRequestBatch < ActiveRecord::Base + has_many :info_requests + belongs_to :user + has_and_belongs_to_many :public_bodies + + validates_presence_of :user + validates_presence_of :title + validates_presence_of :body + + # When constructing a new batch, use this to check user hasn't double submitted. + def InfoRequestBatch.find_existing(user, title, body, public_body_ids) + find(:first, :conditions => ['user_id = ? + AND title = ? + AND body = ? + AND info_request_batches_public_bodies.public_body_id in (?)', + user, title, body, public_body_ids], + :include => :public_bodies) + end + + # Create a batch of information requests, returning a list of public bodies + # that are unrequestable from the initial list of public body ids passed. + def create_batch! + unrequestable = [] + created = [] + ActiveRecord::Base.transaction do + public_bodies.each do |public_body| + if public_body.is_requestable? + created << create_request!(public_body) + else + unrequestable << public_body + end + end + self.sent_at = Time.now + self.save! + end + created.each{ |info_request| info_request.outgoing_messages.first.send_message } + + return unrequestable + end + + # Create and send an FOI request to a public body + def create_request!(public_body) + body = OutgoingMessage.fill_in_salutation(self.body, public_body) + info_request = InfoRequest.create_from_attributes({:title => self.title}, + {:body => body}, + self.user) + info_request.public_body_id = public_body.id + info_request.info_request_batch = self + info_request.save! + info_request + end + + def InfoRequestBatch.send_batches() + find_each(:conditions => "sent_at IS NULL") do |info_request_batch| + unrequestable = info_request_batch.create_batch! + mail_message = InfoRequestBatchMailer.batch_sent(info_request_batch, + unrequestable, + info_request_batch.user).deliver + end + end +end diff --git a/app/models/info_request_event.rb b/app/models/info_request_event.rb index 09eba31ab..5eed5ba83 100644 --- a/app/models/info_request_event.rb +++ b/app/models/info_request_event.rb @@ -1,28 +1,29 @@ # == Schema Information -# Schema version: 114 # # 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 +# 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 -# prominence :string(255) default("normal"), not null # # models/info_request_event.rb: # # Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class InfoRequestEvent < ActiveRecord::Base + + extend XapianQueries + belongs_to :info_request validates_presence_of :info_request @@ -48,10 +49,10 @@ class InfoRequestEvent < ActiveRecord::Base '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' @@ -63,34 +64,12 @@ class InfoRequestEvent < ActiveRecord::Base # user described state (also update in info_request) validate :must_be_valid_state - # whether event is publicly visible - validates_inclusion_of :prominence, :in => [ - 'normal', - 'hidden', - 'requester_only' - ] - 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 - def user_can_view?(user) - if !self.info_request.user_can_view?(user) - raise "internal error, called user_can_view? on event when there is not permission to view entire request" - end - - if self.prominence == 'hidden' - return User.view_hidden_requests?(user) - end - if self.prominence == 'requester_only' - return self.info_request.is_owning_user?(user) - end - return true - end - - # Full text search indexing acts_as_xapian :texts => [ :search_text_main, :title ], :values => [ @@ -259,6 +238,12 @@ class InfoRequestEvent < ActiveRecord::Base 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 @@ -267,6 +252,7 @@ class InfoRequestEvent < ActiveRecord::Base return false end end + def variety self.event_type end @@ -356,6 +342,9 @@ class InfoRequestEvent < ActiveRecord::Base 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 @@ -363,16 +352,19 @@ class InfoRequestEvent < ActiveRecord::Base end def is_sent_sort? - if [ 'sent', 'resent'].include?(self.event_type) - return true - end - return false + ['sent', 'resent'].include?(event_type) end + def is_followup_sort? - if [ 'followup_sent', 'followup_resent'].include?(self.event_type) - return true - end - return false + ['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? @@ -401,7 +393,7 @@ class InfoRequestEvent < ActiveRecord::Base :comment_id => self.comment_id, # XXX would be nice to add links here, but alas the - # code to make them is in views only. See views/request/details.rhtml + # code to make them is in views only. See views/request/details.html.erb # perhaps can call with @template somehow } @@ -416,7 +408,7 @@ class InfoRequestEvent < ActiveRecord::Base 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 + ret[:user] = self.info_request.user_json_for_api end return ret @@ -427,4 +419,5 @@ class InfoRequestEvent < ActiveRecord::Base yield(column.human_name, self.send(column.name), column.type.to_s, column.name) end end + end diff --git a/app/models/mail_server_log.rb b/app/models/mail_server_log.rb index 755584b90..0e5b60ff1 100644 --- a/app/models/mail_server_log.rb +++ b/app/models/mail_server_log.rb @@ -1,23 +1,20 @@ # == Schema Information -# Schema version: 20121010214348 # # Table name: mail_server_logs # -# id :integer not null, primary key +# id :integer not null, primary key # mail_server_log_done_id :integer # info_request_id :integer -# order :integer not null -# line :text not null -# created_at :datetime not null -# updated_at :datetime not null +# order :integer not null +# line :text not null +# created_at :datetime not null +# updated_at :datetime not null # # We load log file lines for requests in here, for display in the admin interface. # # Copyright (c) 2009 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ -# -# $Id: exim_log.rb,v 1.14 2009-09-17 21:10:05 francis Exp $ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class MailServerLog < ActiveRecord::Base belongs_to :info_request @@ -53,7 +50,7 @@ class MailServerLog < ActiveRecord::Base done.save! f = is_gz ? Zlib::GzipReader.open(file_name) : File.open(file_name, 'r') - case(Configuration::mta_log_type.to_sym) + case(AlaveteliConfiguration::mta_log_type.to_sym) when :exim load_exim_log_data(f, done) when :postfix @@ -123,13 +120,13 @@ class MailServerLog < ActiveRecord::Base # We also check the email prefix so that we could, for instance, separately handle a staging and production # instance running on the same server with different email prefixes. def MailServerLog.email_addresses_on_line(line) - prefix = Regexp::quote(Configuration::incoming_email_prefix) - domain = Regexp::quote(Configuration::incoming_email_domain) + prefix = Regexp::quote(AlaveteliConfiguration::incoming_email_prefix) + domain = Regexp::quote(AlaveteliConfiguration::incoming_email_domain) line.scan(/#{prefix}request-[^\s]+@#{domain}/).sort.uniq end def MailServerLog.request_sent?(ir) - case(Configuration::mta_log_type.to_sym) + case(AlaveteliConfiguration::mta_log_type.to_sym) when :exim request_exim_sent?(ir) when :postfix diff --git a/app/models/mail_server_log_done.rb b/app/models/mail_server_log_done.rb index 3fb20f0b3..222b072c5 100644 --- a/app/models/mail_server_log_done.rb +++ b/app/models/mail_server_log_done.rb @@ -1,19 +1,18 @@ # == Schema Information -# Schema version: 20121010214348 # # Table name: mail_server_log_dones # -# id :integer not null, primary key -# filename :text not null -# last_stat :datetime not null -# created_at :datetime not null -# updated_at :datetime not null +# id :integer not null, primary key +# filename :text not null +# last_stat :datetime not null +# created_at :datetime not null +# updated_at :datetime not null # # Stores that a particular mail server log file has been loaded in, see mail_server_log.rb # # Copyright (c) 2009 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class MailServerLogDone < ActiveRecord::Base has_many :mail_server_logs diff --git a/app/models/outgoing_mailer.rb b/app/models/outgoing_mailer.rb deleted file mode 100644 index 503166b8a..000000000 --- a/app/models/outgoing_mailer.rb +++ /dev/null @@ -1,98 +0,0 @@ -# models/outgoing_mailer.rb: -# Emails which go to public bodies on behalf of users. -# -# Copyright (c) 2009 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ - -# Note: The layout for this wraps messages by lines rather than (blank line -# separated) paragraphs, as is the convention for all the other mailers. This -# turned out to fit better with user exepectations when formatting messages. -# -# XXX The other mail templates are written to use blank line separated -# paragraphs. They could be rewritten, and the wrapping method made uniform -# throughout the application. - -class OutgoingMailer < ApplicationMailer - - # Email to public body requesting info - def initial_request(info_request, outgoing_message) - @wrap_lines_as_paragraphs = true - @from = info_request.incoming_name_and_email - @recipients = info_request.recipient_name_and_email - @subject = info_request.email_subject_request - @headers["message-id"] = OutgoingMailer.id_for_message(outgoing_message) - @body = {:info_request => info_request, :outgoing_message => outgoing_message, - :contact_email => Configuration::contact_email } - end - - # Later message to public body regarding existing request - def followup(info_request, outgoing_message, incoming_message_followup) - @wrap_lines_as_paragraphs = true - @from = info_request.incoming_name_and_email - @recipients = OutgoingMailer.name_and_email_for_followup(info_request, incoming_message_followup) - @subject = OutgoingMailer.subject_for_followup(info_request, outgoing_message) - @headers["message-id"] = OutgoingMailer.id_for_message(outgoing_message) - @body = {:info_request => info_request, :outgoing_message => outgoing_message, - :incoming_message_followup => incoming_message_followup, - :contact_email => Configuration::contact_email } - end - - # XXX the condition checking valid_to_reply_to? also appears in views/request/_followup.rhtml, - # it shouldn't really, should call something here. - # XXX also OutgoingMessage.get_salutation - # XXX these look like they should be members of IncomingMessage, but logically they - # need to work even when IncomingMessage is nil - def OutgoingMailer.name_and_email_for_followup(info_request, incoming_message_followup) - if incoming_message_followup.nil? || !incoming_message_followup.valid_to_reply_to? - return info_request.recipient_name_and_email - else - # calling safe_mail_from from so censor rules are run - return MailHandler.address_from_name_and_email(incoming_message_followup.safe_mail_from, - incoming_message_followup.from_email) - end - end - # Used in the preview of followup - def OutgoingMailer.name_for_followup(info_request, incoming_message_followup) - if incoming_message_followup.nil? || !incoming_message_followup.valid_to_reply_to? - return info_request.public_body.name - else - # calling safe_mail_from from so censor rules are run - return incoming_message_followup.safe_mail_from || info_request.public_body.name - end - end - # Used when making list of followup places to remove duplicates - def OutgoingMailer.email_for_followup(info_request, incoming_message_followup) - if incoming_message_followup.nil? || !incoming_message_followup.valid_to_reply_to? - return info_request.recipient_email - else - return incoming_message_followup.from_email - end - end - # Subject to use for followup - def OutgoingMailer.subject_for_followup(info_request, outgoing_message) - if outgoing_message.what_doing == 'internal_review' - return "Internal review of " + info_request.email_subject_request - else - return info_request.email_subject_followup(outgoing_message.incoming_message_followup) - end - end - # Whether we have a valid email address for a followup - def OutgoingMailer.is_followupable?(info_request, incoming_message_followup) - if incoming_message_followup.nil? || !incoming_message_followup.valid_to_reply_to? - return info_request.recipient_email_valid_for_followup? - else - # email has been checked in incoming_message_followup.valid_to_reply_to? above - return true - end - end - # Message-ID to use - def OutgoingMailer.id_for_message(outgoing_message) - message_id = "ogm-" + outgoing_message.id.to_s - t = Time.now - message_id += "+" + '%08x%05x-%04x' % [t.to_i, t.tv_usec, rand(0xffff)] - message_id += "@" + Configuration::incoming_email_domain - return "<" + message_id + ">" - end - -end - diff --git a/app/models/outgoing_message.rb b/app/models/outgoing_message.rb index c75894e6a..a435511d3 100644 --- a/app/models/outgoing_message.rb +++ b/app/models/outgoing_message.rb @@ -1,18 +1,20 @@ # == Schema Information -# Schema version: 114 +# Schema version: 20131024114346 # # Table name: outgoing_messages # -# id :integer not null, primary key -# info_request_id :integer not null -# body :text not null -# status :string(255) not null -# message_type :string(255) not null -# created_at :datetime not null -# updated_at :datetime not null +# id :integer not null, primary key +# info_request_id :integer not null +# body :text not null +# status :string(255) not null +# message_type :string(255) not null +# created_at :datetime not null +# updated_at :datetime not null # last_sent_at :datetime # incoming_message_followup_id :integer -# what_doing :string(255) not null +# what_doing :string(255) not null +# prominence :string(255) default("normal"), not null +# prominence_reason :text # # models/outgoing_message.rb: @@ -20,11 +22,22 @@ # else. e.g. An initial request for information, or a complaint. # # Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class OutgoingMessage < ActiveRecord::Base + extend MessageProminence + include Rails.application.routes.url_helpers + include LinkToHelper + self.default_url_options[:host] = AlaveteliConfiguration::domain + # https links in emails if forcing SSL + if AlaveteliConfiguration::force_ssl + self.default_url_options[:protocol] = "https" + end + strip_attributes! + has_prominence + belongs_to :info_request validates_presence_of :info_request @@ -51,17 +64,34 @@ class OutgoingMessage < ActiveRecord::Base end end + after_initialize :set_default_letter + # How the default letter starts and ends def get_salutation + if self.info_request.is_batch_request_template? + return OutgoingMessage.placeholder_salutation + end ret = "" 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 + self.info_request.public_body.name + return OutgoingMessage.default_salutation(self.info_request.public_body) end salutation = _("Dear {{public_body_name}},", :public_body_name => ret) end + def OutgoingMessage.default_salutation(public_body) + _("Dear {{public_body_name}},", :public_body_name => public_body.name) + end + + def OutgoingMessage.placeholder_salutation + _("Dear [Authority name],") + end + + def OutgoingMessage.fill_in_salutation(body, public_body) + body.gsub(placeholder_salutation, default_salutation(public_body)) + end + def get_signoff 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? return _("Yours sincerely,") @@ -78,15 +108,15 @@ class OutgoingMessage < ActiveRecord::Base end if self.what_doing == 'internal_review' - "Please pass this on to the person who conducts Freedom of Information reviews." + + _("Please pass this on to the person who conducts Freedom of Information reviews.") + "\n\n" + - "I am writing to request an internal review of " + - self.info_request.public_body.name + - "'s handling of my FOI request " + - "'" + self.info_request.title + "'." + + _("I am writing to request an internal review of {{public_body_name}}'s handling of my FOI request '{{info_request_title}}'.", + :public_body_name => self.info_request.public_body.name, + :info_request_title => self.info_request.title) + "\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://" + Configuration::domain + "/request/" + self.info_request.url_title + _("A full history of my FOI request and all correspondence is available on the Internet at this address: {{url}}", + :url => request_url(self.info_request)) + + "\n" else "" end @@ -130,13 +160,6 @@ class OutgoingMessage < ActiveRecord::Base MySociety::Validate.contains_postcode?(self.body) end - # Set default letter - def after_initialize - if self.body.nil? - self.body = get_default_message - end - end - # Deliver outgoing message # Note: You can test this from script/console with, say: # InfoRequest.find(1).outgoing_messages[0].send_message @@ -147,7 +170,7 @@ class OutgoingMessage < ActiveRecord::Base self.status = 'sent' self.save! - mail_message = OutgoingMailer.deliver_initial_request(self.info_request, self) + mail_message = OutgoingMailer.initial_request(self.info_request, self).deliver self.info_request.log_event(log_event_type, { :email => mail_message.to_addrs.join(", "), :outgoing_message_id => self.id, @@ -159,7 +182,7 @@ class OutgoingMessage < ActiveRecord::Base self.status = 'sent' self.save! - mail_message = OutgoingMailer.deliver_followup(self.info_request, self, self.incoming_message_followup) + mail_message = OutgoingMailer.followup(self.info_request, self, self.incoming_message_followup).deliver self.info_request.log_event('followup_' + log_event_type, { :email => mail_message.to_addrs.join(", "), :outgoing_message_id => self.id, @@ -206,11 +229,11 @@ class OutgoingMessage < ActiveRecord::Base end # Returns text for indexing / text display - def get_text_for_indexing + def get_text_for_indexing(strip_salutation=true) text = self.body.strip # Remove salutation - text.sub!(/Dear .+,/, "") + text.sub!(/Dear .+,/, "") if strip_salutation # Remove email addresses from display/index etc. self.remove_privacy_sensitive_things!(text) @@ -230,6 +253,12 @@ class OutgoingMessage < ActiveRecord::Base return text.html_safe end + # Return body for display as text + def get_body_for_text_display + get_text_for_indexing(strip_salutation=false) + end + + def fully_destroy ActiveRecord::Base.transaction do info_request_event = InfoRequestEvent.find_by_outgoing_message_id(self.id) @@ -253,8 +282,14 @@ class OutgoingMessage < ActiveRecord::Base private + def set_default_letter + if self.body.nil? + self.body = get_default_message + end + end + def format_of_body - if self.body.empty? || self.body =~ /\A#{get_salutation}\s+#{get_signoff}/ || self.body =~ /#{get_internal_review_insert_here_note}/ + if self.body.empty? || self.body =~ /\A#{Regexp.escape(get_salutation)}\s+#{Regexp.escape(get_signoff)}/ || self.body =~ /#{Regexp.escape(get_internal_review_insert_here_note)}/ if self.message_type == 'followup' if self.what_doing == 'internal_review' errors.add(:body, _("Please give details explaining why you want a review")) @@ -268,7 +303,7 @@ class OutgoingMessage < ActiveRecord::Base end end if self.body =~ /#{get_signoff}\s*\Z/m - errors.add(:body, _("Please sign at the bottom with your name, or alter the \"%{signoff}\" signature" % { :signoff => get_signoff })) + errors.add(:body, _("Please sign at the bottom with your name, or alter the \"{{signoff}}\" signature", :signoff => get_signoff)) end if !MySociety::Validate.uses_mixed_capitals(self.body) errors.add(:body, _('Please write your message using a mixture of capital and lower case letters. This makes it easier for others to read.')) diff --git a/app/models/post_redirect.rb b/app/models/post_redirect.rb index 31f08c21a..5da3d2742 100644 --- a/app/models/post_redirect.rb +++ b/app/models/post_redirect.rb @@ -1,18 +1,17 @@ # == Schema Information -# Schema version: 114 # # Table name: post_redirects # -# id :integer not null, primary key -# token :text not null -# uri :text not null +# id :integer not null, primary key +# token :text not null +# uri :text not null # post_params_yaml :text -# created_at :datetime not null -# updated_at :datetime not null -# email_token :text not null +# created_at :datetime not null +# updated_at :datetime not null +# email_token :text not null # reason_params_yaml :text # user_id :integer -# circumstance :text default("normal"), not null +# circumstance :text default("normal"), not null # # models/post_redirect.rb: @@ -24,7 +23,7 @@ # fakes the redirect to include POST parameters in request later. # # Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ require 'openssl' # for random bytes function @@ -32,6 +31,8 @@ class PostRedirect < ActiveRecord::Base # Optional, does a login confirm before redirect for use in email links. belongs_to :user + after_initialize :generate_token + # We store YAML version of POST parameters in the database def post_params=(params) self.post_params_yaml = params.to_yaml @@ -62,18 +63,6 @@ class PostRedirect < ActiveRecord::Base MySociety::Util.generate_token end - # Make the token - def after_initialize - # The token is used to return you to what you are doing after the login form. - if not self.token - self.token = PostRedirect.generate_random_token - end - # There is a separate token to use in the URL if we send a confirmation email. - if not self.email_token - self.email_token = PostRedirect.generate_random_token - end - end - # Used by (rspec) test code only def self.get_last_post_redirect # XXX yeuch - no other easy way of getting the token so we can check @@ -89,6 +78,18 @@ class PostRedirect < ActiveRecord::Base PostRedirect.delete_all "updated_at < (now() - interval '2 months')" end + private + + def generate_token + # The token is used to return you to what you are doing after the login form. + if not self.token + self.token = PostRedirect.generate_random_token + end + # There is a separate token to use in the URL if we send a confirmation email. + if not self.email_token + self.email_token = PostRedirect.generate_random_token + end + end end diff --git a/app/models/profile_photo.rb b/app/models/profile_photo.rb index 73d7ca12b..6c3b2cfa0 100644 --- a/app/models/profile_photo.rb +++ b/app/models/profile_photo.rb @@ -1,19 +1,18 @@ # == Schema Information -# Schema version: 114 # # Table name: profile_photos # -# id :integer not null, primary key -# data :binary not null +# id :integer not null, primary key +# data :binary not null # user_id :integer -# draft :boolean default(FALSE), not null +# draft :boolean default(FALSE), not null # # models/profile_photo.rb: # Image of user that goes on their profile. # # Copyright (c) 2009 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class ProfilePhoto < ActiveRecord::Base WIDTH = 96 @@ -29,25 +28,9 @@ class ProfilePhoto < ActiveRecord::Base attr_accessor :x, :y, :w, :h - # convert binary data blob into ImageMagick image when assigned attr_accessor :image - def after_initialize - if data.nil? - self.image = nil - return - end - - image_list = Magick::ImageList.new - begin - image_list.from_blob(data) - rescue Magick::ImageMagickError - self.image = nil - return - end - self.image = image_list[0] # XXX perhaps take largest image or somesuch if there were multiple in the file? - self.convert_image - end + after_initialize :convert_data_to_image # make image valid format and size def convert_image @@ -87,21 +70,25 @@ class ProfilePhoto < ActiveRecord::Base def data_and_draft_checks if self.data.nil? - errors.add(:data, N_("Please choose a file containing your photo.")) + errors.add(:data, _("Please choose a file containing your photo.")) return end if self.image.nil? - errors.add(:data, N_("Couldn't understand the image file that you uploaded. PNG, JPEG, GIF and many other common image file formats are supported.")) + errors.add(:data, _("Couldn't understand the image file that you uploaded. PNG, JPEG, GIF and many other common image file formats are supported.")) return end if self.image.format != 'PNG' - errors.add(:data, N_("Failed to convert image to a PNG")) + errors.add(:data, _("Failed to convert image to a PNG")) end if !self.draft && (self.image.columns != WIDTH || self.image.rows != HEIGHT) - errors.add(:data, N_("Failed to convert image to the correct size: at %{cols}x%{rows}, need %{width}x%{height}" % { :cols => self.image.columns, :rows => self.image.rows, :width => WIDTH, :height => HEIGHT })) + errors.add(:data, _("Failed to convert image to the correct size: at {{cols}}x{{rows}}, need {{width}}x{{height}}", + :cols => self.image.columns, + :rows => self.image.rows, + :width => WIDTH, + :height => HEIGHT)) end if self.draft && self.user_id @@ -112,6 +99,25 @@ class ProfilePhoto < ActiveRecord::Base raise "Internal error, real pictures must have a user" end end + + # Convert binary data blob into ImageMagick image when assigned + def convert_data_to_image + if data.nil? + self.image = nil + return + end + + image_list = Magick::ImageList.new + begin + image_list.from_blob(data) + rescue Magick::ImageMagickError + self.image = nil + return + end + + self.image = image_list[0] # XXX perhaps take largest image or somesuch if there were multiple in the file? + self.convert_image + end end diff --git a/app/models/public_body.rb b/app/models/public_body.rb index 168b9f4c7..7b1ded820 100644 --- a/app/models/public_body.rb +++ b/app/models/public_body.rb @@ -1,27 +1,32 @@ +# -*- coding: utf-8 -*- # == Schema Information -# Schema version: 20120919140404 +# Schema version: 20131024114346 # # Table name: public_bodies # -# id :integer not null, primary key -# name :text not null -# short_name :text not null -# request_email :text not null -# version :integer not null -# last_edit_editor :string(255) not null -# last_edit_comment :text not null -# created_at :datetime not null -# updated_at :datetime not null -# url_name :text not null -# home_page :text default(""), not null -# notes :text default(""), not null -# first_letter :string(255) not null -# publication_scheme :text default(""), not null -# api_key :string(255) not null -# info_requests_count :integer default(0), not null +# id :integer not null, primary key +# name :text not null +# short_name :text not null +# request_email :text not null +# version :integer not null +# last_edit_editor :string(255) not null +# last_edit_comment :text not null +# created_at :datetime not null +# updated_at :datetime not null +# url_name :text not null +# home_page :text default(""), not null +# notes :text default(""), not null +# first_letter :string(255) not null +# publication_scheme :text default(""), not null +# api_key :string(255) not null +# info_requests_count :integer default(0), not null +# disclosure_log :text default(""), not null +# info_requests_successful_count :integer +# info_requests_not_held_count :integer +# info_requests_overdue_count :integer +# info_requests_visible_classified_count :integer # -# -*- coding: utf-8 -*- require 'csv' require 'securerandom' require 'set' @@ -40,12 +45,13 @@ class PublicBody < ActiveRecord::Base has_many :info_requests, :order => 'created_at desc' has_many :track_things, :order => 'created_at desc' has_many :censor_rules, :order => 'created_at desc' + attr_accessor :no_xapian_reindex has_tag_string before_save :set_api_key, :set_default_publication_scheme # Every public body except for the internal admin one is visible - named_scope :visible, lambda { + scope :visible, lambda { { :conditions => "public_bodies.id <> #{PublicBody.internal_admin_body.id}" } @@ -54,18 +60,29 @@ class PublicBody < ActiveRecord::Base translates :name, :short_name, :request_email, :url_name, :notes, :first_letter, :publication_scheme # Convenience methods for creating/editing translations via forms - def translation(locale) + def find_translation_by_locale(locale) self.translations.find_by_locale(locale) end # XXX - Don't like repeating this! def calculate_cached_fields(t) - t.first_letter = t.name.scan(/^./mu)[0].upcase unless t.name.nil? or t.name.empty? + PublicBody.set_first_letter(t) short_long_name = t.name short_long_name = t.short_name if t.short_name and !t.short_name.empty? t.url_name = MySociety::Format.simplify_url_part(short_long_name, 'body') end + # Set the first letter on a public body or translation + def PublicBody.set_first_letter(instance) + unless instance.name.nil? or instance.name.empty? + # we use a regex to ensure it works with utf-8/multi-byte + first_letter = Unicode.upcase instance.name.scan(/^./mu)[0] + if first_letter != instance.first_letter + instance.first_letter = first_letter + end + end + end + def translated_versions translations end @@ -79,7 +96,7 @@ class PublicBody < ActiveRecord::Base if translation_attrs.respond_to? :each_value # Hash => updating translation_attrs.each_value do |attrs| next if skip?(attrs) - t = translation(attrs[:locale]) || PublicBody::Translation.new + t = translation_for(attrs[:locale]) || PublicBody::Translation.new t.attributes = attrs calculate_cached_fields(t) t.save! @@ -106,35 +123,31 @@ class PublicBody < ActiveRecord::Base # like find_by_url_name but also search historic url_name if none found def self.find_by_url_name_with_historic(name) - locale = self.locale || I18n.locale - PublicBody.with_locale(locale) do - found = PublicBody.find(:all, - :conditions => ["public_body_translations.url_name=?", name], - :joins => :translations, - :readonly => false) - # If many bodies are found (usually because the url_name is the same across - # locales) return any of them - return found.first if found.size >= 1 - - # If none found, then search the history of short names - old = PublicBody::Version.find_all_by_url_name(name) - # Find unique public bodies in it - old = old.map { |x| x.public_body_id } - old = old.uniq - # Maybe return the first one, so we show something relevant, - # rather than throwing an error? - raise "Two bodies with the same historical URL name: #{name}" if old.size > 1 - return unless old.size == 1 - # does acts_as_versioned provide a method that returns the current version? - return PublicBody.find(old.first) - end + found = PublicBody.find(:all, + :conditions => ["public_body_translations.url_name=?", name], + :joins => :translations, + :readonly => false) + # If many bodies are found (usually because the url_name is the same across + # locales) return any of them + return found.first if found.size >= 1 + + # If none found, then search the history of short names + old = PublicBody::Version.find_all_by_url_name(name) + # Find unique public bodies in it + old = old.map { |x| x.public_body_id } + old = old.uniq + # Maybe return the first one, so we show something relevant, + # rather than throwing an error? + raise "Two bodies with the same historical URL name: #{name}" if old.size > 1 + return unless old.size == 1 + # does acts_as_versioned provide a method that returns the current version? + return PublicBody.find(old.first) end # Set the first letter, which is used for faster queries before_save(:set_first_letter) def set_first_letter - # we use a regex to ensure it works with utf-8/multi-byte - self.first_letter = self.name.scan(/./mu)[0].upcase + PublicBody.set_first_letter(self) end # If tagged "not_apply", then FOI/EIR no longer applies to authority at all @@ -180,9 +193,13 @@ class PublicBody < ActiveRecord::Base end acts_as_versioned - self.non_versioned_columns << 'created_at' << 'updated_at' << 'first_letter' << 'api_key' << 'info_requests_count' + self.non_versioned_columns << 'created_at' << 'updated_at' << 'first_letter' << 'api_key' + self.non_versioned_columns << 'info_requests_count' << 'info_requests_successful_count' + self.non_versioned_columns << 'info_requests_count' << 'info_requests_visible_classified_count' + self.non_versioned_columns << 'info_requests_not_held_count' << 'info_requests_overdue' + self.non_versioned_columns << 'info_requests_overdue_count' + class Version - attr_accessor :created_at def last_edit_comment_for_html_display text = self.last_edit_comment.strip @@ -234,6 +251,7 @@ class PublicBody < ActiveRecord::Base def reindex_requested_from if self.changes.include?('url_name') for info_request in self.info_requests + for info_request_event in info_request.info_request_events info_request_event.xapian_mark_needs_index end @@ -243,13 +261,13 @@ class PublicBody < ActiveRecord::Base # When name or short name is changed, also change the url name def short_name=(short_name) - globalize.write(self.class.locale || I18n.locale, :short_name, short_name) + globalize.write(Globalize.locale, :short_name, short_name) self[:short_name] = short_name self.update_url_name end def name=(name) - globalize.write(self.class.locale || I18n.locale, :name, name) + globalize.write(Globalize.locale, :name, name) self[:name] = name self.update_url_name end @@ -329,22 +347,26 @@ class PublicBody < ActiveRecord::Base # The "internal admin" is a special body for internal use. def PublicBody.internal_admin_body - PublicBody.with_locale(I18n.default_locale) do - pb = PublicBody.find_by_url_name("internal_admin_authority") - if pb.nil? - pb = PublicBody.new( - :name => 'Internal admin authority', - :short_name => "", - :request_email => Configuration::contact_email, - :home_page => "", - :notes => "", - :publication_scheme => "", - :last_edit_editor => "internal_admin", - :last_edit_comment => "Made by PublicBody.internal_admin_body" - ) - pb.save! + # Use find_by_sql to avoid the search being specific to a + # locale, since url_name is a translated field: + sql = "SELECT * FROM public_bodies WHERE url_name = 'internal_admin_authority'" + matching_pbs = PublicBody.find_by_sql sql + case + when matching_pbs.empty? then + I18n.with_locale(I18n.default_locale) do + PublicBody.create!(:name => 'Internal admin authority', + :short_name => "", + :request_email => AlaveteliConfiguration::contact_email, + :home_page => "", + :notes => "", + :publication_scheme => "", + :last_edit_editor => "internal_admin", + :last_edit_comment => "Made by PublicBody.internal_admin_body") end - return pb + when matching_pbs.length == 1 then + matching_pbs[0] + else + raise "Multiple public bodies (#{matching_pbs.length}) found with url_name 'internal_admin_authority'" end end @@ -352,10 +374,24 @@ class PublicBody < ActiveRecord::Base class ImportCSVDryRun < StandardError end - # Import from CSV. Just tests things and returns messages if dry_run is true. - # Returns an array of [array of errors, array of notes]. If there are errors, - # always rolls back (as with dry_run). + # Import from a string in CSV format. + # Just tests things and returns messages if dry_run is true. + # Returns an array of [array of errors, array of notes]. If there + # are errors, always rolls back (as with dry_run). def self.import_csv(csv, tag, tag_behaviour, dry_run, editor, available_locales = []) + tmp_csv = nil + Tempfile.open('alaveteli') do |f| + f.write csv + tmp_csv = f + end + PublicBody.import_csv_from_file(tmp_csv.path, tag, tag_behaviour, dry_run, editor, available_locales) + end + + # Import from a CSV file. + # Just tests things and returns messages if dry_run is true. + # Returns an array of [array of errors, array of notes]. If there + # are errors, always rolls back (as with dry_run). + def self.import_csv_from_file(csv_filename, tag, tag_behaviour, dry_run, editor, available_locales = []) errors = [] notes = [] available_locales = [I18n.default_locale] if available_locales.empty? @@ -367,7 +403,7 @@ class PublicBody < ActiveRecord::Base # of updating them bodies_by_name = {} set_of_existing = Set.new() - PublicBody.with_locale(I18n.default_locale) do + I18n.with_locale(I18n.default_locale) do bodies = (tag.nil? || tag.empty?) ? PublicBody.find(:all) : PublicBody.find_by_tag(tag) for existing_body in bodies # Hide InternalAdminBody from import notes @@ -381,7 +417,8 @@ class PublicBody < ActiveRecord::Base set_of_importing = Set.new() field_names = { 'name'=>1, 'request_email'=>2 } # Default values in case no field list is given line = 0 - CSV.parse(csv) do |row| + + CSV.foreach(csv_filename) do |row| line = line + 1 # Parse the first line as a field list if it starts with '#' @@ -394,6 +431,8 @@ class PublicBody < ActiveRecord::Base fields = {} field_names.each{|name, i| fields[name] = row[i]} + yield line, fields if block_given? + name = row[field_names['name']] email = row[field_names['request_email']] next if name.nil? @@ -410,7 +449,7 @@ class PublicBody < ActiveRecord::Base if public_body = bodies_by_name[name] # Existing public body available_locales.each do |locale| - PublicBody.with_locale(locale) do + I18n.with_locale(locale) do changed = ActiveSupport::OrderedHash.new field_list.each do |field_name| localized_field_name = (locale.to_s == I18n.default_locale.to_s) ? field_name : "#{field_name}.#{locale}" @@ -445,7 +484,7 @@ class PublicBody < ActiveRecord::Base else # New public body public_body = PublicBody.new(:name=>"", :short_name=>"", :request_email=>"") available_locales.each do |locale| - PublicBody.with_locale(locale) do + I18n.with_locale(locale) do changed = ActiveSupport::OrderedHash.new field_list.each do |field_name| localized_field_name = (locale.to_s == I18n.default_locale.to_s) ? field_name : "#{field_name}.#{locale}" @@ -494,10 +533,8 @@ class PublicBody < ActiveRecord::Base end # Returns all public bodies (except for the internal admin authority) as csv - def self.export_csv - public_bodies = PublicBody.visible.find(:all, :order => 'url_name', - :include => [:translations, :tags]) - FasterCSV.generate() do |csv| + def self.export_csv(output_filename) + CSV.open(output_filename, "w") do |csv| csv << [ 'Name', 'Short name', @@ -512,7 +549,7 @@ class PublicBody < ActiveRecord::Base 'Updated at', 'Version', ] - public_bodies.each do |public_body| + PublicBody.visible.find_each(:include => [:translations, :tags]) do |public_body| # Skip bodies we use only for site admin next if public_body.has_tag?('site_administration') csv << [ @@ -551,7 +588,7 @@ class PublicBody < ActiveRecord::Base # Returns nil if configuration variable not set def override_request_email - e = Configuration::override_all_public_body_request_emails + e = AlaveteliConfiguration::override_all_public_body_request_emails e if e != "" end @@ -635,6 +672,104 @@ class PublicBody < ActiveRecord::Base end end + def self.where_clause_for_stats(minimum_requests, total_column) + # When producing statistics for public bodies, we want to + # exclude any that are tagged with 'test' - we use a + # sub-select to find the IDs of those public bodies. + test_tagged_query = "SELECT model_id FROM has_tag_string_tags" \ + " WHERE model = 'PublicBody' AND name = 'test'" + "#{total_column} >= #{minimum_requests} AND id NOT IN (#{test_tagged_query})" + end + + # Return data for the 'n' public bodies with the highest (or + # lowest) number of requests, but only returning data for those + # with at least 'minimum_requests' requests. + def self.get_request_totals(n, highest, minimum_requests) + ordering = "info_requests_count" + ordering += " DESC" if highest + where_clause = where_clause_for_stats minimum_requests, 'info_requests_count' + public_bodies = PublicBody.order(ordering).where(where_clause).limit(n) + public_bodies.reverse! if highest + y_values = public_bodies.map { |pb| pb.info_requests_count } + return { + 'public_bodies' => public_bodies, + 'y_values' => y_values, + 'y_max' => y_values.max, + 'totals' => y_values} + end + + # Return data for the 'n' public bodies with the highest (or + # lowest) score according to the metric of the value in 'column' + # divided by the total number of requests, expressed as a + # percentage. This only returns data for those public bodies with + # at least 'minimum_requests' requests. + def self.get_request_percentages(column, n, highest, minimum_requests) + total_column = "info_requests_visible_classified_count" + ordering = "y_value" + ordering += " DESC" if highest + y_value_column = "(cast(#{column} as float) / #{total_column})" + where_clause = where_clause_for_stats minimum_requests, total_column + where_clause += " AND #{column} IS NOT NULL" + public_bodies = PublicBody.select("*, #{y_value_column} AS y_value").order(ordering).where(where_clause).limit(n) + public_bodies.reverse! if highest + y_values = public_bodies.map { |pb| pb.y_value.to_f } + + original_values = public_bodies.map { |pb| pb.send(column) } + # If these are all nil, then probably the values have never + # been set; some have to be set by a rake task. In that case, + # just return nil: + return nil unless original_values.any? { |ov| !ov.nil? } + + original_totals = public_bodies.map { |pb| pb.send(total_column) } + # Calculate confidence intervals, as offsets from the proportion: + cis_below = [] + cis_above = [] + original_totals.each_with_index.map { |total, i| + lower_ci, higher_ci = ci_bounds original_values[i], total, 0.05 + cis_below.push(y_values[i] - lower_ci) + cis_above.push(higher_ci - y_values[i]) + } + # Turn the y values and confidence interval offsets into + # percentages: + [y_values, cis_below, cis_above].each { |l| + l.map! { |v| 100 * v } + } + return { + 'public_bodies' => public_bodies, + 'y_values' => y_values, + 'cis_below' => cis_below, + 'cis_above' => cis_above, + 'y_max' => 100, + 'totals' => original_totals} + end + def self.popular_bodies(locale) + # get some example searches and public bodies to display + # either from config, or based on a (slow!) query if not set + body_short_names = AlaveteliConfiguration::frontpage_publicbody_examples.split(/\s*;\s*/) + locale_condition = 'public_body_translations.locale = ?' + underscore_locale = locale.gsub '-', '_' + conditions = [locale_condition, underscore_locale] + bodies = [] + I18n.with_locale(locale) do + if body_short_names.empty? + # This is too slow + bodies = visible.find(:all, + :order => "info_requests_count desc", + :limit => 32, + :conditions => conditions, + :joins => :translations + ) + else + conditions[0] += " and public_bodies.url_name in (?)" + conditions << body_short_names + bodies = find(:all, :conditions => conditions, :joins => :translations) + end + end + return bodies + end + + private + def request_email_if_requestable # Request_email can be blank, meaning we don't have details if self.is_requestable? diff --git a/app/models/public_body_change_request.rb b/app/models/public_body_change_request.rb new file mode 100644 index 000000000..0e59cbecc --- /dev/null +++ b/app/models/public_body_change_request.rb @@ -0,0 +1,131 @@ +# == Schema Information +# +# Table name: public_body_change_requests +# +# id :integer not null, primary key +# user_email :string(255) +# user_name :string(255) +# user_id :integer +# public_body_name :text +# public_body_id :integer +# public_body_email :string(255) +# source_url :text +# notes :text +# is_open :boolean default(TRUE), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class PublicBodyChangeRequest < ActiveRecord::Base + + belongs_to :user + belongs_to :public_body + validates_presence_of :public_body_name, :message => N_("Please enter the name of the authority"), + :unless => proc{ |change_request| change_request.public_body } + validates_presence_of :user_name, :message => N_("Please enter your name"), + :unless => proc{ |change_request| change_request.user } + validates_presence_of :user_email, :message => N_("Please enter your email address"), + :unless => proc{ |change_request| change_request.user } + validate :user_email_format, :unless => proc{ |change_request| change_request.user_email.blank? } + validate :body_email_format, :unless => proc{ |change_request| change_request.public_body_email.blank? } + + scope :new_body_requests, :conditions => ['public_body_id IS NULL'], :order => 'created_at' + scope :body_update_requests, :conditions => ['public_body_id IS NOT NULL'], :order => 'created_at' + scope :open, :conditions => ['is_open = ?', true] + + def self.from_params(params, user) + change_request = new + change_request.update_from_params(params, user) + end + + def update_from_params(params, user) + if user + self.user_id = user.id + else + self.user_name = params[:user_name] + self.user_email = params[:user_email] + end + self.public_body_name = params[:public_body_name] + self.public_body_id = params[:public_body_id] + self.public_body_email = params[:public_body_email] + self.source_url = params[:source_url] + self.notes = params[:notes] + self + end + + def get_user_name + user ? user.name : user_name + end + + def get_user_email + user ? user.email : user_email + end + + def get_public_body_name + public_body ? public_body.name : public_body_name + end + + def send_message + if public_body + ContactMailer.update_public_body_email(self).deliver + else + ContactMailer.add_public_body(self).deliver + end + end + + def thanks_notice + if self.public_body + _("Your request to update the address for {{public_body_name}} has been sent. Thank you for getting in touch! We'll get back to you soon.", + :public_body_name => get_public_body_name) + else + _("Your request to add an authority has been sent. Thank you for getting in touch! We'll get back to you soon.") + end + end + + def send_response(subject, response) + ContactMailer.from_admin_message(get_user_name, + get_user_email, + subject, + response.strip.html_safe).deliver + end + + def comment_for_public_body + comments = ["Requested by: #{get_user_name} (#{get_user_email})"] + if !source_url.blank? + comments << "Source URL: #{source_url}" + end + if !notes.blank? + comments << "Notes: #{notes}" + end + comments.join("\n") + end + + def default_response_subject + if self.public_body + _("Your request to update {{public_body_name}} on {{site_name}}", :site_name => AlaveteliConfiguration::site_name, + :public_body_name => public_body.name) + else + _("Your request to add {{public_body_name}} to {{site_name}}", :site_name => AlaveteliConfiguration::site_name, + :public_body_name => public_body_name) + end + end + + def close! + self.is_open = false + self.save! + end + + private + + def body_email_format + unless MySociety::Validate.is_valid_email(self.public_body_email) + errors.add(:public_body_email, _("The authority email doesn't look like a valid address")) + end + end + + def user_email_format + unless MySociety::Validate.is_valid_email(self.user_email) + errors.add(:user_email, _("Your email doesn't look like a valid address")) + end + end +end diff --git a/app/models/purge_request.rb b/app/models/purge_request.rb index 48a16f9e6..4e6267bd2 100644 --- a/app/models/purge_request.rb +++ b/app/models/purge_request.rb @@ -1,20 +1,19 @@ # == Schema Information -# Schema version: 114 # # Table name: purge_requests # -# id :integer not null, primary key +# id :integer not null, primary key # url :string(255) -# created_at :datetime not null -# model :string(255) not null -# model_id :integer not null +# created_at :datetime not null +# model :string(255) not null +# model_id :integer not null # # models/purge_request.rb: # A queue of URLs to purge # # Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ # class PurgeRequest < ActiveRecord::Base diff --git a/app/models/raw_email.rb b/app/models/raw_email.rb index de7978b82..21a53f493 100644 --- a/app/models/raw_email.rb +++ b/app/models/raw_email.rb @@ -1,16 +1,15 @@ # == Schema Information -# Schema version: 114 # # Table name: raw_emails # -# id :integer not null, primary key +# id :integer not null, primary key # # models/raw_email.rb: # The fat part of models/incoming_message.rb # # Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class RawEmail < ActiveRecord::Base # deliberately don't strip_attributes, so keeps raw email properly @@ -23,10 +22,10 @@ class RawEmail < ActiveRecord::Base raise "Failed to find the id number of the associated request: has it been saved?" end - if ENV["RAILS_ENV"] == "test" + if Rails.env.test? return File.join(Rails.root, 'files/raw_email_test') else - return File.join(Configuration::raw_emails_location, + return File.join(AlaveteliConfiguration::raw_emails_location, request_id[0..2], request_id) end end diff --git a/app/models/request_classification.rb b/app/models/request_classification.rb index f5a1b4bee..6873d468b 100644 --- a/app/models/request_classification.rb +++ b/app/models/request_classification.rb @@ -1,9 +1,8 @@ # == Schema Information -# Schema version: 20120919140404 # # Table name: request_classifications # -# id :integer not null, primary key +# id :integer not null, primary key # user_id :integer # info_request_event_id :integer # created_at :datetime diff --git a/app/models/request_mailer.rb b/app/models/request_mailer.rb deleted file mode 100644 index 493d6961c..000000000 --- a/app/models/request_mailer.rb +++ /dev/null @@ -1,457 +0,0 @@ -# models/request_mailer.rb: -# Alerts relating to requests. -# -# Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ - -require 'alaveteli_file_types' - -class RequestMailer < ApplicationMailer - - - # Used when an FOI officer uploads a response from their web browser - this is - # the "fake" email used to store in the same format in the database as if they - # had emailed it. - def fake_response(info_request, from_user, body, attachment_name, attachment_content) - @from = from_user.name_and_email - @recipients = info_request.incoming_name_and_email - @body = { - :body => body - } - if !attachment_name.nil? && !attachment_content.nil? - content_type = AlaveteliFileTypes.filename_to_mimetype(attachment_name) || 'application/octet-stream' - - attachment :content_type => content_type, - :body => attachment_content, - :filename => attachment_name - end - end - - # Used when a response is uploaded using the API - def external_response(info_request, body, sent_at, attachments) - @from = blackhole_email - @recipients = info_request.incoming_name_and_email - @body = { :body => body } - - # ActionMailer only works properly when the time is in the local timezone: - # see https://rails.lighthouseapp.com/projects/8994/tickets/3113-actionmailer-only-works-correctly-with-sent_on-times-that-are-in-the-local-time-zone - @sent_on = sent_at.dup.localtime - - attachments.each do |attachment_hash| - attachment attachment_hash - end - end - - # Incoming message arrived for a request, but new responses have been stopped. - def stopped_responses(info_request, email, raw_email_data) - @from = contact_from_name_and_email - headers 'Return-Path' => blackhole_email, 'Reply-To' => @from, # we don't care about bounces, likely from spammers - 'Auto-Submitted' => 'auto-replied' # http://tools.ietf.org/html/rfc3834 - @recipients = email.from_addrs[0].to_s - @subject = _("Your response to an FOI request was not delivered") - attachment :content_type => 'message/rfc822', :body => raw_email_data, - :filename => "original.eml", :transfer_encoding => '7bit', :content_disposition => 'inline' - @body = { - :info_request => info_request, - :contact_email => Configuration::contact_email - } - end - - # An FOI response is outside the scope of the system, and needs admin attention - def requires_admin(info_request, set_by = nil) - if !set_by.nil? - user = set_by - else - user = info_request.user - end - @from = user.name_and_email - @recipients = contact_from_name_and_email - @subject = _("FOI response requires admin ({{reason}}) - {{title}}", :reason => info_request.described_state, :title => info_request.title) - url = main_url(request_url(info_request)) - admin_url = request_admin_url(info_request) - @body = {:reported_by => user, :info_request => info_request, :url => url, :admin_url => admin_url } - end - - # Tell the requester that a new response has arrived - def new_response(info_request, incoming_message) - # Don't use login link here, just send actual URL. This is - # because people tend to forward these emails amongst themselves. - url = main_url(incoming_message_url(incoming_message)) - - @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 - 'X-Auto-Response-Suppress' => 'OOF' - @recipients = info_request.user.name_and_email - @subject = _("New response to your FOI request - ") + info_request.title - @body = { :incoming_message => incoming_message, :info_request => info_request, :url => url } - end - - # Tell the requester that the public body is late in replying - def 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 - 'X-Auto-Response-Suppress' => 'OOF' - @recipients = user.name_and_email - @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 - 'X-Auto-Response-Suppress' => 'OOF' - @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 - - # Tell the requester that they need to say if the new response - # contains info or not - def new_response_reminder_alert(info_request, incoming_message) - # Make a link going to the form to describe state, and which logs the - # user in. - post_redirect = PostRedirect.new( - :uri => main_url(request_url(info_request)) + "#describe_state_form_1", - :user_id => info_request.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 - 'X-Auto-Response-Suppress' => 'OOF' - @recipients = info_request.user.name_and_email - @subject = _("Was the response you got to your FOI request any good?") - @body = { :incoming_message => incoming_message, :info_request => info_request, :url => url } - end - - # Tell the requester that someone updated their old unclassified request - def old_unclassified_updated(info_request) - @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 - 'X-Auto-Response-Suppress' => 'OOF' - @recipients = info_request.user.name_and_email - @subject = _("Someone has updated the status of your request") - url = main_url(request_url(info_request)) - @body = {:info_request => info_request, :url => url} - end - - # Tell the requester that they need to clarify their request - def not_clarified_alert(info_request, incoming_message) - respond_url = show_response_url(:id => info_request.id, :incoming_message_id => incoming_message.id) - respond_url = respond_url + "#followup" - - post_redirect = PostRedirect.new( - :uri => respond_url, - :user_id => info_request.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 - 'X-Auto-Response-Suppress' => 'OOF' - @recipients = info_request.user.name_and_email - @subject = _("Clarify your FOI request - ") + info_request.title - @body = { :incoming_message => incoming_message, :info_request => info_request, :url => url } - end - - # Tell requester that somebody add an annotation to their request - def comment_on_alert(info_request, comment) - @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 - 'X-Auto-Response-Suppress' => 'OOF' - @recipients = info_request.user.name_and_email - @subject = _("Somebody added a note to your FOI request - ") + info_request.title - @body = { :comment => comment, :info_request => info_request, :url => main_url(comment_url(comment)) } - end - def comment_on_alert_plural(info_request, count, earliest_unalerted_comment) - @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 - 'X-Auto-Response-Suppress' => 'OOF' - @recipients = info_request.user.name_and_email - @subject = _("Some notes have been added to your FOI request - ") + info_request.title - @body = { :count => count, :info_request => info_request, :url => main_url(comment_url(earliest_unalerted_comment)) } - end - - # Class function, called by script/mailin with all incoming responses. - # [ This is a copy (Monkeypatch!) of function from action_mailer/base.rb, - # but which additionally passes the raw_email to the member function, as we - # want to record it. - # - # That is because we want to be sure we properly record the actual message - # received in its raw form - so any information won't be lost in a round - # trip via the mail handler, or by bugs in it, and so we can use something - # other than TMail at a later date. And so we can offer an option to download the - # actual original mail sent by the authority in the admin interface (so - # can check that attachment decoding failures are problems in the message, - # not in our code). ] - def self.receive(raw_email) - logger.info "Received mail:\n #{raw_email}" unless logger.nil? - mail = MailHandler.mail_from_raw_email(raw_email) - new.receive(mail, raw_email) - end - - # Find which info requests the email is for - def requests_matching_email(email) - # We deliberately don't use Envelope-to here, so ones that are BCC - # drop into the holding pen for checking. - reply_info_requests = [] # XXX should be set? - for address in (email.to || []) + (email.cc || []) - reply_info_request = InfoRequest.find_by_incoming_email(address) - reply_info_requests.push(reply_info_request) if reply_info_request - end - return reply_info_requests - end - - # Member function, called on the new class made in self.receive above - def receive(email, raw_email) - # Find which info requests the email is for - reply_info_requests = self.requests_matching_email(email) - # Nothing found, so save in holding pen - if reply_info_requests.size == 0 - reason = _("Could not identify the request from the email address") - request = InfoRequest.holding_pen_request - request.receive(email, raw_email, false, reason) - return - end - - # Send the message to each request, to be archived with it - for reply_info_request in reply_info_requests - # If environment variable STOP_DUPLICATES is set, don't send message with same id again - if ENV['STOP_DUPLICATES'] - if reply_info_request.already_received?(email, raw_email) - raise "message " + email.message_id + " already received by request" - end - end - reply_info_request.receive(email, raw_email) - end - end - - # Send email alerts for overdue requests - def self.alert_overdue_requests() - info_requests = InfoRequest.find(:all, - :conditions => [ - "described_state = 'waiting_response' - AND awaiting_description = ? - AND user_id is not null - AND (SELECT id - FROM user_info_request_sent_alerts - WHERE alert_type = 'very_overdue_1' - AND info_request_id = info_requests.id - AND user_id = info_requests.user_id - AND info_request_event_id = (SELECT max(id) - FROM info_request_events - WHERE event_type in ('sent', - 'followup_sent', - 'resent', - 'followup_resent') - AND info_request_id = info_requests.id) - ) IS NULL", false - ], - :include => [ :user ] - ) - for info_request in info_requests - alert_event_id = info_request.last_event_forming_initial_request.id - # Only overdue requests - calculated_status = info_request.calculate_status - if ['waiting_response_overdue', 'waiting_response_very_overdue'].include?(calculated_status) - if calculated_status == 'waiting_response_overdue' - alert_type = 'overdue_1' - elsif calculated_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 = ? - 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 - store_sent = UserInfoRequestSentAlert.new - store_sent.info_request = info_request - store_sent.user = info_request.user - 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? - if calculated_status == 'waiting_response_overdue' - RequestMailer.deliver_overdue_alert(info_request, info_request.user) - elsif calculated_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! - end - end - end - end - - # Send email alerts for new responses which haven't been classified. By default, - # it goes out 3 days after last update of event, then after 10, then after 24. - def self.alert_new_response_reminders - Configuration::new_response_reminder_after_days.each_with_index do |days, i| - self.alert_new_response_reminders_internal(days, "new_response_reminder_#{i+1}") - end - end - def self.alert_new_response_reminders_internal(days_since, type_code) - info_requests = InfoRequest.find_old_unclassified(:order => 'info_requests.id', - :include => [:user], - :age_in_days => days_since) - - for info_request in info_requests - alert_event_id = info_request.get_last_response_event_id - last_response_message = info_request.get_last_response - if alert_event_id.nil? - raise "internal error, no last response while making alert new response reminder, request id " + info_request.id.to_s - end - # To the user who created the request - sent_already = UserInfoRequestSentAlert.find(:first, :conditions => [ "alert_type = ? and user_id = ? and info_request_id = ? and info_request_event_id = ?", type_code, info_request.user_id, info_request.id, alert_event_id]) - if sent_already.nil? - # Alert not yet sent for this user - store_sent = UserInfoRequestSentAlert.new - store_sent.info_request = info_request - store_sent.user = info_request.user - store_sent.alert_type = type_code - store_sent.info_request_event_id = alert_event_id - # XXX uses same template for reminder 1 and reminder 2 right now. - RequestMailer.deliver_new_response_reminder_alert(info_request, last_response_message) - store_sent.save! - end - end - end - - # Send email alerts for requests which need clarification. Goes out 3 days - # after last update of event. - def self.alert_not_clarified_request() - info_requests = InfoRequest.find(:all, :conditions => [ "awaiting_description = ? and described_state = 'waiting_clarification' and info_requests.updated_at < ?", false, Time.now() - 3.days ], :include => [ :user ], :order => "info_requests.id" ) - for info_request in info_requests - alert_event_id = info_request.get_last_response_event_id - last_response_message = info_request.get_last_response - if alert_event_id.nil? - raise "internal error, no last response while making alert not clarified reminder, request id " + info_request.id.to_s - end - # To the user who created the request - sent_already = UserInfoRequestSentAlert.find(:first, :conditions => [ "alert_type = 'not_clarified_1' and user_id = ? and info_request_id = ? and info_request_event_id = ?", info_request.user_id, info_request.id, alert_event_id]) - if sent_already.nil? - # Alert not yet sent for this user - store_sent = UserInfoRequestSentAlert.new - store_sent.info_request = info_request - store_sent.user = info_request.user - store_sent.alert_type = 'not_clarified_1' - 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_not_clarified_alert(info_request, last_response_message) - end - store_sent.save! - end - end - end - - # Send email alert to request submitter for new comments on the request. - def self.alert_comment_on_request() - - # We only check comments made in the last month - this means if the - # cron jobs broke for more than a month events would be lost, but no - # matter. I suspect the performance gain will be needed (with an index on updated_at) - - # XXX the :order part info_request_events.created_at is a work around - # for a very old Rails bug which means eager loading does not respect - # association orders. - # http://dev.rubyonrails.org/ticket/3438 - # http://lists.rubyonrails.org/pipermail/rails-core/2006-July/001798.html - # That that patch has not been applied, despite bribes of beer, is - # typical of the lack of quality of Rails. - - info_requests = InfoRequest.find(:all, - :conditions => [ - "info_requests.id in ( - select info_request_id - from info_request_events - where event_type = 'comment' - and created_at > (now() - '1 month'::interval) - )" - ], - :include => [ { :info_request_events => :user_info_request_sent_alerts } ], - :order => "info_requests.id, info_request_events.created_at" - ) - for info_request in info_requests - - next if info_request.is_external? - # Count number of new comments to alert on - earliest_unalerted_comment_event = nil - last_comment_event = nil - count = 0 - for e in info_request.info_request_events.reverse - # alert on comments, which were not made by the user who originally made the request - if e.event_type == 'comment' && e.comment.user_id != info_request.user_id - last_comment_event = e if last_comment_event.nil? - - alerted_for = e.user_info_request_sent_alerts.find(:first, :conditions => [ "alert_type = 'comment_1' and user_id = ?", info_request.user_id]) - if alerted_for.nil? - count = count + 1 - earliest_unalerted_comment_event = e - else - break - end - end - end - - # Alert needs sending if there are new comments - if count > 0 - store_sent = UserInfoRequestSentAlert.new - store_sent.info_request = info_request - store_sent.user = info_request.user - store_sent.alert_type = 'comment_1' - store_sent.info_request_event_id = last_comment_event.id - if count > 1 - RequestMailer.deliver_comment_on_alert_plural(info_request, count, earliest_unalerted_comment_event.comment) - elsif count == 1 - RequestMailer.deliver_comment_on_alert(info_request, last_comment_event.comment) - else - raise "internal error" - end - store_sent.save! - end - end - end - - -end - - diff --git a/app/models/track_mailer.rb b/app/models/track_mailer.rb deleted file mode 100644 index 7dfa87f52..000000000 --- a/app/models/track_mailer.rb +++ /dev/null @@ -1,135 +0,0 @@ -# models/track_mailer.rb: -# Emails which go to users who are tracking things. -# -# Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ - -class TrackMailer < ApplicationMailer - def event_digest(user, email_about_things) - post_redirect = PostRedirect.new( - :uri => main_url(user_url(user)) + "#email_subscriptions", - :user_id => user.id) - post_redirect.save! - unsubscribe_url = confirm_url(:email_token => post_redirect.email_token) - - @from = contact_from_name_and_email - headers 'Auto-Submitted' => 'auto-generated', # http://tools.ietf.org/html/rfc3834 - 'Precedence' => 'bulk' # http://www.vbulletin.com/forum/project.php?issueid=27687 (Exchange hack) - # 'Return-Path' => blackhole_email, 'Reply-To' => @from # we don't care about bounces for tracks - # (We let it return bounces for now, so we can manually kill the tracks that bounce so Yahoo - # etc. don't decide we are spammers.) - - @recipients = user.name_and_email - @subject = _("Your {{site_name}} email alert", :site_name => site_name) - @body = { :user => user, :email_about_things => email_about_things, :unsubscribe_url => unsubscribe_url } - end - - def contact_from_name_and_email - "#{Configuration::track_sender_name} <#{Configuration::track_sender_email}>" - end - - # Send email alerts for tracked things. Never more than one email - # a day, nor about events which are more than a week old, nor - # events about which emails have been sent within the last two - # weeks. - - # Useful query to run by hand to see how many alerts are due: - # User.find(:all, :conditions => [ "last_daily_track_email < ?", Time.now - 1.day ]).size - def self.alert_tracks - done_something = false - now = Time.now() - users = User.find(:all, :conditions => [ "last_daily_track_email < ?", now - 1.day ]) - if users.empty? - return done_something - end - for user in users - next if !user.should_be_emailed? || !user.receive_email_alerts - - 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 - # 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 = {} - 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 - end - - # 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, 100, 1) - # Go through looking for unalerted things - alert_results = [] - for result in xapian_object.results - 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 - email_about_things.push([track_thing, alert_results, xapian_object]) - end - end - - # If we have anything to send, then send everything for the user in one mail - if email_about_things.size > 0 - # Send the email - - previous_locale = I18n.locale - I18n.locale = user.get_locale - TrackMailer.deliver_event_digest(user, email_about_things) - I18n.locale = previous_locale - end - - # Record that we've now sent those alerts to that user - for track_thing, alert_results in email_about_things - for result in alert_results - track_things_sent_email = TrackThingsSentEmail.new - track_things_sent_email.track_thing_id = track_thing.id - if result[:model].class.to_s == "InfoRequestEvent" - track_things_sent_email.info_request_event_id = result[:model].id - else - raise "need to add other types to TrackMailer.alert_tracks (mark alerted)" - end - track_things_sent_email.save! - end - end - user.last_daily_track_email = now - user.no_xapian_reindex = true - user.save! - done_something = true - end - return done_something - end - - def self.alert_tracks_loop - # Run alert_tracks in an endless loop, sleeping when there is nothing to do - while true - sleep_seconds = 1 - while !alert_tracks - sleep sleep_seconds - sleep_seconds *= 2 - sleep_seconds = 300 if sleep_seconds > 300 - end - end - end - -end - - diff --git a/app/models/track_thing.rb b/app/models/track_thing.rb index dfe92b7fe..d5dda7bb5 100644 --- a/app/models/track_thing.rb +++ b/app/models/track_thing.rb @@ -1,16 +1,15 @@ # == Schema Information -# Schema version: 114 # # Table name: track_things # -# id :integer not null, primary key -# tracking_user_id :integer not null -# track_query :string(255) not null +# id :integer not null, primary key +# tracking_user_id :integer not null +# track_query :string(255) not null # info_request_id :integer # tracked_user_id :integer # public_body_id :integer -# track_medium :string(255) not null -# track_type :string(255) default("internal_error"), not null +# track_medium :string(255) not null +# track_type :string(255) default("internal_error"), not null # created_at :datetime # updated_at :datetime # @@ -19,10 +18,12 @@ # When somebody is getting alerts for something. # # Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ require 'set' +# TODO: TrackThing looks like a good candidate for single table inheritance + class TrackThing < ActiveRecord::Base belongs_to :tracking_user, :class_name => 'User' validates_presence_of :track_query @@ -258,7 +259,7 @@ class TrackThing < ActiveRecord::Base :title_in_email => self.public_body.law_only_short + " requests to '" + self.public_body.name + "'", :title_in_rss => self.public_body.law_only_short + " requests to '" + self.public_body.name + "'", # Authentication - :web => _("To follow requests made using {{site_name}} to the public authority '{{public_body_name}}'", :site_name=>Configuration::site_name, :public_body_name=>CGI.escapeHTML(self.public_body.name)), + :web => _("To follow requests made using {{site_name}} to the public authority '{{public_body_name}}'", :site_name=>AlaveteliConfiguration::site_name, :public_body_name=>CGI.escapeHTML(self.public_body.name)), :email => _("Then you will be notified whenever someone requests something or gets a response from '{{public_body_name}}'.", :public_body_name=>CGI.escapeHTML(self.public_body.name)), :email_subject => _("Confirm you want to follow requests to '{{public_body_name}}'", :public_body_name=>self.public_body.name), # RSS sorting @@ -310,7 +311,7 @@ class TrackThing < ActiveRecord::Base end # When constructing a new track, use this to avoid duplicates / double posting - def TrackThing.find_by_existing_track(tracking_user, track) + def TrackThing.find_existing(tracking_user, track) if tracking_user.nil? return nil end diff --git a/app/models/track_things_sent_email.rb b/app/models/track_things_sent_email.rb index a0a4c0f0c..072d3bdea 100644 --- a/app/models/track_things_sent_email.rb +++ b/app/models/track_things_sent_email.rb @@ -1,10 +1,9 @@ # == Schema Information -# Schema version: 114 # # Table name: track_things_sent_emails # -# id :integer not null, primary key -# track_thing_id :integer not null +# id :integer not null, primary key +# track_thing_id :integer not null # info_request_event_id :integer # user_id :integer # public_body_id :integer @@ -16,7 +15,7 @@ # Record that alert has arrived. # # Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class TrackThingsSentEmail < ActiveRecord::Base belongs_to :info_request_event diff --git a/app/models/user.rb b/app/models/user.rb index e6c666e47..e63ce8129 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,26 +1,27 @@ # == Schema Information -# Schema version: 20120919140404 +# Schema version: 20131024114346 # # Table name: users # -# id :integer not null, primary key -# email :string(255) not null -# name :string(255) not null -# hashed_password :string(255) not null -# salt :string(255) not null -# created_at :datetime not null -# updated_at :datetime not null -# email_confirmed :boolean default(FALSE), not null -# url_name :text not null -# last_daily_track_email :datetime default(Sat Jan 01 00:00:00 UTC 2000) -# admin_level :string(255) default("none"), not null -# ban_text :text default(""), not null -# about_me :text default(""), not null +# id :integer not null, primary key +# email :string(255) not null +# name :string(255) not null +# hashed_password :string(255) not null +# salt :string(255) not null +# created_at :datetime not null +# updated_at :datetime not null +# email_confirmed :boolean default(FALSE), not null +# url_name :text not null +# last_daily_track_email :datetime default(2000-01-01 00:00:00 UTC) +# admin_level :string(255) default("none"), not null +# ban_text :text default(""), not null +# about_me :text default(""), not null # locale :string(255) # email_bounced_at :datetime -# email_bounce_message :text default(""), not null -# no_limit :boolean default(FALSE), not null -# receive_email_alerts :boolean default(TRUE), not null +# email_bounce_message :text default(""), not null +# no_limit :boolean default(FALSE), not null +# receive_email_alerts :boolean default(TRUE), not null +# can_make_batch_requests :boolean default(FALSE), not null # require 'digest/sha1' @@ -41,6 +42,7 @@ class User < ActiveRecord::Base has_many :comments, :order => 'created_at desc' has_one :profile_photo has_many :censor_rules, :order => 'created_at desc' + has_many :info_request_batches, :order => 'created_at desc' attr_accessor :password_confirmation, :no_xapian_reindex validates_confirmation_of :password, :message => _("Please enter the same password twice") @@ -58,6 +60,9 @@ class User < ActiveRecord::Base ], :terms => [ [ :variety, 'V', "variety" ] ], :if => :indexed_by_search? + + after_initialize :set_defaults + 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") @@ -67,17 +72,6 @@ class User < ActiveRecord::Base "user" end - def after_initialize - if self.admin_level.nil? - self.admin_level = 'none' - end - if self.new_record? - # make alert emails go out at a random time for each new user, so - # overall they are spread out throughout the day. - self.last_daily_track_email = User.random_time_in_last_day - end - end - # requested_by: and commented_by: search queries also need updating after save after_update :reindex_referencing_models def reindex_referencing_models @@ -98,12 +92,7 @@ class User < ActiveRecord::Base end def get_locale - if !self.locale.nil? - locale = self.locale - else - locale = I18n.locale - end - return locale.to_s + (self.locale || I18n.locale).to_s end def visible_comments @@ -119,7 +108,12 @@ class User < ActiveRecord::Base name.strip! end if self.public_banned? - name = _("{{user_name}} (Account suspended)", :user_name=>name) + # Use interpolation to return a string rather than a SafeBuffer so that + # gsub can be called on it until we upgrade to Rails 3.2. The name returned + # is not marked as HTML safe so will be escaped automatically in views. We + # do this in two steps so the string still gets picked up for translation + name = _("{{user_name}} (Account suspended)", :user_name=> name.html_safe) + name = "#{name}" end name end @@ -141,14 +135,14 @@ class User < ActiveRecord::Base if user # There is user with email, check password if !user.has_this_password?(params[:password]) - user.errors.add_to_base(auth_fail_message) + user.errors.add(:base, auth_fail_message) end else # No user of same email, make one (that we don't save in the database) # for the forms code to use. user = User.new(params) # deliberately same message as above so as not to leak whether registered - user.errors.add_to_base(auth_fail_message) + user.errors.add(:base, auth_fail_message) end user end @@ -201,12 +195,12 @@ class User < ActiveRecord::Base # The "internal admin" is a special user for internal use. def User.internal_admin_user - u = User.find_by_email(Configuration::contact_email) + u = User.find_by_email(AlaveteliConfiguration::contact_email) if u.nil? password = PostRedirect.generate_random_token u = User.new( :name => 'Internal admin user', - :email => Configuration::contact_email, + :email => AlaveteliConfiguration::contact_email, :password => password, :password_confirmation => password ) @@ -251,8 +245,8 @@ class User < ActiveRecord::Base !user.nil? && user.owns_every_request? end - # Can the user see every request, even hidden ones? - def User.view_hidden_requests?(user) + # Can the user see every request, response, and outgoing message, even hidden ones? + def User.view_hidden?(user) !user.nil? && user.super? end @@ -278,17 +272,20 @@ class User < ActiveRecord::Base # Some users have no limit return false if self.no_limit + # Batch request users don't have a limit + return false if self.can_make_batch_requests? + # Has the user issued as many as MAX_REQUESTS_PER_USER_PER_DAY requests in the past 24 hours? - return false if Configuration::max_requests_per_user_per_day.blank? + return false if AlaveteliConfiguration::max_requests_per_user_per_day.blank? recent_requests = InfoRequest.count(:conditions => ["user_id = ? and created_at > now() - '1 day'::interval", self.id]) - return (recent_requests >= Configuration::max_requests_per_user_per_day) + return (recent_requests >= AlaveteliConfiguration::max_requests_per_user_per_day) end def next_request_permitted_at return nil if self.no_limit - n_most_recent_requests = InfoRequest.all(:conditions => ["user_id = ? and created_at > now() - '1 day'::interval", self.id], :order => "created_at DESC", :limit => Configuration::max_requests_per_user_per_day) - return nil if n_most_recent_requests.size < Configuration::max_requests_per_user_per_day + n_most_recent_requests = InfoRequest.all(:conditions => ["user_id = ? and created_at > now() - '1 day'::interval", self.id], :order => "created_at DESC", :limit => AlaveteliConfiguration::max_requests_per_user_per_day) + return nil if n_most_recent_requests.size < AlaveteliConfiguration::max_requests_per_user_per_day nth_most_recent_request = n_most_recent_requests[-1] return nth_most_recent_request.created_at + 1.day @@ -311,7 +308,7 @@ class User < ActiveRecord::Base text = CGI.escapeHTML(text) text = MySociety::Format.make_clickable(text, :contract => 1) text = text.gsub(/\n/, '<br>') - return text + return text.html_safe end # Returns domain part of user's email address @@ -407,6 +404,17 @@ class User < ActiveRecord::Base self.salt = self.object_id.to_s + rand.to_s end + def set_defaults + if self.admin_level.nil? + self.admin_level = 'none' + end + if self.new_record? + # make alert emails go out at a random time for each new user, so + # overall they are spread out throughout the day. + self.last_daily_track_email = User.random_time_in_last_day + end + end + def email_and_name_are_valid if self.email != "" && !MySociety::Validate.is_valid_email(self.email) errors.add(:email, _("Please enter a valid email address")) diff --git a/app/models/user_info_request_sent_alert.rb b/app/models/user_info_request_sent_alert.rb index cf20bcbf5..098b773f8 100644 --- a/app/models/user_info_request_sent_alert.rb +++ b/app/models/user_info_request_sent_alert.rb @@ -1,12 +1,11 @@ # == Schema Information -# Schema version: 114 # # Table name: user_info_request_sent_alerts # -# id :integer not null, primary key -# user_id :integer not null -# info_request_id :integer not null -# alert_type :string(255) not null +# id :integer not null, primary key +# user_id :integer not null +# info_request_id :integer not null +# alert_type :string(255) not null # info_request_event_id :integer # @@ -15,7 +14,7 @@ # given type of alert. # # Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class UserInfoRequestSentAlert < ActiveRecord::Base belongs_to :user diff --git a/app/models/user_mailer.rb b/app/models/user_mailer.rb deleted file mode 100644 index 1be4f8aa3..000000000 --- a/app/models/user_mailer.rb +++ /dev/null @@ -1,48 +0,0 @@ -# models/user_mailer.rb: -# Emails relating to user accounts. e.g. Confirming a new account -# -# Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved. -# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ - -class UserMailer < ApplicationMailer - def confirm_login(user, reasons, 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 = user.name_and_email - @subject = reasons[:email_subject] - @body[:reasons] = reasons - @body[:name] = user.name - @body[:url] = url - end - - def already_registered(user, reasons, 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 = user.name_and_email - @subject = reasons[:email_subject] - @body[:reasons] = reasons - @body[:name] = user.name - @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 {{site_name}}", :site_name=>site_name) - @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 {{site_name}}", :site_name=>site_name) - @body[:old_email] = old_email - @body[:new_email] = new_email - end -end - |