aboutsummaryrefslogtreecommitdiffstats
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/holiday.rb2
-rw-r--r--app/models/holiday_import.rb93
-rw-r--r--app/models/incoming_message.rb158
-rw-r--r--app/models/info_request.rb54
-rw-r--r--app/models/info_request_event.rb9
-rw-r--r--app/models/public_body.rb290
-rw-r--r--app/models/public_body_category.rb72
-rw-r--r--app/models/public_body_category_link.rb9
-rw-r--r--app/models/public_body_heading.rb46
-rw-r--r--app/models/track_thing.rb3
-rw-r--r--app/models/user.rb20
11 files changed, 368 insertions, 388 deletions
diff --git a/app/models/holiday.rb b/app/models/holiday.rb
index 4c4941589..34044683a 100644
--- a/app/models/holiday.rb
+++ b/app/models/holiday.rb
@@ -22,6 +22,8 @@
class Holiday < ActiveRecord::Base
+ validates_presence_of :day
+
def self.holidays
@@holidays ||= all.collect { |h| h.day }.to_set
end
diff --git a/app/models/holiday_import.rb b/app/models/holiday_import.rb
new file mode 100644
index 000000000..c6019fac0
--- /dev/null
+++ b/app/models/holiday_import.rb
@@ -0,0 +1,93 @@
+class HolidayImport
+
+ include ActiveModel::Validations
+
+ attr_accessor :holidays,
+ :ical_feed_url,
+ :start_year,
+ :end_year,
+ :start_date,
+ :end_date,
+ :source,
+ :populated
+
+ validate :all_holidays_valid
+ validates_inclusion_of :source, :in => %w( suggestions feed )
+ validates_presence_of :ical_feed_url,
+ :if => proc { |holiday_import| holiday_import.source == 'feed' }
+
+ def initialize(opts = {})
+ @populated = false
+ @start_year = opts.fetch(:start_year, Time.now.year).to_i
+ @end_year = opts.fetch(:end_year, Time.now.year).to_i
+ @start_date = Date.civil(start_year, 1, 1)
+ @end_date = Date.civil(end_year, 12, 31)
+ @source = opts.fetch(:source, 'suggestions')
+ @ical_feed_url = opts.fetch(:ical_feed_url, nil)
+ @country_code = AlaveteliConfiguration::iso_country_code.downcase
+ self.holidays_attributes = opts.fetch(:holidays_attributes, [])
+ end
+
+ def populate
+ source == 'suggestions' ? populate_from_suggestions : populate_from_ical_feed
+ @populated = true
+ end
+
+ def suggestions_country_name
+ IsoCountryCodes.find(@country_code).name if @country_code
+ end
+
+ def period
+ start_year == end_year ? "#{start_year}" : "#{start_year}-#{end_year}"
+ end
+
+ def save
+ holidays.all?(&:save)
+ end
+
+ def holidays_attributes=(incoming_data)
+ incoming_data.each{ |offset, incoming| self.holidays << Holiday.new(incoming) }
+ end
+
+ def holidays
+ @holidays ||= []
+ end
+
+ private
+
+ def all_holidays_valid
+ errors.add(:base, 'These holidays could not be imported') unless holidays.all?(&:valid?)
+ end
+
+ def populate_from_ical_feed
+ begin
+ cal_file = open(ical_feed_url)
+ cals = Icalendar.parse(cal_file, strict=false)
+ cal = cals.first
+ cal.events.each{ |cal_event| populate_from_ical_event(cal_event) }
+ rescue Errno::ENOENT, Exception => e
+ if e.message == 'Invalid line in calendar string!'
+ errors.add(:ical_feed_url, "Sorry, there's a problem with the format of that feed.")
+ elsif e.message.starts_with 'No such file or directory'
+ errors.add(:ical_feed_url, "Sorry we couldn't find that feed.")
+ else
+ raise e
+ end
+ end
+ end
+
+ def populate_from_ical_event(cal_event)
+ if cal_event.dtstart >= start_date and cal_event.dtstart <= end_date
+ holidays << Holiday.new(:description => cal_event.summary,
+ :day => cal_event.dtstart)
+ end
+ end
+
+ def populate_from_suggestions
+ holiday_info = Holidays.between(start_date, end_date, @country_code.to_sym, :observed)
+ holiday_info.each do |holiday_info_hash|
+ holidays << Holiday.new(:description => holiday_info_hash[:name],
+ :day => holiday_info_hash[:date])
+ end
+ end
+end
diff --git a/app/models/incoming_message.rb b/app/models/incoming_message.rb
index db6722976..658ee969a 100644
--- a/app/models/incoming_message.rb
+++ b/app/models/incoming_message.rb
@@ -52,17 +52,6 @@ class IncomingMessage < ActiveRecord::Base
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 = {
- 'image/tiff' => 1,
- 'image/gif' => 1,
- 'image/jpeg' => 1,
- 'image/png' => 1,
- 'image/bmp' => 1,
- 'application/zip' => 1,
- }
-
# Given that there are in theory many info request events, a convenience method for
# getting the response event
def response_event
@@ -218,111 +207,10 @@ class IncomingMessage < ActiveRecord::Base
end
end
- # Converts email addresses we know about into textual descriptions of them
- def mask_special_emails!(text)
- # TODO: can later display some of these special emails as actual emails,
- # if they are public anyway. For now just be precautionary and only
- # put in descriptions of them in square brackets.
- if self.info_request.public_body.is_followupable?
- 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!(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.
- # Also replaces censor items
- def binary_mask_stuff!(text, content_type)
- # See if content type is one that we mask - things like zip files and
- # images may get broken if we try to. We err on the side of masking too
- # much, as many unknown types will really be text.
- if DoNotBinaryMask.include?(content_type)
- return
- end
-
- # Special cases for some content types
- if content_type == 'application/pdf'
- uncompressed_text = nil
- uncompressed_text = AlaveteliExternalCommand.run("pdftk", "-", "output", "-", "uncompress", :stdin_string => text)
- # if we managed to uncompress the PDF...
- if !uncompressed_text.nil? && !uncompressed_text.empty?
- # then censor stuff (making a copy so can compare again in a bit)
- censored_uncompressed_text = uncompressed_text.dup
- self._binary_mask_stuff_internal!(censored_uncompressed_text)
- # if the censor rule removed something...
- if censored_uncompressed_text != uncompressed_text
- # then use the altered file (recompressed)
- recompressed_text = nil
- if AlaveteliConfiguration::use_ghostscript_compression == true
- command = ["gs", "-sDEVICE=pdfwrite", "-dCompatibilityLevel=1.4", "-dPDFSETTINGS=/screen", "-dNOPAUSE", "-dQUIET", "-dBATCH", "-sOutputFile=-", "-"]
- else
- command = ["pdftk", "-", "output", "-", "compress"]
- end
- recompressed_text = AlaveteliExternalCommand.run(*(command + [{:stdin_string=>censored_uncompressed_text}]))
- if recompressed_text.nil? || recompressed_text.empty?
- # buggy versions of pdftk sometimes fail on
- # compression, I don't see it's a disaster in
- # these cases to save an uncompressed version?
- recompressed_text = censored_uncompressed_text
- logger.warn "Unable to compress PDF; problem with your pdftk version?"
- end
- if !recompressed_text.nil? && !recompressed_text.empty?
- text.replace recompressed_text
- end
- end
- end
- return
- end
-
- self._binary_mask_stuff_internal!(text)
- end
-
- # 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.mb_chars.size
-
- # Replace ASCII email addresses...
- text.gsub!(MySociety::Validate.email_find_regexp) do |email|
- email.gsub(/[^@.]/, 'x')
- end
-
- # And replace UCS-2 ones (for Microsoft Office documents)...
- # Find emails, by finding them in parts of text that have ASCII
- # 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
- 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)
- end
-
- # Replace censor items
- self.info_request.apply_censor_rules_to_binary!(text)
-
- raise "internal error in binary_mask_stuff" if text.mb_chars.size != orig_size
- return text
- end
-
- # Removes censored stuff from from HTML conversion of downloaded binaries
- def html_mask_stuff!(html)
- self.mask_special_emails!(html)
- self.remove_privacy_sensitive_things!(html)
+ def apply_masks!(text, content_type)
+ mask_options = { :censor_rules => info_request.applicable_censor_rules,
+ :masks => info_request.masks }
+ AlaveteliTextMasker.apply_masks!(text, content_type, mask_options)
end
# Lotus notes quoting yeuch!
@@ -346,26 +234,6 @@ class IncomingMessage < ActiveRecord::Base
end
- # Remove emails, mobile phones and other details FOI officers ask us to remove.
- def remove_privacy_sensitive_things!(text)
- # Remove any email addresses - we don't want bounce messages to leak out
- # either the requestor's email address or the request's response email
- # address out onto the internet
- text.gsub!(MySociety::Validate.email_find_regexp, "[email address]")
-
- # Mobile phone numbers
- # http://www.whatdotheyknow.com/request/failed_test_purchases_off_licenc#incoming-1013
- # http://www.whatdotheyknow.com/request/selective_licensing_statistics_i#incoming-550
- # http://www.whatdotheyknow.com/request/common_purpose_training_graduate#incoming-774
- text.gsub!(/(Mobile|Mob)([\s\/]*(Fax|Tel))*\s*:?[\s\d]*\d/, "[mobile number]")
-
- # Remove WhatDoTheyKnow signup links
- text.gsub!(/http:\/\/#{AlaveteliConfiguration::domain}\/c\/[^\s]+/, "[WDTK login link]")
-
- # Remove things from censor rules
- self.info_request.apply_censor_rules_to_text!(text)
- end
-
# Remove quoted sections from emails (eventually the aim would be for this
# to do as good a job as GMail does) TODO: bet it needs a proper parser
@@ -465,9 +333,8 @@ class IncomingMessage < ActiveRecord::Base
raise "main body text more than 1 MB, need to implement clipping like for attachment text, or there is some other MIME decoding problem or similar"
end
- # remove emails for privacy/anti-spam reasons
- self.mask_special_emails!(text)
- self.remove_privacy_sensitive_things!(text)
+ # apply masks for this message
+ apply_masks!(text, 'text/html')
# Remove existing quoted sections
folded_quoted_text = self.remove_lotus_quoting(text, 'FOLDED_QUOTED_SECTION')
@@ -735,7 +602,14 @@ class IncomingMessage < ActiveRecord::Base
text = MySociety::Format.simplify_angle_bracketed_urls(text)
text = CGI.escapeHTML(text)
text = MySociety::Format.make_clickable(text, :contract => 1)
- text.gsub!(/\[(email address|mobile number)\]/, '[<a href="/help/officers#mobiles">\1</a>]')
+
+ # add a helpful link to email addresses and mobile numbers removed
+ # by apply_masks!
+ email_pattern = Regexp.escape(_("email address"))
+ mobile_pattern = Regexp.escape(_("mobile number"))
+ text.gsub!(/\[(#{email_pattern}|#{mobile_pattern})\]/,
+ '[<a href="/help/officers#mobiles">\1</a>]')
+
if collapse_quoted_sections
text = text.gsub(/(\s*FOLDED_QUOTED_SECTION\s*)+/m, "FOLDED_QUOTED_SECTION")
text.strip!
@@ -773,8 +647,8 @@ class IncomingMessage < ActiveRecord::Base
# Returns text version of attachment text
def get_attachment_text_full
text = self._get_attachment_text_internal
- self.mask_special_emails!(text)
- self.remove_privacy_sensitive_things!(text)
+ apply_masks!(text, 'text/html')
+
# This can be useful for memory debugging
#STDOUT.puts 'xxx '+ MySociety::DebugHelpers::allocated_string_size_around_gc
diff --git a/app/models/info_request.rb b/app/models/info_request.rb
index d0052603a..fd42ccd9c 100644
--- a/app/models/info_request.rb
+++ b/app/models/info_request.rb
@@ -187,11 +187,9 @@ class InfoRequest < ActiveRecord::Base
@@custom_states_loaded = false
begin
- if !Rails.env.test?
- require 'customstates'
- include InfoRequestCustomStates
- @@custom_states_loaded = true
- end
+ require 'customstates'
+ include InfoRequestCustomStates
+ @@custom_states_loaded = true
rescue MissingSourceFile, NameError
end
@@ -210,16 +208,6 @@ class InfoRequest < ActiveRecord::Base
OLD_AGE_IN_DAYS = 21.days
- def after_initialize
- if self.described_state.nil?
- self.described_state = 'waiting_response'
- end
- # FOI or EIR?
- if !self.public_body.nil? && self.public_body.eir_only?
- self.law_used = 'eir'
- end
- end
-
def visible_comments
self.comments.find(:all, :conditions => 'visible')
end
@@ -292,13 +280,18 @@ public
end
# Subject lines for emails about the request
- def email_subject_request
- _('{{law_used_full}} request - {{title}}',:law_used_full=>self.law_used_full,:title=>self.title.html_safe)
+ def email_subject_request(opts = {})
+ html = opts.fetch(:html, true)
+ _('{{law_used_full}} request - {{title}}',
+ :law_used_full => self.law_used_full,
+ :title => (html ? title : title.html_safe))
end
- def email_subject_followup(incoming_message = nil)
+ def email_subject_followup(opts = {})
+ incoming_message = opts.fetch(:incoming_message, nil)
+ html = opts.fetch(:html, true)
if incoming_message.nil? || !incoming_message.valid_to_reply_to? || !incoming_message.subject
- 'Re: ' + self.email_subject_request
+ 'Re: ' + self.email_subject_request(:html => html)
else
if incoming_message.subject.match(/^Re:/i)
incoming_message.subject
@@ -753,7 +746,6 @@ public
# This is a long stop - even with UK public interest test extensions, 40
# days is a very long time.
def date_very_overdue_after
- last_sent = last_event_forming_initial_request
if self.public_body.is_school?
# schools have 60 working days maximum (even over a long holiday)
Holiday.due_date_from(self.date_initial_request_last_sent_at, AlaveteliConfiguration::special_reply_very_late_after_days, AlaveteliConfiguration::working_or_calendar_days)
@@ -1148,6 +1140,22 @@ public
return binary
end
+ # Masks we apply to text associated with this request convert email addresses
+ # we know about into textual descriptions of them
+ def masks
+ masks = [{ :to_replace => incoming_email,
+ :replacement => _('[FOI #{{request}} email]',
+ :request => id.to_s) },
+ { :to_replace => AlaveteliConfiguration::contact_email,
+ :replacement => _("[{{site_name}} contact email]",
+ :site_name => AlaveteliConfiguration::site_name)} ]
+ if public_body.is_followupable?
+ masks << { :to_replace => public_body.request_email,
+ :replacement => _("[{{public_body}} request email]",
+ :public_body => public_body.short_or_long_name) }
+ end
+ end
+
def is_owning_user?(user)
!user.nil? && (user.id == user_id || user.owns_every_request?)
end
@@ -1345,9 +1353,9 @@ public
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")
+ select("*, #{ last_event_time_clause } as last_event_time").
+ where(:described_state => state).
+ order('last_event_time')
end
private
diff --git a/app/models/info_request_event.rb b/app/models/info_request_event.rb
index 9dde3ba80..635ba8f58 100644
--- a/app/models/info_request_event.rb
+++ b/app/models/info_request_event.rb
@@ -161,11 +161,10 @@ class InfoRequestEvent < ActiveRecord::Base
end
def incoming_message_selective_columns(fields)
- message = IncomingMessage.find(:all,
- :select => fields + ", incoming_messages.info_request_id",
- :joins => "INNER JOIN info_request_events ON incoming_messages.id = incoming_message_id ",
- :conditions => "info_request_events.id = #{self.id}"
- )
+ message = IncomingMessage.select("#{ fields }, incoming_messages.info_request_id").
+ joins('INNER JOIN info_request_events ON incoming_messages.id = incoming_message_id').
+ where('info_request_events.id = ?', id)
+
message = message[0]
if !message.nil?
message.info_request = InfoRequest.find(message.info_request_id)
diff --git a/app/models/public_body.rb b/app/models/public_body.rb
index 829625cac..232c0ffa1 100644
--- a/app/models/public_body.rb
+++ b/app/models/public_body.rb
@@ -64,7 +64,7 @@ class PublicBody < ActiveRecord::Base
}
translates :name, :short_name, :request_email, :url_name, :notes, :first_letter, :publication_scheme
- accepts_nested_attributes_for :translations
+ accepts_nested_attributes_for :translations, :reject_if => :empty_translation_in_params?
# Default fields available for importing from CSV, in the format
# [field_name, 'short description of field (basic html allowed)']
@@ -152,33 +152,15 @@ class PublicBody < ActiveRecord::Base
translations
end
- def translations_attributes=(translation_attrs)
- def empty_translation?(attrs)
- attrs_with_values = attrs.select{ |key, value| value != '' and key.to_s != 'locale' }
- attrs_with_values.empty?
- end
- if translation_attrs.respond_to? :each_value # Hash => updating
- translation_attrs.each_value do |attrs|
- next if empty_translation?(attrs)
- t = translation_for(attrs[:locale]) || PublicBody::Translation.new
- t.attributes = attrs
- calculate_cached_fields(t)
- t.save!
- end
- else # Array => creating
- warn "[DEPRECATION] PublicBody#translations_attributes= " \
- "will no longer accept an Array as of release 0.22. " \
- "Use Hash arguments instead. See " \
- "spec/models/public_body_spec.rb and " \
- "app/views/admin_public_body/_form.html.erb for more " \
- "details."
-
- translation_attrs.each do |attrs|
- next if empty_translation?(attrs)
- new_translation = PublicBody::Translation.new(attrs)
- calculate_cached_fields(new_translation)
- translations << new_translation
- end
+ def ordered_translations
+ translations.
+ select { |t| I18n.available_locales.include?(t.locale) }.
+ sort_by { |t| I18n.available_locales.index(t.locale) }
+ end
+
+ def build_all_translations
+ I18n.available_locales.each do |locale|
+ translations.build(:locale => locale) unless translations.detect{ |t| t.locale == locale }
end
end
@@ -235,39 +217,38 @@ class PublicBody < ActiveRecord::Base
return self.has_tag?('defunct')
end
- # Can an FOI (etc.) request be made to this body, and if not why not?
+ # Can an FOI (etc.) request be made to this body?
def is_requestable?
- if self.defunct?
- return false
- end
- if self.not_apply?
- return false
- end
- if self.request_email.nil?
- return false
- end
- return !self.request_email.empty? && self.request_email != 'blank'
+ has_request_email? && !defunct? && !not_apply?
end
+
# Strict superset of is_requestable?
def is_followupable?
- if self.request_email.nil?
- return false
- end
- return !self.request_email.empty? && self.request_email != 'blank'
+ has_request_email?
+ end
+
+ def has_request_email?
+ !request_email.blank? && request_email != 'blank'
end
+
# Also used as not_followable_reason
def not_requestable_reason
if self.defunct?
return 'defunct'
elsif self.not_apply?
return 'not_apply'
- elsif self.request_email.nil? or self.request_email.empty? or self.request_email == 'blank'
+ elsif !has_request_email?
return 'bad_contact'
else
- raise "requestable_failure_reason called with type that has no reason"
+ raise "not_requestable_reason called with type that has no reason"
end
end
+ def special_not_requestable_reason?
+ self.defunct? || self.not_apply?
+ end
+
+
class Version
def last_edit_comment_for_html_display
@@ -346,39 +327,6 @@ class PublicBody < ActiveRecord::Base
end
end
-
- # Use tags to describe what type of thing this is
- def type_of_authority(html = false)
- types = []
- first = true
- for tag in self.tags
- if PublicBodyCategory.get().by_tag().include?(tag.name)
- desc = PublicBodyCategory.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)
- desc = desc[0,1].capitalize + desc[1,desc.size]
- first = false
- end
- if html
- # TODO: this should call proper route helpers, but is in model sigh
- desc = '<a href="/body/list/' + tag.name + '">' + desc + '</a>'
- end
- types.push(desc)
- end
- end
- if types.size > 0
- ret = types[0, types.size - 1].join(", ")
- if types.size > 1
- ret = ret + " and "
- end
- ret = ret + types[-1]
- return ret.html_safe
- else
- return _("A public authority")
- end
- end
-
# Guess home page from the request email, or use explicit override, or nil
# if not known.
def calculated_home_page
@@ -458,8 +406,6 @@ class PublicBody < ActiveRecord::Base
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?
-
begin
ActiveRecord::Base.transaction do
# Use the default locale when retrieving existing bodies; otherwise
@@ -480,9 +426,18 @@ class PublicBody < ActiveRecord::Base
end
set_of_importing = Set.new()
- field_names = { 'name'=>1, 'request_email'=>2 } # Default values in case no field list is given
+ # Default values in case no field list is given
+ field_names = { 'name' => 1, 'request_email' => 2 }
line = 0
+ import_options = {:field_names => field_names,
+ :available_locales => available_locales,
+ :tag => tag,
+ :tag_behaviour => tag_behaviour,
+ :editor => editor,
+ :notes => notes,
+ :errors => errors }
+
CSV.foreach(csv_filename) do |row|
line = line + 1
@@ -494,7 +449,7 @@ class PublicBody < ActiveRecord::Base
end
fields = {}
- field_names.each{|name, i| fields[name] = row[i]}
+ field_names.each{ |name, i| fields[name] = row[i] }
yield line, fields if block_given?
@@ -510,83 +465,11 @@ class PublicBody < ActiveRecord::Base
next
end
- field_list = []
- self.csv_import_fields.each do |field_name, field_notes|
- field_list.push field_name
- end
-
- if public_body = bodies_by_name[name] # Existing public body
- available_locales.each do |locale|
- 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}"
- 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
-
- 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
- public_body = PublicBody.new(:name=>"", :short_name=>"", :request_email=>"")
- available_locales.each do |locale|
- 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}"
- 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'
-
- begin
- public_body.save!
- rescue ActiveRecord::RecordInvalid
- public_body.errors.full_messages.each do |msg|
- errors.push "error: line #{ line }: #{ msg } for authority '#{ name }'"
- end
- next
- end
- end
- end
- end
- end
+ public_body = bodies_by_name[name] || PublicBody.new(:name => "",
+ :short_name => "",
+ :request_email => "")
+ public_body.import_values_from_csv_row(row, line, name, import_options)
set_of_importing.add(name)
end
@@ -608,6 +491,77 @@ class PublicBody < ActiveRecord::Base
return [errors, notes]
end
+ def self.localized_csv_field_name(locale, field_name)
+ (locale.to_s == I18n.default_locale.to_s) ? field_name : "#{field_name}.#{locale}"
+ end
+
+
+ # import values from a csv row (that may include localized columns)
+ def import_values_from_csv_row(row, line, name, options)
+ is_new = new_record?
+ edit_info = if is_new
+ { :action => "creating new authority",
+ :comment => 'Created from spreadsheet' }
+ else
+ { :action => "updating authority",
+ :comment => 'Updated from spreadsheet' }
+ end
+ locales = options[:available_locales]
+ locales = [I18n.default_locale] if locales.empty?
+ locales.each do |locale|
+ I18n.with_locale(locale) do
+ changed = set_locale_fields_from_csv_row(is_new, locale, row, options)
+ unless changed.empty?
+ options[:notes].push "line #{ line }: #{ edit_info[:action] } '#{ name }' (locale: #{ locale }):\n\t#{ changed.to_json }"
+ self.last_edit_comment = edit_info[:comment]
+ self.publication_scheme = publication_scheme || ""
+ self.last_edit_editor = options[:editor]
+
+ begin
+ save!
+ rescue ActiveRecord::RecordInvalid
+ errors.full_messages.each do |msg|
+ options[:errors].push "error: line #{ line }: #{ msg } for authority '#{ name }'"
+ end
+ next
+ end
+ end
+ end
+ end
+ end
+
+ # Sets attribute values for a locale from a csv row
+ def set_locale_fields_from_csv_row(is_new, locale, row, options)
+ changed = ActiveSupport::OrderedHash.new
+ csv_field_names = options[:field_names]
+ csv_import_fields.each do |field_name, field_notes|
+ localized_field_name = self.class.localized_csv_field_name(locale, field_name)
+ column = csv_field_names[localized_field_name]
+ value = column && row[column]
+ # Tags are a special case, as we support adding to the field, not just setting a new value
+ if field_name == 'tag_string'
+ new_tags = [value, options[:tag]].select{ |new_tag| !new_tag.blank? }
+ if new_tags.empty?
+ value = nil
+ else
+ value = new_tags.join(" ")
+ value = "#{value} #{tag_string}"if options[:tag_behaviour] == 'add'
+ end
+
+ end
+
+ if value and read_attribute_value(field_name, locale) != value
+ if is_new
+ changed[field_name] = value
+ else
+ changed[field_name] = "#{read_attribute_value(field_name, locale)}: #{value}"
+ end
+ assign_attributes({ field_name => value })
+ end
+ end
+ changed
+ end
+
# Does this user have the power of FOI officer for this body?
def is_foi_officer?(user)
user_domain = user.email_domain
@@ -806,6 +760,26 @@ class PublicBody < ActiveRecord::Base
private
+ # Read an attribute value (without using locale fallbacks if the attribute is translated)
+ def read_attribute_value(name, locale)
+ if self.class.translates.include?(name.to_sym)
+ if globalize.stash.contains?(locale, name)
+ globalize.stash.read(locale, name)
+ else
+ translation_for(locale).send(name)
+ end
+ else
+ send(name)
+ end
+ end
+
+ def empty_translation_in_params?(attributes)
+ attrs_with_values = attributes.select do |key, value|
+ value != '' and key.to_s != 'locale'
+ end
+ attrs_with_values.empty?
+ end
+
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_category.rb b/app/models/public_body_category.rb
index bb83c4c82..b88c683de 100644
--- a/app/models/public_body_category.rb
+++ b/app/models/public_body_category.rb
@@ -2,23 +2,23 @@
#
# Table name: public_body_categories
#
-# id :integer not null, primary key
-# title :text not null
-# category_tag :text not null
-# description :text not null
-# display_order :integer
+# id :integer not null, primary key
+# category_tag :text not null
#
require 'forwardable'
class PublicBodyCategory < ActiveRecord::Base
attr_accessible :locale, :category_tag, :title, :description,
- :translated_versions, :display_order
+ :translated_versions, :translations_attributes,
+ :display_order
has_many :public_body_category_links, :dependent => :destroy
has_many :public_body_headings, :through => :public_body_category_links
translates :title, :description
+ accepts_nested_attributes_for :translations, :reject_if => :empty_translation_in_params?
+
validates_uniqueness_of :category_tag, :message => 'Tag is already taken'
validates_presence_of :title, :message => "Title can't be blank"
validates_presence_of :category_tag, :message => "Tag can't be blank"
@@ -52,11 +52,6 @@ class PublicBodyCategory < ActiveRecord::Base
PublicBodyCategory.find_by_sql(sql)
end
- # Called from the old-style public_body_categories_[locale].rb data files
- def self.add(locale, data_list)
- CategoryAndHeadingMigrator.add_categories_and_headings_from_list(locale, data_list)
- end
-
# Convenience methods for creating/editing translations via forms
def find_translation_by_locale(locale)
translations.find_by_locale(locale)
@@ -67,25 +62,48 @@ class PublicBodyCategory < ActiveRecord::Base
end
def translated_versions=(translation_attrs)
- def empty_translation?(attrs)
- attrs_with_values = attrs.select{ |key, value| value != '' and key != 'locale' }
- attrs_with_values.empty?
+ warn "[DEPRECATION] PublicBodyCategory#translated_versions= will be replaced " \
+ "by PublicBodyCategory#translations_attributes= as of release 0.22"
+ self.translations_attributes = translation_attrs
+ end
+
+ def ordered_translations
+ translations.
+ select { |t| I18n.available_locales.include?(t.locale) }.
+ sort_by { |t| I18n.available_locales.index(t.locale) }
+ end
+
+ def build_all_translations
+ I18n.available_locales.each do |locale|
+ translations.build(:locale => locale) unless translations.detect{ |t| t.locale == locale }
end
- if translation_attrs.respond_to? :each_value # Hash => updating
- translation_attrs.each_value do |attrs|
- next if empty_translation?(attrs)
- t = translation_for(attrs[:locale]) || PublicBodyCategory::Translation.new
- t.attributes = attrs
- t.save!
- end
- else # Array => creating
- translation_attrs.each do |attrs|
- next if empty_translation?(attrs)
- new_translation = PublicBodyCategory::Translation.new(attrs)
- translations << new_translation
- end
+ end
+
+ private
+
+ def empty_translation_in_params?(attributes)
+ attrs_with_values = attributes.select do |key, value|
+ value != '' and key.to_s != 'locale'
end
+ attrs_with_values.empty?
end
+
end
+PublicBodyCategory::Translation.class_eval do
+ with_options :if => lambda { |t| !t.default_locale? && t.required_attribute_submitted? } do |required|
+ required.validates :title, :presence => { :message => "Title can't be blank" }
+ required.validates :description, :presence => { :message => "Description can't be blank" }
+ end
+ def default_locale?
+ locale == I18n.default_locale
+ end
+
+ def required_attribute_submitted?
+ PublicBodyCategory.required_translated_attributes.compact.any? do |attribute|
+ !read_attribute(attribute).blank?
+ end
+ end
+
+end
diff --git a/app/models/public_body_category_link.rb b/app/models/public_body_category_link.rb
index ba3ff1f95..8c3eb8060 100644
--- a/app/models/public_body_category_link.rb
+++ b/app/models/public_body_category_link.rb
@@ -1,10 +1,11 @@
# == Schema Information
#
-# Table name: public_body_category_link
+# Table name: public_body_category_links
#
-# public_body_category_id :integer not null
-# public_body_heading_id :integer not null
-# category_display_order :integer
+# public_body_category_id :integer not null
+# public_body_heading_id :integer not null
+# category_display_order :integer
+# id :integer not null, primary key
#
class PublicBodyCategoryLink < ActiveRecord::Base
diff --git a/app/models/public_body_heading.rb b/app/models/public_body_heading.rb
index f1916d233..8c160ba8b 100644
--- a/app/models/public_body_heading.rb
+++ b/app/models/public_body_heading.rb
@@ -2,19 +2,20 @@
#
# Table name: public_body_headings
#
-# id :integer not null, primary key
-# name :text not null
+# id :integer not null, primary key
# display_order :integer
#
class PublicBodyHeading < ActiveRecord::Base
- attr_accessible :name, :display_order, :translated_versions
+ attr_accessible :locale, :name, :display_order, :translated_versions,
+ :translations_attributes
has_many :public_body_category_links, :dependent => :destroy
has_many :public_body_categories, :order => :category_display_order, :through => :public_body_category_links
default_scope order('display_order ASC')
translates :name
+ accepts_nested_attributes_for :translations, :reject_if => :empty_translation_in_params?
validates_uniqueness_of :name, :message => 'Name is already taken'
validates_presence_of :name, :message => 'Name can\'t be blank'
@@ -37,24 +38,20 @@ class PublicBodyHeading < ActiveRecord::Base
end
def translated_versions=(translation_attrs)
- def empty_translation?(attrs)
- attrs_with_values = attrs.select{ |key, value| value != '' and key != 'locale' }
- attrs_with_values.empty?
- end
+ warn "[DEPRECATION] PublicBodyHeading#translated_versions= will be replaced " \
+ "by PublicBodyHeading#translations_attributes= as of release 0.22"
+ self.translations_attributes = translation_attrs
+ end
+
+ def ordered_translations
+ translations.
+ select { |t| I18n.available_locales.include?(t.locale) }.
+ sort_by { |t| I18n.available_locales.index(t.locale) }
+ end
- if translation_attrs.respond_to? :each_value # Hash => updating
- translation_attrs.each_value do |attrs|
- next if empty_translation?(attrs)
- t = translation_for(attrs[:locale]) || PublicBodyHeading::Translation.new
- t.attributes = attrs
- t.save!
- end
- else # Array => creating
- translation_attrs.each do |attrs|
- next if empty_translation?(attrs)
- new_translation = PublicBodyHeading::Translation.new(attrs)
- translations << new_translation
- end
+ def build_all_translations
+ I18n.available_locales.each do |locale|
+ translations.build(:locale => locale) unless translations.detect{ |t| t.locale == locale }
end
end
@@ -72,4 +69,13 @@ class PublicBodyHeading < ActiveRecord::Base
end
end
+ private
+
+ def empty_translation_in_params?(attributes)
+ attrs_with_values = attributes.select do |key, value|
+ value != '' and key.to_s != 'locale'
+ end
+ attrs_with_values.empty?
+ end
+
end
diff --git a/app/models/track_thing.rb b/app/models/track_thing.rb
index 5819876ff..cd90c4a9e 100644
--- a/app/models/track_thing.rb
+++ b/app/models/track_thing.rb
@@ -231,8 +231,7 @@ class TrackThing < ActiveRecord::Base
{ # Website
:verb_on_page => _("Follow requests to {{public_body_name}}",
:public_body_name => public_body.name),
- :verb_on_page_already => _("You are already following requests to {{public_body_name}}",
- :public_body_name => public_body.name),
+ :verb_on_page_already => _("Following"),
# Email
:title_in_email => _("{{foi_law}} requests to '{{public_body_name}}'",
:foi_law => public_body.law_only_short,
diff --git a/app/models/user.rb b/app/models/user.rb
index 1c6dc0eb0..920c0da46 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -207,7 +207,7 @@ class User < ActiveRecord::Base
if not name.nil?
name.strip!
end
- if public_banned?
+ if banned?
# 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
@@ -264,11 +264,9 @@ class User < ActiveRecord::Base
# Returns list of requests which the user hasn't described (and last
# changed more than a day ago)
def get_undescribed_requests
- info_requests.find(
- :all,
- :conditions => [ 'awaiting_description = ? and ' + InfoRequest.last_event_time_clause + ' < ?',
- true, Time.now() - 1.day
- ]
+ info_requests.where(
+ "awaiting_description = ? and #{ InfoRequest.last_event_time_clause } < ?",
+ true, 1.day.ago
)
end
@@ -296,10 +294,18 @@ class User < ActiveRecord::Base
def admin_page_links?
super?
end
+
# Is it public that they are banned?
+ def banned?
+ !ban_text.empty?
+ end
+
def public_banned?
- !ban_text.empty?
+ warn %q([DEPRECATION] User#public_banned? will be replaced with
+ User#banned? as of 0.22).squish
+ banned?
end
+
# Various ways the user can be banned, and text to describe it if failed
def can_file_requests?
ban_text.empty? && !exceeded_limit?