diff options
Diffstat (limited to 'app/models/public_body.rb')
-rw-r--r-- | app/models/public_body.rb | 520 |
1 files changed, 256 insertions, 264 deletions
diff --git a/app/models/public_body.rb b/app/models/public_body.rb index 232c0ffa1..ac924a941 100644 --- a/app/models/public_body.rb +++ b/app/models/public_body.rb @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# -*- encoding : utf-8 -*- # == Schema Information # Schema version: 20131024114346 # @@ -32,7 +32,32 @@ require 'securerandom' require 'set' class PublicBody < ActiveRecord::Base - strip_attributes! + include AdminColumn + + class ImportCSVDryRun < StandardError ; end + + @non_admin_columns = %w(name last_edit_comment) + + attr_accessor :no_xapian_reindex + + # Default fields available for importing from CSV, in the format + # [field_name, 'short description of field (basic html allowed)'] + cattr_accessor :csv_import_fields do + [ + ['name', '(i18n)<strong>Existing records cannot be renamed</strong>'], + ['short_name', '(i18n)'], + ['request_email', '(i18n)'], + ['notes', '(i18n)'], + ['publication_scheme', '(i18n)'], + ['disclosure_log', '(i18n)'], + ['home_page', ''], + ['tag_string', '(tags separated by spaces)'], + ] + end + + has_many :info_requests, :order => 'created_at desc' + has_many :track_things, :order => 'created_at desc' + has_many :censor_rules, :order => 'created_at desc' validates_presence_of :name, :message => N_("Name can't be blank") validates_presence_of :url_name, :message => N_("URL name can't be blank") @@ -43,19 +68,12 @@ class PublicBody < ActiveRecord::Base validate :request_email_if_requestable - has_many :info_requests, :order => 'created_at desc' - has_many :track_things, :order => 'created_at desc' - has_many :censor_rules, :order => 'created_at desc' - attr_accessor :no_xapian_reindex - - has_tag_string - - before_save :set_api_key, - :set_default_publication_scheme, - :set_first_letter + before_save :set_api_key!, :unless => :api_key + before_save :set_default_publication_scheme after_save :purge_in_cache after_update :reindex_requested_from + # Every public body except for the internal admin one is visible scope :visible, lambda { { @@ -63,39 +81,83 @@ class PublicBody < ActiveRecord::Base } } + acts_as_versioned + acts_as_xapian :texts => [:name, :short_name, :notes], + :values => [ + # for sorting + [:created_at_numeric, 1, "created_at", :number] + ], + :terms => [ + [:variety, 'V', "variety"], + [:tag_array_for_search, 'U', "tag"] + ] + has_tag_string + strip_attributes! translates :name, :short_name, :request_email, :url_name, :notes, :first_letter, :publication_scheme - 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)'] - cattr_accessor :csv_import_fields do - [ - ['name', '(i18n)<strong>Existing records cannot be renamed</strong>'], - ['short_name', '(i18n)'], - ['request_email', '(i18n)'], - ['notes', '(i18n)'], - ['publication_scheme', '(i18n)'], - ['disclosure_log', '(i18n)'], - ['home_page', ''], - ['tag_string', '(tags separated by spaces)'], - ] - end + # Cannot be grouped at top as it depends on the `translates` macro + include Translatable - acts_as_xapian :texts => [ :name, :short_name, :notes ], - :values => [ - [ :created_at_numeric, 1, "created_at", :number ] # for sorting - ], - :terms => [ [ :variety, 'V', "variety" ], - [ :tag_array_for_search, 'U', "tag" ] - ] + # Cannot be grouped at top as it depends on the `translates` macro + include PublicBodyDerivedFields + + # Cannot be grouped at top as it depends on the `translates` macro + class Translation + include PublicBodyDerivedFields + end - acts_as_versioned self.non_versioned_columns << 'created_at' << 'updated_at' << 'first_letter' << 'api_key' self.non_versioned_columns << 'info_requests_count' << 'info_requests_successful_count' self.non_versioned_columns << 'info_requests_count' << 'info_requests_visible_classified_count' self.non_versioned_columns << 'info_requests_not_held_count' << 'info_requests_overdue' self.non_versioned_columns << 'info_requests_overdue_count' + # Cannot be defined directly under `include` statements as this is opening + # the PublicBody::Version class dynamically defined by the + # `acts_as_versioned` macro. + # + # TODO: acts_as_versioned accepts an extend parameter [1] so these methods + # could be extracted to a module: + # + # acts_as_versioned :extend => PublicBodyVersionExtensions + # + # This includes the module in both the parent class (PublicBody) and the + # Version class (PublicBody::Version), so the behaviour is slightly + # different to opening up PublicBody::Version. + # + # We could add an `extend_version_class` option pretty trivially by + # following the pattern for the existing `extend` option. + # + # [1] http://git.io/vIetK + class Version + def last_edit_comment_for_html_display + text = self.last_edit_comment.strip + text = CGI.escapeHTML(text) + text = MySociety::Format.make_clickable(text) + text = text.gsub(/\n/, '<br>') + return text + end + + def compare(previous = nil) + if previous.nil? + yield([]) + else + v = self + changes = self.class.content_columns.inject([]) {|memo, c| + unless %w(version last_edit_editor last_edit_comment updated_at).include?(c.name) + from = previous.send(c.name) + to = self.send(c.name) + memo << { :name => c.human_name, :from => from, :to => to } if from != to + end + memo + } + changes.each do |change| + yield(change) + end + end + end + end + # Public: Search for Public Bodies whose name, short_name, request_email or # tags contain the given query # @@ -124,97 +186,62 @@ class PublicBody < ActiveRecord::Base uniq end - # Convenience methods for creating/editing translations via forms - def find_translation_by_locale(locale) - self.translations.find_by_locale(locale) - end - - # TODO: - Don't like repeating this! - def calculate_cached_fields(t) - PublicBody.set_first_letter(t) - short_long_name = t.name - short_long_name = t.short_name if t.short_name and !t.short_name.empty? - t.url_name = MySociety::Format.simplify_url_part(short_long_name, 'body') - end - - # Set the first letter on a public body or translation - def PublicBody.set_first_letter(instance) - unless instance.name.nil? or instance.name.empty? - # we use a regex to ensure it works with utf-8/multi-byte - first_letter = Unicode.upcase instance.name.scan(/^./mu)[0] - if first_letter != instance.first_letter - instance.first_letter = first_letter - end - end - end - - def translated_versions - translations - end - - def ordered_translations - translations. - select { |t| I18n.available_locales.include?(t.locale) }. - sort_by { |t| I18n.available_locales.index(t.locale) } + def set_api_key + set_api_key! if api_key.nil? end - def build_all_translations - I18n.available_locales.each do |locale| - translations.build(:locale => locale) unless translations.detect{ |t| t.locale == locale } - end + def set_api_key! + self.api_key = SecureRandom.base64(33) end - def translated_versions=(translation_attrs) - warn "[DEPRECATION] PublicBody#translated_versions= will be replaced " \ - "by PublicBody#translations_attributes= as of release 0.22" - self.translations_attributes = translation_attrs - end + # like find_by_url_name but also search historic url_name if none found + def self.find_by_url_name_with_historic(name) + # If many bodies are found (usually because the url_name is the same + # across locales) return any of them. + found = joins(:translations). + where("public_body_translations.url_name = ?", name). + readonly(false). + first - def set_default_publication_scheme - # Make sure publication_scheme gets the correct default value. - # (This would work automatically, were publication_scheme not a translated attribute) - self.publication_scheme = "" if self.publication_scheme.nil? - end + return found if found - def set_api_key - self.api_key = SecureRandom.base64(33) if self.api_key.nil? - end + # If none found, then search the history of short names and find unique + # public bodies in it + old = PublicBody::Version. + where(:url_name => name). + pluck('DISTINCT public_body_id') - # like find_by_url_name but also search historic url_name if none found - def self.find_by_url_name_with_historic(name) - found = PublicBody.find(:all, - :conditions => ["public_body_translations.url_name=?", name], - :joins => :translations, - :readonly => false) - # If many bodies are found (usually because the url_name is the same across - # locales) return any of them - return found.first if found.size >= 1 - - # If none found, then search the history of short names - old = PublicBody::Version.find_all_by_url_name(name) - # Find unique public bodies in it - old = old.map { |x| x.public_body_id } - old = old.uniq # Maybe return the first one, so we show something relevant, # rather than throwing an error? raise "Two bodies with the same historical URL name: #{name}" if old.size > 1 return unless old.size == 1 # does acts_as_versioned provide a method that returns the current version? - return PublicBody.find(old.first) - end - - # Set the first letter, which is used for faster queries - def set_first_letter - PublicBody.set_first_letter(self) + PublicBody.find(old.first) end # If tagged "not_apply", then FOI/EIR no longer applies to authority at all def not_apply? - return self.has_tag?('not_apply') + has_tag?('not_apply') end + # If tagged "defunct", then the authority no longer exists at all def defunct? - return self.has_tag?('defunct') + has_tag?('defunct') + end + + # Are all requests to this body under the Environmental Information + # Regulations? + def eir_only? + has_tag?('eir_only') + end + + # Schools are allowed more time in holidays, so we change some wordings + def is_school? + has_tag?('school') + end + + def site_administration? + has_tag?('site_administration') end # Can an FOI (etc.) request be made to this body? @@ -233,102 +260,39 @@ class PublicBody < ActiveRecord::Base # Also used as not_followable_reason def not_requestable_reason - if self.defunct? - return 'defunct' - elsif self.not_apply? - return 'not_apply' + if defunct? + 'defunct' + elsif not_apply? + 'not_apply' elsif !has_request_email? - return 'bad_contact' + 'bad_contact' else 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 - text = self.last_edit_comment.strip - text = CGI.escapeHTML(text) - text = MySociety::Format.make_clickable(text) - text = text.gsub(/\n/, '<br>') - return text - end - - def compare(previous = nil) - if previous.nil? - yield([]) - else - v = self - changes = self.class.content_columns.inject([]) {|memo, c| - unless %w(version last_edit_editor last_edit_comment updated_at).include?(c.name) - from = previous.send(c.name) - to = self.send(c.name) - memo << { :name => c.human_name, :from => from, :to => to } if from != to - end - memo - } - changes.each do |change| - yield(change) - end - end - end + defunct? || not_apply? end def created_at_numeric # format it here as no datetime support in Xapian's value ranges - return self.created_at.strftime("%Y%m%d%H%M%S") - end - def variety - return "authority" - end - - # if the URL name has changed, then all requested_from: queries - # will break unless we update index for every event for every - # request linked to it - def reindex_requested_from - if self.changes.include?('url_name') - for info_request in self.info_requests - - for info_request_event in info_request.info_request_events - info_request_event.xapian_mark_needs_index - end - end - end - end - - # When name or short name is changed, also change the url name - def short_name=(short_name) - globalize.write(Globalize.locale, :short_name, short_name) - self[:short_name] = short_name - self.update_url_name - end - - def name=(name) - globalize.write(Globalize.locale, :name, name) - self[:name] = name - self.update_url_name + created_at.strftime("%Y%m%d%H%M%S") end - def update_url_name - self.url_name = MySociety::Format.simplify_url_part(self.short_or_long_name, 'body') + def variety + "authority" end - # Return the short name if present, or else long name - def short_or_long_name - if self.short_name.nil? || self.short_name.empty? # 'nil' can happen during construction - self.name.nil? ? "" : self.name - else - self.short_name - end + def law_only_short + eir_only? ? 'EIR' : 'FOI' end # Guess home page from the request email, or use explicit override, or nil # if not known. + # + # TODO: PublicBody#calculated_home_page would be a good candidate to cache + # in an instance variable def calculated_home_page if home_page && !home_page.empty? home_page[URI::regexp(%w(http https))] ? home_page : "http://#{home_page}" @@ -337,25 +301,8 @@ class PublicBody < ActiveRecord::Base end end - # Are all requests to this body under the Environmental Information Regulations? - def eir_only? - return self.has_tag?('eir_only') - end - def law_only_short - if self.eir_only? - return "EIR" - else - return "FOI" - end - end - - # Schools are allowed more time in holidays, so we change some wordings - def is_school? - return self.has_tag?('school') - end - # The "internal admin" is a special body for internal use. - def PublicBody.internal_admin_body + def self.internal_admin_body # Use find_by_sql to avoid the search being specific to a # locale, since url_name is a translated field: sql = "SELECT * FROM public_bodies WHERE url_name = 'internal_admin_authority'" @@ -379,13 +326,6 @@ class PublicBody < ActiveRecord::Base end end - def site_administration? - has_tag?('site_administration') - end - - class ImportCSVDryRun < StandardError - end - # Import from a string in CSV format. # Just tests things and returns messages if dry_run is true. # Returns an array of [array of errors, array of notes]. If there @@ -412,7 +352,7 @@ class PublicBody < ActiveRecord::Base # matching names won't work afterwards, and we'll create new bodies instead # of updating them bodies_by_name = {} - set_of_existing = Set.new() + set_of_existing = Set.new internal_admin_body_id = PublicBody.internal_admin_body.id I18n.with_locale(I18n.default_locale) do bodies = (tag.nil? || tag.empty?) ? PublicBody.find(:all, :include => :translations) : PublicBody.find_by_tag(tag) @@ -425,7 +365,7 @@ class PublicBody < ActiveRecord::Base end end - set_of_importing = Set.new() + set_of_importing = Set.new # Default values in case no field list is given field_names = { 'name' => 1, 'request_email' => 2 } line = 0 @@ -565,40 +505,32 @@ class PublicBody < ActiveRecord::Base # Does this user have the power of FOI officer for this body? def is_foi_officer?(user) user_domain = user.email_domain - our_domain = self.request_email_domain - - if user_domain.nil? or our_domain.nil? - return false - end - - return our_domain == user_domain - end - def foi_officer_domain_required - return self.request_email_domain - end + our_domain = request_email_domain - # Returns nil if configuration variable not set - def override_request_email - e = AlaveteliConfiguration::override_all_public_body_request_emails - e if e != "" + return false if user_domain.nil? or our_domain.nil? + our_domain == user_domain end def request_email - if override_request_email - override_request_email - else + if AlaveteliConfiguration::override_all_public_body_request_emails.blank? || read_attribute(:request_email).blank? read_attribute(:request_email) + else + AlaveteliConfiguration::override_all_public_body_request_emails end end # Domain name of the request email def request_email_domain - return PublicBody.extract_domain_from_email(self.request_email) + PublicBody.extract_domain_from_email(request_email) end + alias_method :foi_officer_domain_required, :request_email_domain + # Return the domain part of an email address, canonicalised and with common # extra UK Government server name parts removed. - def PublicBody.extract_domain_from_email(email) + # + # TODO: Extract to library class + def self.extract_domain_from_email(email) email =~ /@(.*)/ if $1.nil? return nil @@ -615,51 +547,53 @@ class PublicBody < ActiveRecord::Base return ret end + # TODO: Could this be defined as `sorted_versions.reverse`? def reverse_sorted_versions - self.versions.sort { |a,b| b.version <=> a.version } + versions.sort { |a,b| b.version <=> a.version } end + def sorted_versions - self.versions.sort { |a,b| a.version <=> b.version } + versions.sort { |a,b| a.version <=> b.version } end def has_notes? - return !self.notes.nil? && self.notes != "" + !notes.nil? && notes != "" end + + # TODO: Deprecate this method. Its only used in a couple of views so easy to + # update to just call PublicBody#notes def notes_as_html - self.notes + notes end def notes_without_html - # assume notes are reasonably behaved HTML, so just use simple regexp on this - @notes_without_html ||= (self.notes.nil? ? '' : self.notes.gsub(/<\/?[^>]*>/, "")) + # assume notes are reasonably behaved HTML, so just use simple regexp + # on this + @notes_without_html ||= (notes.nil? ? '' : notes.gsub(/<\/?[^>]*>/, "")) end def json_for_api - return { - :id => self.id, - :url_name => self.url_name, - :name => self.name, - :short_name => self.short_name, - # :request_email # we hide this behind a captcha, to stop people doing bulk requests easily - :created_at => self.created_at, - :updated_at => self.updated_at, - # don't add the history as some edit comments contain sensitive information + { + :id => id, + :url_name => url_name, + :name => name, + :short_name => short_name, + # :request_email # we hide this behind a captcha, to stop people + # doing bulk requests easily + :created_at => created_at, + :updated_at => updated_at, + # don't add the history as some edit comments contain sensitive + # information # :version, :last_edit_editor, :last_edit_comment - :home_page => self.calculated_home_page, - :notes => self.notes, - :publication_scheme => self.publication_scheme, - :tags => self.tag_array, + :home_page => calculated_home_page, + :notes => notes, + :publication_scheme => publication_scheme, + :tags => tag_array, } end def purge_in_cache - self.info_requests.each {|x| x.purge_in_cache} - end - - def for_admin_column - self.class.content_columns.map{|c| c unless %w(name last_edit_comment).include?(c.name)}.compact.each do |column| - yield(column.human_name, self.send(column.name), column.type.to_s, column.name) - end + info_requests.each { |x| x.purge_in_cache } end def self.where_clause_for_stats(minimum_requests, total_column) @@ -732,6 +666,7 @@ class PublicBody < ActiveRecord::Base 'y_max' => 100, 'totals' => original_totals} end + def self.popular_bodies(locale) # get some example searches and public bodies to display # either from config, or based on a (slow!) query if not set @@ -758,6 +693,70 @@ class PublicBody < ActiveRecord::Base return bodies end + # Methods to privatise + # -------------------------------------------------------------------------- + + # TODO: This could be removed by updating the default value (to '') of the + # `publication_scheme` column in the `public_body_translations` table. + # + # TODO: Can't actually deprecate this because spec/script/mailin_spec.rb:28 + # fails due to the deprecation notice output + def set_default_publication_scheme + # warn %q([DEPRECATION] PublicBody#set_default_publication_scheme will + # become a private method in 0.23).squish + + # Make sure publication_scheme gets the correct default value. + # (This would work automatically, were publication_scheme not a + # translated attribute) + self.publication_scheme = "" if publication_scheme.nil? + end + + # if the URL name has changed, then all requested_from: queries + # will break unless we update index for every event for every + # request linked to it + # + # TODO: Can't actually deprecate this because spec/script/mailin_spec.rb:28 + # fails due to the deprecation notice output + def reindex_requested_from + # warn %q([DEPRECATION] PublicBody#reindex_requested_from will become a + # private method in 0.23).squish + + if changes.include?('url_name') + info_requests.each do |info_request| + info_request.info_request_events.each do |info_request_event| + info_request_event.xapian_mark_needs_index + end + end + end + end + + # Methods to remove + # -------------------------------------------------------------------------- + + # Set the first letter on a public body or translation + def self.set_first_letter(instance) + warn %q([DEPRECATION] PublicBody.set_first_letter will be removed + in 0.23).squish + + unless instance.name.nil? or instance.name.empty? + # we use a regex to ensure it works with utf-8/multi-byte + first_letter = Unicode.upcase instance.name.scan(/^./mu)[0] + if first_letter != instance.first_letter + instance.first_letter = first_letter + end + end + end + + def calculate_cached_fields(t) + warn %q([DEPRECATION] PublicBody#calculate_cached_fields will be removed + in 0.23).squish + + PublicBody.set_first_letter(t) + short_long_name = t.name + short_long_name = t.short_name if t.short_name and !t.short_name.empty? + t.url_name = MySociety::Format.simplify_url_part(short_long_name, 'body') + end + private # Read an attribute value (without using locale fallbacks if the attribute is translated) @@ -773,13 +772,6 @@ class PublicBody < ActiveRecord::Base 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? |