aboutsummaryrefslogtreecommitdiffstats
path: root/app/models/public_body.rb
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/public_body.rb')
-rw-r--r--app/models/public_body.rb520
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?