aboutsummaryrefslogtreecommitdiffstats
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/application_mailer.rb4
-rw-r--r--app/models/exim_log.rb6
-rw-r--r--app/models/incoming_message.rb54
-rw-r--r--app/models/info_request.rb21
-rw-r--r--app/models/info_request_event.rb81
-rw-r--r--app/models/public_body.rb177
-rw-r--r--app/models/request_mailer.rb48
-rw-r--r--app/models/track_mailer.rb18
-rw-r--r--app/models/track_thing.rb107
-rw-r--r--app/models/user.rb39
-rw-r--r--app/models/user_mailer.rb2
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