diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/holiday.rb | 2 | ||||
-rw-r--r-- | app/models/holiday_import.rb | 93 | ||||
-rw-r--r-- | app/models/incoming_message.rb | 158 | ||||
-rw-r--r-- | app/models/info_request.rb | 54 | ||||
-rw-r--r-- | app/models/info_request_event.rb | 9 | ||||
-rw-r--r-- | app/models/public_body.rb | 290 | ||||
-rw-r--r-- | app/models/public_body_category.rb | 72 | ||||
-rw-r--r-- | app/models/public_body_category_link.rb | 9 | ||||
-rw-r--r-- | app/models/public_body_heading.rb | 46 | ||||
-rw-r--r-- | app/models/track_thing.rb | 3 | ||||
-rw-r--r-- | app/models/user.rb | 20 |
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? |