diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/application_mailer.rb | 4 | ||||
-rw-r--r-- | app/models/exim_log.rb | 6 | ||||
-rw-r--r-- | app/models/incoming_message.rb | 54 | ||||
-rw-r--r-- | app/models/info_request.rb | 21 | ||||
-rw-r--r-- | app/models/info_request_event.rb | 81 | ||||
-rw-r--r-- | app/models/public_body.rb | 177 | ||||
-rw-r--r-- | app/models/request_mailer.rb | 48 | ||||
-rw-r--r-- | app/models/track_mailer.rb | 18 | ||||
-rw-r--r-- | app/models/track_thing.rb | 107 | ||||
-rw-r--r-- | app/models/user.rb | 39 | ||||
-rw-r--r-- | app/models/user_mailer.rb | 2 |
11 files changed, 352 insertions, 205 deletions
diff --git a/app/models/application_mailer.rb b/app/models/application_mailer.rb index 9628d7339..e9f82a2c3 100644 --- a/app/models/application_mailer.rb +++ b/app/models/application_mailer.rb @@ -15,8 +15,8 @@ class ApplicationMailer < ActionMailer::Base self.raise_delivery_errors = true def contact_from_name_and_email - contact_name = MySociety::Config.get("CONTACT_NAME", 'contact@localhost') - contact_email = MySociety::Config.get("CONTACT_EMAIL", 'Alaveteli') + contact_name = MySociety::Config.get("CONTACT_NAME", 'Alaveteli') + contact_email = MySociety::Config.get("CONTACT_EMAIL", 'contact@localhost') return "#{contact_name} <#{contact_email}>" end diff --git a/app/models/exim_log.rb b/app/models/exim_log.rb index 80535ab41..83f031a92 100644 --- a/app/models/exim_log.rb +++ b/app/models/exim_log.rb @@ -108,10 +108,10 @@ class EximLog < ActiveRecord::Base # be sure we are parsing the exim line right) envelope_from = " from <" + ir.incoming_email + "> " if !exim_log.line.include?(envelope_from) - raise "unexpected parsing of exim line" + $stderr.puts("unexpected parsing of exim line: [#{exim_log.line.chomp}]") + else + found = true end - - found = true end end if !found diff --git a/app/models/incoming_message.rb b/app/models/incoming_message.rb index 16ae38b92..2b795ddf5 100644 --- a/app/models/incoming_message.rb +++ b/app/models/incoming_message.rb @@ -29,7 +29,6 @@ # general not specific to IncomingMessage. require 'alaveteli_file_types' -require 'external_command' require 'htmlentities' require 'rexml/document' require 'zip/zip' @@ -1121,38 +1120,38 @@ class IncomingMessage < ActiveRecord::Base tempfile.print body tempfile.flush if content_type == 'application/vnd.ms-word' - external_command("/usr/bin/wvText", tempfile.path, tempfile.path + ".txt") + AlaveteliExternalCommand.run("/usr/bin/wvText", tempfile.path, tempfile.path + ".txt") # Try catdoc if we get into trouble (e.g. for InfoRequestEvent 2701) if not File.exists?(tempfile.path + ".txt") - external_command("/usr/bin/catdoc", tempfile.path, :append_to => text) + AlaveteliExternalCommand.run("/usr/bin/catdoc", tempfile.path, :append_to => text) else text += File.read(tempfile.path + ".txt") + "\n\n" File.unlink(tempfile.path + ".txt") end elsif content_type == 'application/rtf' # catdoc on RTF prodcues less comments and extra bumf than --text option to unrtf - external_command("/usr/bin/catdoc", tempfile.path, :append_to => text) + AlaveteliExternalCommand.run("/usr/bin/catdoc", tempfile.path, :append_to => text) elsif content_type == 'text/html' # lynx wordwraps links in its output, which then don't get formatted properly # by Alaveteli. We use elinks instead, which doesn't do that. - external_command("/usr/bin/elinks", "-eval", "'set document.codepage.assume = \"utf-8\"'", "-dump-charset", "utf-8", "-force-html", "-dump", + AlaveteliExternalCommand.run("/usr/bin/elinks", "-eval", "'set document.codepage.assume = \"utf-8\"'", "-dump-charset", "utf-8", "-force-html", "-dump", tempfile.path, :append_to => text) elsif content_type == 'application/vnd.ms-excel' # Bit crazy using /usr/bin/strings - but xls2csv, xlhtml and # py_xls2txt only extract text from cells, not from floating # notes. catdoc may be fooled by weird character sets, but will # probably do for UK FOI requests. - external_command("/usr/bin/strings", tempfile.path, :append_to => text) + AlaveteliExternalCommand.run("/usr/bin/strings", tempfile.path, :append_to => text) elsif content_type == 'application/vnd.ms-powerpoint' # ppthtml seems to catch more text, but only outputs HTML when # we want text, so just use catppt for now - external_command("/usr/bin/catppt", tempfile.path, :append_to => text) + AlaveteliExternalCommand.run("/usr/bin/catppt", tempfile.path, :append_to => text) elsif content_type == 'application/pdf' - external_command("/usr/bin/pdftotext", tempfile.path, "-", :append_to => text) + AlaveteliExternalCommand.run("/usr/bin/pdftotext", tempfile.path, "-", :append_to => text) elsif content_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' # This is Microsoft's XML office document format. # Just pull out the main XML file, and strip it of text. - xml = external_command("/usr/bin/unzip", "-qq", "-c", tempfile.path, "word/document.xml") + xml = AlaveteliExternalCommand.run("/usr/bin/unzip", "-qq", "-c", tempfile.path, "word/document.xml") if !xml.nil? doc = REXML::Document.new(xml) text += doc.each_element( './/text()' ){}.join(" ") @@ -1305,10 +1304,15 @@ class IncomingMessage < ActiveRecord::Base prefix = email prefix =~ /^(.*)@/ prefix = $1 - if !prefix.nil? && prefix.downcase.match(/^(postmaster|mailer-daemon|auto_reply|donotreply|no.reply)$/) + if !prefix.nil? && prefix.downcase.match(/^(postmaster|mailer-daemon|auto_reply|do.?not.?reply|no.reply)$/) + return false + end + if !self.mail['return-path'].nil? && self.mail['return-path'].addr == "<>" + return false + end + if !self.mail['auto-submitted'].nil? && !self.mail['auto-submitted'].keys.empty? return false end - return true end @@ -1336,34 +1340,6 @@ class IncomingMessage < ActiveRecord::Base end private :normalise_content_type - def self.external_command(program_name, *args) - # Run an external program, and return its output. - # Standard error is suppressed unless the program - # fails (i.e. returns a non-zero exit status). - opts = {} - if !args.empty? && args[-1].is_a?(Hash) - opts = args.pop - end - - xc = ExternalCommand.new(program_name, *args) - if opts.has_key? :append_to - xc.out = opts[:append_to] - end - xc.run() - if xc.status != 0 - # Error - $stderr.puts("Error from #{program_name} #{args.join(' ')}:") - $stderr.print(xc.err) - return nil - else - if opts.has_key? :append_to - opts[:append_to] << "\n\n" - else - return xc.out - end - end - end - private_class_method :external_command end diff --git a/app/models/info_request.rb b/app/models/info_request.rb index c667e1499..92322f74f 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -117,13 +117,13 @@ class InfoRequest < ActiveRecord::Base # only check on create, so existing models with mixed case are allowed def validate_on_create if !self.title.nil? && !MySociety::Validate.uses_mixed_capitals(self.title, 10) - errors.add(:title, N_('Please write the summary using a mixture of capital and lower case letters. This makes it easier for others to read.')) + 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, N_('Please keep the summary short, like in the subject of an email. You can use a phrase, rather than a full sentence.')) + 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, N_('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.')) + 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 @@ -451,7 +451,7 @@ public 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) end @@ -564,6 +564,7 @@ public def calculate_event_states curr_state = nil for event in self.info_request_events.reverse + event.xapian_mark_needs_index # we need to reindex all events in order to update their latest_* terms if curr_state.nil? if !event.described_state.nil? curr_state = event.described_state @@ -779,8 +780,7 @@ public # Display version of status - def display_status - status = self.calculate_status + def InfoRequest.get_status_description(status) if status == 'waiting_classification' _("Awaiting classification.") elsif status == 'waiting_response' @@ -818,6 +818,10 @@ public end end + def display_status + InfoRequest.get_status_description(self.calculate_status) + end + # Completely delete this request and all objects depending on it def fully_destroy self.track_things.each do |track_thing| @@ -918,10 +922,13 @@ public end # List of incoming messages to followup, by unique email - def who_can_followup_to + def who_can_followup_to(skip_message = nil) ret = [] done = {} for incoming_message in self.incoming_messages.reverse + if incoming_message == skip_message + next + end incoming_message.safe_mail_from email = OutgoingMailer.email_for_followup(self, incoming_message) diff --git a/app/models/info_request_event.rb b/app/models/info_request_event.rb index d79647c98..4ea89bf81 100644 --- a/app/models/info_request_event.rb +++ b/app/models/info_request_event.rb @@ -57,22 +57,7 @@ class InfoRequestEvent < ActiveRecord::Base ] # user described state (also update in info_request) - validates_inclusion_of :described_state, :in => [ - nil, - 'waiting_response', - 'waiting_clarification', - 'gone_postal', - 'deadline_extended', - 'wrong_response', - 'not_held', - 'rejected', - 'successful', - 'partially_successful', - 'internal_review', - 'error_message', - 'requires_admin', - 'user_withdrawn' - ] + validate :must_be_valid_state # whether event is publicly visible validates_inclusion_of :prominence, :in => [ @@ -81,6 +66,12 @@ class InfoRequestEvent < ActiveRecord::Base '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" @@ -103,7 +94,7 @@ class InfoRequestEvent < ActiveRecord::Base [ :created_at_numeric, 1, "created_at", :number ], # for sorting [ :described_at_numeric, 2, "described_at", :number ], # XXX using :number for lack of :datetime support in Xapian values [ :request, 3, "request_collapse", :string ], - [ :request_title_collapse, 4, "request_title_collapse", :string ] + [ :request_title_collapse, 4, "request_title_collapse", :string ], ], :terms => [ [ :calculated_state, 'S', "status" ], [ :requested_by, 'B', "requested_by" ], @@ -111,6 +102,9 @@ class InfoRequestEvent < ActiveRecord::Base [ :commented_by, 'C', "commented_by" ], [ :request, 'R', "request" ], [ :variety, 'V', "variety" ], + [ :latest_variety, 'K', "latest_variety" ], + [ :latest_status, 'L', "latest_status" ], + [ :waiting_classification, 'W', "waiting_classification" ], [ :filetype, 'T', "filetype" ], [ :tags, 'U', "tag" ] ], @@ -138,6 +132,27 @@ class InfoRequestEvent < ActiveRecord::Base def request self.info_request.url_title end + + def latest_variety + for event in self.info_request.info_request_events.reverse + if !event.variety.nil? and !event.variety.empty? + return event.variety + end + end + end + + def latest_status + for event in self.info_request.info_request_events.reverse + if !event.calculated_state.nil? and !event.calculated_state.empty? + return event.calculated_state + end + end + end + + def waiting_classification + self.info_request.awaiting_description == true ? "yes" : "no" + end + def request_title_collapse url_title = self.info_request.url_title # remove numeric section from the end, use this to group lots @@ -288,37 +303,7 @@ class InfoRequestEvent < ActiveRecord::Base def display_status if is_incoming_message? status = self.calculated_state - if !status.nil? - if status == 'waiting_response' - return _("Acknowledgement") - elsif status == 'waiting_clarification' - return _("Clarification required") - elsif status == 'gone_postal' - return _("Handled by post") - elsif status == 'deadline_extended' - return _("Deadline Extended") - elsif status == 'wrong_response' - return _("Wrong Response") - elsif status == 'not_held' - return _("Information not held") - elsif status == 'rejected' - return _("Refused") - elsif status == 'partially_successful' - return _("Some information sent") - elsif status == 'successful' - return _("All information sent") - elsif status == 'internal_review' - return _("Internal review acknowledgement") - elsif status == 'user_withdrawn' - return _("Withdrawn by requester") - elsif status == 'error_message' - return _("Delivery error") - elsif status == 'requires_admin' - return _("Unusual response") - end - raise "unknown status " + status - end - return "Response" + return status.nil? ? _("Response") : InfoRequest.get_status_description(status) end if is_outgoing_message? diff --git a/app/models/public_body.rb b/app/models/public_body.rb index b75da4331..ab836657b 100644 --- a/app/models/public_body.rb +++ b/app/models/public_body.rb @@ -38,7 +38,7 @@ class PublicBody < ActiveRecord::Base validates_uniqueness_of :short_name, :message => N_("Short name is already taken"), :if => Proc.new { |pb| pb.short_name != "" } validates_uniqueness_of :name, :message => N_("Name is already taken") - + has_many :info_requests, :order => 'created_at desc' has_many :track_things, :order => 'created_at desc' @@ -46,6 +46,40 @@ 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) + 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? + 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 + + def translated_versions + translations + end + + def translated_versions=(translation_attrs) + if translation_attrs.respond_to? :each_value # Hash => updating + translation_attrs.each_value do |attrs| + t = translation(attrs[:locale]) || PublicBody::Translation.new + t.attributes = attrs + calculate_cached_fields(t) + t.save! + end + else # Array => creating + translation_attrs.each do |attrs| + new_translation = PublicBody::Translation.new(attrs) + calculate_cached_fields(new_translation) + translations << new_translation + end + end + end + # Make sure publication_scheme gets the correct default value. # (This would work automatically, were publication_scheme not a translated attribute) def after_initialize @@ -78,16 +112,6 @@ class PublicBody < ActiveRecord::Base end end - # XXX this should be saner; probably implement categories as data - begin - load "public_body_categories_#{I18n.locale.to_s}.rb" - rescue MissingSourceFile - begin - load "public_body_categories_#{I18n.default_locale.to_s}.rb" - rescue MissingSourceFile - load "public_body_categories.rb" - end - end # Set the first letter, which is used for faster queries before_save(:set_first_letter) def set_first_letter @@ -172,7 +196,7 @@ class PublicBody < ActiveRecord::Base return self.created_at.strftime("%Y%m%d%H%M%S") end def variety - "authority" + return "authority" end # if the URL name has changed, then all requested_from: queries @@ -191,7 +215,6 @@ 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) self[:short_name] = short_name self.update_url_name @@ -204,15 +227,15 @@ class PublicBody < ActiveRecord::Base end def update_url_name - url_name = MySociety::Format.simplify_url_part(self.short_or_long_name, 'body') - self.url_name = url_name + self.url_name = MySociety::Format.simplify_url_part(self.short_or_long_name, 'body') end + # Return the short name if present, or else long name def short_or_long_name - if self.short_name.nil? # can happen during construction + if self.short_name.nil? || self.short_name.empty? # 'nil' can happen during construction self.name else - self.short_name.empty? ? self.name : self.short_name + self.short_name end end @@ -222,8 +245,8 @@ class PublicBody < ActiveRecord::Base types = [] first = true for tag in self.tags - if PublicBodyCategories::CATEGORIES_BY_TAG.include?(tag.name) - desc = PublicBodyCategories::CATEGORY_SINGULAR_BY_TAG[tag.name] + if PublicBodyCategories::get().by_tag().include?(tag.name) + desc = PublicBodyCategories::get().singular_by_tag()[tag.name] if first # terrible that Ruby/Rails doesn't have an equivalent of ucfirst # (capitalize shockingly converts later characters to lowercase) @@ -311,9 +334,10 @@ class PublicBody < ActiveRecord::Base # 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). - def self.import_csv(csv, tag, dry_run, editor, additional_locales = []) + def self.import_csv(csv, tag, tag_behaviour, dry_run, editor, available_locales = []) errors = [] notes = [] + available_locales = [I18n.default_locale] if available_locales.empty? begin ActiveRecord::Base.transaction do @@ -323,14 +347,18 @@ class PublicBody < ActiveRecord::Base bodies_by_name = {} set_of_existing = Set.new() PublicBody.with_locale(I18n.default_locale) do - for existing_body in PublicBody.find_by_tag(tag) + bodies = (tag.nil? || tag.empty?) ? PublicBody.find(:all) : PublicBody.find_by_tag(tag) + for existing_body in bodies + # Hide InternalAdminBody from import notes + next if existing_body.id == PublicBody.internal_admin_body.id + bodies_by_name[existing_body.name] = existing_body set_of_existing.add(existing_body.name) end end set_of_importing = Set.new() - field_names = { 'name'=>1, 'email'=>2 } # Default values in case no field list is given + field_names = { 'name'=>1, 'request_email'=>2 } # Default values in case no field list is given line = 0 CSV::Reader.parse(csv) do |row| line = line + 1 @@ -341,57 +369,82 @@ class PublicBody < ActiveRecord::Base row.each_with_index {|field, i| field_names[field] = i} next end + + fields = {} + field_names.each{|name, i| fields[name] = row[i]} name = row[field_names['name']] - email = row[field_names['email']] + email = row[field_names['request_email']] next if name.nil? - if email.nil? - email = '' # unknown/bad contact is empty string - end name.strip! - email.strip! + email.strip! unless email.nil? - if email != "" && !MySociety::Validate.is_valid_email(email) - errors.push "error: line " + line.to_s + ": invalid email " + email + " for authority '" + name + "'" + if !email.nil? && !email.empty? && !MySociety::Validate.is_valid_email(email) + errors.push "error: line #{line.to_s}: invalid email '#{email}' for authority '#{name}'" next end + + field_list = ['name', 'short_name', 'request_email', 'notes', 'publication_scheme', 'home_page', 'tag_string'] + + if public_body = bodies_by_name[name] # Existing public body + available_locales.each do |locale| + PublicBody.with_locale(locale) do + changed = {} + field_list.each do |field_name| + localized_field_name = (locale.to_s == I18n.default_locale.to_s) ? field_name : "#{field_name}.#{locale}" + localized_value = field_names[localized_field_name] && row[field_names[localized_field_name]] + + # Tags are a special case, as we support adding to the field, not just setting a new value + if localized_field_name == 'tag_string' + if localized_value.nil? + localized_value = tag unless tag.empty? + else + if tag_behaviour == 'add' + localized_value = "#{localized_value} #{tag}" unless tag.empty? + localized_value = "#{localized_value} #{public_body.tag_string}" + end + end + end + + if !localized_value.nil? and public_body.send(field_name) != localized_value + changed[field_name] = "#{public_body.send(field_name)}: #{localized_value}" + public_body.send("#{field_name}=", localized_value) + end + end - if bodies_by_name[name] - # Already have the public body, just update email - public_body = bodies_by_name[name] - if public_body.request_email != email - notes.push "line " + line.to_s + ": updating email for '" + name + "' from " + public_body.request_email + " to " + email - public_body.request_email = email - public_body.last_edit_editor = editor - public_body.last_edit_comment = 'Updated from spreadsheet' - public_body.save! - end - - additional_locales.each do |locale| - localized_name = field_names["name.#{locale}"] && row[field_names["name.#{locale}"]] - PublicBody.with_locale(locale) do - if !localized_name.nil? and public_body.name != localized_name - notes.push "line " + line.to_s + ": updating name for '#{name}' from '#{public_body.name}' to '#{localized_name}' (locale: #{locale})." - public_body.name = localized_name + unless changed.empty? + notes.push "line #{line.to_s}: updating authority '#{name}' (locale: #{locale}):\n\t#{changed.to_json}" + public_body.last_edit_editor = editor + public_body.last_edit_comment = 'Updated from spreadsheet' public_body.save! end end end - else - # New public body - notes.push "line " + line.to_s + ": new authority '" + name + "' with email " + email - public_body = PublicBody.new(:name => name, :request_email => email, :short_name => "", :home_page => "", :publication_scheme => "", :notes => "", :last_edit_editor => editor, :last_edit_comment => 'Created from spreadsheet') - public_body.tag_string = tag - public_body.save! - - additional_locales.each do |locale| - localized_name = field_names["name.#{locale}"] && row[field_names["name.#{locale}"]] - if !localized_name.nil? - PublicBody.with_locale(locale) do - notes.push "line " + line.to_s + ": (aka '#{localized_name}' in locale #{locale})" - public_body.name = localized_name - public_body.publication_scheme = "" + else # New public body + public_body = PublicBody.new(:name=>"", :short_name=>"", :request_email=>"") + available_locales.each do |locale| + PublicBody.with_locale(locale) do + changed = {} + field_list.each do |field_name| + localized_field_name = (locale.to_s == I18n.default_locale.to_s) ? field_name : "#{field_name}.#{locale}" + localized_value = field_names[localized_field_name] && row[field_names[localized_field_name]] + + if localized_field_name == 'tag_string' and tag_behaviour == 'add' + localized_value = "#{localized_value} #{tag}" unless tag.empty? + end + + if !localized_value.nil? and public_body.send(field_name) != localized_value + changed[field_name] = localized_value + public_body.send("#{field_name}=", localized_value) + end + end + + unless changed.empty? + notes.push "line #{line.to_s}: creating new authority '#{name}' (locale: #{locale}):\n\t#{changed.to_json}" + public_body.publication_scheme = public_body.publication_scheme || "" + public_body.last_edit_editor = editor + public_body.last_edit_comment = 'Created from spreadsheet' public_body.save! end end @@ -404,7 +457,7 @@ class PublicBody < ActiveRecord::Base # Give an error listing ones that are to be deleted deleted_ones = set_of_existing - set_of_importing if deleted_ones.size > 0 - notes.push "notes: Some " + tag + " bodies are in database, but not in CSV file:\n " + Array(deleted_ones).join("\n ") + "\nYou may want to delete them manually.\n" + notes.push "Notes: Some " + tag + " bodies are in database, but not in CSV file:\n " + Array(deleted_ones).join("\n ") + "\nYou may want to delete them manually.\n" end # Rollback if a dry run, or we had errors @@ -473,7 +526,7 @@ class PublicBody < ActiveRecord::Base end def notes_without_html # assume notes are reasonably behaved HTML, so just use simple regexp on this - self.notes.gsub(/<\/?[^>]*>/, "") + self.notes.nil? ? '' : self.notes.gsub(/<\/?[^>]*>/, "") end def json_for_api diff --git a/app/models/request_mailer.rb b/app/models/request_mailer.rb index fc317d20d..75dc58447 100644 --- a/app/models/request_mailer.rb +++ b/app/models/request_mailer.rb @@ -47,7 +47,7 @@ class RequestMailer < ApplicationMailer def requires_admin(info_request) @from = info_request.user.name_and_email @recipients = contact_from_name_and_email - @subject = "FOI response requires admin - " + info_request.title + @subject = _("FOI response requires admin - ") + info_request.title url = main_url(request_url(info_request)) admin_url = request_admin_url(info_request) @body = {:info_request => info_request, :url => url, :admin_url => admin_url } @@ -61,9 +61,10 @@ class RequestMailer < ApplicationMailer @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 + '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 + @subject = _("New response to your FOI request - ") + info_request.title @body = { :incoming_message => incoming_message, :info_request => info_request, :url => url } end @@ -79,9 +80,10 @@ class RequestMailer < ApplicationMailer @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 + '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 + @subject = _("Delayed response to your FOI request - ") + info_request.title @body = { :info_request => info_request, :url => url } end @@ -97,9 +99,10 @@ class RequestMailer < ApplicationMailer @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 + '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 + @subject = _("You're long overdue a response to your FOI request - ") + info_request.title @body = { :info_request => info_request, :url => url } end @@ -116,9 +119,10 @@ class RequestMailer < ApplicationMailer @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 + '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?" + @subject = _("Was the response you got to your FOI request any good?") @body = { :incoming_message => incoming_message, :info_request => info_request, :url => url } end @@ -126,7 +130,8 @@ class RequestMailer < ApplicationMailer 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 + '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)) @@ -146,7 +151,8 @@ class RequestMailer < ApplicationMailer @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 + '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 } @@ -156,17 +162,19 @@ class RequestMailer < ApplicationMailer 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 + '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 + @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 + '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 + @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 @@ -266,12 +274,12 @@ class RequestMailer < ApplicationMailer end end - # Send email alerts for new responses which haven't been classified. Goes - # out 3 days after last update of event, then after 7, then after 24. + # 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 - self.alert_new_response_reminders_internal(3, 'new_response_reminder_1') - self.alert_new_response_reminders_internal(10, 'new_response_reminder_2') - self.alert_new_response_reminders_internal(24, 'new_response_reminder_3') + MySociety::Config.get("NEW_RESPONSE_REMINDER_AFTER_DAYS", [3, 10, 24]).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', diff --git a/app/models/track_mailer.rb b/app/models/track_mailer.rb index 4b7c603a7..0c053c4ad 100644 --- a/app/models/track_mailer.rb +++ b/app/models/track_mailer.rb @@ -26,6 +26,12 @@ class TrackMailer < ApplicationMailer @body = { :user => user, :email_about_things => email_about_things, :unsubscribe_url => unsubscribe_url } end + def contact_from_name_and_email + contact_name = MySociety::Config.get("TRACK_SENDER_NAME", 'Alaveteli') + contact_email = MySociety::Config.get("TRACK_SENDER_EMAIL", 'contact@localhost') + return "#{contact_name} <#{contact_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 @@ -34,12 +40,15 @@ class TrackMailer < ApplicationMailer # 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 false + return done_something end for user in users + next if !user.should_be_emailed? + 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 @@ -85,7 +94,11 @@ class TrackMailer < ApplicationMailer # 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 @@ -104,8 +117,9 @@ class TrackMailer < ApplicationMailer user.last_daily_track_email = now user.no_xapian_reindex = true user.save! + done_something = true end - return true + return done_something end def self.alert_tracks_loop diff --git a/app/models/track_thing.rb b/app/models/track_thing.rb index 16a0dab87..b74f7dad5 100644 --- a/app/models/track_thing.rb +++ b/app/models/track_thing.rb @@ -22,6 +22,7 @@ # Email: francis@mysociety.org; WWW: http://www.mysociety.org/ # # $Id: track_thing.rb,v 1.53 2009-09-17 21:10:05 francis Exp $ +require 'set' class TrackThing < ActiveRecord::Base belongs_to :tracking_user, :class_name => 'User' @@ -67,6 +68,63 @@ class TrackThing < ActiveRecord::Base TrackThing.track_type_description(self.track_type) end + def track_query_description + # XXX this is very brittle... we should probably ask users + # simply to name their tracks when they make them? + self.track_query = self.track_query.gsub(/([()]|OR)/, "") + filters = self.track_query.scan /\b\S+:\S+\b/ + text = self.track_query + varieties = Set.new + date = "" + statuses = Set.new + for filter in filters + text = text.sub(filter, "") + if filter =~ /variety:user/ + varieties << _("users") + end + if filter =~ /variety:comment/ + varieties << _("comments") + end + if filter =~ /variety:authority/ + varieties << _("authorities") + end + if filter =~ /(variety:(sent|followup_sent|response)|latest_status)/ + varieties << _("requests") + end + if filter =~ /[0-9\/]+\.\.[0-9\/]+/ + date = _("between two dates") + end + if filter =~ /(rejected|not_held)/ + statuses << _("unsuccessful") + end + if filter =~ /(:successful|:partially_successful)/ + statuses << _("successful") + end + if filter =~ /waiting/ + statuses << _("awaiting a response") + end + end + if filters.empty? + text = self.track_query + end + descriptions = [] + if varieties.include? _("requests") + descriptions << _("requests which are {{list_of_statuses}}", :list_of_statuses => Array(statuses).join(_(' or '))) + varieties -= [_("requests")] + end + if descriptions.empty? and varieties.empty? + varieties << _("anything") + end + descriptions += Array(varieties) + text = text.strip + descriptions = descriptions.join(_(" or ")) + if !text.empty? + descriptions += _("{{list_of_things}} matching text '{{search_query}}'", :list_of_things => "", :search_query => text) + end + return descriptions + end + + def TrackThing.create_track_for_request(info_request) track_thing = TrackThing.new track_thing.track_type = 'request_updates' @@ -105,10 +163,25 @@ class TrackThing < ActiveRecord::Base return track_thing end - def TrackThing.create_track_for_search_query(query) + def TrackThing.create_track_for_search_query(query, variety_postfix = nil) track_thing = TrackThing.new track_thing.track_type = 'search_query' + if !(query =~ /variety:/) + case variety_postfix + when "requests" + query += " variety:sent" + when "users" + query += " variety:user" + when "authorities" + query += " variety:authority" + end + end track_thing.track_query = query + # XXX should extract requested_by:, request:, requested_from: + # and stick their values into the respective relations. + # Should also update "params" to make the list_description + # nicer and more generic. It will need to do some clever + # parsing of the query to do this nicely return track_thing end @@ -119,16 +192,16 @@ class TrackThing < ActiveRecord::Base if self.track_type == 'request_updates' @params = { # Website - :list_description => "'<a href=\"/request/" + CGI.escapeHTML(self.info_request.url_title) + "\">" + CGI.escapeHTML(self.info_request.title) + "</a>', a request", # XXX yeuch, sometimes I just want to call view helpers from the model, sorry! can't work out how + :list_description => _("'{{link_to_request}}', a request", :link_to_request => "<a href=\"/request/" + CGI.escapeHTML(self.info_request.url_title) + "\">" + CGI.escapeHTML(self.info_request.title) + "</a>"), # XXX yeuch, sometimes I just want to call view helpers from the model, sorry! can't work out how :verb_on_page => _("Track this request by email"), :verb_on_page_already => _("You are already tracking this request by email"), # Email - :title_in_email => "New updates for the request '" + self.info_request.title + "'", - :title_in_rss => "New updates for the request '" + self.info_request.title + "'", + :title_in_email => _("New updates for the request '{{request_title}}'", :request_title => self.info_request.title), + :title_in_rss => _("New updates for the request '{{request_title}}'", :request_title => self.info_request.title), # Authentication - :web => "To follow updates to the request '" + CGI.escapeHTML(self.info_request.title) + "'", - :email => "Then you will be emailed whenever the request '" + CGI.escapeHTML(self.info_request.title) + "' is updated.", - :email_subject => "Confirm you want to follow updates to the request '" + self.info_request.title + "'", + :web => _("To follow updates to the request '{{request_title}}'", :request_title => CGI.escapeHTML(self.info_request.title)), + :email => _("Then you will be emailed whenever the request '{{request_title}}' is updated.", :request_title => CGI.escapeHTML(self.info_request.title)), + :email_subject => _("Confirm you want to follow updates to the request '{{request_title}}'", :request_title => self.info_request.title), # RSS sorting :feed_sortby => 'newest' } @@ -170,7 +243,7 @@ class TrackThing < ActiveRecord::Base elsif self.track_type == 'public_body_updates' @params = { # Website - :list_description => "'<a href=\"/body/" + CGI.escapeHTML(self.public_body.url_name) + "\">" + CGI.escapeHTML(self.public_body.name) + "</a>', a public authority", # XXX yeuch, sometimes I just want to call view helpers from the model, sorry! can't work out how + :list_description => _("'{{link_to_authority}}', a public authority", :link_to_authority => "<a href=\"/body/" + CGI.escapeHTML(self.public_body.url_name) + "\">" + CGI.escapeHTML(self.public_body.name) + "</a>"), # XXX yeuch, sometimes I just want to call view helpers from the model, sorry! can't work out how :verb_on_page => _("Track requests to {{public_body_name}} by email",:public_body_name=>CGI.escapeHTML(self.public_body.name)), :verb_on_page_already => _("You are already tracking requests to {{public_body_name}} by email", :public_body_name=>CGI.escapeHTML(self.public_body.name)), # Email @@ -186,7 +259,7 @@ class TrackThing < ActiveRecord::Base elsif self.track_type == 'user_updates' @params = { # Website - :list_description => "'<a href=\"/user/" + CGI.escapeHTML(self.tracked_user.url_name) + "\">" + CGI.escapeHTML(self.tracked_user.name) + "</a>', a person", # XXX yeuch, sometimes I just want to call view helpers from the model, sorry! can't work out how + :list_description => _("'{{link_to_user}}', a person", :link_to_user => "<a href=\"/user/" + CGI.escapeHTML(self.tracked_user.url_name) + "\">" + CGI.escapeHTML(self.tracked_user.name) + "</a>"), # XXX yeuch, sometimes I just want to call view helpers from the model, sorry! can't work out how :verb_on_page => _("Track this person by email"), :verb_on_page_already => _("You are already tracking this person by email"), # Email @@ -202,16 +275,16 @@ class TrackThing < ActiveRecord::Base elsif self.track_type == 'search_query' @params = { # Website - :list_description => "'<a href=\"/search/" + CGI.escapeHTML(self.track_query) + "/newest\">" + CGI.escapeHTML(self.track_query) + "</a>' in new requests/responses", # XXX yeuch, sometimes I just want to call view helpers from the model, sorry! can't work out how - :verb_on_page => _("Track things matching '{{query}}' by email", :query=>CGI.escapeHTML(self.track_query)), - :verb_on_page_already => _("You are already tracking things matching '{{query}}' by email", :query=>CGI.escapeHTML(self.track_query)), + :list_description => "<a href=\"/search/" + CGI.escapeHTML(self.track_query) + "/newest/advanced\">" + self.track_query_description + "</a>", # XXX yeuch, sometimes I just want to call view helpers from the model, sorry! can't work out how + :verb_on_page => _("Track things matching this search by email"), + :verb_on_page_already => _("You are already tracking things matching this search by email"), # Email - :title_in_email => _("Requests or responses matching '{{query}}'", :query=>self.track_query), - :title_in_rss => _("Requests or responses matching '{{query}}'", :query=>self.track_query), + :title_in_email => _("Requests or responses matching your saved search"), + :title_in_rss => _("Requests or responses matching your saved search"), # Authentication - :web => _("To follow requests and responses matching '{{query}}'", :query=>CGI.escapeHTML(self.track_query)), - :email => _("Then you will be emailed whenever a new request or response matches '{{query}}'.", :query=>CGI.escapeHTML(self.track_query)), - :email_subject => _("Confirm you want to be emailed about new requests or responses matching '{{query}}'", :query=>self.track_query), + :web => _("To follow requests and responses matching your search"), + :email => _("Then you will be emailed whenever a new request or response matches your search."), + :email_subject => _("Confirm you want to be emailed about new requests or responses matching your search"), # RSS sorting - XXX hmmm, we don't really know which to use # here for sorting. Might be a query term (e.g. 'cctv'), in # which case newest is good, or might be something like diff --git a/app/models/user.rb b/app/models/user.rb index fddb6b035..e98d777b1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -16,6 +16,8 @@ # admin_level :string(255) default("none"), not null # ban_text :text default(""), not null # about_me :text default(""), not null +# email_bounced_at :datetime +# email_bounce_message :text default(""), not null # # models/user.rb: @@ -96,6 +98,15 @@ class User < ActiveRecord::Base end end end + + def get_locale + if !self.locale.nil? + locale = self.locale + else + locale = I18n.locale + end + return locale.to_s + end def visible_comments self.comments.find(:all, :conditions => 'visible') @@ -341,15 +352,37 @@ class User < ActiveRecord::Base } end + def record_bounce(message) + self.email_bounced_at = Time.now + self.email_bounce_message = message + self.save! + end + + def should_be_emailed? + return (self.email_confirmed && self.email_bounced_at.nil?) + end + + ## Private instance methods private + def create_new_salt + self.salt = self.object_id.to_s + rand.to_s + end + + ## Class methods def User.encrypted_password(password, salt) string_to_hash = password + salt # XXX need to add a secret here too? Digest::SHA1.hexdigest(string_to_hash) end - - def create_new_salt - self.salt = self.object_id.to_s + rand.to_s + + def User.record_bounce_for_email(email, message) + user = User.find_user_by_email(email) + return false if user.nil? + + if user.email_bounced_at.nil? + user.record_bounce(message) + end + return true end end diff --git a/app/models/user_mailer.rb b/app/models/user_mailer.rb index 0972e167d..7adf5b63c 100644 --- a/app/models/user_mailer.rb +++ b/app/models/user_mailer.rb @@ -46,7 +46,5 @@ class UserMailer < ApplicationMailer @body[:old_email] = old_email @body[:new_email] = new_email end - - end |