aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/admin.js3
-rw-r--r--app/assets/javascripts/admin/category-order.js42
-rw-r--r--app/assets/stylesheets/admin.scss15
-rw-r--r--app/assets/stylesheets/responsive/_global_style.scss3
-rw-r--r--app/assets/stylesheets/responsive/_new_request_layout.scss5
-rw-r--r--app/assets/stylesheets/responsive/_user_layout.scss9
-rw-r--r--app/controllers/admin_general_controller.rb7
-rw-r--r--app/controllers/admin_public_body_categories_controller.rb84
-rw-r--r--app/controllers/admin_public_body_change_requests_controller.rb8
-rw-r--r--app/controllers/admin_public_body_controller.rb2
-rw-r--r--app/controllers/admin_public_body_headings_controller.rb113
-rw-r--r--app/controllers/admin_request_controller.rb25
-rw-r--r--app/controllers/admin_spam_addresses_controller.rb4
-rw-r--r--app/controllers/general_controller.rb2
-rw-r--r--app/controllers/health_checks_controller.rb16
-rw-r--r--app/controllers/public_body_controller.rb4
-rw-r--r--app/controllers/request_controller.rb33
-rw-r--r--app/controllers/user_controller.rb15
-rw-r--r--app/helpers/admin_public_body_category_helper.rb14
-rw-r--r--app/helpers/health_checks_helper.rb8
-rw-r--r--app/models/about_me_validator.rb2
-rw-r--r--app/models/censor_rule.rb75
-rw-r--r--app/models/change_email_validator.rb45
-rw-r--r--app/models/comment.rb66
-rw-r--r--app/models/contact_validator.rb4
-rw-r--r--app/models/holiday.rb24
-rw-r--r--app/models/info_request_batch.rb8
-rw-r--r--app/models/mail_server_log_done.rb3
-rw-r--r--app/models/outgoing_message.rb272
-rw-r--r--app/models/post_redirect.rb78
-rw-r--r--app/models/profile_photo.rb54
-rw-r--r--app/models/public_body.rb73
-rw-r--r--app/models/public_body_category.rb90
-rw-r--r--app/models/public_body_category/category_collection.rb54
-rw-r--r--app/models/public_body_category_link.rb34
-rw-r--r--app/models/public_body_heading.rb75
-rw-r--r--app/models/purge_request.rb19
-rw-r--r--app/models/raw_email.rb32
-rw-r--r--app/models/track_thing.rb410
-rw-r--r--app/models/user.rb356
-rw-r--r--app/models/user_info_request_sent_alert.rb24
-rw-r--r--app/views/admin_general/_admin_navbar.html.erb1
-rw-r--r--app/views/admin_general/index.html.erb8
-rw-r--r--app/views/admin_public_body/_tag_help.html.erb2
-rw-r--r--app/views/admin_public_body/import_csv.html.erb20
-rw-r--r--app/views/admin_public_body_categories/_category_list_item.html.erb5
-rw-r--r--app/views/admin_public_body_categories/_form.html.erb66
-rw-r--r--app/views/admin_public_body_categories/_heading_list.html.erb26
-rw-r--r--app/views/admin_public_body_categories/edit.html.erb30
-rw-r--r--app/views/admin_public_body_categories/index.html.erb28
-rw-r--r--app/views/admin_public_body_categories/new.html.erb21
-rw-r--r--app/views/admin_public_body_headings/_form.html.erb41
-rw-r--r--app/views/admin_public_body_headings/edit.html.erb30
-rw-r--r--app/views/admin_public_body_headings/new.html.erb21
-rw-r--r--app/views/admin_request/_incoming_message_actions.html.erb2
-rw-r--r--app/views/admin_spam_addresses/index.html.erb4
-rw-r--r--app/views/health_checks/index.html.erb12
-rw-r--r--app/views/public_body/_more_info.html.erb17
-rw-r--r--app/views/public_body/list.html.erb2
-rw-r--r--app/views/public_body/show.html.erb22
-rw-r--r--app/views/user/show.html.erb3
-rw-r--r--config/application.rb2
-rw-r--r--config/general.yml-example753
-rw-r--r--config/initializers/alaveteli.rb5
-rw-r--r--config/initializers/health_checks.rb23
-rw-r--r--config/routes.rb22
-rw-r--r--db/migrate/20140710094405_create_public_body_headings_and_categories.rb27
-rw-r--r--db/migrate/20140716131107_create_category_translation_tables.rb163
-rw-r--r--db/migrate/20140804120601_add_display_order_to_categories_and_headings.rb15
-rw-r--r--doc/CHANGES.md19
-rw-r--r--lib/alaveteli_localization.rb1
-rw-r--r--lib/category_and_heading_migrator.rb91
-rw-r--r--lib/configuration.rb5
-rw-r--r--lib/health_checks/checks/days_ago_check.rb28
-rw-r--r--lib/health_checks/health_checkable.rb28
-rw-r--r--lib/health_checks/health_checks.rb37
-rw-r--r--lib/public_body_categories.rb61
-rw-r--r--lib/public_body_categories_en.rb19
-rw-r--r--lib/routing_filters.rb8
-rwxr-xr-xscript/migrate-public-body-categories4
-rw-r--r--spec/controllers/admin_general_controller_spec.rb7
-rw-r--r--spec/controllers/admin_public_body_categories_controller_spec.rb192
-rw-r--r--spec/controllers/admin_public_body_change_requests_controller_spec.rb35
-rw-r--r--spec/controllers/admin_public_body_headings_controller_spec.rb240
-rw-r--r--spec/controllers/admin_spam_addresses_controller_spec.rb4
-rw-r--r--spec/controllers/health_checks_controller_spec.rb30
-rw-r--r--spec/controllers/public_body_controller_spec.rb22
-rw-r--r--spec/controllers/request_controller_spec.rb20
-rw-r--r--spec/controllers/user_controller_spec.rb12
-rw-r--r--spec/factories/outgoing_messages.rb19
-rw-r--r--spec/factories/public_body_categories.rb8
-rw-r--r--spec/factories/public_body_category_links.rb6
-rw-r--r--spec/factories/public_body_headings.rb5
-rw-r--r--spec/fixtures/info_request_events.yml4
-rw-r--r--spec/helpers/health_checks_helper_spec.rb15
-rw-r--r--spec/integration/localisation_spec.rb29
-rw-r--r--spec/lib/health_checks/checks/days_ago_check_spec.rb66
-rw-r--r--spec/lib/health_checks/health_checkable_spec.rb128
-rw-r--r--spec/lib/health_checks/health_checks_spec.rb77
-rw-r--r--spec/lib/public_body_categories_spec.rb42
-rw-r--r--spec/models/censor_rule_spec.rb33
-rw-r--r--spec/models/change_email_validator_spec.rb124
-rw-r--r--spec/models/info_request_spec.rb11
-rw-r--r--spec/models/public_body_category/category_collection_spec.rb81
-rw-r--r--spec/models/public_body_category_link_spec.rb53
-rw-r--r--spec/models/public_body_category_spec.rb65
-rw-r--r--spec/models/public_body_heading_spec.rb68
-rw-r--r--spec/models/public_body_spec.rb52
-rw-r--r--spec/spec_helper.rb8
109 files changed, 4243 insertions, 1095 deletions
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index 0b5d56525..4925a65a4 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,7 +1,10 @@
// ...
//= require jquery
//= require jquery.ui.tabs
+//= require jquery.ui.sortable
+//= require jquery.ui.effect-highlight
//= require admin/bootstrap-collapse
//= require admin/bootstrap-tab
//= require admin/admin
+//= require admin/category-order
//= require jquery_ujs
diff --git a/app/assets/javascripts/admin/category-order.js b/app/assets/javascripts/admin/category-order.js
new file mode 100644
index 000000000..3be82e46f
--- /dev/null
+++ b/app/assets/javascripts/admin/category-order.js
@@ -0,0 +1,42 @@
+$(function() {
+ $('.save-order').each(function(index){
+
+ // identify the elements that will work together
+ var save_button = $(this);
+ var save_notice = save_button.next();
+ var save_panel = save_button.parent();
+ var list_element = $(save_button.data('list-id'));
+ var endpoint = save_button.data('endpoint');
+
+ // on the first list change, show that there are unsaved changes
+ list_element.sortable({
+ update: function (event, ui) {
+ if (save_button.is('.disabled')){
+ save_button.removeClass("disabled");
+ save_notice.html(save_notice.data('unsaved-text'));
+ save_panel.effect('highlight', {}, 2000);
+ }
+ }
+ });
+ // on save, POST to endpoint
+ save_button.click(function(){
+ if (!save_button.is('.disabled')){
+ var data = list_element.sortable('serialize', {'attribute': 'data-id'});
+ var update_call = $.ajax({ data: data, type: 'POST', url: endpoint });
+
+ // on success, disable the save button again, and show success notice
+ update_call.done(function(msg) {
+ save_button.addClass('disabled');
+ save_panel.effect('highlight', {}, 2000);
+ save_notice.html(save_notice.data('success-text'));
+ })
+ // on failure, show error message
+ update_call.fail(function(jqXHR, msg) {
+ save_panel.effect('highlight', {'color': '#cc0000'}, 2000);
+ save_notice.html(save_notice.data('error-text') + jqXHR.responseText);
+ });
+ }
+ return false;
+ })
+ });
+});
diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss
index b0de2eb7b..863a6c808 100644
--- a/app/assets/stylesheets/admin.scss
+++ b/app/assets/stylesheets/admin.scss
@@ -27,6 +27,9 @@ body.admin {
}
.admin {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 20px;
@import "compass/css3";
@import "bootstrap";
@@ -47,6 +50,9 @@ body.admin {
.accordion-group {
border: none;
+ div {
+ clear: both;
+ }
}
.accordion-heading {
.btn {
@@ -104,5 +110,14 @@ body.admin {
width: 750px;
}
+ .save-notice {
+ display: inline-block;
+ padding-left: 1em;
+ }
+
+ .category-list-item {
+ padding: 3px 0;
+ }
+
}
diff --git a/app/assets/stylesheets/responsive/_global_style.scss b/app/assets/stylesheets/responsive/_global_style.scss
index 290591b5f..af25fb0b0 100644
--- a/app/assets/stylesheets/responsive/_global_style.scss
+++ b/app/assets/stylesheets/responsive/_global_style.scss
@@ -17,6 +17,9 @@ a {
&:focus {
color: #333333;
}
+ &:visited {
+ color: darken(#2688dc, 10%);
+ }
}
h1, h2, h3, h4, h5, h6 {
diff --git a/app/assets/stylesheets/responsive/_new_request_layout.scss b/app/assets/stylesheets/responsive/_new_request_layout.scss
index eec95ae77..aba4ffc29 100644
--- a/app/assets/stylesheets/responsive/_new_request_layout.scss
+++ b/app/assets/stylesheets/responsive/_new_request_layout.scss
@@ -29,6 +29,11 @@
@include lte-ie7 {
width: 26.188em;
}
+ /* Don't nest public body grid row in this context */
+ #public_body_show {
+ @include grid-row();
+ }
+
}
/* Hide some elements of the public body that aren't appropriate in this
diff --git a/app/assets/stylesheets/responsive/_user_layout.scss b/app/assets/stylesheets/responsive/_user_layout.scss
index 8087f978c..84ddbf562 100644
--- a/app/assets/stylesheets/responsive/_user_layout.scss
+++ b/app/assets/stylesheets/responsive/_user_layout.scss
@@ -1,2 +1,11 @@
/* Layout for user pages */
+#user_profile_search {
+ #search_form {
+ margin-top: 2rem;
+ }
+
+ #request_latest_status {
+ width: 300px;
+ }
+}
diff --git a/app/controllers/admin_general_controller.rb b/app/controllers/admin_general_controller.rb
index 753208c9a..f2414eeab 100644
--- a/app/controllers/admin_general_controller.rb
+++ b/app/controllers/admin_general_controller.rb
@@ -7,13 +7,6 @@
class AdminGeneralController < AdminController
def index
- # ensure we have a trailing slash
- current_uri = request.env['REQUEST_URI']
- if params[:suppress_redirect].nil? && !(current_uri =~ /\/$/)
- redirect_to admin_general_index_url + "/"
- return
- end
-
# Overview counts of things
@public_body_count = PublicBody.count
diff --git a/app/controllers/admin_public_body_categories_controller.rb b/app/controllers/admin_public_body_categories_controller.rb
new file mode 100644
index 000000000..fda09fa4a
--- /dev/null
+++ b/app/controllers/admin_public_body_categories_controller.rb
@@ -0,0 +1,84 @@
+class AdminPublicBodyCategoriesController < AdminController
+ def index
+ @locale = self.locale_from_params
+ @category_headings = PublicBodyHeading.all
+ @without_heading = PublicBodyCategory.without_headings
+ end
+
+ def new
+ @category = PublicBodyCategory.new
+ render :formats => [:html]
+ end
+
+ def edit
+ @category = PublicBodyCategory.find(params[:id])
+ @tagged_public_bodies = PublicBody.find_by_tag(@category.category_tag)
+ end
+
+ def update
+ @category = PublicBodyCategory.find(params[:id])
+ @tagged_public_bodies = PublicBody.find_by_tag(@category.category_tag)
+ heading_ids = []
+
+ I18n.with_locale(I18n.default_locale) do
+ if params[:public_body_category][:category_tag] && PublicBody.find_by_tag(@category.category_tag).count > 0 && @category.category_tag != params[:public_body_category][:category_tag]
+ flash[:notice] = 'There are authorities associated with this category, so the tag can\'t be renamed'
+ else
+ if params[:headings]
+ heading_ids = params[:headings].values
+ removed_headings = @category.public_body_heading_ids - heading_ids
+ added_headings = heading_ids - @category.public_body_heading_ids
+
+ unless removed_headings.empty?
+ # remove the link objects
+ deleted_links = PublicBodyCategoryLink.where(
+ :public_body_category_id => @category.id,
+ :public_body_heading_id => [removed_headings]
+ )
+ deleted_links.delete_all
+
+ #fix the category object
+ @category.public_body_heading_ids = heading_ids
+ end
+
+ added_headings.each do |heading_id|
+ PublicBodyHeading.find(heading_id).add_category(@category)
+ end
+ end
+
+ if @category.update_attributes(params[:public_body_category])
+ flash[:notice] = 'Category was successfully updated.'
+ end
+ end
+
+ render :action => 'edit'
+ end
+ end
+
+ def create
+ I18n.with_locale(I18n.default_locale) do
+ @category = PublicBodyCategory.new(params[:public_body_category])
+ if @category.save
+ if params[:headings]
+ params[:headings].values.each do |heading_id|
+ PublicBodyHeading.find(heading_id).add_category(@category)
+ end
+ end
+ flash[:notice] = 'Category was successfully created.'
+ redirect_to admin_categories_path
+ else
+ render :action => 'new'
+ end
+ end
+ end
+
+ def destroy
+ @locale = self.locale_from_params
+ I18n.with_locale(@locale) do
+ category = PublicBodyCategory.find(params[:id])
+ category.destroy
+ flash[:notice] = "Category was successfully destroyed."
+ redirect_to admin_categories_path
+ end
+ end
+end
diff --git a/app/controllers/admin_public_body_change_requests_controller.rb b/app/controllers/admin_public_body_change_requests_controller.rb
index d76cdc0e5..6ff03a2bd 100644
--- a/app/controllers/admin_public_body_change_requests_controller.rb
+++ b/app/controllers/admin_public_body_change_requests_controller.rb
@@ -7,8 +7,12 @@ class AdminPublicBodyChangeRequestsController < AdminController
def update
@change_request = PublicBodyChangeRequest.find(params[:id])
@change_request.close!
- @change_request.send_response(params[:subject], params[:response])
- flash[:notice] = 'The change request has been closed and the user has been notified'
+ if params[:subject] && params[:response]
+ @change_request.send_response(params[:subject], params[:response])
+ flash[:notice] = 'The change request has been closed and the user has been notified'
+ else
+ flash[:notice] = 'The change request has been closed'
+ end
redirect_to admin_general_index_path
end
diff --git a/app/controllers/admin_public_body_controller.rb b/app/controllers/admin_public_body_controller.rb
index 120419a27..f7a80476c 100644
--- a/app/controllers/admin_public_body_controller.rb
+++ b/app/controllers/admin_public_body_controller.rb
@@ -4,8 +4,6 @@
# Copyright (c) 2007 UK Citizens Online Democracy. All rights reserved.
# Email: hello@mysociety.org; WWW: http://www.mysociety.org/
-require "public_body_categories"
-
class AdminPublicBodyController < AdminController
def index
list
diff --git a/app/controllers/admin_public_body_headings_controller.rb b/app/controllers/admin_public_body_headings_controller.rb
new file mode 100644
index 000000000..c7c80e802
--- /dev/null
+++ b/app/controllers/admin_public_body_headings_controller.rb
@@ -0,0 +1,113 @@
+class AdminPublicBodyHeadingsController < AdminController
+
+ def edit
+ @heading = PublicBodyHeading.find(params[:id])
+ render :formats => [:html]
+ end
+
+ def update
+ I18n.with_locale(I18n.default_locale) do
+ @heading = PublicBodyHeading.find(params[:id])
+ if @heading.update_attributes(params[:public_body_heading])
+ flash[:notice] = 'Category heading was successfully updated.'
+ end
+ render :action => 'edit'
+ end
+ end
+
+ def reorder
+ transaction = reorder_headings(params[:headings])
+ if transaction[:success]
+ render :nothing => true, :status => :ok
+ else
+ render :text => transaction[:error], :status => :unprocessable_entity
+ end
+ end
+
+ def reorder_categories
+ transaction = reorder_categories_for_heading(params[:id], params[:categories])
+ if transaction[:success]
+ render :nothing => true, :status => :ok and return
+ else
+ render :text => transaction[:error], :status => :unprocessable_entity
+ end
+ end
+
+ def new
+ @heading = PublicBodyHeading.new
+ render :formats => [:html]
+ end
+
+ def create
+ I18n.with_locale(I18n.default_locale) do
+ @heading = PublicBodyHeading.new(params[:public_body_heading])
+ if @heading.save
+ flash[:notice] = 'Category heading was successfully created.'
+ redirect_to admin_categories_url
+ else
+ render :action => 'new'
+ end
+ end
+ end
+
+ def destroy
+ @locale = self.locale_from_params()
+ I18n.with_locale(@locale) do
+ heading = PublicBodyHeading.find(params[:id])
+
+ if heading.public_body_categories.count > 0
+ flash[:notice] = "There are categories associated with this heading, so can't destroy it"
+ redirect_to edit_admin_heading_url(heading)
+ return
+ end
+
+ heading.destroy
+ flash[:notice] = "Category heading was successfully destroyed."
+ redirect_to admin_categories_url
+ end
+ end
+
+ protected
+
+ def reorder_headings(headings)
+ error = nil
+ ActiveRecord::Base.transaction do
+ headings.each_with_index do |heading_id, index|
+ begin
+ heading = PublicBodyHeading.find(heading_id)
+ rescue ActiveRecord::RecordNotFound => e
+ error = e.message
+ raise ActiveRecord::Rollback
+ end
+ heading.display_order = index
+ unless heading.save
+ error = heading.errors.full_messages.join(",")
+ raise ActiveRecord::Rollback
+ end
+ end
+ end
+ { :success => error.nil? ? true : false, :error => error }
+ end
+
+ def reorder_categories_for_heading(heading_id, categories)
+ error = nil
+ ActiveRecord::Base.transaction do
+ categories.each_with_index do |category_id, index|
+ conditions = { :public_body_category_id => category_id,
+ :public_body_heading_id => heading_id }
+ link = PublicBodyCategoryLink.where(conditions).first
+ unless link
+ error = "Couldn't find PublicBodyCategoryLink for category #{category_id}, heading #{heading_id}"
+ raise ActiveRecord::Rollback
+ end
+ link.category_display_order = index
+ unless link.save
+ error = link.errors.full_messages.join(",")
+ raise ActiveRecord::Rollback
+ end
+ end
+ end
+ { :success => error.nil? ? true : false, :error => error }
+ end
+
+end
diff --git a/app/controllers/admin_request_controller.rb b/app/controllers/admin_request_controller.rb
index 21120e4ad..8f023bf12 100644
--- a/app/controllers/admin_request_controller.rb
+++ b/app/controllers/admin_request_controller.rb
@@ -37,7 +37,30 @@ class AdminRequestController < AdminController
def resend
@outgoing_message = OutgoingMessage.find(params[:outgoing_message_id])
- @outgoing_message.resend_message
+ @outgoing_message.prepare_message_for_resend
+
+ mail_message = case @outgoing_message.message_type
+ when 'initial_request'
+ OutgoingMailer.initial_request(
+ @outgoing_message.info_request,
+ @outgoing_message
+ ).deliver
+ when 'followup'
+ OutgoingMailer.followup(
+ @outgoing_message.info_request,
+ @outgoing_message,
+ @outgoing_message.incoming_message_followup
+ ).deliver
+ else
+ raise "Message id #{id} has type '#{message_type}' which cannot be resent"
+ end
+
+ @outgoing_message.record_email_delivery(
+ mail_message.to_addrs.join(', '),
+ mail_message.message_id,
+ 'resent'
+ )
+
flash[:notice] = "Outgoing message resent"
redirect_to admin_request_show_url(@outgoing_message.info_request)
end
diff --git a/app/controllers/admin_spam_addresses_controller.rb b/app/controllers/admin_spam_addresses_controller.rb
index f5c7e93da..fff7e2a4a 100644
--- a/app/controllers/admin_spam_addresses_controller.rb
+++ b/app/controllers/admin_spam_addresses_controller.rb
@@ -10,7 +10,7 @@ class AdminSpamAddressesController < AdminController
if @spam_address.save
notice = "#{ @spam_address.email } has been added to the spam addresses list"
- redirect_to spam_addresses_path, :notice => notice
+ redirect_to admin_spam_addresses_path, :notice => notice
else
@spam_addresses = SpamAddress.all
render :index
@@ -21,7 +21,7 @@ class AdminSpamAddressesController < AdminController
@spam_address = SpamAddress.find(params[:id])
@spam_address.destroy
notice = "#{ @spam_address.email } has been removed from the spam addresses list"
- redirect_to spam_addresses_path, :notice => notice
+ redirect_to admin_spam_addresses_path, :notice => notice
end
end
diff --git a/app/controllers/general_controller.rb b/app/controllers/general_controller.rb
index 158492eb2..2c8abbaf4 100644
--- a/app/controllers/general_controller.rb
+++ b/app/controllers/general_controller.rb
@@ -32,7 +32,7 @@ class GeneralController < ApplicationController
if !content.empty?
@data = XmlSimple.xml_in(content)
@channel = @data['channel'][0]
- @blog_items = @channel['item']
+ @blog_items = @channel.fetch('item') { [] }
@feed_autodetect = [{:url => @feed_url, :title => "#{site_name} blog"}]
end
end
diff --git a/app/controllers/health_checks_controller.rb b/app/controllers/health_checks_controller.rb
new file mode 100644
index 000000000..473a1aacc
--- /dev/null
+++ b/app/controllers/health_checks_controller.rb
@@ -0,0 +1,16 @@
+class HealthChecksController < ApplicationController
+
+ def index
+ @health_checks = HealthChecks.all
+
+ respond_to do |format|
+ if HealthChecks.ok?
+ format.html { render :action => :index, :layout => false }
+ else
+ format.html { render :action => :index, :layout => false , :status => 500 }
+ end
+ end
+
+ end
+
+end
diff --git a/app/controllers/public_body_controller.rb b/app/controllers/public_body_controller.rb
index d2c84d820..e64644a1b 100644
--- a/app/controllers/public_body_controller.rb
+++ b/app/controllers/public_body_controller.rb
@@ -111,7 +111,7 @@ class PublicBodyController < ApplicationController
if @tag.nil? || @tag == 'all'
@tag = 'all'
elsif @tag == 'other'
- category_list = PublicBodyCategories.get.tags.map{ |c| %Q('#{ c }') }.join(",")
+ category_list = PublicBodyCategory.get.tags.map{ |c| %Q('#{ c }') }.join(",")
where_condition += base_tag_condition + " AND has_tag_string_tags.name in (#{category_list})) = 0"
elsif @tag.scan(/./mu).size == 1
@tag = Unicode.upcase(@tag)
@@ -132,7 +132,7 @@ class PublicBodyController < ApplicationController
elsif @tag.size == 1
@description = _("beginning with ‘{{first_letter}}’", :first_letter => @tag)
else
- category_name = PublicBodyCategories.get.by_tag[@tag]
+ category_name = PublicBodyCategory.get.by_tag[@tag]
if category_name.nil?
@description = _("matching the tag ‘{{tag_name}}’", :tag_name => @tag)
else
diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb
index 3fa0ef0ce..9e2c291dc 100644
--- a/app/controllers/request_controller.rb
+++ b/app/controllers/request_controller.rb
@@ -365,8 +365,21 @@ class RequestController < ApplicationController
end
# This automatically saves dependent objects, such as @outgoing_message, in the same transaction
@info_request.save!
- # TODO: send_message needs the database id, so we send after saving, which isn't ideal if the request broke here.
- @outgoing_message.send_message
+
+ # TODO: Sending the message needs the database id, so we send after
+ # saving, which isn't ideal if the request broke here.
+ if @outgoing_message.sendable?
+ mail_message = OutgoingMailer.initial_request(
+ @outgoing_message.info_request,
+ @outgoing_message
+ ).deliver
+
+ @outgoing_message.record_email_delivery(
+ mail_message.to_addrs.join(', '),
+ mail_message.message_id
+ )
+ end
+
flash[:notice] = _("<p>Your {{law_used_full}} request has been <strong>sent on its way</strong>!</p>
<p><strong>We will email you</strong> when there is a response, or after {{late_number_of_days}} working days if the authority still hasn't
replied by then.</p>
@@ -668,13 +681,27 @@ class RequestController < ApplicationController
end
# Send a follow up message
- @outgoing_message.send_message
+ @outgoing_message.sendable?
+
+ mail_message = OutgoingMailer.followup(
+ @outgoing_message.info_request,
+ @outgoing_message,
+ @outgoing_message.incoming_message_followup
+ ).deliver
+
+ @outgoing_message.record_email_delivery(
+ mail_message.to_addrs.join(', '),
+ mail_message.message_id
+ )
+
@outgoing_message.save!
+
if @outgoing_message.what_doing == 'internal_review'
flash[:notice] = _("Your internal review request has been sent on its way.")
else
flash[:notice] = _("Your follow up message has been sent on its way.")
end
+
redirect_to request_url(@info_request)
end
else
diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb
index f23343ddb..baeaab18a 100644
--- a/app/controllers/user_controller.rb
+++ b/app/controllers/user_controller.rb
@@ -49,13 +49,28 @@ class UserController < ApplicationController
# TODO: really should just use SQL query here rather than Xapian.
if @show_requests
begin
+
+ request_states = @display_user.info_requests.pluck(:described_state).uniq
+
+ option_item = Struct.new(:value, :text)
+ @request_states = request_states.map do |state|
+ option_item.new(state, InfoRequest.get_status_description(state))
+ end
+
requests_query = 'requested_by:' + @display_user.url_name
comments_query = 'commented_by:' + @display_user.url_name
if !params[:user_query].nil?
requests_query += " " + params[:user_query]
comments_query += " " + params[:user_query]
@match_phrase = _("{{search_results}} matching '{{query}}'", :search_results => "", :query => params[:user_query])
+
+ unless params[:request_latest_status].blank?
+ requests_query << ' latest_status:' << params[:request_latest_status]
+ comments_query << ' latest_status:' << params[:request_latest_status]
+ @match_phrase << _(" filtered by status: '{{status}}'", :status => params[:request_latest_status])
+ end
end
+
@xapian_requests = perform_search([InfoRequestEvent], requests_query, 'newest', 'request_collapse')
@xapian_comments = perform_search([InfoRequestEvent], comments_query, 'newest', nil)
diff --git a/app/helpers/admin_public_body_category_helper.rb b/app/helpers/admin_public_body_category_helper.rb
new file mode 100644
index 000000000..9c5e5cc5e
--- /dev/null
+++ b/app/helpers/admin_public_body_category_helper.rb
@@ -0,0 +1,14 @@
+module AdminPublicBodyCategoryHelper
+ def heading_is_selected?(heading)
+ if params[:headings]
+ if params[:headings]["heading_#{heading.id}"]
+ return true
+ else
+ return false
+ end
+ elsif @category.public_body_headings.include?(heading)
+ return true
+ end
+ false
+ end
+end
diff --git a/app/helpers/health_checks_helper.rb b/app/helpers/health_checks_helper.rb
new file mode 100644
index 000000000..f5769a9ba
--- /dev/null
+++ b/app/helpers/health_checks_helper.rb
@@ -0,0 +1,8 @@
+module HealthChecksHelper
+
+ def check_status(check)
+ style = check.ok? ? {} : "color: red"
+ content_tag(:b, check.message, :style => style)
+ end
+
+end
diff --git a/app/models/about_me_validator.rb b/app/models/about_me_validator.rb
index 7df70fb61..8c24cfd67 100644
--- a/app/models/about_me_validator.rb
+++ b/app/models/about_me_validator.rb
@@ -21,7 +21,7 @@ class AboutMeValidator
private
def length_of_about_me
- if !self.about_me.blank? && self.about_me.size > 500
+ if !about_me.blank? && about_me.size > 500
errors.add(:about_me, _("Please keep it shorter than 500 characters"))
end
end
diff --git a/app/models/censor_rule.rb b/app/models/censor_rule.rb
index 3c5c77563..3b5c2d805 100644
--- a/app/models/censor_rule.rb
+++ b/app/models/censor_rule.rb
@@ -26,18 +26,46 @@ class CensorRule < ActiveRecord::Base
belongs_to :user
belongs_to :public_body
- # a flag to allow the require_user_request_or_public_body validation to be skipped
+ # a flag to allow the require_user_request_or_public_body
+ # validation to be skipped
attr_accessor :allow_global
- validate :require_user_request_or_public_body, :unless => proc{ |rule| rule.allow_global == true }
- validate :require_valid_regexp, :if => proc{ |rule| rule.regexp? == true }
- validates_presence_of :text
- scope :global, {:conditions => {:info_request_id => nil,
- :user_id => nil,
- :public_body_id => nil}}
+ validate :require_user_request_or_public_body, :unless => proc { |rule| rule.allow_global == true }
+ validate :require_valid_regexp, :if => proc { |rule| rule.regexp? == true }
+
+ validates_presence_of :text,
+ :replacement,
+ :last_edit_comment,
+ :last_edit_editor
+
+ scope :global, { :conditions => { :info_request_id => nil,
+ :user_id => nil,
+ :public_body_id => nil } }
+
+ def apply_to_text!(text_to_censor)
+ return nil if text_to_censor.nil?
+ text_to_censor.gsub!(to_replace, replacement)
+ end
+
+ def apply_to_binary!(binary_to_censor)
+ return nil if binary_to_censor.nil?
+ binary_to_censor.gsub!(to_replace) { |match| match.gsub(/./, 'x') }
+ end
+
+ def for_admin_column
+ self.class.content_columns.each do |column|
+ yield(column.human_name, send(column.name), column.type.to_s, column.name)
+ end
+ end
+
+ def is_global?
+ info_request_id.nil? && user_id.nil? && public_body_id.nil?
+ end
+
+ private
def require_user_request_or_public_body
- if self.info_request.nil? && self.user.nil? && self.public_body.nil?
+ if info_request.nil? && user.nil? && public_body.nil?
[:info_request, :user, :public_body].each do |a|
errors.add(a, "Rule must apply to an info request, a user or a body")
end
@@ -46,41 +74,18 @@ class CensorRule < ActiveRecord::Base
def require_valid_regexp
begin
- self.make_regexp()
+ make_regexp
rescue RegexpError => e
errors.add(:text, e.message)
end
end
def make_regexp
- return Regexp.new(self.text, Regexp::MULTILINE)
- end
-
- def apply_to_text!(text)
- if text.nil?
- return nil
- end
- to_replace = regexp? ? self.make_regexp() : self.text
- text.gsub!(to_replace, self.replacement)
- end
-
- def apply_to_binary!(binary)
- if binary.nil?
- return nil
- end
- to_replace = regexp? ? self.make_regexp() : self.text
- binary.gsub!(to_replace){ |match| match.gsub(/./, 'x') }
+ Regexp.new(text, Regexp::MULTILINE)
end
- def for_admin_column
- self.class.content_columns.each do |column|
- yield(column.human_name, self.send(column.name), column.type.to_s, column.name)
- end
- end
-
- def is_global?
- return true if (info_request_id.nil? && user_id.nil? && public_body_id.nil?)
- return false
+ def to_replace
+ regexp? ? make_regexp : text
end
end
diff --git a/app/models/change_email_validator.rb b/app/models/change_email_validator.rb
index 5cc13d4c2..7ee6654bb 100644
--- a/app/models/change_email_validator.rb
+++ b/app/models/change_email_validator.rb
@@ -7,11 +7,22 @@
class ChangeEmailValidator
include ActiveModel::Validations
- attr_accessor :old_email, :new_email, :password, :user_circumstance, :logged_in_user
+ attr_accessor :old_email,
+ :new_email,
+ :password,
+ :user_circumstance,
+ :logged_in_user
+
+ validates_presence_of :old_email,
+ :message => N_("Please enter your old email address")
+
+ validates_presence_of :new_email,
+ :message => N_("Please enter your new email address")
+
+ validates_presence_of :password,
+ :message => N_("Please enter your password"),
+ :unless => :changing_email
- validates_presence_of :old_email, :message => N_("Please enter your old email address")
- validates_presence_of :new_email, :message => N_("Please enter your new email address")
- validates_presence_of :password, :message => N_("Please enter your password"), :unless => :changing_email
validate :password_and_format_of_email
def initialize(attributes = {})
@@ -20,7 +31,6 @@ class ChangeEmailValidator
end
end
-
def changing_email
self.user_circumstance == 'change_email'
end
@@ -28,22 +38,33 @@ class ChangeEmailValidator
private
def password_and_format_of_email
- if !self.old_email.blank? && !MySociety::Validate.is_valid_email(self.old_email)
- errors.add(:old_email, _("Old email doesn't look like a valid address"))
- end
+ check_email_is_present_and_valid(:old_email)
if errors[:old_email].blank?
- if self.old_email.downcase != self.logged_in_user.email.downcase
+ if !email_belongs_to_user?(old_email)
errors.add(:old_email, _("Old email address isn't the same as the address of the account you are logged in with"))
- elsif (!self.changing_email) && (!self.logged_in_user.has_this_password?(self.password))
+ elsif !changing_email && !correct_password?
if errors[:password].blank?
errors.add(:password, _("Password is not correct"))
end
end
end
- if !self.new_email.blank? && !MySociety::Validate.is_valid_email(self.new_email)
- errors.add(:new_email, _("New email doesn't look like a valid address"))
+ check_email_is_present_and_valid(:new_email)
+ end
+
+ def check_email_is_present_and_valid(email)
+ if !send(email).blank? && !MySociety::Validate.is_valid_email(send(email))
+ errors.add(email, _("#{ email.to_s.humanize } doesn't look like a valid address"))
end
end
+
+ def email_belongs_to_user?(email)
+ email.downcase == logged_in_user.email.downcase
+ end
+
+ def correct_password?
+ logged_in_user.has_this_password?(password)
+ end
+
end
diff --git a/app/models/comment.rb b/app/models/comment.rb
index a62c086d5..a286aa1f5 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -28,15 +28,32 @@ class Comment < ActiveRecord::Base
#validates_presence_of :user # breaks during construction of new ones :(
validates_inclusion_of :comment_type, :in => [ 'request' ]
- validate :body_of_comment
+ validate :check_body_has_content,
+ :check_body_uses_mixed_capitals
+
+ after_save :event_xapian_update
+
+ # When posting a new comment, use this to check user hasn't double
+ # submitted.
+ def self.find_existing(info_request_id, body)
+ # TODO: can add other databases here which have regexp_replace
+ if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
+ # Exclude spaces from the body comparison using regexp_replace
+ regex_replace_sql = "regexp_replace(body, '[[:space:]]', '', 'g') = regexp_replace(?, '[[:space:]]', '', 'g')"
+ Comment.where(["info_request_id = ? AND #{ regex_replace_sql }", info_request_id, body ]).first
+ else
+ # For other databases (e.g. SQLite) not the end of the world being
+ # space-sensitive for this check
+ Comment.where(:info_request_id => info_request_id, :body => body).first
+ end
+ end
def body
ret = read_attribute(:body)
- if ret.nil?
- return ret
- end
+ return ret if ret.nil?
ret = ret.strip
- ret = ret.gsub(/(?:\n\s*){2,}/, "\n\n") # remove excess linebreaks that unnecessarily space it out
+ # remove excess linebreaks that unnecessarily space it out
+ ret = ret.gsub(/(?:\n\s*){2,}/, "\n\n")
ret
end
@@ -45,48 +62,39 @@ class Comment < ActiveRecord::Base
end
# So when takes changes it updates, or when made invisble it vanishes
- after_save :event_xapian_update
def event_xapian_update
- for event in self.info_request_events
- event.xapian_mark_needs_index
- end
+ info_request_events.each { |event| event.xapian_mark_needs_index }
end
# Return body for display as HTML
def get_body_for_html_display
- text = self.body.strip
+ text = body.strip
text = CGI.escapeHTML(text)
text = MySociety::Format.make_clickable(text, :contract => 1)
text = text.gsub(/\n/, '<br>')
- return text.html_safe
- end
-
- # When posting a new comment, use this to check user hasn't double submitted.
- def Comment.find_existing(info_request_id, body)
- # TODO: can add other databases here which have regexp_replace
- if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
- # Exclude spaces from the body comparison using regexp_replace
- return Comment.find(:first, :conditions => [ "info_request_id = ? and regexp_replace(body, '[[:space:]]', '', 'g') = regexp_replace(?, '[[:space:]]', '', 'g')", info_request_id, body ])
- else
- # For other databases (e.g. SQLite) not the end of the world being space-sensitive for this check
- return Comment.find(:first, :conditions => [ "info_request_id = ? and body = ?", info_request_id, body ])
- end
+ text.html_safe
end
def for_admin_column
self.class.content_columns.each do |column|
- yield(column.human_name, self.send(column.name), column.type.to_s, column.name)
+ yield(column.human_name, send(column.name), column.type.to_s, column.name)
end
end
- private
+ private
- def body_of_comment
- if self.body.empty? || self.body =~ /^\s+$/
+ def check_body_has_content
+ if body.empty? || body =~ /^\s+$/
errors.add(:body, _("Please enter your annotation"))
end
- if !MySociety::Validate.uses_mixed_capitals(self.body)
- errors.add(:body, _('Please write your annotation using a mixture of capital and lower case letters. This makes it easier for others to read.'))
+ end
+
+ def check_body_uses_mixed_capitals
+ unless MySociety::Validate.uses_mixed_capitals(body)
+ msg = 'Please write your annotation using a mixture of capital and ' \
+ 'lower case letters. This makes it easier for others to read.'
+ errors.add(:body, _(msg))
end
end
+
end
diff --git a/app/models/contact_validator.rb b/app/models/contact_validator.rb
index e9a6e491c..8d7e4ff08 100644
--- a/app/models/contact_validator.rb
+++ b/app/models/contact_validator.rb
@@ -24,6 +24,8 @@ class ContactValidator
private
def email_format
- errors.add(:email, _("Email doesn't look like a valid address")) unless MySociety::Validate.is_valid_email(self.email)
+ unless MySociety::Validate.is_valid_email(email)
+ errors.add(:email, _("Email doesn't look like a valid address"))
+ end
end
end
diff --git a/app/models/holiday.rb b/app/models/holiday.rb
index 3076cc0fd..4c4941589 100644
--- a/app/models/holiday.rb
+++ b/app/models/holiday.rb
@@ -22,15 +22,15 @@
class Holiday < ActiveRecord::Base
- def Holiday.holidays
- @@holidays ||= self.all.collect { |h| h.day }.to_set
+ def self.holidays
+ @@holidays ||= all.collect { |h| h.day }.to_set
end
- def Holiday.weekend_or_holiday?(date)
+ def self.weekend_or_holiday?(date)
date.wday == 0 || date.wday == 6 || Holiday.holidays.include?(date)
end
- def Holiday.due_date_from(start_date, days, type_of_days)
+ def self.due_date_from(start_date, days, type_of_days)
case type_of_days
when "working"
Holiday.due_date_from_working_days(start_date, days)
@@ -44,14 +44,14 @@ class Holiday < ActiveRecord::Base
# Calculate the date on which a request made on a given date falls due when
# days are given in working days
# i.e. it is due by the end of that day.
- def Holiday.due_date_from_working_days(start_date, working_days)
+ def self.due_date_from_working_days(start_date, working_days)
# convert date/times into dates
start_date = start_date.to_date
- # Count forward the number of working days. We start with today as "day zero". The
- # first of the full working days is the next day. We return the
- # date of the last of the number of working days.
-
+ # Count forward the number of working days. We start with today as "day
+ # zero". The first of the full working days is the next day. We return
+ # the date of the last of the number of working days.
+ #
# This response for example of a public authority complains that we had
# it wrong. We didn't (even thought I changed the code for a while,
# it's changed back now). A day is a day, our lawyer tells us.
@@ -71,9 +71,9 @@ class Holiday < ActiveRecord::Base
# Calculate the date on which a request made on a given date falls due when
# the days are given in calendar days (rather than working days)
- # If the due date falls on a weekend or a holiday then the due date is the next
- # weekday that isn't a holiday.
- def Holiday.due_date_from_calendar_days(start_date, days)
+ # If the due date falls on a weekend or a holiday then the due date is the
+ # next weekday that isn't a holiday.
+ def self.due_date_from_calendar_days(start_date, days)
# convert date/times into dates
start_date = start_date.to_date
diff --git a/app/models/info_request_batch.rb b/app/models/info_request_batch.rb
index d7c5eb9af..8a5ebeaba 100644
--- a/app/models/info_request_batch.rb
+++ b/app/models/info_request_batch.rb
@@ -46,7 +46,13 @@ class InfoRequestBatch < ActiveRecord::Base
self.sent_at = Time.now
self.save!
end
- created.each{ |info_request| info_request.outgoing_messages.first.send_message }
+ created.each do |info_request|
+ outgoing_message = info_request.outgoing_messages.first
+
+ outgoing_message.sendable?
+ mail_message = OutgoingMailer.initial_request(outgoing_message.info_request, outgoing_message).deliver
+ outgoing_message.record_email_delivery(mail_message.to_addrs.join(', '), mail_message.message_id)
+ end
return unrequestable
end
diff --git a/app/models/mail_server_log_done.rb b/app/models/mail_server_log_done.rb
index 222b072c5..1bbb23ac4 100644
--- a/app/models/mail_server_log_done.rb
+++ b/app/models/mail_server_log_done.rb
@@ -17,6 +17,3 @@
class MailServerLogDone < ActiveRecord::Base
has_many :mail_server_logs
end
-
-
-
diff --git a/app/models/outgoing_message.rb b/app/models/outgoing_message.rb
index 160f69d0b..fa83c7381 100644
--- a/app/models/outgoing_message.rb
+++ b/app/models/outgoing_message.rb
@@ -28,106 +28,115 @@ class OutgoingMessage < ActiveRecord::Base
extend MessageProminence
include Rails.application.routes.url_helpers
include LinkToHelper
- self.default_url_options[:host] = AlaveteliConfiguration::domain
- # https links in emails if forcing SSL
- if AlaveteliConfiguration::force_ssl
- self.default_url_options[:protocol] = "https"
- end
-
- strip_attributes!
- has_prominence
+ # To override the default letter
+ attr_accessor :default_letter
- belongs_to :info_request
validates_presence_of :info_request
-
validates_inclusion_of :status, :in => ['ready', 'sent', 'failed']
- validates_inclusion_of :message_type, :in => ['initial_request', 'followup' ] #, 'complaint']
+ validates_inclusion_of :message_type, :in => ['initial_request', 'followup']
validate :format_of_body
+ belongs_to :info_request
belongs_to :incoming_message_followup, :foreign_key => 'incoming_message_followup_id', :class_name => 'IncomingMessage'
# can have many events, for items which were resent by site admin e.g. if
# contact address changed
has_many :info_request_events
- # To override the default letter
- attr_accessor :default_letter
-
+ after_initialize :set_default_letter
+ after_save :purge_in_cache
# reindex if body text is edited (e.g. by admin interface)
after_update :xapian_reindex_after_update
- def xapian_reindex_after_update
- if self.changes.include?('body')
- for info_request_event in self.info_request_events
- info_request_event.xapian_mark_needs_index
- end
- end
- end
- after_initialize :set_default_letter
+ strip_attributes!
+ has_prominence
- # How the default letter starts and ends
- def get_salutation
- if self.info_request.is_batch_request_template?
- return OutgoingMessage.placeholder_salutation
- end
- ret = ""
- if self.message_type == 'followup' && !self.incoming_message_followup.nil? && !self.incoming_message_followup.safe_mail_from.nil? && self.incoming_message_followup.valid_to_reply_to?
- ret = ret + OutgoingMailer.name_for_followup(self.info_request, self.incoming_message_followup)
- else
- return OutgoingMessage.default_salutation(self.info_request.public_body)
- end
- salutation = _("Dear {{public_body_name}},", :public_body_name => ret)
+ self.default_url_options[:host] = AlaveteliConfiguration.domain
+
+ # https links in emails if forcing SSL
+ if AlaveteliConfiguration::force_ssl
+ self.default_url_options[:protocol] = "https"
end
- def OutgoingMessage.default_salutation(public_body)
+ def self.default_salutation(public_body)
_("Dear {{public_body_name}},", :public_body_name => public_body.name)
end
- def OutgoingMessage.placeholder_salutation
+ def self.placeholder_salutation
_("Dear [Authority name],")
end
- def OutgoingMessage.fill_in_salutation(body, public_body)
+ def self.fill_in_salutation(body, public_body)
body.gsub(placeholder_salutation, default_salutation(public_body))
end
+ # How the default letter starts and ends
+ def get_salutation
+ if info_request.is_batch_request_template?
+ return OutgoingMessage.placeholder_salutation
+ end
+
+ ret = ""
+ if message_type == 'followup' &&
+ !incoming_message_followup.nil? &&
+ !incoming_message_followup.safe_mail_from.nil? &&
+ incoming_message_followup.valid_to_reply_to?
+
+ ret += OutgoingMailer.name_for_followup(info_request, incoming_message_followup)
+ else
+ return OutgoingMessage.default_salutation(info_request.public_body)
+ end
+ salutation = _("Dear {{public_body_name}},", :public_body_name => ret)
+ end
+
def get_signoff
- if self.message_type == 'followup' && !self.incoming_message_followup.nil? && !self.incoming_message_followup.safe_mail_from.nil? && self.incoming_message_followup.valid_to_reply_to?
- return _("Yours sincerely,")
+ if message_type == 'followup' &&
+ !incoming_message_followup.nil? &&
+ !incoming_message_followup.safe_mail_from.nil? &&
+ incoming_message_followup.valid_to_reply_to?
+
+ _("Yours sincerely,")
else
- return _("Yours faithfully,")
+ _("Yours faithfully,")
end
end
+
def get_internal_review_insert_here_note
- return _("GIVE DETAILS ABOUT YOUR COMPLAINT HERE")
+ _("GIVE DETAILS ABOUT YOUR COMPLAINT HERE")
end
- def get_default_letter
- if self.default_letter
- return self.default_letter
- end
- if self.what_doing == 'internal_review'
- _("Please pass this on to the person who conducts Freedom of Information reviews.") +
- "\n\n" +
- _("I am writing to request an internal review of {{public_body_name}}'s handling of my FOI request '{{info_request_title}}'.",
- :public_body_name => self.info_request.public_body.name,
- :info_request_title => self.info_request.title) +
- "\n\n\n\n [ " + self.get_internal_review_insert_here_note + " ] \n\n\n\n" +
- _("A full history of my FOI request and all correspondence is available on the Internet at this address: {{url}}",
- :url => request_url(self.info_request)) +
- "\n"
+ def get_default_letter
+ return default_letter if default_letter
+
+ if what_doing == 'internal_review'
+ letter = _("Please pass this on to the person who conducts Freedom of Information reviews.")
+ letter += "\n\n"
+ letter += _("I am writing to request an internal review of {{public_body_name}}'s handling of my FOI request '{{info_request_title}}'.",
+ :public_body_name => info_request.public_body.name,
+ :info_request_title => info_request.title)
+ letter += "\n\n\n\n [ #{ get_internal_review_insert_here_note } ] \n\n\n\n"
+ letter += _("A full history of my FOI request and all correspondence is available on the Internet at this address: {{url}}",
+ :url => request_url(info_request))
+ letter += "\n"
else
""
end
end
+
def get_default_message
- get_salutation + "\n\n" + get_default_letter + "\n\n" + get_signoff + "\n\n"
+ msg = get_salutation
+ msg += "\n\n"
+ msg += get_default_letter
+ msg += "\n\n"
+ msg += get_signoff
+ msg += "\n\n"
end
+
def set_signature_name(name)
# TODO: We use raw_body here to get unstripped one
- if self.raw_body == self.get_default_message
- self.body = self.raw_body + name
+ if raw_body == get_default_message
+ self.body = raw_body + name
end
end
@@ -142,84 +151,73 @@ class OutgoingMessage < ActiveRecord::Base
ret.gsub!(/(?:\n\s*){2,}/, "\n\n") # remove excess linebreaks that unnecessarily space it out
# Remove things from censor rules
- if !self.info_request.nil?
+ unless info_request.nil?
self.info_request.apply_censor_rules_to_text!(ret)
end
ret
end
+
def raw_body
read_attribute(:body)
end
# Used to give warnings when writing new messages
def contains_email?
- MySociety::Validate.email_find_regexp.match(self.body)
+ MySociety::Validate.email_find_regexp.match(body)
end
+
def contains_postcode?
- MySociety::Validate.contains_postcode?(self.body)
+ MySociety::Validate.contains_postcode?(body)
end
- # Deliver outgoing message
- # Note: You can test this from script/console with, say:
- # InfoRequest.find(1).outgoing_messages[0].send_message
- def send_message(log_event_type = 'sent')
- if self.status == 'ready'
- if self.message_type == 'initial_request'
- self.last_sent_at = Time.now
- self.status = 'sent'
- self.save!
-
- mail_message = OutgoingMailer.initial_request(self.info_request, self).deliver
- self.info_request.log_event(log_event_type, {
- :email => mail_message.to_addrs.join(", "),
- :outgoing_message_id => self.id,
- :smtp_message_id => mail_message.message_id
- })
- self.info_request.set_described_state('waiting_response')
- elsif self.message_type == 'followup'
- self.last_sent_at = Time.now
- self.status = 'sent'
- self.save!
-
- mail_message = OutgoingMailer.followup(self.info_request, self, self.incoming_message_followup).deliver
- self.info_request.log_event('followup_' + log_event_type, {
- :email => mail_message.to_addrs.join(", "),
- :outgoing_message_id => self.id,
- :smtp_message_id => mail_message.message_id
- })
- if self.info_request.described_state == 'waiting_clarification'
- self.info_request.set_described_state('waiting_response')
- end
- if self.what_doing == 'internal_review'
- self.info_request.set_described_state('internal_review')
- end
+ def record_email_delivery(to_addrs, message_id, log_event_type = 'sent')
+ self.last_sent_at = Time.now
+ self.status = 'sent'
+ save!
+
+ log_event_type = "followup_#{ log_event_type }" if message_type == 'followup'
+
+ info_request.log_event(log_event_type, { :email => to_addrs,
+ :outgoing_message_id => id,
+ :smtp_message_id => message_id })
+ set_info_request_described_state
+ end
+
+ def sendable?
+ if status == 'ready'
+ if message_type == 'initial_request'
+ return true
+ elsif message_type == 'followup'
+ return true
else
- raise "Message id #{self.id} has type '#{self.message_type}' which send_message can't handle"
+ raise "Message id #{id} has type '#{message_type}' which cannot be sent"
end
- elsif self.status == 'sent'
- raise "Message id #{self.id} has already been sent"
+ elsif status == 'sent'
+ raise "Message id #{id} has already been sent"
else
- raise "Message id #{self.id} not in state for send_message"
+ raise "Message id #{id} not in state for sending"
end
end
# An admin function
- def resend_message
- if ['initial_request', 'followup'].include?(self.message_type) and self.status == 'sent'
+ def prepare_message_for_resend
+ if ['initial_request', 'followup'].include?(message_type) and status == 'sent'
self.status = 'ready'
- send_message('resent')
else
- raise "Message id #{self.id} has type '#{self.message_type}' status '#{self.status}' which resend_message can't handle"
+ raise "Message id #{id} has type '#{message_type}' status " \
+ "'#{status}' which prepare_message_for_resend can't handle"
end
end
# Returns the text to quote the original message when sending this one
def quoted_part_to_append_to_email
- if self.message_type == 'followup' && !self.incoming_message_followup.nil?
- return "\n\n-----Original Message-----\n\n" + self.incoming_message_followup.get_body_for_quoting + "\n"
+ if message_type == 'followup' && !incoming_message_followup.nil?
+ quoted = "\n\n-----Original Message-----\n\n"
+ quoted += incoming_message_followup.get_body_for_quoting
+ quoted += "\n"
else
- return ""
+ ""
end
end
@@ -229,8 +227,8 @@ class OutgoingMessage < ActiveRecord::Base
end
# Returns text for indexing / text display
- def get_text_for_indexing(strip_salutation=true)
- text = self.body.strip
+ def get_text_for_indexing(strip_salutation = true)
+ text = body.strip
# Remove salutation
text.sub!(/Dear .+,/, "") if strip_salutation
@@ -238,19 +236,20 @@ class OutgoingMessage < ActiveRecord::Base
# Remove email addresses from display/index etc.
self.remove_privacy_sensitive_things!(text)
- return text
+ text
end
# Return body for display as HTML
def get_body_for_html_display
- text = self.body.strip
+ text = body.strip
self.remove_privacy_sensitive_things!(text)
- text = MySociety::Format.wrap_email_body_by_lines(text) # reparagraph and wrap it so is good preview of emails
+ # reparagraph and wrap it so is good preview of emails
+ text = MySociety::Format.wrap_email_body_by_lines(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>]')
text = text.gsub(/\n/, '<br>')
- return text.html_safe
+ text.html_safe
end
# Return body for display as text
@@ -261,17 +260,16 @@ class OutgoingMessage < ActiveRecord::Base
def fully_destroy
ActiveRecord::Base.transaction do
- info_request_event = InfoRequestEvent.find_by_outgoing_message_id(self.id)
+ info_request_event = InfoRequestEvent.find_by_outgoing_message_id(id)
info_request_event.track_things_sent_emails.each { |a| a.destroy }
info_request_event.user_info_request_sent_alerts.each { |a| a.destroy }
info_request_event.destroy
- self.destroy
+ destroy
end
end
- after_save(:purge_in_cache)
def purge_in_cache
- self.info_request.purge_in_cache
+ info_request.purge_in_cache
end
def for_admin_column
@@ -280,18 +278,37 @@ class OutgoingMessage < ActiveRecord::Base
end
end
+ def xapian_reindex_after_update
+ if changes.include?('body')
+ info_request_events.each do |event|
+ event.xapian_mark_needs_index
+ end
+ end
+ end
+
private
- def set_default_letter
- if self.body.nil?
- self.body = get_default_message
+ def set_info_request_described_state
+ if message_type == 'initial_request'
+ info_request.set_described_state('waiting_response')
+ elsif message_type == 'followup'
+ if info_request.described_state == 'waiting_clarification'
+ info_request.set_described_state('waiting_response')
+ end
+ if what_doing == 'internal_review'
+ info_request.set_described_state('internal_review')
+ end
end
end
+ def set_default_letter
+ self.body = get_default_message if body.nil?
+ end
+
def format_of_body
- if self.body.empty? || self.body =~ /\A#{Regexp.escape(get_salutation)}\s+#{Regexp.escape(get_signoff)}/ || self.body =~ /#{Regexp.escape(get_internal_review_insert_here_note)}/
- if self.message_type == 'followup'
- if self.what_doing == 'internal_review'
+ if body.empty? || body =~ /\A#{Regexp.escape(get_salutation)}\s+#{Regexp.escape(get_signoff)}/ || body =~ /#{Regexp.escape(get_internal_review_insert_here_note)}/
+ if message_type == 'followup'
+ if what_doing == 'internal_review'
errors.add(:body, _("Please give details explaining why you want a review"))
else
errors.add(:body, _("Please enter your follow up message"))
@@ -299,16 +316,19 @@ class OutgoingMessage < ActiveRecord::Base
elsif
errors.add(:body, _("Please enter your letter requesting information"))
else
- raise "Message id #{self.id} has type '#{self.message_type}' which validate can't handle"
+ raise "Message id #{id} has type '#{message_type}' which validate can't handle"
end
end
- if self.body =~ /#{get_signoff}\s*\Z/m
+
+ if body =~ /#{get_signoff}\s*\Z/m
errors.add(:body, _("Please sign at the bottom with your name, or alter the \"{{signoff}}\" signature", :signoff => get_signoff))
end
- if !MySociety::Validate.uses_mixed_capitals(self.body)
+
+ unless MySociety::Validate.uses_mixed_capitals(body)
errors.add(:body, _('Please write your message using a mixture of capital and lower case letters. This makes it easier for others to read.'))
end
- if self.what_doing.nil? || !['new_information', 'internal_review', 'normal_sort'].include?(self.what_doing)
+
+ if what_doing.nil? || !['new_information', 'internal_review', 'normal_sort'].include?(what_doing)
errors.add(:what_doing_dummy, _('Please choose what sort of reply you are making.'))
end
end
diff --git a/app/models/post_redirect.rb b/app/models/post_redirect.rb
index 6f288b471..8049349d0 100644
--- a/app/models/post_redirect.rb
+++ b/app/models/post_redirect.rb
@@ -31,66 +31,66 @@ class PostRedirect < ActiveRecord::Base
# Optional, does a login confirm before redirect for use in email links.
belongs_to :user
- after_initialize :generate_token
+ after_initialize :generate_token,
+ :generate_email_token
+
+ # Makes a random token, suitable for using in URLs e.g confirmation
+ # messages.
+ def self.generate_random_token
+ MySociety::Util.generate_token
+ end
+
+ # Used by (rspec) test code only
+ def self.get_last_post_redirect
+ # TODO: yeuch - no other easy way of getting the token so we can check
+ # the redirect URL, as it is by definition opaque to the controller
+ # apart from in the place that it redirects to.
+ post_redirects = PostRedirect.find_by_sql("select * from post_redirects order by id desc limit 1")
+ post_redirects.size.should == 1
+ post_redirects[0]
+ end
+
+ # Called from cron job delete-old-things
+ def self.delete_old_post_redirects
+ PostRedirect.delete_all("updated_at < (now() - interval '2 months')")
+ end
# We store YAML version of POST parameters in the database
def post_params=(params)
self.post_params_yaml = params.to_yaml
end
+
def post_params
- if self.post_params_yaml.nil?
- return {}
- end
- YAML.load(self.post_params_yaml)
+ return {} if post_params_yaml.nil?
+ YAML.load(post_params_yaml)
end
# We store YAML version of textual "reason for redirect" parameters
def reason_params=(reason_params)
self.reason_params_yaml = reason_params.to_yaml
end
+
def reason_params
- YAML.load(self.reason_params_yaml)
+ YAML.load(reason_params_yaml)
end
# Extract just local path part, without domain or #
def local_part_uri
- self.uri.match(/^http:\/\/.+?(\/[^#]+)/)
- return $1
- end
-
- # Makes a random token, suitable for using in URLs e.g confirmation messages.
- def self.generate_random_token
- MySociety::Util.generate_token
- end
-
- # Used by (rspec) test code only
- def self.get_last_post_redirect
- # TODO: yeuch - no other easy way of getting the token so we can check
- # the redirect URL, as it is by definition opaque to the controller
- # apart from in the place that it redirects to.
- post_redirects = PostRedirect.find_by_sql("select * from post_redirects order by id desc limit 1")
- post_redirects.size.should == 1
- return post_redirects[0]
- end
-
- # Called from cron job delete-old-things
- def self.delete_old_post_redirects
- PostRedirect.delete_all "updated_at < (now() - interval '2 months')"
+ uri.match(/^http:\/\/.+?(\/[^#]+)/)
+ $1
end
private
+ # The token is used to return you to what you are doing after the login
+ # form.
def generate_token
- # The token is used to return you to what you are doing after the login form.
- if not self.token
- self.token = PostRedirect.generate_random_token
- end
- # There is a separate token to use in the URL if we send a confirmation email.
- if not self.email_token
- self.email_token = PostRedirect.generate_random_token
- end
+ self.token = PostRedirect.generate_random_token unless token
end
-end
-
-
+ # There is a separate token to use in the URL if we send a confirmation
+ # email.
+ def generate_email_token
+ self.email_token = PostRedirect.generate_random_token unless email_token
+ end
+end
diff --git a/app/models/profile_photo.rb b/app/models/profile_photo.rb
index 3c0be222c..61f88faf3 100644
--- a/app/models/profile_photo.rb
+++ b/app/models/profile_photo.rb
@@ -15,87 +15,84 @@
# Email: hello@mysociety.org; WWW: http://www.mysociety.org/
class ProfilePhoto < ActiveRecord::Base
+ # deliberately don't strip_attributes, so keeps raw photo properly
+
WIDTH = 96
HEIGHT = 96
-
MAX_DRAFT = 500 # keep even pre-cropped images reasonably small
belongs_to :user
validate :data_and_draft_checks
- # deliberately don't strip_attributes, so keeps raw photo properly
-
attr_accessor :x, :y, :w, :h
-
attr_accessor :image
after_initialize :convert_data_to_image
# make image valid format and size
def convert_image
- if self.data.nil?
- return
- end
- if self.image.nil?
- return
- end
+ return if data.nil?
+ return if image.nil?
# convert to PNG if it isn't, and to right size
altered = false
- if self.image.format != 'PNG'
+ if image.format != 'PNG'
self.image.format = 'PNG'
altered = true
end
+
# draft images are before the user has cropped them
- if !self.draft && (image.columns != WIDTH || image.rows != HEIGHT)
+ if !draft && (image.columns != WIDTH || image.rows != HEIGHT)
# do any exact cropping (taken from Jcrop interface)
- if self.w && self.h
- image.crop!(self.x.to_i, self.y.to_i, self.w.to_i, self.h.to_i)
+ if w && h
+ image.crop!(x.to_i, y.to_i, w.to_i, h.to_i)
end
# do any further cropping
image.resize_to_fill!(WIDTH, HEIGHT)
altered = true
end
- if self.draft && (image.columns > MAX_DRAFT || image.rows > MAX_DRAFT)
+
+ if draft && (image.columns > MAX_DRAFT || image.rows > MAX_DRAFT)
image.resize_to_fit!(MAX_DRAFT, MAX_DRAFT)
altered = true
end
+
if altered
- write_attribute(:data, self.image.to_blob)
+ write_attribute(:data, image.to_blob)
end
end
private
def data_and_draft_checks
- if self.data.nil?
+ if data.nil?
errors.add(:data, _("Please choose a file containing your photo."))
return
end
- if self.image.nil?
+ if image.nil?
errors.add(:data, _("Couldn't understand the image file that you uploaded. PNG, JPEG, GIF and many other common image file formats are supported."))
return
end
- if self.image.format != 'PNG'
+ if image.format != 'PNG'
errors.add(:data, _("Failed to convert image to a PNG"))
end
- if !self.draft && (self.image.columns != WIDTH || self.image.rows != HEIGHT)
+ if !draft && (image.columns != WIDTH || image.rows != HEIGHT)
errors.add(:data, _("Failed to convert image to the correct size: at {{cols}}x{{rows}}, need {{width}}x{{height}}",
- :cols => self.image.columns,
- :rows => self.image.rows,
+ :cols => image.columns,
+ :rows => image.rows,
:width => WIDTH,
:height => HEIGHT))
end
- if self.draft && self.user_id
+ if draft && user_id
raise "Internal error, draft pictures must not have a user"
end
- if !self.draft && !self.user_id
+ if !draft && !user_id
raise "Internal error, real pictures must have a user"
end
end
@@ -108,6 +105,7 @@ class ProfilePhoto < ActiveRecord::Base
end
image_list = Magick::ImageList.new
+
begin
image_list.from_blob(data)
rescue Magick::ImageMagickError
@@ -115,9 +113,9 @@ class ProfilePhoto < ActiveRecord::Base
return
end
- self.image = image_list[0] # TODO: perhaps take largest image or somesuch if there were multiple in the file?
- self.convert_image
+ # TODO: perhaps take largest image or somesuch if there were multiple
+ # in the file?
+ self.image = image_list[0]
+ convert_image
end
end
-
-
diff --git a/app/models/public_body.rb b/app/models/public_body.rb
index b22482541..1929272ea 100644
--- a/app/models/public_body.rb
+++ b/app/models/public_body.rb
@@ -49,7 +49,12 @@ class PublicBody < ActiveRecord::Base
attr_accessor :no_xapian_reindex
has_tag_string
- before_save :set_api_key, :set_default_publication_scheme
+
+ before_save :set_api_key,
+ :set_default_publication_scheme,
+ :set_first_letter
+ after_save :purge_in_cache
+ after_update :reindex_requested_from
# Every public body except for the internal admin one is visible
scope :visible, lambda {
@@ -60,6 +65,36 @@ class PublicBody < ActiveRecord::Base
translates :name, :short_name, :request_email, :url_name, :notes, :first_letter, :publication_scheme
+ # 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
+
+ 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" ]
+ ]
+
+ 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'
+
# Public: Search for Public Bodies whose name, short_name, request_email or
# tags contain the given query
#
@@ -117,14 +152,14 @@ class PublicBody < ActiveRecord::Base
end
def translated_versions=(translation_attrs)
- def skip?(attrs)
- valueless = attrs.inject({}) { |h, (k, v)| h[k] = v if v != '' and k != 'locale'; h } # because we want to fall back to alternative translations where there are empty values
- return valueless.length == 0
+ def empty_translation?(attrs)
+ attrs_with_values = attrs.select{ |key, value| value != '' and key != 'locale' }
+ attrs_with_values.empty?
end
if translation_attrs.respond_to? :each_value # Hash => updating
translation_attrs.each_value do |attrs|
- next if skip?(attrs)
+ next if empty_translation?(attrs)
t = translation_for(attrs[:locale]) || PublicBody::Translation.new
t.attributes = attrs
calculate_cached_fields(t)
@@ -132,7 +167,7 @@ class PublicBody < ActiveRecord::Base
end
else # Array => creating
translation_attrs.each do |attrs|
- next if skip?(attrs)
+ next if empty_translation?(attrs)
new_translation = PublicBody::Translation.new(attrs)
calculate_cached_fields(new_translation)
translations << new_translation
@@ -174,7 +209,6 @@ class PublicBody < ActiveRecord::Base
end
# Set the first letter, which is used for faster queries
- before_save(:set_first_letter)
def set_first_letter
PublicBody.set_first_letter(self)
end
@@ -221,13 +255,6 @@ class PublicBody < ActiveRecord::Base
end
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'
-
class Version
def last_edit_comment_for_html_display
@@ -258,13 +285,6 @@ class PublicBody < ActiveRecord::Base
end
end
- 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" ]
- ]
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")
@@ -276,7 +296,6 @@ class PublicBody < ActiveRecord::Base
# 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
- after_update :reindex_requested_from
def reindex_requested_from
if self.changes.include?('url_name')
for info_request in self.info_requests
@@ -320,8 +339,8 @@ class PublicBody < ActiveRecord::Base
types = []
first = true
for tag in self.tags
- if PublicBodyCategories::get().by_tag().include?(tag.name)
- desc = PublicBodyCategories::get().singular_by_tag()[tag.name]
+ 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)
@@ -477,7 +496,10 @@ class PublicBody < ActiveRecord::Base
next
end
- field_list = ['name', 'short_name', 'request_email', 'notes', 'publication_scheme', 'disclosure_log', 'home_page', 'tag_string']
+ 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|
@@ -662,7 +684,6 @@ class PublicBody < ActiveRecord::Base
}
end
- after_save(:purge_in_cache)
def purge_in_cache
self.info_requests.each {|x| x.purge_in_cache}
end
diff --git a/app/models/public_body_category.rb b/app/models/public_body_category.rb
new file mode 100644
index 000000000..8eaecd596
--- /dev/null
+++ b/app/models/public_body_category.rb
@@ -0,0 +1,90 @@
+# == Schema Information
+#
+# 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
+#
+
+require 'forwardable'
+
+class PublicBodyCategory < ActiveRecord::Base
+ attr_accessible :locale, :category_tag, :title, :description,
+ :translated_versions, :display_order
+
+ has_many :public_body_category_links, :dependent => :destroy
+ has_many :public_body_headings, :through => :public_body_category_links
+
+ translates :title, :description
+ validates_uniqueness_of :category_tag, :message => N_('Tag is already taken')
+ validates_presence_of :title, :message => N_("Title can't be blank")
+ validates_presence_of :category_tag, :message => N_("Tag can't be blank")
+
+ def self.get
+ locale = I18n.locale.to_s || default_locale.to_s || ""
+ categories = CategoryCollection.new
+ I18n.with_locale(locale) do
+ headings = PublicBodyHeading.all
+ headings.each do |heading|
+ categories << heading.name
+ heading.public_body_categories.each do |category|
+ categories << [
+ category.category_tag,
+ category.title,
+ category.description
+ ]
+ end
+ end
+ end
+ categories
+ end
+
+ def self.without_headings
+ sql = %Q| SELECT * FROM public_body_categories pbc
+ WHERE pbc.id NOT IN (
+ SELECT public_body_category_id AS id
+ FROM public_body_category_links
+ ) |
+ 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)
+ end
+
+ def translated_versions
+ translations
+ 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
+ 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
+ end
+end
+
+
diff --git a/app/models/public_body_category/category_collection.rb b/app/models/public_body_category/category_collection.rb
new file mode 100644
index 000000000..8286e2710
--- /dev/null
+++ b/app/models/public_body_category/category_collection.rb
@@ -0,0 +1,54 @@
+# replicate original file-based PublicBodyCategories functionality
+class PublicBodyCategory::CategoryCollection
+ include Enumerable
+ extend Forwardable
+ def_delegators :@categories, :each, :<<
+
+ def initialize
+ @categories = []
+ end
+
+ def with_headings
+ @categories
+ end
+
+ def with_description
+ @categories.select() { |a| a.instance_of?(Array) }
+ end
+
+ def tags
+ tags = with_description.map() { |a| a[0] }
+ end
+
+ def by_tag
+ Hash[*with_description.map() { |a| a[0..1] }.flatten]
+ end
+
+ def singular_by_tag
+ Hash[*with_description.map() { |a| [a[0],a[2]] }.flatten]
+ end
+
+ def by_heading
+ output = {}
+ heading = nil
+ @categories.each do |row|
+ if row.is_a?(Array)
+ output[heading] << row[0]
+ else
+ heading = row
+ output[heading] = []
+ end
+ end
+ output
+ end
+
+ def headings
+ output = []
+ @categories.each do |row|
+ unless row.is_a?(Array)
+ output << row
+ end
+ end
+ output
+ end
+end
diff --git a/app/models/public_body_category_link.rb b/app/models/public_body_category_link.rb
new file mode 100644
index 000000000..eb233b56f
--- /dev/null
+++ b/app/models/public_body_category_link.rb
@@ -0,0 +1,34 @@
+# == Schema Information
+#
+# Table name: public_body_category_link
+#
+# public_body_category_id :integer not null
+# public_body_heading_id :integer not null
+# category_display_order :integer
+#
+
+class PublicBodyCategoryLink < ActiveRecord::Base
+ attr_accessible :public_body_category_id, :public_body_heading_id, :category_display_order
+
+ belongs_to :public_body_category
+ belongs_to :public_body_heading
+ validates_presence_of :public_body_category
+ validates_presence_of :public_body_heading
+ validates :category_display_order, :numericality => { :only_integer => true,
+ :message => N_('Display order must be a number') }
+
+ before_validation :on => :create do
+ unless self.category_display_order
+ self.category_display_order = PublicBodyCategoryLink.next_display_order(public_body_heading_id)
+ end
+ end
+
+ def self.next_display_order(heading_id)
+ if last = where(:public_body_heading_id => heading_id).order(:category_display_order).last
+ last.category_display_order + 1
+ else
+ 0
+ end
+ end
+
+end
diff --git a/app/models/public_body_heading.rb b/app/models/public_body_heading.rb
new file mode 100644
index 000000000..c38800561
--- /dev/null
+++ b/app/models/public_body_heading.rb
@@ -0,0 +1,75 @@
+# == Schema Information
+#
+# Table name: public_body_headings
+#
+# id :integer not null, primary key
+# name :text not null
+# display_order :integer
+#
+
+class PublicBodyHeading < ActiveRecord::Base
+ attr_accessible :name, :display_order, :translated_versions
+
+ 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
+
+ validates_uniqueness_of :name, :message => N_('Name is already taken')
+ validates_presence_of :name, :message => N_('Name can\'t be blank')
+ validates :display_order, :numericality => { :only_integer => true,
+ :message => N_('Display order must be a number') }
+
+ before_validation :on => :create do
+ unless self.display_order
+ self.display_order = PublicBodyHeading.next_display_order
+ end
+ end
+
+ # Convenience methods for creating/editing translations via forms
+ def find_translation_by_locale(locale)
+ translations.find_by_locale(locale)
+ end
+
+ def translated_versions
+ translations
+ 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
+
+ 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
+ end
+ end
+
+ def add_category(category)
+ unless public_body_categories.include?(category)
+ public_body_categories << category
+ end
+ end
+
+ def self.next_display_order
+ if max = maximum(:display_order)
+ max + 1
+ else
+ 0
+ end
+ end
+
+end
diff --git a/app/models/purge_request.rb b/app/models/purge_request.rb
index 4e6267bd2..81980188d 100644
--- a/app/models/purge_request.rb
+++ b/app/models/purge_request.rb
@@ -19,15 +19,17 @@
class PurgeRequest < ActiveRecord::Base
def self.purge_all
done_something = false
- for item in PurgeRequest.all()
+
+ PurgeRequest.all.each do |item|
item.purge
done_something = true
end
- return done_something
+
+ done_something
end
+ # Run purge_all in an endless loop, sleeping when there is nothing to do
def self.purge_all_loop
- # Run purge_all in an endless loop, sleeping when there is nothing to do
while true
sleep_seconds = 1
while !purge_all
@@ -39,13 +41,8 @@ class PurgeRequest < ActiveRecord::Base
end
def purge
- config = MySociety::Config.load_default()
- varnish_url = config['VARNISH_HOST']
- result = quietly_try_to_purge(varnish_url, self.url)
- self.delete()
+ config = MySociety::Config.load_default
+ result = quietly_try_to_purge(config['VARNISH_HOST'], url)
+ delete
end
end
-
-
-
-
diff --git a/app/models/raw_email.rb b/app/models/raw_email.rb
index 21a53f493..3b466cb81 100644
--- a/app/models/raw_email.rb
+++ b/app/models/raw_email.rb
@@ -17,44 +17,46 @@ class RawEmail < ActiveRecord::Base
has_one :incoming_message
def directory
- request_id = self.incoming_message.info_request.id.to_s
if request_id.empty?
raise "Failed to find the id number of the associated request: has it been saved?"
end
if Rails.env.test?
- return File.join(Rails.root, 'files/raw_email_test')
+ File.join(Rails.root, 'files/raw_email_test')
else
- return File.join(AlaveteliConfiguration::raw_emails_location,
- request_id[0..2], request_id)
+ File.join(AlaveteliConfiguration::raw_emails_location,
+ request_id[0..2], request_id)
end
end
def filepath
- incoming_message_id = self.incoming_message.id.to_s
if incoming_message_id.empty?
raise "Failed to find the id number of the associated incoming message: has it been saved?"
end
- File.join(self.directory, incoming_message_id)
+
+ File.join(directory, incoming_message_id)
end
def data=(d)
- if !File.exists?(self.directory)
- FileUtils.mkdir_p self.directory
- end
- File.atomic_write(self.filepath) { |file|
- file.write d
- }
+ FileUtils.mkdir_p(directory) unless File.exists?(directory)
+ File.atomic_write(filepath) { |file| file.write(d) }
end
def data
- File.open(self.filepath, "r").read
+ File.open(filepath, "r").read
end
def destroy_file_representation!
- File.delete(self.filepath)
+ File.delete(filepath)
end
-end
+ private
+ def request_id
+ incoming_message.info_request.id.to_s
+ end
+ def incoming_message_id
+ incoming_message.id.to_s
+ end
+end
diff --git a/app/models/track_thing.rb b/app/models/track_thing.rb
index 10ba28f4a..5819876ff 100644
--- a/app/models/track_thing.rb
+++ b/app/models/track_thing.rb
@@ -25,120 +25,86 @@ require 'set'
# TODO: TrackThing looks like a good candidate for single table inheritance
class TrackThing < ActiveRecord::Base
- belongs_to :tracking_user, :class_name => 'User'
- validates_presence_of :track_query
- validates_presence_of :track_type
+ # { TRACK_TYPE => DESCRIPTION }
+ TRACK_TYPES = { 'request_updates' => _('Individual requests'),
+ 'all_new_requests' => _('Many requests'),
+ 'all_successful_requests' => _('Many requests'),
+ 'public_body_updates' => _('Public authorities'),
+ 'user_updates' => _('People'),
+ 'search_query' => _('Search queries') }
+
+ TRACK_MEDIUMS = %w(email_daily feed)
belongs_to :info_request
belongs_to :public_body
+ belongs_to :tracking_user, :class_name => 'User'
belongs_to :tracked_user, :class_name => 'User'
-
has_many :track_things_sent_emails
- validates_inclusion_of :track_type, :in => [
- 'request_updates',
- 'all_new_requests',
- 'all_successful_requests',
- 'public_body_updates',
- 'user_updates',
- 'search_query'
- ]
-
- validates_inclusion_of :track_medium, :in => [
- 'email_daily',
- 'feed'
- ]
-
- def TrackThing.track_type_description(track_type)
- if track_type == 'request_updates'
- _("Individual requests")
- elsif track_type == 'all_new_requests' || track_type == "all_successful_requests"
- _("Many requests")
- elsif track_type == 'public_body_updates'
- _("Public authorities")
- elsif track_type == 'user_updates'
- _("People")
- elsif track_type == 'search_query'
- _("Search queries")
- else
- raise "internal error " + track_type
- end
- end
- def track_type_description
- TrackThing.track_type_description(self.track_type)
- end
+ validates_presence_of :track_query
+ validates_presence_of :track_type
+ validates_inclusion_of :track_type, :in => TRACK_TYPES.keys
+ validates_inclusion_of :track_medium, :in => TRACK_MEDIUMS
- def track_query_description
- filter_description = query_filter_description('(variety:sent OR variety:followup_sent OR variety:response OR variety:comment)',
- :no_query => N_("all requests or comments"),
- :query => N_("all requests or comments matching text '{{query}}'"))
- return filter_description if filter_description
- filter_description = query_filter_description('(latest_status:successful OR latest_status:partially_successful)',
- :no_query => N_("requests which are successful"),
- :query => N_("requests which are successful matching text '{{query}}'"))
- return filter_description if filter_description
- return _("anything matching text '{{query}}'", :query => track_query)
+ # When constructing a new track, use this to avoid duplicates / double
+ # posting
+ def self.find_existing(tracking_user, track)
+ return nil if tracking_user.nil?
+ where(:tracking_user_id => tracking_user.id,
+ :track_query => track.track_query,
+ :track_type => track.track_type).first
end
- # Return a readable query description for queries involving commonly used filter clauses
- def query_filter_description(string, options)
- parsed_query = track_query.gsub(string, '')
- if parsed_query != track_query
- parsed_query.strip!
- if parsed_query.empty?
- _(options[:no_query])
- else
- _(options[:query], :query => parsed_query)
- end
- end
+ def self.track_type_description(track_type)
+ TRACK_TYPES.fetch(track_type) { raise "internal error #{ track_type }" }
end
- def TrackThing.create_track_for_request(info_request)
+ def self.create_track_for_request(info_request)
track_thing = TrackThing.new
track_thing.track_type = 'request_updates'
track_thing.info_request = info_request
- track_thing.track_query = "request:" + info_request.url_title
- return track_thing
+ track_thing.track_query = "request:#{ info_request.url_title }"
+ track_thing
end
- def TrackThing.create_track_for_all_new_requests
+ def self.create_track_for_all_new_requests
track_thing = TrackThing.new
track_thing.track_type = 'all_new_requests'
track_thing.track_query = "variety:sent"
- return track_thing
+ track_thing
end
- def TrackThing.create_track_for_all_successful_requests
+ def self.create_track_for_all_successful_requests
track_thing = TrackThing.new
track_thing.track_type = 'all_successful_requests'
track_thing.track_query = 'variety:response (status:successful OR status:partially_successful)'
- return track_thing
+ track_thing
end
- def TrackThing.create_track_for_public_body(public_body, event_type = nil)
+ def self.create_track_for_public_body(public_body, event_type = nil)
track_thing = TrackThing.new
track_thing.track_type = 'public_body_updates'
track_thing.public_body = public_body
- query = "requested_from:" + public_body.url_name
+ query = "requested_from:#{ public_body.url_name }"
if InfoRequestEvent.enumerate_event_types.include?(event_type)
- query += " variety:" + event_type
+ query += " variety:#{ event_type }"
end
track_thing.track_query = query
- return track_thing
+ track_thing
end
- def TrackThing.create_track_for_user(user)
+ def self.create_track_for_user(user)
track_thing = TrackThing.new
track_thing.track_type = 'user_updates'
track_thing.tracked_user = user
- track_thing.track_query = "requested_by:" + user.url_name + " OR commented_by:" + user.url_name
- return track_thing
+ track_thing.track_query = "requested_by:#{ user.url_name } OR commented_by: #{ user.url_name }"
+ track_thing
end
- def TrackThing.create_track_for_search_query(query, variety_postfix = nil)
+ def self.create_track_for_search_query(query, variety_postfix = nil)
track_thing = TrackThing.new
track_thing.track_type = 'search_query'
- if !(query =~ /variety:/)
+ unless query =~ /variety:/
case variety_postfix
when "requests"
query += " variety:sent"
@@ -154,146 +120,180 @@ class TrackThing < ActiveRecord::Base
# Should also update "params" to make the list_description
# nicer and more generic. It will need to do some clever
# parsing of the query to do this nicely
- return track_thing
+ track_thing
end
- # Return hash of text parameters describing the request etc.
- def params
- if @params.nil?
- if self.track_type == 'request_updates'
- @params = {
- # Website
+ def track_type_description
+ TrackThing.track_type_description(track_type)
+ end
+
+ def track_query_description
+ filter_description = query_filter_description('(variety:sent OR variety:followup_sent OR variety:response OR variety:comment)',
+ :no_query => N_("all requests or comments"),
+ :query => N_("all requests or comments matching text '{{query}}'"))
+ return filter_description if filter_description
+
+ filter_description = query_filter_description('(latest_status:successful OR latest_status:partially_successful)',
+ :no_query => N_("requests which are successful"),
+ :query => N_("requests which are successful matching text '{{query}}'"))
+ return filter_description if filter_description
- :verb_on_page => _("Follow this request"),
- :verb_on_page_already => _("You are already following this request"),
- # Email
- :title_in_email => _("New updates for the request '{{request_title}}'",
- :request_title => self.info_request.title.html_safe),
- :title_in_rss => _("New updates for the request '{{request_title}}'",
- :request_title => self.info_request.title),
- # Authentication
- :web => _("To follow the request '{{request_title}}'",
- :request_title => self.info_request.title),
- :email => _("Then you will be updated whenever the request '{{request_title}}' is updated.",
- :request_title => self.info_request.title),
- :email_subject => _("Confirm you want to follow the request '{{request_title}}'",
- :request_title => self.info_request.title),
- # RSS sorting
- :feed_sortby => 'newest'
- }
- elsif self.track_type == 'all_new_requests'
- @params = {
- # Website
- :verb_on_page => _("Follow all new requests"),
- :verb_on_page_already => _("You are already following new requests"),
- # Email
- :title_in_email => _("New Freedom of Information requests"),
- :title_in_rss => _("New Freedom of Information requests"),
- # Authentication
- :web => _("To follow new requests"),
- :email => _("Then you will be following all new FOI requests."),
- :email_subject => _("Confirm you want to follow new requests"),
- # RSS sorting
- :feed_sortby => 'newest'
- }
- elsif self.track_type == 'all_successful_requests'
- @params = {
- # Website
- :verb_on_page => _("Follow new successful responses"),
- :verb_on_page_already => _("You are following all new successful responses"),
- # Email
- :title_in_email => _("Successful Freedom of Information requests"),
- :title_in_rss => _("Successful Freedom of Information requests"),
- # Authentication
- :web => _("To follow all successful requests"),
- :email => _("Then you will be notified whenever an FOI request succeeds."),
- :email_subject => _("Confirm you want to follow all successful FOI requests"),
- # RSS sorting - used described date, as newest would give a
- # date for responses possibly days before description, so
- # wouldn't appear at top of list when description (known
- # success) causes match.
- :feed_sortby => 'described'
- }
- elsif self.track_type == 'public_body_updates'
- @params = {
- # Website
- :verb_on_page => _("Follow requests to {{public_body_name}}",
- :public_body_name => self.public_body.name),
- :verb_on_page_already => _("You are already following requests to {{public_body_name}}",
- :public_body_name => self.public_body.name),
- # Email
- :title_in_email => _("{{foi_law}} requests to '{{public_body_name}}'",
- :foi_law => self.public_body.law_only_short,
- :public_body_name => self.public_body.name),
- :title_in_rss => _("{{foi_law}} requests to '{{public_body_name}}'",
- :foi_law => self.public_body.law_only_short,
- :public_body_name => self.public_body.name),
- # Authentication
- :web => _("To follow requests made using {{site_name}} to the public authority '{{public_body_name}}'",
- :site_name => AlaveteliConfiguration::site_name,
- :public_body_name => self.public_body.name),
- :email => _("Then you will be notified whenever someone requests something or gets a response from '{{public_body_name}}'.",
- :public_body_name => self.public_body.name),
- :email_subject => _("Confirm you want to follow requests to '{{public_body_name}}'",
- :public_body_name => self.public_body.name),
- # RSS sorting
- :feed_sortby => 'newest'
- }
- elsif self.track_type == 'user_updates'
- @params = {
- # Website
- :verb_on_page => _("Follow this person"),
- :verb_on_page_already => _("You are already following this person"),
- # Email
- :title_in_email => _("FOI requests by '{{user_name}}'",
- :user_name => self.tracked_user.name.html_safe),
- :title_in_rss => _("FOI requests by '{{user_name}}'",
- :user_name => self.tracked_user.name),
- # Authentication
- :web => _("To follow requests by '{{user_name}}'",
- :user_name=> self.tracked_user.name),
- :email => _("Then you will be notified whenever '{{user_name}}' requests something or gets a response.",
- :user_name => self.tracked_user.name),
- :email_subject => _("Confirm you want to follow requests by '{{user_name}}'",
- :user_name => self.tracked_user.name),
- # RSS sorting
- :feed_sortby => 'newest'
- }
- elsif self.track_type == 'search_query'
- @params = {
- # Website
- :verb_on_page => _("Follow things matching this search"),
- :verb_on_page_already => _("You are already following things matching this search"),
- # Email
- :title_in_email => _("Requests or responses matching your saved search"),
- :title_in_rss => _("Requests or responses matching your saved search"),
- # Authentication
- :web => _("To follow requests and responses matching your search"),
- :email => _("Then you will be notified whenever a new request or response matches your search."),
- :email_subject => _("Confirm you want to follow new requests or responses matching your search"),
- # RSS sorting - TODO: hmmm, we don't really know which to use
- # here for sorting. Might be a query term (e.g. 'cctv'), in
- # which case newest is good, or might be something like
- # all refused requests in which case want to sort by
- # described (when we discover criteria is met). Rather
- # conservatively am picking described, as that will make
- # things appear in feed more than the should, rather than less.
- :feed_sortby => 'described'
- }
- else
- raise "unknown tracking type " + self.track_type
+ _("anything matching text '{{query}}'", :query => track_query)
+ end
+
+ # Return a readable query description for queries involving commonly used
+ # filter clauses
+ def query_filter_description(string, options)
+ parsed_query = track_query.gsub(string, '')
+ if parsed_query != track_query
+ parsed_query.strip!
+ if parsed_query.empty?
+ _(options[:no_query])
+ else
+ _(options[:query], :query => parsed_query)
end
end
- return @params
end
- # When constructing a new track, use this to avoid duplicates / double posting
- def TrackThing.find_existing(tracking_user, track)
- if tracking_user.nil?
- return nil
+ # Return hash of text parameters based on the track_type describing the
+ # request etc.
+ def params
+ @params ||= params_for(track_type)
+ end
+
+ private
+
+ def params_for(track_type)
+ if respond_to?("#{ track_type }_params", true)
+ send("#{ track_type }_params")
+ else
+ raise "unknown tracking type #{ track_type }"
end
- return TrackThing.find(:first, :conditions => [ 'tracking_user_id = ? and track_query = ? and track_type = ?', tracking_user.id, track.track_query, track.track_type ] )
end
-end
+ def request_updates_params
+ { # Website
+ :verb_on_page => _("Follow this request"),
+ :verb_on_page_already => _("You are already following this request"),
+ # Email
+ :title_in_email => _("New updates for the request '{{request_title}}'",
+ :request_title => info_request.title.html_safe),
+ :title_in_rss => _("New updates for the request '{{request_title}}'",
+ :request_title => info_request.title),
+ # Authentication
+ :web => _("To follow the request '{{request_title}}'",
+ :request_title => info_request.title),
+ :email => _("Then you will be updated whenever the request '{{request_title}}' is updated.",
+ :request_title => info_request.title),
+ :email_subject => _("Confirm you want to follow the request '{{request_title}}'",
+ :request_title => info_request.title),
+ # RSS sorting
+ :feed_sortby => 'newest'
+ }
+ end
+
+ def all_new_requests_params
+ { # Website
+ :verb_on_page => _("Follow all new requests"),
+ :verb_on_page_already => _("You are already following new requests"),
+ # Email
+ :title_in_email => _("New Freedom of Information requests"),
+ :title_in_rss => _("New Freedom of Information requests"),
+ # Authentication
+ :web => _("To follow new requests"),
+ :email => _("Then you will be following all new FOI requests."),
+ :email_subject => _("Confirm you want to follow new requests"),
+ # RSS sorting
+ :feed_sortby => 'newest'
+ }
+ end
+ def all_successful_requests_params
+ { # Website
+ :verb_on_page => _("Follow new successful responses"),
+ :verb_on_page_already => _("You are following all new successful responses"),
+ # Email
+ :title_in_email => _("Successful Freedom of Information requests"),
+ :title_in_rss => _("Successful Freedom of Information requests"),
+ # Authentication
+ :web => _("To follow all successful requests"),
+ :email => _("Then you will be notified whenever an FOI request succeeds."),
+ :email_subject => _("Confirm you want to follow all successful FOI requests"),
+ # RSS sorting - used described date, as newest would give a
+ # date for responses possibly days before description, so
+ # wouldn't appear at top of list when description (known
+ # success) causes match.
+ :feed_sortby => 'described'
+ }
+ end
+
+ def public_body_updates_params
+ { # 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),
+ # Email
+ :title_in_email => _("{{foi_law}} requests to '{{public_body_name}}'",
+ :foi_law => public_body.law_only_short,
+ :public_body_name => public_body.name),
+ :title_in_rss => _("{{foi_law}} requests to '{{public_body_name}}'",
+ :foi_law => public_body.law_only_short,
+ :public_body_name => public_body.name),
+ # Authentication
+ :web => _("To follow requests made using {{site_name}} to the public authority '{{public_body_name}}'",
+ :site_name => AlaveteliConfiguration.site_name,
+ :public_body_name => public_body.name),
+ :email => _("Then you will be notified whenever someone requests something or gets a response from '{{public_body_name}}'.",
+ :public_body_name => public_body.name),
+ :email_subject => _("Confirm you want to follow requests to '{{public_body_name}}'",
+ :public_body_name => public_body.name),
+ # RSS sorting
+ :feed_sortby => 'newest'
+ }
+ end
+
+ def user_updates_params
+ { # Website
+ :verb_on_page => _("Follow this person"),
+ :verb_on_page_already => _("You are already following this person"),
+ # Email
+ :title_in_email => _("FOI requests by '{{user_name}}'",
+ :user_name => tracked_user.name.html_safe),
+ :title_in_rss => _("FOI requests by '{{user_name}}'",
+ :user_name => tracked_user.name),
+ # Authentication
+ :web => _("To follow requests by '{{user_name}}'",
+ :user_name => tracked_user.name),
+ :email => _("Then you will be notified whenever '{{user_name}}' requests something or gets a response.",
+ :user_name => tracked_user.name),
+ :email_subject => _("Confirm you want to follow requests by '{{user_name}}'",
+ :user_name => tracked_user.name),
+ # RSS sorting
+ :feed_sortby => 'newest'
+ }
+ end
+
+ def search_query_params
+ { # Website
+ :verb_on_page => _("Follow things matching this search"),
+ :verb_on_page_already => _("You are already following things matching this search"),
+ # Email
+ :title_in_email => _("Requests or responses matching your saved search"),
+ :title_in_rss => _("Requests or responses matching your saved search"),
+ # Authentication
+ :web => _("To follow requests and responses matching your search"),
+ :email => _("Then you will be notified whenever a new request or response matches your search."),
+ :email_subject => _("Confirm you want to follow new requests or responses matching your search"),
+ # RSS sorting - TODO: hmmm, we don't really know which to use
+ # here for sorting. Might be a query term (e.g. 'cctv'), in
+ # which case newest is good, or might be something like
+ # all refused requests in which case want to sort by
+ # described (when we discover criteria is met). Rather
+ # conservatively am picking described, as that will make
+ # things appear in feed more than the should, rather than less.
+ :feed_sortby => 'described'
+ }
+ end
+
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 4b83d8572..1c6dc0eb0 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -28,11 +28,7 @@ require 'digest/sha1'
class User < ActiveRecord::Base
strip_attributes!
- validates_presence_of :email, :message => _("Please enter your email address")
-
- validates_presence_of :name, :message => _("Please enter your name")
-
- validates_presence_of :hashed_password, :message => _("Please enter a password")
+ attr_accessor :password_confirmation, :no_xapian_reindex
has_many :info_requests, :order => 'created_at desc'
has_many :user_info_request_sent_alerts
@@ -43,9 +39,10 @@ class User < ActiveRecord::Base
has_many :censor_rules, :order => 'created_at desc'
has_many :info_request_batches, :order => 'created_at desc'
- attr_accessor :password_confirmation, :no_xapian_reindex
+ validates_presence_of :email, :message => _("Please enter your email address")
+ validates_presence_of :name, :message => _("Please enter your name")
+ validates_presence_of :hashed_password, :message => _("Please enter a password")
validates_confirmation_of :password, :message => _("Please enter the same password twice")
-
validates_inclusion_of :admin_level, :in => [
'none',
'super',
@@ -53,6 +50,10 @@ class User < ActiveRecord::Base
validate :email_and_name_are_valid
+ after_initialize :set_defaults
+ after_save :purge_in_cache
+ after_update :reindex_referencing_models
+
acts_as_xapian :texts => [ :name, :about_me ],
:values => [
[ :created_at_numeric, 1, "created_at", :number ] # for sorting
@@ -60,11 +61,111 @@ class User < ActiveRecord::Base
:terms => [ [ :variety, 'V', "variety" ] ],
:if => :indexed_by_search?
- after_initialize :set_defaults
+ # Return user given login email, password and other form parameters (e.g. name)
+ #
+ # The specific_user_login parameter says that login as a particular user is
+ # expected, so no parallel registration form is being displayed.
+ def self.authenticate_from_form(params, specific_user_login = false)
+ params[:email].strip!
+
+ if specific_user_login
+ auth_fail_message = _("Either the email or password was not recognised, please try again.")
+ else
+ auth_fail_message = _("Either the email or password was not recognised, please try again. Or create a new account using the form on the right.")
+ end
+
+ user = find_user_by_email(params[:email])
+ if user
+ # There is user with email, check password
+ unless user.has_this_password?(params[:password])
+ user.errors.add(:base, auth_fail_message)
+ end
+ else
+ # No user of same email, make one (that we don't save in the database)
+ # for the forms code to use.
+ user = User.new(params)
+ # deliberately same message as above so as not to leak whether registered
+ user.errors.add(:base, auth_fail_message)
+ end
+ user
+ end
+
+ # Case-insensitively find a user from their email
+ def self.find_user_by_email(email)
+ self.find(:first, :conditions => [ 'lower(email) = lower(?)', email ] )
+ end
+
+ # The "internal admin" is a special user for internal use.
+ def self.internal_admin_user
+ user = User.find_by_email(AlaveteliConfiguration::contact_email)
+ if user.nil?
+ password = PostRedirect.generate_random_token
+ user = User.new(
+ :name => 'Internal admin user',
+ :email => AlaveteliConfiguration.contact_email,
+ :password => password,
+ :password_confirmation => password
+ )
+ user.save!
+ end
+
+ user
+ end
+
+ def self.owns_every_request?(user)
+ !user.nil? && user.owns_every_request?
+ end
+
+ # Can the user see every request, response, and outgoing message, even hidden ones?
+ def self.view_hidden?(user)
+ !user.nil? && user.super?
+ end
+
+ # Should the user be kept logged into their own account
+ # if they follow a /c/ redirect link belonging to another user?
+ def self.stay_logged_in_on_redirect?(user)
+ !user.nil? && user.super?
+ end
+
+ # Used for default values of last_daily_track_email
+ def self.random_time_in_last_day
+ earliest_time = Time.now - 1.day
+ latest_time = Time.now
+ earliest_time + rand(latest_time - earliest_time).seconds
+ end
+
+ # Alters last_daily_track_email for every user, so alerts will be sent
+ # spread out fairly evenly throughout the day, balancing load on the
+ # server. This is intended to be called by hand from the Ruby console. It
+ # will mean quite a few users may get more than one email alert the day you
+ # do it, so have a care and run it rarely.
+ #
+ # This SQL statement is useful for seeing how spread out users are at the moment:
+ # select extract(hour from last_daily_track_email) as h, count(*) from users group by extract(hour from last_daily_track_email) order by h;
+ def self.spread_alert_times_across_day
+ self.find(:all).each do |user|
+ user.last_daily_track_email = User.random_time_in_last_day
+ user.save!
+ end
+ nil # so doesn't print all users on console
+ end
+
+ def self.encrypted_password(password, salt)
+ string_to_hash = password + salt # TODO: need to add a secret here too?
+ Digest::SHA1.hexdigest(string_to_hash)
+ end
+
+ def self.record_bounce_for_email(email, message)
+ user = User.find_user_by_email(email)
+ return false if user.nil?
+
+ user.record_bounce(message) if user.email_bounced_at.nil?
+ return true
+ 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")
+ created_at.strftime("%Y%m%d%H%M%S")
end
def variety
@@ -72,18 +173,18 @@ class User < ActiveRecord::Base
end
# requested_by: and commented_by: search queries also need updating after save
- after_update :reindex_referencing_models
def reindex_referencing_models
return if no_xapian_reindex == true
- if self.changes.include?('url_name')
- for comment in self.comments
- for info_request_event in comment.info_request_events
+ if changes.include?('url_name')
+ comments.each do |comment|
+ comment.info_request_events.each do |info_request_event|
info_request_event.xapian_mark_needs_index
end
end
- for info_request in self.info_requests
- for info_request_event in info_request.info_request_events
+
+ 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
@@ -91,11 +192,11 @@ class User < ActiveRecord::Base
end
def get_locale
- (self.locale || I18n.locale).to_s
+ (locale || I18n.locale).to_s
end
def visible_comments
- self.comments.find(:all, :conditions => 'visible')
+ comments.find(:all, :conditions => 'visible')
end
# Don't display any leading/trailing spaces
@@ -106,62 +207,29 @@ class User < ActiveRecord::Base
if not name.nil?
name.strip!
end
- if self.public_banned?
+ if public_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
# do this in two steps so the string still gets picked up for translation
- name = _("{{user_name}} (Account suspended)", :user_name=> name.html_safe)
+ name = _("{{user_name}} (Account suspended)", :user_name => name.html_safe)
name = "#{name}"
end
name
end
- # Return user given login email, password and other form parameters (e.g. name)
- #
- # The specific_user_login parameter says that login as a particular user is
- # expected, so no parallel registration form is being displayed.
- def User.authenticate_from_form(params, specific_user_login = false)
- params[:email].strip!
-
- if specific_user_login
- auth_fail_message = _("Either the email or password was not recognised, please try again.")
- else
- auth_fail_message = _("Either the email or password was not recognised, please try again. Or create a new account using the form on the right.")
- end
-
- user = self.find_user_by_email(params[:email])
- if user
- # There is user with email, check password
- if !user.has_this_password?(params[:password])
- user.errors.add(:base, auth_fail_message)
- end
- else
- # No user of same email, make one (that we don't save in the database)
- # for the forms code to use.
- user = User.new(params)
- # deliberately same message as above so as not to leak whether registered
- user.errors.add(:base, auth_fail_message)
- end
- user
- end
-
- # Case-insensitively find a user from their email
- def User.find_user_by_email(email)
- return self.find(:first, :conditions => [ 'lower(email) = lower(?)', email ] )
- end
-
# When name is changed, also change the url name
def name=(name)
write_attribute(:name, name)
- self.update_url_name
+ update_url_name
end
+
def update_url_name
- url_name = MySociety::Format.simplify_url_part(self.name, 'user', 32)
+ url_name = MySociety::Format.simplify_url_part(name, 'user', 32)
# For user with same name as others, add on arbitary numeric identifier
unique_url_name = url_name
suffix_num = 2 # as there's already one without numeric suffix
- while not User.find_by_url_name(unique_url_name, :conditions => self.id.nil? ? nil : ["id <> ?", self.id] ).nil?
+ while not User.find_by_url_name(unique_url_name, :conditions => id.nil? ? nil : ["id <> ?", id] ).nil?
unique_url_name = url_name + "_" + suffix_num.to_s
suffix_num = suffix_num + 1
end
@@ -172,6 +240,7 @@ class User < ActiveRecord::Base
def password
@password
end
+
def password=(pwd)
@password = pwd
if pwd.blank?
@@ -179,40 +248,23 @@ class User < ActiveRecord::Base
return
end
create_new_salt
- self.hashed_password = User.encrypted_password(self.password, self.salt)
+ self.hashed_password = User.encrypted_password(password, salt)
end
def has_this_password?(password)
- expected_password = User.encrypted_password(password, self.salt)
- return self.hashed_password == expected_password
+ expected_password = User.encrypted_password(password, salt)
+ hashed_password == expected_password
end
# For use in to/from in email messages
def name_and_email
- return MailHandler.address_from_name_and_email(self.name, self.email)
- end
-
- # The "internal admin" is a special user for internal use.
- def User.internal_admin_user
- u = User.find_by_email(AlaveteliConfiguration::contact_email)
- if u.nil?
- password = PostRedirect.generate_random_token
- u = User.new(
- :name => 'Internal admin user',
- :email => AlaveteliConfiguration::contact_email,
- :password => password,
- :password_confirmation => password
- )
- u.save!
- end
-
- return u
+ MailHandler.address_from_name_and_email(name, email)
end
# Returns list of requests which the user hasn't described (and last
# changed more than a day ago)
def get_undescribed_requests
- self.info_requests.find(
+ info_requests.find(
:all,
:conditions => [ 'awaiting_description = ? and ' + InfoRequest.last_event_time_clause + ' < ?',
true, Time.now() - 1.day
@@ -223,7 +275,7 @@ class User < ActiveRecord::Base
# Can the user make new requests, without having to describe state of (most) existing ones?
def can_leave_requests_undescribed?
# TODO: should be flag in database really
- if self.url_name == "heather_brooke" || self.url_name == "heather_brooke_2"
+ if url_name == "heather_brooke" || url_name == "heather_brooke_2"
return true
end
return false
@@ -232,140 +284,102 @@ class User < ActiveRecord::Base
# Does the user magically gain powers as if they owned every request?
# e.g. Can classify it
def owns_every_request?
- self.super?
+ super?
end
# Does this user have extraordinary powers?
def super?
- self.admin_level == 'super'
- end
-
- def User.owns_every_request?(user)
- !user.nil? && user.owns_every_request?
- end
-
- # Can the user see every request, response, and outgoing message, even hidden ones?
- def User.view_hidden?(user)
- !user.nil? && user.super?
- end
-
- # Should the user be kept logged into their own account
- # if they follow a /c/ redirect link belonging to another user?
- def User.stay_logged_in_on_redirect?(user)
- !user.nil? && user.super?
+ admin_level == 'super'
end
# Does the user get "(admin)" links on each page on the main site?
def admin_page_links?
- self.super?
+ super?
end
# Is it public that they are banned?
def public_banned?
- !self.ban_text.empty?
+ !ban_text.empty?
end
# Various ways the user can be banned, and text to describe it if failed
def can_file_requests?
- self.ban_text.empty? && !self.exceeded_limit?
+ ban_text.empty? && !exceeded_limit?
end
def exceeded_limit?
# Some users have no limit
- return false if self.no_limit
+ return false if no_limit
# Batch request users don't have a limit
- return false if self.can_make_batch_requests?
+ return false if can_make_batch_requests?
# Has the user issued as many as MAX_REQUESTS_PER_USER_PER_DAY requests in the past 24 hours?
- return false if AlaveteliConfiguration::max_requests_per_user_per_day.blank?
- recent_requests = InfoRequest.count(:conditions => ["user_id = ? and created_at > now() - '1 day'::interval", self.id])
+ return false if AlaveteliConfiguration.max_requests_per_user_per_day.blank?
+ recent_requests = InfoRequest.count(:conditions => ["user_id = ? and created_at > now() - '1 day'::interval", id])
- return (recent_requests >= AlaveteliConfiguration::max_requests_per_user_per_day)
+ recent_requests >= AlaveteliConfiguration.max_requests_per_user_per_day
end
def next_request_permitted_at
- return nil if self.no_limit
+ return nil if no_limit
- n_most_recent_requests = InfoRequest.all(:conditions => ["user_id = ? and created_at > now() - '1 day'::interval", self.id], :order => "created_at DESC", :limit => AlaveteliConfiguration::max_requests_per_user_per_day)
+ n_most_recent_requests = InfoRequest.all(:conditions => ["user_id = ? and created_at > now() - '1 day'::interval", id],
+ :order => "created_at DESC",
+ :limit => AlaveteliConfiguration::max_requests_per_user_per_day)
return nil if n_most_recent_requests.size < AlaveteliConfiguration::max_requests_per_user_per_day
nth_most_recent_request = n_most_recent_requests[-1]
- return nth_most_recent_request.created_at + 1.day
+ nth_most_recent_request.created_at + 1.day
end
def can_make_followup?
- self.ban_text.empty?
+ ban_text.empty?
end
def can_make_comments?
- self.ban_text.empty?
+ ban_text.empty?
end
def can_contact_other_users?
- self.ban_text.empty?
+ ban_text.empty?
end
def can_fail_html
if ban_text
- text = self.ban_text.strip
+ text = ban_text.strip
else
raise "Unknown reason for ban"
end
text = CGI.escapeHTML(text)
text = MySociety::Format.make_clickable(text, :contract => 1)
text = text.gsub(/\n/, '<br>')
- return text.html_safe
+ text.html_safe
end
# Returns domain part of user's email address
def email_domain
- return PublicBody.extract_domain_from_email(self.email)
+ PublicBody.extract_domain_from_email(email)
end
# A photograph of the user (to make it all more human)
def set_profile_photo(new_profile_photo)
ActiveRecord::Base.transaction do
- if !self.profile_photo.nil?
- self.profile_photo.destroy
- end
+ profile_photo.destroy unless profile_photo.nil?
self.profile_photo = new_profile_photo
- self.save
+ save
end
end
- # Used for default values of last_daily_track_email
- def User.random_time_in_last_day
- earliest_time = Time.now() - 1.day
- latest_time = Time.now
- return earliest_time + rand(latest_time - earliest_time).seconds
- end
-
- # Alters last_daily_track_email for every user, so alerts will be sent
- # spread out fairly evenly throughout the day, balancing load on the
- # server. This is intended to be called by hand from the Ruby console. It
- # will mean quite a few users may get more than one email alert the day you
- # do it, so have a care and run it rarely.
- #
- # This SQL statement is useful for seeing how spread out users are at the moment:
- # select extract(hour from last_daily_track_email) as h, count(*) from users group by extract(hour from last_daily_track_email) order by h;
- def User.spread_alert_times_across_day
- for user in self.find(:all)
- user.last_daily_track_email = User.random_time_in_last_day
- user.save!
- end
- nil # so doesn't print all users on console
- end
-
# Return about me text for display as HTML
# TODO: Move this to a view helper
def get_about_me_for_html_display
- text = self.about_me.strip
+ text = about_me.strip
text = CGI.escapeHTML(text)
text = MySociety::Format.make_clickable(text, :contract => 1)
text = text.gsub(/\n/, '<br>')
- return text.html_safe
+ text.html_safe
end
def json_for_api
- return {
- :id => self.id,
- :url_name => self.url_name,
- :name => self.name,
- :ban_text => self.ban_text,
- :about_me => self.about_me,
+ {
+ :id => id,
+ :url_name => url_name,
+ :name => name,
+ :ban_text => ban_text,
+ :about_me => about_me,
# :profile_photo => self.profile_photo # ought to have this, but too hard to get URL out for now
# created_at / updated_at we only show the year on the main page for privacy reasons, so don't put here
}
@@ -374,40 +388,41 @@ class User < ActiveRecord::Base
def record_bounce(message)
self.email_bounced_at = Time.now
self.email_bounce_message = message
- self.save!
+ save!
end
def should_be_emailed?
- return (self.email_confirmed && self.email_bounced_at.nil?)
+ email_confirmed && email_bounced_at.nil?
end
def indexed_by_search?
- return self.email_confirmed
+ email_confirmed
end
def for_admin_column(complete = false)
if complete
columns = self.class.content_columns
else
- columns = self.class.content_columns.map{|c| c if %w(created_at updated_at admin_level email_confirmed).include?(c.name) }.compact
+ columns = self.class.content_columns.map do |c|
+ c if %w(created_at updated_at admin_level email_confirmed).include?(c.name)
+ end.compact
end
columns.each do |column|
- yield(column.human_name, self.send(column.name), column.type.to_s, column.name)
+ yield(column.human_name, send(column.name), column.type.to_s, column.name)
end
end
- ## Private instance methods
private
def create_new_salt
- self.salt = self.object_id.to_s + rand.to_s
+ self.salt = object_id.to_s + rand.to_s
end
def set_defaults
- if self.admin_level.nil?
+ if admin_level.nil?
self.admin_level = 'none'
end
- if self.new_record?
+ if new_record?
# make alert emails go out at a random time for each new user, so
# overall they are spread out throughout the day.
self.last_daily_track_email = User.random_time_in_last_day
@@ -415,35 +430,16 @@ class User < ActiveRecord::Base
end
def email_and_name_are_valid
- if self.email != "" && !MySociety::Validate.is_valid_email(self.email)
+ if email != "" && !MySociety::Validate.is_valid_email(email)
errors.add(:email, _("Please enter a valid email address"))
end
- if MySociety::Validate.is_valid_email(self.name)
+ if MySociety::Validate.is_valid_email(name)
errors.add(:name, _("Please enter your name, not your email address, in the name field."))
end
end
- ## Class methods
- def User.encrypted_password(password, salt)
- string_to_hash = password + salt # TODO: need to add a secret here too?
- Digest::SHA1.hexdigest(string_to_hash)
- end
-
- def User.record_bounce_for_email(email, message)
- user = User.find_user_by_email(email)
- return false if user.nil?
-
- if user.email_bounced_at.nil?
- user.record_bounce(message)
- end
- return true
- end
-
- after_save(:purge_in_cache)
- def purge_in_cache
- if self.name_changed?
- self.info_requests.each {|x| x.purge_in_cache}
- end
+ def purge_in_cache
+ info_requests.each { |x| x.purge_in_cache } if name_changed?
end
end
diff --git a/app/models/user_info_request_sent_alert.rb b/app/models/user_info_request_sent_alert.rb
index 098b773f8..cd163d14b 100644
--- a/app/models/user_info_request_sent_alert.rb
+++ b/app/models/user_info_request_sent_alert.rb
@@ -17,18 +17,22 @@
# Email: hello@mysociety.org; WWW: http://www.mysociety.org/
class UserInfoRequestSentAlert < ActiveRecord::Base
- belongs_to :user
- belongs_to :info_request
-
- validates_inclusion_of :alert_type, :in => [
+ ALERT_TYPES = [
'overdue_1', # tell user that info request has become overdue
'very_overdue_1', # tell user that info request has become very overdue
- 'new_response_reminder_1', # reminder user to classify the recent response
- 'new_response_reminder_2', # repeat reminder user to classify the recent response
- 'new_response_reminder_3', # repeat reminder user to classify the recent response
- 'not_clarified_1', # reminder that user has to explain part of the request
- 'comment_1', # tell user that info request has a new comment
+ 'new_response_reminder_1', # reminder user to classify the recent
+ # response
+ 'new_response_reminder_2', # repeat reminder user to classify the
+ # recent response
+ 'new_response_reminder_3', # repeat reminder user to classify the
+ # recent response
+ 'not_clarified_1', # reminder that user has to explain part of the
+ # request
+ 'comment_1' # tell user that info request has a new comment
]
-end
+ belongs_to :user
+ belongs_to :info_request
+ validates_inclusion_of :alert_type, :in => ALERT_TYPES
+end
diff --git a/app/views/admin_general/_admin_navbar.html.erb b/app/views/admin_general/_admin_navbar.html.erb
index 5cc740f70..14fc06092 100644
--- a/app/views/admin_general/_admin_navbar.html.erb
+++ b/app/views/admin_general/_admin_navbar.html.erb
@@ -10,6 +10,7 @@
<li><%= link_to 'Stats', admin_stats_path %></li>
<li><%= link_to 'Debug', admin_debug_path %></li>
<li><%= link_to 'Authorities', admin_body_list_path %></li>
+ <li><%= link_to 'Categories', admin_categories_path %></li>
<li><%= link_to 'Requests', admin_request_list_path %></li>
<li><%= link_to 'Users', admin_user_list_path %></li>
<li><%= link_to 'Tracks', admin_track_list_path %></li>
diff --git a/app/views/admin_general/index.html.erb b/app/views/admin_general/index.html.erb
index f29258162..a1f2e1d2d 100644
--- a/app/views/admin_general/index.html.erb
+++ b/app/views/admin_general/index.html.erb
@@ -183,8 +183,12 @@
<div id="new-authorities" class="accordion-body collapse">
<% for @change_request in @new_body_requests %>
<%= render :partial => 'change_request_summary'%>
- <%= link_to("Close and respond", admin_change_request_edit_path(@change_request), :class => 'btn') %>
- <%= link_to("Add authority", admin_body_new_path(:change_request_id => @change_request.id), :class => 'btn btn-primary') %>
+ <%= form_tag admin_change_request_update_path(@change_request), :class => "form form-horizontal" do %>
+ <%= submit_tag 'Close', :class => "btn btn-danger" %>
+ <%= link_to("Close and respond", admin_change_request_edit_path(@change_request), :class => 'btn') %>
+ <%= link_to("Add authority", admin_body_new_path(:change_request_id => @change_request.id), :class => 'btn btn-primary') %>
+ <% end %>
+
<% end %>
</div>
</div>
diff --git a/app/views/admin_public_body/_tag_help.html.erb b/app/views/admin_public_body/_tag_help.html.erb
index b64e65877..5d6990400 100644
--- a/app/views/admin_public_body/_tag_help.html.erb
+++ b/app/views/admin_public_body/_tag_help.html.erb
@@ -1,6 +1,6 @@
<h2>List of tags</h2>
<% first_row = true %>
-<% for row in PublicBodyCategories::get().with_headings() %>
+<% for row in PublicBodyCategory.get().with_headings() %>
<% if row.instance_of?(Array) %>
<% if row[0] != 'other' %>
<strong><%= row[0] %></strong>=<%= row[1] %>
diff --git a/app/views/admin_public_body/import_csv.html.erb b/app/views/admin_public_body/import_csv.html.erb
index d15ef1791..4b14226d1 100644
--- a/app/views/admin_public_body/import_csv.html.erb
+++ b/app/views/admin_public_body/import_csv.html.erb
@@ -51,19 +51,11 @@ Another One,another@example.com,Otro organismo,a_tag
</pre>
<p><strong>Supported fields:</strong>
- <ul>
- <li>
- <code>name</code> (i18n)
- <strong>Existing records cannot be renamed</strong>
- </li>
- <li><code>short_name</code> (i18n)</li>
- <li><code>request_email</code> (i18n)</li>
- <li><code>notes</code> (i18n)</li>
- <li><code>publication_scheme</code> (i18n)</li>
- <li><code>disclosure_log</code> (i18n)</li>
- <li><code>home_page</code></li>
- <li><code>tag_string</code> (tags separated by spaces)</li>
- </ul>
+ <ul>
+ <% PublicBody.csv_import_fields.each do |field, notes| %>
+ <li><code><%= field %></code> <%= sanitize(notes) %></li>
+ <% end %>
+ </ul>
</p>
<p><strong>Note:</strong> Choose <strong>dry run</strong> to test, without
@@ -84,7 +76,7 @@ Another One,another@example.com,Otro organismo,a_tag
<hr>
<p>Standard tags:
- <% for category, description in PublicBodyCategories::get().by_tag() %>
+ <% for category, description in PublicBodyCategory.get().by_tag() %>
<% if category != "other" %>
<strong><%= category %></strong>=<%= description %>;
<% end %>
diff --git a/app/views/admin_public_body_categories/_category_list_item.html.erb b/app/views/admin_public_body_categories/_category_list_item.html.erb
new file mode 100644
index 000000000..056ab6148
--- /dev/null
+++ b/app/views/admin_public_body_categories/_category_list_item.html.erb
@@ -0,0 +1,5 @@
+<div class="category-list-item" <% if heading %> data-id="categories_<%= category.id %>"<% end %>>
+ <%= link_to(category.title, edit_admin_category_path(category), :title => "view full details") %>
+</div>
+
+
diff --git a/app/views/admin_public_body_categories/_form.html.erb b/app/views/admin_public_body_categories/_form.html.erb
new file mode 100644
index 000000000..b0778d371
--- /dev/null
+++ b/app/views/admin_public_body_categories/_form.html.erb
@@ -0,0 +1,66 @@
+<%= error_messages_for 'category' %>
+
+<!--[form:public_body_category]-->
+
+<div id="div-locales">
+ <ul class="locales nav nav-tabs">
+ <% I18n.available_locales.each_with_index do |locale, i| %>
+ <li><a href="#div-locale-<%=locale.to_s%>" data-toggle="tab" ><%=locale_name(locale.to_s) || "Default locale"%></a></li>
+ <% end %>
+ </ul>
+ <div class="tab-content">
+<%
+ I18n.available_locales.each do |locale|
+ if locale==I18n.default_locale # The default locale is submitted as part of the bigger object...
+ prefix = 'public_body_category'
+ object = @category
+ else # ...but additional locales go "on the side"
+ prefix = "public_body_category[translated_versions][]"
+ object = @category.new_record? ?
+ PublicBodyCategory::Translation.new :
+ @category.find_translation_by_locale(locale.to_s) || PublicBodyCategory::Translation.new
+ end
+%>
+ <%= fields_for prefix, object do |t| %>
+ <div class="tab-pane" id="div-locale-<%=locale.to_s%>">
+ <div class="control-group">
+ <%= t.hidden_field :locale, :value => locale.to_s %>
+ <label for="<%= form_tag_id(t.object_name, :title, locale) %>" class="control-label">Title</label>
+ <div class="controls">
+ <%= t.text_field :title, :id => form_tag_id(t.object_name, :title, locale), :class => "span4" %>
+ </div>
+ </div>
+ <div class="control-group">
+ <label for="<%= form_tag_id(t.object_name, :description, locale) %>" class="control-label">Description</label>
+ <div class="controls">
+ <%= t.text_field :description, :id => form_tag_id(t.object_name, :description, locale), :class => "span4" %>
+ </div>
+ </div>
+ </div>
+ <%
+ end
+end
+%>
+ </div>
+</div>
+
+<% if PublicBody.find_by_tag(@category.category_tag).count == 0 or @category.errors.messages.keys.include?(:category_tag) %>
+ <h3>Common Fields</h3>
+
+ <div class="control-group">
+ <label for="public_body_category_category_tag" class="control-label">Category tag</label>
+ <div class="controls">
+ <%= f.text_field :category_tag, :class => "span4" %>
+ </div>
+ </div>
+<% end %>
+
+<h3>Headings</h3>
+<div class="control-group">
+ <% PublicBodyHeading.all.each do |heading| %>
+ <div class="span3">
+ &nbsp;<%= check_box_tag "headings[heading_#{heading.id}]", heading.id, heading_is_selected?(heading) %> <label for="headings_heading_<%= heading.id %>" class="control-label"><%= heading.name %></label>
+ </div>
+ <% end %>
+</div>
+<!--[eoform:public_body_category]-->
diff --git a/app/views/admin_public_body_categories/_heading_list.html.erb b/app/views/admin_public_body_categories/_heading_list.html.erb
new file mode 100644
index 000000000..4bd8bdc90
--- /dev/null
+++ b/app/views/admin_public_body_categories/_heading_list.html.erb
@@ -0,0 +1,26 @@
+<div class="accordion" id="category_list">
+ <% for heading in category_headings %>
+ <div class="accordion-group" data-id="headings_<%=heading.id%>">
+ <div class="accordion-heading accordion-toggle row">
+ <span class="item-title span6">
+ <a href="#heading_<%=heading.id%>_categories" data-toggle="collapse" data-parent="#categories" ><%= chevron_right %></a>
+ <strong><%= link_to(heading.name, edit_admin_heading_path(heading), :title => "view full details") %></strong>
+ </span>
+ </div>
+
+ <div id="heading_<%= heading.id %>_categories" class="accordion-body collapse row ">
+ <div class="well">
+ <div class="span12" id="heading_<%= heading.id %>_category_list" class="category-list">
+ <% heading.public_body_categories.each do |category| %>
+ <%= render :partial => 'category_list_item', :locals => { :category => category, :heading => heading } %>
+ <% end %>
+ </div>
+
+ <div class="form-actions save-panel">
+ <%= link_to "Save", '#', :class => "btn btn-primary disabled save-order", "data-heading-id" => heading.id, "data-list-id" => "#heading_#{heading.id}_category_list", 'data-endpoint' => reorder_categories_admin_heading_path(heading) %><p class="save-notice" data-unsaved-text="There are unsaved changes to the order of categories." data-success-text="Changes saved." data-error-text="There was an error saving your changes: ">Drag and drop to change the order of categories.</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ <% end %>
+</div>
diff --git a/app/views/admin_public_body_categories/edit.html.erb b/app/views/admin_public_body_categories/edit.html.erb
new file mode 100644
index 000000000..95988d688
--- /dev/null
+++ b/app/views/admin_public_body_categories/edit.html.erb
@@ -0,0 +1,30 @@
+<h1><%=@title%></h1>
+
+<div class="row">
+ <div class="span8">
+ <div id="public_body_category_form">
+ <%= form_for @category, :url => admin_category_path(@category), :html => { :class => "form form-horizontal" } do |f| %>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+ <div class="form-actions">
+ <%= f.submit 'Save', :accesskey => 's', :class => "btn btn-success" %></p>
+ </div>
+ <% end %>
+ </div>
+</div>
+
+<div class="row">
+ <div class="span8 well">
+ <%= link_to 'List all', admin_categories_path, :class => "btn" %>
+ </div>
+</div>
+
+<% if @tagged_public_bodies.empty? %>
+ <div class="row">
+ <div class="span8">
+ <%= form_tag(admin_category_path(@category), :method => 'delete', :class => "form form-inline") do %>
+ <%= hidden_field_tag(:public_body_id, { :value => @category.id } ) %>
+ <%= submit_tag "Destroy #{@category.title}", :title => @category.title, :class => "btn btn-danger" %> (this is permanent!)
+ <% end %>
+ </div>
+ </div>
+<% end %>
diff --git a/app/views/admin_public_body_categories/index.html.erb b/app/views/admin_public_body_categories/index.html.erb
new file mode 100644
index 000000000..62ec4623d
--- /dev/null
+++ b/app/views/admin_public_body_categories/index.html.erb
@@ -0,0 +1,28 @@
+<% @title = 'Listing public authority categories' %>
+
+<h1><%=@title%></h1>
+
+<div class="btn-toolbar">
+ <div class="btn-group">
+ <%= link_to 'New category', new_admin_category_path, :class => "btn btn-primary" %>
+ </div>
+ <div class="btn-group">
+ <%= link_to 'New category heading', new_admin_heading_path, :class => "btn" %>
+ </div>
+</div>
+
+<h2>All category headings</h2>
+<div>
+<%= render :partial => 'heading_list', :locals => { :category_headings => @category_headings, :table_name => 'exact' } %>
+
+<% if @without_heading.count > 0 %>
+
+ <h3>Categories with no heading</h3>
+
+ <% @without_heading.each do |category| %>
+ <%= render :partial => 'category_list_item', :locals => { :category => category, :heading => nil } %>
+ <% end %>
+<% end %>
+<div class="form-actions save-panel">
+<%= link_to "Save", '#', :class => "btn btn-primary disabled save-order", "data-list-id" => '#category_list', 'data-endpoint' => reorder_admin_headings_path %><p class="save-notice" data-unsaved-text="There are unsaved changes to the order of category headings." data-success-text="Changes saved." data-error-text="There was an error saving your changes: ">Drag and drop to change the order of category headings.</p>
+</div>
diff --git a/app/views/admin_public_body_categories/new.html.erb b/app/views/admin_public_body_categories/new.html.erb
new file mode 100644
index 000000000..8b1b1103f
--- /dev/null
+++ b/app/views/admin_public_body_categories/new.html.erb
@@ -0,0 +1,21 @@
+<% @title = 'New category' %>
+
+<h1><%=@title%></h1>
+<div class="row">
+ <div class="span8">
+ <div id="public_category_form">
+ <%= form_for @category, :url => admin_categories_path, :html => {:class => "form form-horizontal"} do |f| %>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+
+ <div class="form-actions">
+ <%= f.submit "Create", :class => "btn btn-primary" %>
+ </div>
+ <% end %>
+ <div class="row">
+ <div class="span8 well">
+ <%= link_to 'List all', admin_categories_path, :class => "btn" %>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/app/views/admin_public_body_headings/_form.html.erb b/app/views/admin_public_body_headings/_form.html.erb
new file mode 100644
index 000000000..d4e914ca1
--- /dev/null
+++ b/app/views/admin_public_body_headings/_form.html.erb
@@ -0,0 +1,41 @@
+<%= error_messages_for 'heading' %>
+
+<!--[form:public_body_heading]-->
+
+<div id="div-locales">
+ <ul class="locales nav nav-tabs">
+ <% I18n.available_locales.each_with_index do |locale, i| %>
+ <li><a href="#div-locale-<%=locale.to_s%>" data-toggle="tab" ><%=locale_name(locale.to_s) || "Default locale"%></a></li>
+ <% end %>
+ </ul>
+ <div class="tab-content">
+<%
+ for locale in I18n.available_locales do
+ if locale==I18n.default_locale # The default locale is submitted as part of the bigger object...
+ prefix = 'public_body_heading'
+ object = @heading
+ else # ...but additional locales go "on the side"
+ prefix = "public_body_heading[translated_versions][]"
+ object = @heading.new_record? ?
+ PublicBodyHeading::Translation.new :
+ @heading.find_translation_by_locale(locale.to_s) || PublicBodyHeading::Translation.new
+ end
+%>
+ <%= fields_for prefix, object do |t| %>
+ <div class="tab-pane" id="div-locale-<%=locale.to_s%>">
+ <div class="control-group">
+ <%= t.hidden_field :locale, :value => locale.to_s %>
+ <label for="<%= form_tag_id(t.object_name, :name, locale) %>" class="control-label">Name</label>
+ <div class="controls">
+ <%= t.text_field :name, :id => form_tag_id(t.object_name, :name, locale), :class => "span4" %>
+ </div>
+ </div>
+ </div>
+ <%
+ end
+end
+%>
+ </div>
+</div>
+
+<!--[eoform:public_body_heading]-->
diff --git a/app/views/admin_public_body_headings/edit.html.erb b/app/views/admin_public_body_headings/edit.html.erb
new file mode 100644
index 000000000..eff89285a
--- /dev/null
+++ b/app/views/admin_public_body_headings/edit.html.erb
@@ -0,0 +1,30 @@
+<h1><%=@title%></h1>
+
+<div class="row">
+ <div class="span8">
+ <div id="public_body_heading_form">
+ <%= form_for @heading, :url => admin_heading_path(@heading), :html => { :class => "form form-horizontal" } do |f| %>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+ <div class="form-actions">
+ <%= f.submit 'Save', :accesskey => 's', :class => "btn btn-success" %></p>
+ </div>
+ <% end %>
+ </div>
+</div>
+
+<div class="row">
+ <div class="span8 well">
+ <%= link_to 'List all', admin_categories_path, :class => "btn" %>
+ </div>
+</div>
+
+<% if @heading.public_body_categories.empty? %>
+ <div class="row">
+ <div class="span8">
+ <%= form_tag(admin_heading_path(@heading), :method => 'delete', :class => "form form-inline") do %>
+ <%= hidden_field_tag(:public_body_heading_id, { :value => @heading.id } ) %>
+ <%= submit_tag "Destroy #{@heading.name}", :name => @heading.name, :class => "btn btn-danger" %> (this is permanent!)
+ <% end %>
+ </div>
+ </div>
+<% end %>
diff --git a/app/views/admin_public_body_headings/new.html.erb b/app/views/admin_public_body_headings/new.html.erb
new file mode 100644
index 000000000..91d5d4a9d
--- /dev/null
+++ b/app/views/admin_public_body_headings/new.html.erb
@@ -0,0 +1,21 @@
+<% @title = 'New category heading' %>
+
+<h1><%=@title%></h1>
+<div class="row">
+ <div class="span8">
+ <div id="public_heading_form">
+ <%= form_for @heading, :url => admin_headings_path, :html => {:class => "form form-horizontal"} do |f| %>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+
+ <div class="form-actions">
+ <%= f.submit "Create", :class => "btn btn-primary" %>
+ </div>
+ <% end %>
+ <div class="row">
+ <div class="span8 well">
+ <%= link_to 'List all', admin_categories_path, :class => "btn" %>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/app/views/admin_request/_incoming_message_actions.html.erb b/app/views/admin_request/_incoming_message_actions.html.erb
index dd50eb047..22effcce5 100644
--- a/app/views/admin_request/_incoming_message_actions.html.erb
+++ b/app/views/admin_request/_incoming_message_actions.html.erb
@@ -25,7 +25,7 @@
<div class="control-group">
<label class="control-label">Mark <code>To:</code> address as spam</label>
<div class="controls">
- <%= link_to 'Spam Addresses', spam_addresses_path %>
+ <%= link_to 'Spam Addresses', admin_spam_addresses_path %>
</div>
</div>
diff --git a/app/views/admin_spam_addresses/index.html.erb b/app/views/admin_spam_addresses/index.html.erb
index 9846bc017..7a11f70e1 100644
--- a/app/views/admin_spam_addresses/index.html.erb
+++ b/app/views/admin_spam_addresses/index.html.erb
@@ -16,7 +16,7 @@
<div class="row">
<div class="span12">
- <%= form_for(@spam_address, :html => { :class => 'form-inline' }) do |f| -%>
+ <%= form_for(@spam_address, :url => admin_spam_addresses_path, :html => { :class => 'form-inline' }) do |f| -%>
<%= error_messages_for @spam_address %>
<%= f.text_field :email, :class => 'input-xxlarge', :placeholder => 'Enter email' %>
<%= f.submit 'Add Spam Address', :class => 'btn btn-warning' %>
@@ -39,7 +39,7 @@
<% @spam_addresses.each do |spam| %>
<tr>
<td><%= spam.email %></td>
- <td><%= link_to 'Remove', spam,
+ <td><%= link_to 'Remove', admin_spam_address_path(spam),
:method => :delete,
:confirm => 'This is permanent! Are you sure?',
:class => 'btn btn-mini btn-danger' %></td>
diff --git a/app/views/health_checks/index.html.erb b/app/views/health_checks/index.html.erb
new file mode 100644
index 000000000..67b1050a9
--- /dev/null
+++ b/app/views/health_checks/index.html.erb
@@ -0,0 +1,12 @@
+<h1>Health Checks</h1>
+
+<div class="checks">
+ <% @health_checks.each do |check| %>
+ <div class="check">
+ <ul>
+ <li>Message: <%= check_status(check) %></li>
+ <li>OK? <%= check.ok? %></li>
+ </ul>
+ </div>
+ <% end %>
+</div>
diff --git a/app/views/public_body/_more_info.html.erb b/app/views/public_body/_more_info.html.erb
new file mode 100644
index 000000000..8f0e448b6
--- /dev/null
+++ b/app/views/public_body/_more_info.html.erb
@@ -0,0 +1,17 @@
+<h2><%= _('More about this authority')%></h2>
+
+<% if !public_body.calculated_home_page.nil? %>
+ <%= link_to _('Home page of authority'), public_body.calculated_home_page %><br>
+<% end %>
+
+<% if !public_body.publication_scheme.empty? %>
+ <%= link_to _('Publication scheme'), public_body.publication_scheme %><br>
+<% end %>
+
+<% unless public_body.disclosure_log.empty? %>
+ <%= link_to _('Disclosure log'), public_body.disclosure_log %><br>
+<% end %>
+
+<%= link_to _('View FOI email address'), view_public_body_email_path(public_body.url_name) %><br>
+
+<%= link_to _("Ask us to update FOI email"), new_change_request_path(:body => public_body.url_name) %><br>
diff --git a/app/views/public_body/list.html.erb b/app/views/public_body/list.html.erb
index ce24daaf9..0750c7655 100644
--- a/app/views/public_body/list.html.erb
+++ b/app/views/public_body/list.html.erb
@@ -7,7 +7,7 @@
</li>
</ul>
<% first_row = true %>
- <% for row in PublicBodyCategories::get().with_headings() %>
+ <% for row in PublicBodyCategory.get().with_headings() %>
<% if row.instance_of?(Array) %>
<li>
<%= link_to_unless (@tag == row[0]), row[1], list_public_bodies_path(:tag => row[0]) %>
diff --git a/app/views/public_body/show.html.erb b/app/views/public_body/show.html.erb
index 9352747ea..011aea535 100644
--- a/app/views/public_body/show.html.erb
+++ b/app/views/public_body/show.html.erb
@@ -12,27 +12,7 @@
</p>
<%= render :partial => 'track/tracking_links', :locals => { :track_thing => @track_thing, :own_request => false, :location => 'sidebar' } %>
- <h2><%= _('More about this authority')%></h2>
- <% if !@public_body.calculated_home_page.nil? %>
- <%= link_to _('Home page of authority'), @public_body.calculated_home_page %><br>
- <% end %>
- <% if !@public_body.publication_scheme.empty? %>
- <%= link_to _('Publication scheme'), @public_body.publication_scheme %><br>
- <% end %>
- <% unless @public_body.disclosure_log.empty? %>
- <%= link_to _('Disclosure log'), @public_body.disclosure_log %><br>
- <% end %>
- <% if @public_body.has_tag?("charity") %>
- <% for tag_value in @public_body.get_tag_values("charity") %>
- <% if tag_value.match(/^SC/) %>
- <%= link_to _('Charity registration'), "http://www.oscr.org.uk/CharityIndexDetails.aspx?id=" + tag_value %><br>
- <% else %>
- <%= link_to _('Charity registration'), "http://www.charity-commission.gov.uk/SHOWCHARITY/RegisterOfCharities/CharityFramework.aspx?RegisteredCharityNumber=" + tag_value %><br>
- <% end %>
- <% end %>
- <% end %>
- <%= link_to _('View FOI email address'), view_public_body_email_path(@public_body.url_name) %><br>
- <%= link_to _("Ask us to update FOI email"), new_change_request_path(:body => @public_body.url_name) %><br>
+ <%= render :partial => 'more_info', :locals => { :public_body => @public_body } %>
</div>
<div id="header_left">
diff --git a/app/views/user/show.html.erb b/app/views/user/show.html.erb
index 7ae577565..b23f74326 100644
--- a/app/views/user/show.html.erb
+++ b/app/views/user/show.html.erb
@@ -121,6 +121,9 @@
<%= form_tag(show_user_url, :method => "get", :id=>"search_form") do %>
<div>
<%= text_field_tag(:user_query, params[:user_query], {:title => "type your search term here" }) %>
+ <%= select_tag :request_latest_status,
+ options_from_collection_for_select(@request_states, 'value', 'text', params[:request_latest_status]),
+ :prompt => _('Filter by Request Status (optional)') %>
<% if @is_you %>
<%= submit_tag(_("Search your contributions")) %>
<% else %>
diff --git a/config/application.rb b/config/application.rb
index fc8e0059e..ed4f07819 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -61,7 +61,6 @@ module Alaveteli
config.action_dispatch.rack_cache = nil
config.after_initialize do |app|
- require 'routing_filters.rb'
# Add a catch-all route to force routing errors to be handled by the application,
# rather than by middleware.
app.routes.append{ match '*path', :to => 'general#not_found' }
@@ -69,6 +68,7 @@ module Alaveteli
config.autoload_paths << "#{Rails.root.to_s}/lib/mail_handler"
config.autoload_paths << "#{Rails.root.to_s}/lib/attachment_to_html"
+ config.autoload_paths << "#{Rails.root.to_s}/lib/health_checks"
# See Rails::Configuration for more options
ENV['RECAPTCHA_PUBLIC_KEY'] = ::AlaveteliConfiguration::recaptcha_public_key
diff --git a/config/general.yml-example b/config/general.yml-example
index 0f32f6192..ac96b5e50 100644
--- a/config/general.yml-example
+++ b/config/general.yml-example
@@ -1,260 +1,771 @@
# general.yml-example:
# Example values for the "general" config file.
#
+# Documentation on configuring Alaveteli is available at
+# http://alaveteli.org/docs/customising/
+#
# Configuration parameters, in YAML syntax.
#
# Copy this file to one called "general.yml" in the same directory. Or
# have multiple config files and use a symlink to change between them.
+#
+# Default values for these settings can be found in
+# RAILS_ROOT/lib/configuration.rb
+#
+# ==============================================================================
# Site name appears in various places throughout the site
+#
+# SITE_NAME - String name of the site (default: 'Alaveteli')
+#
+# Examples:
+#
+# SITE_NAME: 'Alaveteli'
+# SITE_NAME: 'WhatDoTheyKnow'
+#
+# ---
SITE_NAME: 'Alaveteli'
# Domain used in URLs generated by scripts (e.g. for going in some emails)
-DOMAIN: '127.0.0.1:3000'
+#
+# DOMAIN - String domain or IP address (default: 'localhost:3000')
+#
+# Examples:
+#
+# DOMAIN: '127.0.0.1:3000'
+# DOMAIN: 'www.example.com'
+#
+# ---
+DOMAIN: 'www.example.org'
-# If true forces everyone (in the production environment) to use encrypted connections
-# (via https) by redirecting unencrypted connections. This is *highly* recommended
-# so that logins can't be intercepted by naughty people.
+# If true forces everyone (in the production environment) to use encrypted
+# connections (via https) by redirecting unencrypted connections. This is
+# *highly* recommended so that logins can't be intercepted by naughty people.
+#
+# FORCE_SSL - Boolean (default: true)
+#
+# ---
FORCE_SSL: true
# ISO country code of country currrently deployed in
# (http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
+#
+# ISO_COUNTRY_CODE - String country code (default: GB)
+#
+# Examples:
+#
+# ISO_COUNTRY_CODE: GB
+#
+# ---
ISO_COUNTRY_CODE: GB
# This is the timezone that times and dates are displayed in
# If not set defaults to UTC.
-TIME_ZONE: Australia/Sydney
+#
+# TIME_ZONE - String time zone (default: UTC)
+#
+# Examples:
+#
+# TIME_ZONE: Australia/Sydney
+# TIME_ZONE: Europe/London
+#
+# ---
+TIME_ZONE: UTC
# These feeds are displayed accordingly on the Alaveteli "blog" page:
-BLOG_FEED: 'https://www.mysociety.org/category/projects/whatdotheyknow/feed/'
-TWITTER_USERNAME: 'whatdotheyknow'
+#
+# BLOG_FEED - String url to the blog feed (default: nil)
+#
+# Examples:
+#
+# BLOG_FEED: https://www.mysociety.org/category/projects/whatdotheyknow/feed/
+#
+# ---
+BLOG_FEED: ''
+
+# If you want a twitter feed displayed on the "blog" page, provide the
+# widget ID and username.
+#
+# TWITTER_USERNAME - String Twitter username (default: nil)
+#
+# Examples:
+#
+# TWITTER_USERNAME: 'whatdotheyknow'
+#
+# ---
+TWITTER_USERNAME: ''
+
# Set the widget_id to get the Twitter sidebar on the blog page.
# To get one https://twitter.com/settings/widgets
+#
+# TWITTER_WIDGET_ID - String widget ID (default: false)
+#
+# Examples:
+#
+# TWITTER_WIDGET_ID: '833549204689320031'
+#
+# ---
TWITTER_WIDGET_ID: ''
-# Locales we wish to support in this app, space-delimited
-AVAILABLE_LOCALES: 'en es'
+# The locales you want your site to support. If there is more than one, use
+# spaces betwween the entries.
+#
+# AVAILABLE_LOCALES – String of space-separated locales (default: nil)
+#
+# Examples:
+#
+# AVAILABLE_LOCALES: 'en es'
+#
+# ---
+AVAILABLE_LOCALES: 'en'
+
+# Nominate one of the AVAILABLE_LOCALES locales as the default
+#
+# DEFAULT_LOCALE – String locale (default: nil)
+#
+# Examples:
+#
+# DEFAULT_LOCALE: 'en'
+#
+# ---
DEFAULT_LOCALE: 'en'
+
+# Should Alaveteli try to use the default language of the user's browser?
+#
+# USE_DEFAULT_BROWSER_LANGUAGE - Boolean (default: true)
+#
+# Examples:
+#
+# USE_DEFAULT_BROWSER_LANGUAGE: true
+#
+# ---
USE_DEFAULT_BROWSER_LANGUAGE: true
-# If you don't want the default locale to be included in URLs generated
-# by the application, set this to false
+# Normally, Alaveteli will put the locale into its URLs, like this
+# www.example.com/en/body/list/all. If you don't want this behaviour whenever
+# the locale is the default one, set INCLUDE_DEFAULT_LOCALE_IN_URLS to false.
+#
+# INCLUDE_DEFAULT_LOCALE_IN_URLS: Boolean (default: true)
+#
+# Examples:
+#
+# INCLUDE_DEFAULT_LOCALE_IN_URLS: false
+#
+# ---
INCLUDE_DEFAULT_LOCALE_IN_URLS: true
-# How many days should have passed before an answer to a request is officially late?
+# The REPLY...AFTER_DAYS settings define how many days must have passed before
+# an answer to a request is officially late. The SPECIAL case is for some types
+# of authority (for example: in the UK, schools) which are granted a bit longer
+# than everyone else to respond to questions.
+#
+# REPLY_LATE_AFTER_DAYS - Integer (default: 20)
+# REPLY_VERY_LATE_AFTER_DAYS - Integer (default: 40)
+# SPECIAL_REPLY_VERY_LATE_AFTER_DAYS - Integer (default: 60)
+#
+# Examples:
+#
+# REPLY_LATE_AFTER_DAYS: 20
+# REPLY_VERY_LATE_AFTER_DAYS: 40
+# SPECIAL_REPLY_VERY_LATE_AFTER_DAYS: 60
+#
+# ---
REPLY_LATE_AFTER_DAYS: 20
REPLY_VERY_LATE_AFTER_DAYS: 40
-# We give some types of authority like schools a bit longer than everyone else
SPECIAL_REPLY_VERY_LATE_AFTER_DAYS: 60
-# Whether the days above are given in working or calendar days. Value can be "working" or "calendar".
-# Default is "working".
+
+# The WORKING_OR_CALENDAR_DAYS setting can be either "working" (the default) or
+# "calendar", and determines which days are counted when calculating whether a
+# request is officially late.
+#
+# WORKING_OR_CALENDAR_DAYS - String in [working, calendar] (default: working)
+#
+# Examples:
+#
+# WORKING_OR_CALENDAR_DAYS: working
+# WORKING_OR_CALENDAR_DAYS: calendar
+#
+# ---
WORKING_OR_CALENDAR_DAYS: working
-# example public bodies for the home page, semicolon delimited - short_names
-# Comment out if you want this to be auto-generated. WARNING: this is slow & don't use production!
-FRONTPAGE_PUBLICBODY_EXAMPLES: 'tgq'
+# Specify which public bodies you want to be listed as examples on the home
+# page, using their short_names. If you want more than one, separate them with
+# semicolons. List is auto-generated if not set.
+#
+# *Warning:* this is slow — don't use in production!
+#
+# FRONTPAGE_PUBLICBODY_EXAMPLES - String semicolon-separated list of public
+# bodies (default: nil)
+#
+# Examples:
+#
+# FRONTPAGE_PUBLICBODY_EXAMPLES: 'tgq'
+# FRONTPAGE_PUBLICBODY_EXAMPLES: 'tgq;foo;bar'
+#
+# ---
+FRONTPAGE_PUBLICBODY_EXAMPLES: ''
-# URLs of themes to download and use (when running rails-post-deploy
-# script). Earlier in the list means the templates have a higher
-# priority.
+# URLs of themes to download and use (when running the rails-post-deploy
+# script). The earlier in the list means the templates have a higher priority.
+#
+# THEME_URLS - Array of theme URLs (default: [])
+#
+# Examples:
+#
+# THEME_URLS:
+# - 'git://github.com/mysociety/alavetelitheme.git'
+# - 'git://github.com/mysociety/whatdotheyknow-theme.git'
+#
+# ---
THEME_URLS:
- - 'git://github.com/mysociety/alavetelitheme.git'
+ - 'git://github.com/mysociety/alavetelitheme.git'
-# When rails-post-deploy installs the themes it will try this branch first
-# (but only if this config is set). If the branch doesn't exist it will fall
-# back to using a tagged version specific to your installed alaveteli version.
-# If that doesn't exist it will back to master.
+# When rails-post-deploy installs the themes, it will try to use the branch
+# specified by THEME_BRANCH first. If the branch doesn't exist it will fall
+# back to using a tagged version specific to your installed alaveteli version,
+# and if that doesn't exist it will fall back to master.
+#
+# THEME_BRANCH - Boolean (default: false)
+#
+# Examples:
+#
+# # Use the develop branch if it exists, otherwise fall back as described
+# THEME_BRANCH: 'develop'
+#
+# # try the use-with-alaveteli-xxx branch/tag, otherwise fall back to HEAD
+# THEME_BRANCH: false
+#
+# ---
THEME_BRANCH: false
-# Whether a user needs to sign in to start the New Request process
+# Does a user needs to sign in to start the New Request process?
+#
+# FORCE_REGISTRATION_ON_NEW_REQUEST - Boolean (default: false)
+#
+# ---
FORCE_REGISTRATION_ON_NEW_REQUEST: false
-
-## Incoming email
-# Your email domain, e.g. 'foifa.com'
+# Your email domain for incoming mail.
+#
+# INCOMING_EMAIL_DOMAIN – String domain (default: localhost)
+#
+# Examples:
+#
+# INCOMING_EMAIL_DOMAIN: 'localhost'
+# INCOMING_EMAIL_DOMAIN: 'foifa.com'
+#
+# ---
INCOMING_EMAIL_DOMAIN: 'localhost'
-# An optional prefix to help you distinguish FOI requests, e.g. 'foi+'
-INCOMING_EMAIL_PREFIX: ''
+# An optional prefix to help you distinguish FOI requests.
+#
+# INCOMING_EMAIL_PREFIX - String (default: foi+)
+#
+# Examples:
+#
+# INCOMING_EMAIL_PREFIX: '' # No prefix
+# INCOMING_EMAIL_PREFIX: 'alaveteli+'
+#
+# ---
+INCOMING_EMAIL_PREFIX: 'foi+'
-# used for hash in request email address
+# Used for hash in request email address.
+#
+# INCOMING_EMAIL_SECRET - String (default: dummysecret)
+#
+# Examples:
+#
+# INCOMING_EMAIL_SECRET: '11ae 4e3b 70ff c001 3682 4a51 e86d ef5f'
+#
+# ---
INCOMING_EMAIL_SECRET: 'xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx'
-# used as envelope from at the incoming email domain for cases where we don't care about failure
+# Used as envelope from at the incoming email domain for cases where you don't
+# care about failure.
+#
+# BLACKHOLE_PREFIX - String (default: do-not-reply-to-this-address)
+#
+# Examples:
+#
+# BLACKHOLE_PREFIX: 'do-not-reply-to-this-address'
+# BLACKHOLE_PREFIX: 'do-not-reply'
+#
+# ---
BLACKHOLE_PREFIX: 'do-not-reply-to-this-address'
-## Administration
-
-# The emergency user
+# Emergency admin user login username. YOU SHOULD CHANGE THIS.
+#
+# ADMIN_USERNAME - String (default: nil)
+#
+# Examples:
+#
+# ADMIN_USERNAME: 'admin-alaveteli'
+#
+# ---
ADMIN_USERNAME: 'adminxxxx'
+
+# Emergency admin user login password. YOU SHOULD CHANGE THIS.
+#
+# ADMIN_USERNAME - String (default: nil)
+#
+# Examples:
+#
+# ADMIN_PASSWORD: 'b38bCHBl;28'
+#
+# ---
ADMIN_PASSWORD: 'passwordx'
+
+# Disable the emergency admin user?
+#
+# DISABLE_EMERGENCY_USER - Boolean (default: false)
+#
+# ---
DISABLE_EMERGENCY_USER: false
-# Set this to true, and the admin interface will be available to anonymous users
+# Set this to true, and the admin interface will be available to anonymous
+# users. Obviously, you should not set this to be true in production
+# environments.
+#
+# SKIP_ADMIN_AUTH - Boolean (default: false)
+#
+# ---
SKIP_ADMIN_AUTH: false
-# Email "from" details
-CONTACT_EMAIL: 'postmaster@localhost'
-CONTACT_NAME: 'Alaveteli Webmaster'
+# Email "from" email address
+#
+# CONTACT_EMAIL: String email address (default: contact@localhost)
+#
+# ---
+CONTACT_EMAIL: 'contact@localhost'
+
+# Email "from" name
+#
+# CONTACT_NAME - String contact name (default: Alaveteli)
+#
+# ---
+CONTACT_NAME: 'Alaveteli'
+
+# Email "from" email address for track messages
+#
+# TRACK_SENDER_EMAIL - String email address (default: contact@localhost)
+#
+# ---
+TRACK_SENDER_EMAIL: 'contact@localhost'
-# Email "from" details for track messages
-TRACK_SENDER_EMAIL: 'postmaster@localhost'
-TRACK_SENDER_NAME: 'Alaveteli Webmaster'
+# Email "from" name for track messages
+#
+# TRACK_SENDER_NAME - String contact name (default: Alaveteli)
+#
+# ---
+TRACK_SENDER_NAME: 'Alaveteli'
-# Where the raw incoming email data gets stored; make sure you back
+# Directory where the raw incoming email data gets stored; make sure you back
# this up!
+#
+# RAW_EMAILS_LOCATION - String path (default: files/raw_emails)
+#
+# ---
RAW_EMAILS_LOCATION: 'files/raw_emails'
-# Secret key for signing cookie_store sessions
+# Secret key for signing cookie_store sessions. Make it long and random.
+#
+# COOKIE_STORE_SESSION_SECRET - String (default: 'this default is insecure as
+# code is open source, please override
+# for live sites in config/general; this
+# will do for local development')
+#
+# Examples:
+#
+# COOKIE_STORE_SESSION_SECRET: 'uIngVC238Jn9NsaQizMNf89pliYmDBFugPjHS2JJmzOp8'
+#
+# ---
COOKIE_STORE_SESSION_SECRET: 'your secret key here, make it long and random'
# If present, puts the site in read only mode, and uses the text as reason
# (whole paragraph). Please use a read-only database user as well, as it only
-# checks in a few obvious places.
+# checks in a few obvious places. Typically, you do not want to run your site
+# in read-only mode.
+#
+# READ_ONLY - String (default: nil)
+#
+# Examples:
+#
+# READ_ONLY: 'The site is not currently accepting requests while we move the
+# server.'
+#
+# ---
READ_ONLY: ''
-# Is this a staging or dev site (1) or a live site (0).
-# Controls whether or not the rails-post-deploy script
-# will create the file config/rails_env.rb file to force
-# Rails into production environment.
-STAGING_SITE: 1
+# Is this a staging or development site? If not, it's a live production site.
+# This setting controls whether or not the rails-post-deploy script will create
+# the file config/rails_env.rb file to force Rails into production environment.
+#
+# STAGING_SITE: Integer in [0, 1]
+#
+# Examples:
+#
+# # For staging or development:
+# STAGING_SITE: 1
+#
+# # For production:
+# STAGING_SITE: 0
+#
+# ---
+STAGING_SITE: 0
-# Recaptcha, for detecting humans. Get keys here: http://recaptcha.net/whyrecaptcha.html
+# Recaptcha, for detecting humans. Get keys here:
+# http://recaptcha.net/whyrecaptcha.html
+#
+# RECAPTCHA_PUBLIC_KEY - String (default: 'x')
+#
+# ---
RECAPTCHA_PUBLIC_KEY: 'x'
+
+# Recaptcha, for detecting humans. Get keys here:
+# http://recaptcha.net/whyrecaptcha.html
+#
+# RECAPTCHA_PRIVATE_KEY - String (default: 'x')
+#
+# ---
RECAPTCHA_PRIVATE_KEY: 'x'
# Number of days after which to send a 'new response reminder'
+#
+# NEW_RESPONSE_REMINDER_AFTER_DAYS – Array of Integers (default: [3, 10, 24])
+#
+# Examples:
+#
+# NEW_RESPONSE_REMINDER_AFTER_DAYS: [3, 7]
+#
+# ---
NEW_RESPONSE_REMINDER_AFTER_DAYS: [3, 10, 24]
-# For debugging memory problems. If true, the app logs
-# the memory use increase of the Ruby process due to the
-# request (Linux only). Since Ruby never returns memory to the OS, if the
-# existing process previously served a larger request, this won't
-# show any consumption for the later request.
+# For debugging memory problems. If true, Alaveteli logs the memory use
+# increase of the Ruby process due to the request (Linux only). Since Ruby
+# never returns memory to the OS, if the existing process previously served a
+# larger request, this won't show any consumption for the later request.
+#
+# DEBUG_RECORD_MEMORY - Boolean (default: false)
+#
+# ---
DEBUG_RECORD_MEMORY: false
-# Currently we default to using pdftk to compress PDFs. You can
-# optionally try Ghostscript, which should do a better job of
-# compression. Some versions of pdftk are buggy with respect to
-# compression, in which case Alaveteli doesn't recompress the PDFs at
-# all and logs a warning message "Unable to compress PDF"; which would
-# be another reason to try this setting.
-USE_GHOSTSCRIPT_COMPRESSION: true
+# Currently we default to using pdftk to compress PDFs. You can optionally try
+# Ghostscript, which should do a better job of compression. Some versions of
+# pdftk are buggy with respect to compression, in which case Alaveteli doesn't
+# recompress the PDFs at all and logs a warning message "Unable to compress
+# PDF" — which would be another reason to try this setting.
+#
+# USE_GHOSTSCRIPT_COMPRESSION - Boolean (default: false)
+#
+# ---
+USE_GHOSTSCRIPT_COMPRESSION: false
-# mySociety's gazeteer service. Shouldn't change.
+# Alateveli uses mySociety's gazeteer service to determine country from
+# incoming IP address (this lets us suggest an Alaveteli in the user's country
+# if one exists). You shouldn't normally need to change this.
+#
+# GAZE_URL - String (default: http://gaze.mysociety.org)
+#
+# Examples:
+#
+# GAZE_URL: http://gaze.example.org
+#
+# ---
GAZE_URL: http://gaze.mysociety.org
-# The email address to which non-bounce responses should be forwarded
+# The email address to which non-bounce responses to emails sent out by
+# Alaveteli should be forwarded
+#
+# FORWARD_NONBOUNCE_RESPONSES_TO - String (default: user-support@localhost)
+#
+# Examples:
+#
+# FORWARD_NONBOUNCE_RESPONSES_TO: user-support@example.com
+#
+# ---
FORWARD_NONBOUNCE_RESPONSES_TO: user-support@localhost
-# Path to a program that converts an HTML page in a file to PDF. It
-#should take two arguments: the URL, and a path to an output file.
+# Path to a program that converts an HTML page in a file to PDF. Also used to
+# download a zip file of all the correspondence for a request. It should take
+# two arguments: the URL, and a path to an output file.
+#
# A static binary of wkhtmltopdf is recommended:
# http://code.google.com/p/wkhtmltopdf/downloads/list
# If the command is not present, a text-only version will be rendered
# instead.
+#
+# HTML_TO_PDF_COMMAND - String (default: nil)
+#
+# Examples:
+#
+# HTML_TO_PDF_COMMAND: /usr/local/bin/wkhtmltopdf
+# HTML_TO_PDF_COMMAND: /usr/local/bin/wkhtmltopdf-amd64
+#
+# ---
HTML_TO_PDF_COMMAND: /usr/local/bin/wkhtmltopdf-amd64
-# Exception notifications
-EXCEPTION_NOTIFICATIONS_FROM: do-not-reply-to-this-address@example.com
+# Email address used for sending exception notifications.
+#
+# EXCEPTION_NOTIFICATIONS_FROM - String (default: nil)
+#
+# Examples:
+#
+# EXCEPTION_NOTIFICATIONS_FROM: do-not-reply-to-this-address@example.com
+#
+# ---
+EXCEPTION_NOTIFICATIONS_FROM: do-not-reply-to-this-address@localhost
+
+# Email address(es) used for receiving exception notifications.
+#
+# EXCEPTION_NOTIFICATIONS_TO - Array of Strings (default: nil)
+#
+# Examples:
+#
+# EXCEPTION_NOTIFICATIONS_TO:
+# - robin@example.com
+# - seb@example.com
+#
+# ---
EXCEPTION_NOTIFICATIONS_TO:
- - robin@example.org
- - seb@example.org
+ - alaveteli@localhost
# This rate limiting can be turned off per-user via the admin interface
+#
+# MAX_REQUESTS_PER_USER_PER_DAY - Integer (default: 6)
+#
+# Examples:
+#
+# MAX_REQUESTS_PER_USER_PER_DAY: 1
+# MAX_REQUESTS_PER_USER_PER_DAY: '' # No limit
+#
+# ---
MAX_REQUESTS_PER_USER_PER_DAY: 6
+# If you're running behind Varnish set this to work out where to send purge
+# requests. Otherwise, don't set it.
+#
+# VARNISH_HOST - String (default: nil)
+#
+# Examples:
+#
+# VARNISH_HOST: localhost
+#
+# ---
+VARNISH_HOST: null
-# This is used to work out where to send purge requests. Should be
-# unset if you aren't running behind varnish
-VARNISH_HOST: localhost
-
-# Adding a value here will enable Google Analytics on all non-admin pages for non-admin users.
+# Adding a value here will enable Google Analytics on all non-admin pages for
+# non-admin users.
+#
+# GA_CODE - String (default: nil)
+#
+# Examples:
+#
+# GA_CODE: 'AB-8222142-14'
+#
+# ---
GA_CODE: ''
-# If you want to override *all* the public body request emails with your own
-# email so that request emails that would normally go to the public body
-# go to you, then uncomment below and fill in your email.
-# Useful for a staging server to play with the whole process of sending requests
-# without inadvertently sending an email to a real authority
-#OVERRIDE_ALL_PUBLIC_BODY_REQUEST_EMAILS: test-email@foo.com
+# If you want to override all the public body request emails with your own
+# email address so that request emails that would normally go to the public
+# body go to you, use this setting. This is useful for a staging server, so you
+# can play with the whole process of sending requests without inadvertently
+# sending an email to a real authority.
+#
+# OVERRIDE_ALL_PUBLIC_BODY_REQUEST_EMAILS - String (default: nil)
+#
+# Examples:
+#
+# OVERRIDE_ALL_PUBLIC_BODY_REQUEST_EMAILS: test-email@example.com
+#
+# ---
+# OVERRIDE_ALL_PUBLIC_BODY_REQUEST_EMAILS: test-email@example.com
-# Search path for external commandline utilities (such as pdftohtml, pdftk, unrtf)
+# Search path for external commandline utilities (such as pdftohtml, pdftk,
+# unrtf)
+#
+# UTILITY_SEARCH_PATH - Array of Strings
+# (default: ["/usr/bin", "/usr/local/bin"])
+#
+# Examples:
+#
+# UTILITY_SEARCH_PATH: ["/usr/bin"]
+# UTILITY_SEARCH_PATH: ["/usr/local/bin", "/opt/bin"]
+#
+# ---
UTILITY_SEARCH_PATH: ["/usr/bin", "/usr/local/bin"]
-# Path to your exim or postfix log files that will get sucked up by script/load-mail-server-logs
+# Path to your exim or postfix log files that will get sucked up by
+# script/load-mail-server-logs
+#
+# MTA_LOG_PATH - String (default: /var/log/exim4/exim-mainlog-*)
+#
+# Examples:
+#
+# MTA_LOG_PATH: '/var/log/exim4/exim-mainlog-*'
+#
+# ---
MTA_LOG_PATH: '/var/log/exim4/exim-mainlog-*'
-# Whether we are using "exim" or "postfix" for our MTA
-MTA_LOG_TYPE: "exim"
+# Are you using "exim" or "postfix" for your Mail Transfer Agent (MTA)?
+#
+# MTA_LOG_TYPE - String (default: exim)
+#
+# Examples:
+#
+# MTA_LOG_TYPE: exim
+# MTA_LOG_TYPE: postfix
+#
+# ---
+MTA_LOG_TYPE: exim
# URL where people can donate to the organisation running the site. If set,
# this will be included in the message people see when their request is
# successful.
+#
+# DONATION_URL - String (default: nil)
+#
+# Examples:
+#
+# DONATION_URL: http://www.mysociety.org/donate
+#
+# ---
DONATION_URL: "http://www.mysociety.org/donate/"
-# If you set this to 'true' then a page of statistics on the
-# performance of public bodies will be available:
+# If PUBLIC_BODY_STATISTICS_PAGE is set to true, Alaveteli will make a page of
+# statistics on the performance of public bodies (which you can see at
+# /body_statistics).
+#
+# PUBLIC_BODY_STATISTICS_PAGE - Boolean (default: false)
+#
+# ---
PUBLIC_BODY_STATISTICS_PAGE: false
# The page of statistics for public bodies will only consider public
-# bodies that have had at least this number of requests:
-MINIMUM_REQUESTS_FOR_STATISTICS: 50
+# bodies that have had at least the number of requests set by
+# MINIMUM_REQUESTS_FOR_STATISTICS.
+#
+# MINIMUM_REQUESTS_FOR_STATISTICS - Integer (default: 100)
+#
+# ---
+MINIMUM_REQUESTS_FOR_STATISTICS: 100
-# If only some of the public bodies have been translated into every
-# available locale, you can allow a fallback to the default locale for
-# listing of public bodies.
+# If you would like the public body list page to include bodies that have no
+# translation in the current locale (but which do have a translation in the
+# default locale), set this to true.
+#
+# PUBLIC_BODY_LIST_FALLBACK_TO_DEFAULT_LOCALE - Boolean (default: false)
+#
+# ---
PUBLIC_BODY_LIST_FALLBACK_TO_DEFAULT_LOCALE: false
# If true, while in development mode, try to send mail by SMTP to port
-# 1025 (the port the mailcatcher listens on by default):
+# 1025 (the port the mailcatcher listens on by default)
+#
+# USE_MAILCATCHER_IN_DEVELOPMENT - Boolean (default: true)
+#
+# ---
USE_MAILCATCHER_IN_DEVELOPMENT: true
-# Use memcached to cache HTML fragments for better performance. Will
+# Use memcached to cache HTML fragments for better performance. This will
# only have an effect in environments where
# config.action_controller.perform_caching is set to true
+#
+# CACHE_FRAGMENTS - Boolean (default: true)
+#
+# ---
CACHE_FRAGMENTS: true
-# The default bundle path is vendor/bundle; you can set this option to
-# change it.
+# The default bundle path is vendor/bundle; you can set this option to change it
+#
+# BUNDLE_PATH - String
+#
+# Examples:
+#
+# BUNDLE_PATH: vendor/bundle
+# BUNDLE_PATH: /var/alaveteli/bundle
+#
+# ---
BUNDLE_PATH: vendor/bundle
# In some deployments of Alaveteli you may wish to install each newly
# deployed version alongside the previous ones, in which case certain
-# files and resources should be shared between these installations:
-# for example, the 'files' directory, the 'cache' directory and the
+# files and resources should be shared between these installations.
+# For example, the 'files' directory, the 'cache' directory and the
# generated graphs such as 'public/foi-live-creation.png'. If you're
# installing Alaveteli in such a setup then set SHARED_FILES_PATH to
-# the directory you're keeping these files under. Otherwise, leave it
+# the directory you're keeping these files under. Otherwise, leave it
# blank.
+#
+# SHARED_FILES_PATH - String
+#
+# Examples:
+#
+# SHARED_FILES_PATH: /var/www/alaveteli/shared
+#
+# ---
SHARED_FILES_PATH: ''
# If you have SHARED_FILES_PATH set, then these options list the files
-# and directories that are shared; i.e. those that the deploy scripts
-# should create symlinks to from the repository.
+# that are shared; i.e. those that the deploy scripts should create symlinks to
+# from the repository.
+#
+# SHARED_FILES - Array of Strings
+#
+# Examples:
+#
+# SHARED_FILES:
+# - config/database.yml
+# - config/general.yml
+#
+# ---
SHARED_FILES:
- - config/database.yml
- - config/general.yml
- - config/rails_env.rb
- - config/newrelic.yml
- - config/httpd.conf
- - public/foi-live-creation.png
- - public/foi-user-use.png
- - config/aliases
+ - config/database.yml
+ - config/general.yml
+ - config/rails_env.rb
+ - config/newrelic.yml
+ - config/httpd.conf
+ - public/foi-live-creation.png
+ - public/foi-user-use.png
+ - config/aliases
+
+# If you have SHARED_FILES_PATH set, then these options list the directories
+# that are shared; i.e. those that the deploy scripts should create symlinks to
+# from the repository.
+#
+# SHARED_DIRECTORIES - Array of Strings
+#
+# Examples:
+#
+# SHARED_DIRECTORIES:
+# - files/
+# - cache/
+#
+# ---
SHARED_DIRECTORIES:
- - files/
- - cache/
- - lib/acts_as_xapian/xapiandbs/
- - log/
- - tmp/pids
- - vendor/bundle
- - public/assets
+ - files/
+ - cache/
+ - lib/acts_as_xapian/xapiandbs/
+ - log/
+ - tmp/pids
+ - vendor/bundle
+ - public/assets
# Allow some users to make batch requests to multiple authorities. Once
# this is set to true, you can enable batch requests for an individual
# user via the user admin page.
-
+#
+# ALLOW_BATCH_REQUESTS - Boolean (default: false)
+#
+# ---
ALLOW_BATCH_REQUESTS: false
-# Should we use the responsive stylesheets?
+# Use the responsive base stylesheets and templates, rather than those that
+# only render the site at a fixed width. They allow the site to render nicely
+# on mobile devices as well as larger screens. Set this to false if you want to
+# continue using fixed width stylesheets.
+#
+# RESPONSIVE_STYLING - Boolean (default: true)
+#
+# ---
RESPONSIVE_STYLING: true
diff --git a/config/initializers/alaveteli.rb b/config/initializers/alaveteli.rb
index 9ea6428ba..3a1220326 100644
--- a/config/initializers/alaveteli.rb
+++ b/config/initializers/alaveteli.rb
@@ -44,7 +44,6 @@ require 'world_foi_websites.rb'
require 'alaveteli_external_command.rb'
require 'quiet_opener.rb'
require 'mail_handler'
-require 'public_body_categories'
require 'ability'
require 'normalize_string'
require 'alaveteli_file_types'
@@ -54,6 +53,9 @@ require 'theme'
require 'xapian_queries'
require 'date_quarter'
require 'public_body_csv'
+require 'category_and_heading_migrator'
+require 'public_body_categories'
+require 'routing_filters'
AlaveteliLocalization.set_locales(AlaveteliConfiguration::available_locales,
AlaveteliConfiguration::default_locale)
@@ -62,3 +64,4 @@ AlaveteliLocalization.set_locales(AlaveteliConfiguration::available_locales,
if Rails.env == 'test' and ActiveRecord::Base.configurations['test']['constraint_disabling'] == false
require 'no_constraint_disabling'
end
+
diff --git a/config/initializers/health_checks.rb b/config/initializers/health_checks.rb
new file mode 100644
index 000000000..7fd1d3dda
--- /dev/null
+++ b/config/initializers/health_checks.rb
@@ -0,0 +1,23 @@
+Rails.application.config.after_initialize do
+ user_last_created = HealthChecks::Checks::DaysAgoCheck.new(
+ :failure_message => _('The last user was created over a day ago'),
+ :success_message => _('The last user was created in the last day')) do
+ User.last.created_at
+ end
+
+ incoming_message_last_created = HealthChecks::Checks::DaysAgoCheck.new(
+ :failure_message => _('The last incoming message was created over a day ago'),
+ :success_message => _('The last incoming message was created in the last day')) do
+ IncomingMessage.last.created_at
+ end
+
+ outgoing_message_last_created = HealthChecks::Checks::DaysAgoCheck.new(
+ :failure_message => _('The last outgoing message was created over a day ago'),
+ :success_message => _('The last outgoing message was created in the last day')) do
+ OutgoingMessage.last.created_at
+ end
+
+ HealthChecks.add user_last_created
+ HealthChecks.add incoming_message_last_created
+ HealthChecks.add outgoing_message_last_created
+end
diff --git a/config/routes.rb b/config/routes.rb
index f557e681b..ff99e884c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -61,6 +61,8 @@ Alaveteli::Application.routes.draw do
match '/request/:url_title/download' => 'request#download_entire_request', :as => :download_entire_request
####
+ resources :health_checks, :only => [:index]
+
resources :request, :only => [] do
resource :report, :only => [:new, :create]
end
@@ -179,6 +181,24 @@ Alaveteli::Application.routes.draw do
match '/admin/body/mass_tag_add' => 'admin_public_body#mass_tag_add', :as => :admin_body_mass_tag_add
####
+ #### AdminPublicBodyCategory controller
+ scope '/admin', :as => 'admin' do
+ resources :categories,
+ :controller => 'admin_public_body_categories'
+ end
+ ####
+
+ #### AdminPublicBodyHeading controller
+ scope '/admin', :as => 'admin' do
+ resources :headings,
+ :controller => 'admin_public_body_headings',
+ :except => [:index] do
+ post 'reorder', :on => :collection
+ post 'reorder_categories', :on => :member
+ end
+ end
+ ####
+
#### AdminPublicBodyChangeRequest controller
match '/admin/change_request/edit/:id' => 'admin_public_body_change_requests#edit', :as => :admin_change_request_edit
match '/admin/change_request/update/:id' => 'admin_public_body_change_requests#update', :as => :admin_change_request_update
@@ -250,7 +270,7 @@ Alaveteli::Application.routes.draw do
####
#### AdminSpamAddresses controller
- scope '/admin' do
+ scope '/admin', :as => 'admin' do
resources :spam_addresses,
:controller => 'admin_spam_addresses',
:only => [:index, :create, :destroy]
diff --git a/db/migrate/20140710094405_create_public_body_headings_and_categories.rb b/db/migrate/20140710094405_create_public_body_headings_and_categories.rb
new file mode 100644
index 000000000..0ba7f64a0
--- /dev/null
+++ b/db/migrate/20140710094405_create_public_body_headings_and_categories.rb
@@ -0,0 +1,27 @@
+class CreatePublicBodyHeadingsAndCategories < ActiveRecord::Migration
+ def up
+ create_table :public_body_headings, :force => true do |t|
+ t.string :locale
+ t.text :name, :null => false
+ t.integer :display_order
+ end
+
+ create_table :public_body_categories, :force => true do |t|
+ t.string :locale
+ t.text :title, :null => false
+ t.text :category_tag, :null => false
+ t.text :description, :null => false
+ end
+
+ create_table :public_body_categories_public_body_headings, :id => false do |t|
+ t.integer :public_body_category_id, :null => false
+ t.integer :public_body_heading_id, :null => false
+ end
+ end
+
+ def down
+ drop_table :public_body_categories
+ drop_table :public_body_headings
+ drop_table :public_body_categories_public_body_headings
+ end
+end \ No newline at end of file
diff --git a/db/migrate/20140716131107_create_category_translation_tables.rb b/db/migrate/20140716131107_create_category_translation_tables.rb
new file mode 100644
index 000000000..f4b90b330
--- /dev/null
+++ b/db/migrate/20140716131107_create_category_translation_tables.rb
@@ -0,0 +1,163 @@
+class CreateCategoryTranslationTables < ActiveRecord::Migration
+ class PublicBodyCategory < ActiveRecord::Base
+ translates :title, :description
+ end
+ class PublicBodyHeading < ActiveRecord::Base
+ translates :name
+ end
+ def up
+ default_locale = I18n.locale.to_s
+
+ fields = {:title => :text,
+ :description => :text}
+ PublicBodyCategory.create_translation_table!(fields)
+
+ # copy current values across to the default locale
+ PublicBodyCategory.where(:locale => default_locale).each do |category|
+ category.translated_attributes.each do |a, default|
+ value = category.read_attribute(a)
+ unless value.nil?
+ category.send(:"#{a}=", value)
+ end
+ end
+ category.save!
+ end
+
+ # copy current values across to the non-default locale(s)
+ PublicBodyCategory.where('locale != ?', default_locale).each do |category|
+ default_category = PublicBodyCategory.find_by_category_tag_and_locale(category.category_tag, default_locale)
+ I18n.with_locale(category.locale) do
+ category.translated_attributes.each do |a, default|
+ value = category.read_attribute(a)
+ unless value.nil?
+ if default_category
+ default_category.send(:"#{a}=", value)
+ else
+ category.send(:"#{a}=", value)
+ end
+ end
+ category.delete if default_category
+ end
+ end
+ if default_category
+ default_category.save!
+ category.delete
+ else
+ category.save!
+ end
+ end
+
+ fields = { :name => :text }
+ PublicBodyHeading.create_translation_table!(fields)
+
+ # copy current values across to the default locale
+ PublicBodyHeading.where(:locale => default_locale).each do |heading|
+ heading.translated_attributes.each do |a, default|
+ value = heading.read_attribute(a)
+ unless value.nil?
+ heading.send(:"#{a}=", value)
+ end
+ end
+ heading.save!
+ end
+
+ # copy current values across to the non-default locale(s)
+ PublicBodyHeading.where('locale != ?', default_locale).each do |heading|
+ default_heading = PublicBodyHeading.find_by_name_and_locale(heading.name, default_locale)
+ I18n.with_locale(heading.locale) do
+ heading.translated_attributes.each do |a, default|
+ value = heading.read_attribute(a)
+ unless value.nil?
+ if default_heading
+ default_heading.send(:"#{a}=", value)
+ else
+ heading.send(:"#{a}=", value)
+ end
+ end
+ heading.delete if default_heading
+ end
+ end
+ if default_heading
+ default_heading.save!
+ heading.delete
+ else
+ heading.save!
+ end
+ end
+
+ # finally, drop the old locale column from both tables
+ remove_column :public_body_headings, :locale
+ remove_column :public_body_categories, :locale
+ remove_column :public_body_headings, :name
+ remove_column :public_body_categories, :title
+ remove_column :public_body_categories, :description
+
+ # and set category_tag to be unique
+ add_index :public_body_categories, :category_tag, :unique => true
+ end
+
+ def down
+ # reinstate the columns
+ add_column :public_body_categories, :locale, :string
+ add_column :public_body_headings, :locale, :string
+ add_column :public_body_headings, :name, :string
+ add_column :public_body_categories, :title, :string
+ add_column :public_body_categories, :description, :string
+
+ # drop the index
+ remove_index :public_body_categories, :category_tag
+
+ # restore the data
+ new_categories = []
+ PublicBodyCategory.all.each do |category|
+ category.locale = category.translation.locale.to_s
+ I18n.available_locales.each do |locale|
+ if locale.to_s != category.locale
+ translation = category.translations.find_by_locale(locale)
+ if translation
+ new_cat = category.dup
+ category.translated_attributes.each do |a, _|
+ value = translation.read_attribute(a)
+ new_cat.send(:"#{a}=", value)
+ end
+ new_cat.locale = locale.to_s
+ new_categories << new_cat
+ end
+ else
+ category.save!
+ end
+ end
+ end
+ new_categories.each do |cat|
+ cat.save!
+ end
+
+ new_headings = []
+ PublicBodyHeading.all.each do |heading|
+ heading.locale = heading.translation.locale.to_s
+ I18n.available_locales.each do |locale|
+ if locale.to_s != heading.locale
+ new_heading = heading.dup
+ translation = heading.translations.find_by_locale(locale)
+ if translation
+ heading.translated_attributes.each do |a, _|
+ value = translation.read_attribute(a)
+ new_heading.send(:"#{a}=", value)
+ end
+ new_heading.locale = locale.to_s
+ new_headings << new_heading
+ end
+ else
+ heading.save!
+ end
+ end
+ end
+ new_headings.each do |heading|
+ heading.save!
+ end
+
+ # drop the translation tables
+ PublicBodyCategory.drop_translation_table!
+ PublicBodyHeading.drop_translation_table!
+ end
+end
diff --git a/db/migrate/20140804120601_add_display_order_to_categories_and_headings.rb b/db/migrate/20140804120601_add_display_order_to_categories_and_headings.rb
new file mode 100644
index 000000000..c2e7e2ac3
--- /dev/null
+++ b/db/migrate/20140804120601_add_display_order_to_categories_and_headings.rb
@@ -0,0 +1,15 @@
+class AddDisplayOrderToCategoriesAndHeadings < ActiveRecord::Migration
+ def up
+ add_column :public_body_categories_public_body_headings, :category_display_order, :integer
+ rename_table :public_body_categories_public_body_headings, :public_body_category_links
+ add_column :public_body_category_links, :id, :primary_key
+ add_index :public_body_category_links, [:public_body_category_id, :public_body_heading_id], :name => "index_public_body_category_links_on_join_ids", :primary => true
+ end
+
+ def down
+ remove_index :public_body_category_links, :name => "index_public_body_category_links_on_join_ids"
+ remove_column :public_body_category_links, :category_display_order
+ remove_column :public_body_category_links, :id
+ rename_table :public_body_category_links, :public_body_categories_public_body_headings
+ end
+end
diff --git a/doc/CHANGES.md b/doc/CHANGES.md
index 237355c1d..7a93f9cb0 100644
--- a/doc/CHANGES.md
+++ b/doc/CHANGES.md
@@ -1,3 +1,18 @@
+# rails-3-develop
+
+## Highlighted Features
+
+## Upgrade Notes
+
+* `CensorRule` now validates the presence of all attributes at the model layer,
+ rather than only as a database constraint. If you have added a `CensorRule` in
+ your theme, you will now have to satisfy the additional validations on the
+ `:replacement`, `:last_edit_comment` and `:last_edit_editor` attributes.
+* Public body categories will now be stored in the database rather than being read directly from the `lib/public_body_categories_LOCALE` files. Once you have upgraded, run `script/migrate-public-body-categories`to import the contents of the files into the database. All further changes will then need to be made via the administrative interface. You can then remove any `pubic_body_categories_[locale].rb` files from your theme. If your theme has any calls to `PublicBodyCategories` methods outside these files, you should update them to call the corresponding method on `PublicBodyCategory` instead.
+* `CensorRule#require_user_request_or_public_body`, `CensorRule#make_regexp` and
+ `CensorRule#require_valid_regexp` have become private methods. If you override
+ them in your theme, ensure they are preceded by the `private` keyword.
+
# Version 0.19
## Highlighted Features
@@ -58,11 +73,11 @@ candidate:
* Install `lockfile-progs` so that the `run-with-lockfile` shell script can be
used instead of the C program
-* Use responsive stylesheets in `config/general.yml`:
+* Use responsive stylesheets in `config/general.yml`:
`RESPONSIVE_STYLING: true`. If you don't currently use responsive styling,
and you don't want to get switched over just set `RESPONSIVE_STYLING: false`
and the fixed-width stylesheets will be used as before.
-* Allow access to public body stats page if desired in `config/general/yml`:
+* Allow access to public body stats page if desired in `config/general/yml`:
`PUBLIC_BODY_STATISTICS_PAGE: true`
* Run migrations to define track_things constraint correctly (Robin Houston) and
add additional index for `event_type` on `info_request_events` (Steven Day)
diff --git a/lib/alaveteli_localization.rb b/lib/alaveteli_localization.rb
index 6daab124a..2b6978c92 100644
--- a/lib/alaveteli_localization.rb
+++ b/lib/alaveteli_localization.rb
@@ -7,6 +7,7 @@ class AlaveteliLocalization
I18n.locale = default_locale
I18n.available_locales = available_locales.map { |locale_name| locale_name.to_sym }
I18n.default_locale = default_locale
+ RoutingFilter::Conditionallyprependlocale.locales = available_locales
end
def set_default_text_domain(name, path)
diff --git a/lib/category_and_heading_migrator.rb b/lib/category_and_heading_migrator.rb
new file mode 100644
index 000000000..402ea7204
--- /dev/null
+++ b/lib/category_and_heading_migrator.rb
@@ -0,0 +1,91 @@
+module CategoryAndHeadingMigrator
+
+ # This module migrates data from public_body_categories_[locale].rb files
+ # into PublicBodyHeading and PublicBodyCategory models
+
+ # Load all the data from public_body_categories_[locale].rb files.
+ def self.migrate_categories_and_headings
+ if PublicBodyCategory.count > 0
+ puts "PublicBodyCategories exist already, not migrating."
+ else
+ @first_locale = true
+ I18n.available_locales.each do |locale|
+ begin
+ load "public_body_categories_#{locale}.rb"
+ rescue MissingSourceFile
+ end
+ @first_locale = false
+ end
+ end
+ end
+
+ # Load the categories and headings for a locale
+ def self.add_categories_and_headings_from_list(locale, data_list)
+ # set the counter for headings loaded from this locale
+ @@locale_heading_display_order = 0
+ current_heading = nil
+ data_list.each do |list_item|
+ if list_item.is_a?(Array)
+ # item is list of category data
+ add_category(list_item, current_heading, locale)
+ else
+ # item is heading name
+ current_heading = add_heading(list_item, locale, @first_locale)
+ end
+ end
+ end
+
+ def self.add_category(category_data, heading, locale)
+ tag, title, description = category_data
+ category = PublicBodyCategory.find_by_category_tag(tag)
+ if category
+ add_category_in_locale(category, title, description, locale)
+ else
+ category = PublicBodyCategory.create(:category_tag => tag,
+ :title => title,
+ :description => description)
+
+ # add the translation if this is not the default locale
+ # (occurs when a category is not defined in default locale)
+ unless category.translations.map { |t| t.locale }.include?(locale)
+ add_category_in_locale(category, title, description, locale)
+ end
+ end
+ heading.add_category(category)
+ end
+
+ def self.add_category_in_locale(category, title, description, locale)
+ I18n.with_locale(locale) do
+ category.title = title
+ category.description = description
+ category.save
+ end
+ end
+
+ def self.add_heading(name, locale, first_locale)
+ heading = nil
+ I18n.with_locale(locale) do
+ heading = PublicBodyHeading.find_by_name(name)
+ end
+ # For multi-locale installs, we assume that all public_body_[locale].rb files
+ # use the same headings in the same order, so we add translations to the heading
+ # that was in the same position in the list loaded from other public_body_[locale].rb
+ # files.
+ if heading.nil? && !@first_locale
+ heading = PublicBodyHeading.where(:display_order => @@locale_heading_display_order).first
+ end
+
+ if heading
+ I18n.with_locale(locale) do
+ heading.name = name
+ heading.save
+ end
+ else
+ I18n.with_locale(locale) do
+ heading = PublicBodyHeading.create(:name => name)
+ end
+ end
+ @@locale_heading_display_order += 1
+ heading
+ end
+end
diff --git a/lib/configuration.rb b/lib/configuration.rb
index bd2d31ac2..2144f9954 100644
--- a/lib/configuration.rb
+++ b/lib/configuration.rb
@@ -42,11 +42,12 @@ module AlaveteliConfiguration
:HTML_TO_PDF_COMMAND => '',
:INCLUDE_DEFAULT_LOCALE_IN_URLS => true,
:INCOMING_EMAIL_DOMAIN => 'localhost',
- :INCOMING_EMAIL_PREFIX => '',
+ :INCOMING_EMAIL_PREFIX => 'foi+',
:INCOMING_EMAIL_SECRET => 'dummysecret',
:ISO_COUNTRY_CODE => 'GB',
:MINIMUM_REQUESTS_FOR_STATISTICS => 100,
- :MAX_REQUESTS_PER_USER_PER_DAY => '',
+ :MAX_REQUESTS_PER_USER_PER_DAY => 6,
+ :MTA_LOG_PATH => '/var/log/exim4/exim-mainlog-*',
:MTA_LOG_TYPE => 'exim',
:NEW_RESPONSE_REMINDER_AFTER_DAYS => [3, 10, 24],
:OVERRIDE_ALL_PUBLIC_BODY_REQUEST_EMAILS => '',
diff --git a/lib/health_checks/checks/days_ago_check.rb b/lib/health_checks/checks/days_ago_check.rb
new file mode 100644
index 000000000..793fff586
--- /dev/null
+++ b/lib/health_checks/checks/days_ago_check.rb
@@ -0,0 +1,28 @@
+module HealthChecks
+ module Checks
+ class DaysAgoCheck
+ include HealthChecks::HealthCheckable
+
+ attr_reader :days, :subject
+
+ def initialize(args = {}, &block)
+ @days = args.fetch(:days) { 1 }
+ @subject = block
+ super(args)
+ end
+
+ def failure_message
+ "#{ super }: #{ subject.call }"
+ end
+
+ def success_message
+ "#{ super }: #{ subject.call }"
+ end
+
+ def check
+ subject.call >= days.days.ago
+ end
+
+ end
+ end
+end
diff --git a/lib/health_checks/health_checkable.rb b/lib/health_checks/health_checkable.rb
new file mode 100644
index 000000000..5d674ca32
--- /dev/null
+++ b/lib/health_checks/health_checkable.rb
@@ -0,0 +1,28 @@
+module HealthChecks
+ module HealthCheckable
+
+ attr_accessor :failure_message, :success_message
+
+ def initialize(args = {})
+ self.failure_message = args.fetch(:failure_message) { _('Failed') }
+ self.success_message = args.fetch(:success_message) { _('Success') }
+ end
+
+ def name
+ self.class.to_s
+ end
+
+ def check
+ raise NotImplementedError
+ end
+
+ def ok?
+ check ? true : false
+ end
+
+ def message
+ ok? ? success_message : failure_message
+ end
+
+ end
+end
diff --git a/lib/health_checks/health_checks.rb b/lib/health_checks/health_checks.rb
new file mode 100644
index 000000000..6f0c9de8e
--- /dev/null
+++ b/lib/health_checks/health_checks.rb
@@ -0,0 +1,37 @@
+require 'health_checkable'
+
+Dir[File.dirname(__FILE__) + '/checks/*.rb'].each do |file|
+ require file
+end
+
+module HealthChecks
+ extend self
+
+ def all
+ @checks ||= []
+ end
+
+ def add(check)
+ if assert_valid_check(check)
+ all << check
+ check
+ else
+ false
+ end
+ end
+
+ def each(&block)
+ all.each(&block)
+ end
+
+ def ok?
+ all.all? { |check| check.ok? }
+ end
+
+ private
+
+ def assert_valid_check(check)
+ check.respond_to?(:check)
+ end
+
+end
diff --git a/lib/public_body_categories.rb b/lib/public_body_categories.rb
index 7f548b130..3528e85b1 100644
--- a/lib/public_body_categories.rb
+++ b/lib/public_body_categories.rb
@@ -1,60 +1,11 @@
-# lib/public_body_categories.rb:
-# Categorisations of public bodies.
-#
-# Copyright (c) 2009 UK Citizens Online Democracy. All rights reserved.
-# Email: hello@mysociety.org; WWW: http://www.mysociety.org/
-
+# Allow the PublicBodyCategory model to be addressed using the same syntax
+# as the old PublicBodyCategories class without needing to rename everything,
+# make sure we're not going to break any themes
class PublicBodyCategories
- attr_reader :with_description,
- :with_headings,
- :tags,
- :by_tag,
- :singular_by_tag,
- :by_heading,
- :headings
-
- def initialize(categories)
- @with_headings = categories
- # Arranged in different ways for different sorts of displaying
- @with_description = @with_headings.select() { |a| a.instance_of?(Array) }
- @tags = @with_description.map() { |a| a[0] }
- @by_tag = Hash[*@with_description.map() { |a| a[0..1] }.flatten]
- @singular_by_tag = Hash[*@with_description.map() { |a| [a[0],a[2]] }.flatten]
- @by_heading = {}
- heading = nil
- @headings = []
- @with_headings.each do |row|
- if ! row.instance_of?(Array)
- heading = row
- @headings << row
- @by_heading[row] = []
- else
- @by_heading[heading] << row[0]
- end
- end
- end
-
-
- def PublicBodyCategories.get
- load_categories if @@CATEGORIES.empty?
- @@CATEGORIES[I18n.locale.to_s] || @@CATEGORIES[I18n.default_locale.to_s] || PublicBodyCategories.new([])
+ def self.method_missing(method, *args, &block)
+ warn 'Use of PublicBodyCategories is deprecated and will be removed in release 0.21. Please use PublicBodyCategory instead.'
+ PublicBodyCategory.send(method, *args, &block)
end
- # Called from the data files themselves
- def PublicBodyCategories.add(locale, categories)
- @@CATEGORIES[locale.to_s] = PublicBodyCategories.new(categories)
- end
-
- private
- @@CATEGORIES = {}
-
- def PublicBodyCategories.load_categories()
- I18n.available_locales.each do |locale|
- begin
- load "public_body_categories_#{locale}.rb"
- rescue MissingSourceFile
- end
- end
- end
end
diff --git a/lib/public_body_categories_en.rb b/lib/public_body_categories_en.rb
deleted file mode 100644
index 95eed750b..000000000
--- a/lib/public_body_categories_en.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# The PublicBodyCategories structure works like this:
-# [
-# "Main category name",
-# [ "tag_to_use_as_category", "Sub category title", "sentence that can describes things in this subcategory" ],
-# [ "another_tag", "Second sub category title", "another descriptive sentence for things in this subcategory"],
-# "Another main category name",
-# [ "another_tag_2", "Another sub category title", "another descriptive sentence"]
-# ])
-#
-# DO NOT EDIT THIS FILE! It should be overridden in a custom theme.
-# See doc/THEMES.md for more info
-
-PublicBodyCategories.add(:en, [
- "Silly ministries",
- [ "useless_agency", "Useless ministries", "a useless ministry" ],
- [ "lonely_agency", "Lonely agencies", "a lonely agency"],
- "Popular agencies",
- [ "popular_agency", "Popular agencies", "a lonely agency"]
-])
diff --git a/lib/routing_filters.rb b/lib/routing_filters.rb
index a9a62b8db..5b5da6870 100644
--- a/lib/routing_filters.rb
+++ b/lib/routing_filters.rb
@@ -22,5 +22,13 @@ module RoutingFilter
prepend_segment!(result, locale) if prepend_locale?(locale)
end
end
+
+ # Reset the locale pattern when the locales are set.
+ class << self
+ def locales=(locales)
+ @@locales_pattern = nil
+ @@locales = locales.map(&:to_sym)
+ end
+ end
end
end
diff --git a/script/migrate-public-body-categories b/script/migrate-public-body-categories
new file mode 100755
index 000000000..23abe4648
--- /dev/null
+++ b/script/migrate-public-body-categories
@@ -0,0 +1,4 @@
+#!/bin/bash
+TOP_DIR="$(dirname "$BASH_SOURCE")/.."
+cd "$TOP_DIR"
+bundle exec rails runner 'CategoryAndHeadingMigrator.migrate_categories_and_headings'
diff --git a/spec/controllers/admin_general_controller_spec.rb b/spec/controllers/admin_general_controller_spec.rb
index 971960762..cc2ec41b4 100644
--- a/spec/controllers/admin_general_controller_spec.rb
+++ b/spec/controllers/admin_general_controller_spec.rb
@@ -8,13 +8,8 @@ describe AdminGeneralController do
before { basic_auth_login @request }
it "should render the front page" do
- get :index, :suppress_redirect => 1
- response.should render_template('index')
- end
-
- it "should redirect to include trailing slash" do
get :index
- response.should redirect_to admin_general_index_url(:trailing_slash => true)
+ response.should render_template('index')
end
end
diff --git a/spec/controllers/admin_public_body_categories_controller_spec.rb b/spec/controllers/admin_public_body_categories_controller_spec.rb
new file mode 100644
index 000000000..35454990d
--- /dev/null
+++ b/spec/controllers/admin_public_body_categories_controller_spec.rb
@@ -0,0 +1,192 @@
+require 'spec_helper'
+
+describe AdminPublicBodyCategoriesController do
+ context 'when showing the index of categories and headings' do
+ render_views
+
+ it 'shows the index page' do
+ get :index
+ end
+ end
+
+ context 'when showing the form for a new public body category' do
+ it 'should assign a new public body category to the view' do
+ get :new
+ assigns[:category].should be_a(PublicBodyCategory)
+ end
+ end
+
+ context 'when creating a public body category' do
+ it "creates a new public body category in one locale" do
+ n = PublicBodyCategory.count
+ post :create, {
+ :public_body_category => {
+ :title => 'New Category',
+ :category_tag => 'new_test_category',
+ :description => 'New category for testing stuff'
+ }
+ }
+ PublicBodyCategory.count.should == n + 1
+
+ category = PublicBodyCategory.find_by_title("New Category")
+ response.should redirect_to(admin_categories_path)
+ end
+
+ it "saves the public body category's heading associations" do
+ heading = FactoryGirl.create(:public_body_heading)
+ category_attributes = FactoryGirl.attributes_for(:public_body_category)
+ post :create, {
+ :public_body_category => category_attributes,
+ :headings => {"heading_#{heading.id}" => heading.id}
+ }
+ request.flash[:notice].should include('successful')
+ category = PublicBodyCategory.find_by_title(category_attributes[:title])
+ category.public_body_headings.should == [heading]
+ end
+
+
+ it 'creates a new public body category with multiple locales' do
+ n = PublicBodyCategory.count
+ post :create, {
+ :public_body_category => {
+ :title => 'New Category',
+ :category_tag => 'new_test_category',
+ :description => 'New category for testing stuff',
+ :translated_versions => [{ :locale => "es",
+ :title => "Mi Nuevo Category" }]
+ }
+ }
+ PublicBodyCategory.count.should == n + 1
+
+ category = PublicBodyCategory.find_by_title("New Category")
+ category.translations.map {|t| t.locale.to_s}.sort.should == ["en", "es"]
+ I18n.with_locale(:en) do
+ category.title.should == "New Category"
+ end
+ I18n.with_locale(:es) do
+ category.title.should == "Mi Nuevo Category"
+ end
+
+ response.should redirect_to(admin_categories_path)
+ end
+ end
+
+ context 'when editing a public body category' do
+ before do
+ @category = FactoryGirl.create(:public_body_category)
+ I18n.with_locale('es') do
+ @category.title = 'Los category'
+ @category.save!
+ end
+ end
+
+ render_views
+
+ it "edits a public body category" do
+ get :edit, :id => @category.id
+ end
+
+ it "edits a public body in another locale" do
+ get :edit, {:id => @category.id, :locale => :en}
+
+ # When editing a body, the controller returns all available translations
+ assigns[:category].find_translation_by_locale("es").title.should == 'Los category'
+ response.should render_template('edit')
+ end
+ end
+
+ context 'when updating a public body category' do
+
+ before do
+ @heading = FactoryGirl.create(:public_body_heading)
+ @category = FactoryGirl.create(:public_body_category)
+ link = FactoryGirl.create(:public_body_category_link,
+ :public_body_category => @category,
+ :public_body_heading => @heading,
+ :category_display_order => 0)
+ @tag = @category.category_tag
+ I18n.with_locale('es') do
+ @category.title = 'Los category'
+ @category.save!
+ end
+ end
+
+ render_views
+
+ it "saves edits to a public body category" do
+ post :update, { :id => @category.id,
+ :public_body_category => { :title => "Renamed" } }
+ request.flash[:notice].should include('successful')
+ pbc = PublicBodyCategory.find(@category.id)
+ pbc.title.should == "Renamed"
+ end
+
+ it "saves edits to a public body category's heading associations" do
+ @category.public_body_headings.should == [@heading]
+ heading = FactoryGirl.create(:public_body_heading)
+ post :update, { :id => @category.id,
+ :public_body_category => { :title => "Renamed" },
+ :headings => {"heading_#{heading.id}" => heading.id} }
+ request.flash[:notice].should include('successful')
+ pbc = PublicBodyCategory.find(@category.id)
+ pbc.public_body_headings.should == [heading]
+ end
+
+ it "saves edits to a public body category in another locale" do
+ I18n.with_locale(:es) do
+ @category.title.should == 'Los category'
+ post :update, {
+ :id => @category.id,
+ :public_body_category => {
+ :title => "Category",
+ :translated_versions => {
+ @category.id => {:locale => "es",
+ :title => "Renamed"}
+ }
+ }
+ }
+ request.flash[:notice].should include('successful')
+ end
+
+ pbc = PublicBodyCategory.find(@category.id)
+ I18n.with_locale(:es) do
+ pbc.title.should == "Renamed"
+ end
+ I18n.with_locale(:en) do
+ pbc.title.should == "Category"
+ end
+ end
+
+ it "does not save edits to category_tag if the category has associated bodies" do
+ body = FactoryGirl.create(:public_body, :tag_string => @tag)
+ post :update, { :id => @category.id,
+ :public_body_category => { :category_tag => "renamed" } }
+ request.flash[:notice].should include('can\'t')
+ pbc = PublicBodyCategory.find(@category.id)
+ pbc.category_tag.should == @tag
+ end
+
+
+ it "save edits to category_tag if the category has no associated bodies" do
+ category = PublicBodyCategory.create(:title => "Empty Category", :category_tag => "empty", :description => "-")
+ post :update, { :id => category.id,
+ :public_body_category => { :category_tag => "renamed" } }
+ request.flash[:notice].should include('success')
+ pbc = PublicBodyCategory.find(category.id)
+ pbc.category_tag.should == "renamed"
+ end
+ end
+
+ context 'when destroying a public body category' do
+
+ it "destroys a public body category" do
+ pbc = PublicBodyCategory.create(:title => "Empty Category", :category_tag => "empty", :description => "-")
+ n = PublicBodyCategory.count
+ post :destroy, { :id => pbc.id }
+ response.should redirect_to(admin_categories_path)
+ PublicBodyCategory.count.should == n - 1
+ end
+ end
+
+
+end
diff --git a/spec/controllers/admin_public_body_change_requests_controller_spec.rb b/spec/controllers/admin_public_body_change_requests_controller_spec.rb
index b478e851d..003510e60 100644
--- a/spec/controllers/admin_public_body_change_requests_controller_spec.rb
+++ b/spec/controllers/admin_public_body_change_requests_controller_spec.rb
@@ -15,21 +15,36 @@ describe AdminPublicBodyChangeRequestsController, 'updating a change request' do
before do
@change_request = FactoryGirl.create(:add_body_request)
- post :update, { :id => @change_request.id,
- :response => 'Thanks but no',
- :subject => 'Your request' }
end
it 'should close the change request' do
+ post :update, { :id => @change_request.id }
PublicBodyChangeRequest.find(@change_request.id).is_open.should == false
end
- it 'should send a response email to the user who requested the change' do
- deliveries = ActionMailer::Base.deliveries
- deliveries.size.should == 1
- mail = deliveries[0]
- mail.subject.should == 'Your request'
- mail.to.should == [@change_request.get_user_email]
- mail.body.should =~ /Thanks but no/
+ context 'when a response and subject are passed' do
+
+ it 'should send a response email to the user who requested the change' do
+ post :update, { :id => @change_request.id,
+ :response => 'Thanks but no',
+ :subject => 'Your request' }
+ deliveries = ActionMailer::Base.deliveries
+ deliveries.size.should == 1
+ mail = deliveries[0]
+ mail.subject.should == 'Your request'
+ mail.to.should == [@change_request.get_user_email]
+ mail.body.should =~ /Thanks but no/
+ end
+
+ end
+
+ context 'when no response or subject are passed' do
+
+ it 'should send a response email to the user who requested the change' do
+ post :update, { :id => @change_request.id }
+ deliveries = ActionMailer::Base.deliveries
+ deliveries.size.should == 0
+ end
end
+
end
diff --git a/spec/controllers/admin_public_body_headings_controller_spec.rb b/spec/controllers/admin_public_body_headings_controller_spec.rb
new file mode 100644
index 000000000..31517d238
--- /dev/null
+++ b/spec/controllers/admin_public_body_headings_controller_spec.rb
@@ -0,0 +1,240 @@
+require 'spec_helper'
+
+describe AdminPublicBodyHeadingsController do
+
+ context 'when showing the form for a new public body category' do
+ it 'should assign a new public body heading to the view' do
+ get :new
+ assigns[:heading].should be_a(PublicBodyHeading)
+ end
+ end
+
+ context 'when creating a public body heading' do
+ it "creates a new public body heading in one locale" do
+ n = PublicBodyHeading.count
+ post :create, {
+ :public_body_heading => {
+ :name => 'New Heading'
+ }
+ }
+ PublicBodyHeading.count.should == n + 1
+
+ heading = PublicBodyHeading.find_by_name("New Heading")
+ response.should redirect_to(admin_categories_path)
+ end
+
+ it 'creates a new public body heading with multiple locales' do
+ n = PublicBodyHeading.count
+ post :create, {
+ :public_body_heading => {
+ :name => 'New Heading',
+ :translated_versions => [{ :locale => "es",
+ :name => "Mi Nuevo Heading" }]
+ }
+ }
+ PublicBodyHeading.count.should == n + 1
+
+ heading = PublicBodyHeading.find_by_name("New Heading")
+ heading.translations.map {|t| t.locale.to_s}.sort.should == ["en", "es"]
+ I18n.with_locale(:en) do
+ heading.name.should == "New Heading"
+ end
+ I18n.with_locale(:es) do
+ heading.name.should == "Mi Nuevo Heading"
+ end
+
+ response.should redirect_to(admin_categories_path)
+ end
+ end
+
+ context 'when editing a public body heading' do
+ before do
+ @heading = FactoryGirl.create(:public_body_heading)
+ end
+
+ render_views
+
+ it "edits a public body heading" do
+ get :edit, :id => @heading.id
+ end
+ end
+
+ context 'when updating a public body heading' do
+ before do
+ @heading = FactoryGirl.create(:public_body_heading)
+ @name = @heading.name
+ end
+
+ it "saves edits to a public body heading" do
+ post :update, { :id => @heading.id,
+ :public_body_heading => { :name => "Renamed" } }
+ request.flash[:notice].should include('successful')
+ found_heading = PublicBodyHeading.find(@heading.id)
+ found_heading.name.should == "Renamed"
+ end
+
+ it "saves edits to a public body heading in another locale" do
+ I18n.with_locale(:es) do
+ post :update, {
+ :id => @heading.id,
+ :public_body_heading => {
+ :name => @name,
+ :translated_versions => {
+ @heading.id => {:locale => "es",
+ :name => "Renamed"}
+ }
+ }
+ }
+ request.flash[:notice].should include('successful')
+ end
+
+ heading = PublicBodyHeading.find(@heading.id)
+ I18n.with_locale(:es) do
+ heading.name.should == "Renamed"
+ end
+ I18n.with_locale(:en) do
+ heading.name.should == @name
+ end
+ end
+ end
+
+ context 'when destroying a public body heading' do
+
+ before do
+ @heading = FactoryGirl.create(:public_body_heading)
+ end
+
+ it "does not destroy a public body heading that has associated categories" do
+ category = FactoryGirl.create(:public_body_category)
+ link = FactoryGirl.create(:public_body_category_link,
+ :public_body_category => category,
+ :public_body_heading => @heading,
+ :category_display_order => 0)
+ n = PublicBodyHeading.count
+ post :destroy, { :id => @heading.id }
+ response.should redirect_to(edit_admin_heading_path(@heading))
+ PublicBodyHeading.count.should == n
+ end
+
+ it "destroys an empty public body heading" do
+ n = PublicBodyHeading.count
+ post :destroy, { :id => @heading.id }
+ response.should redirect_to(admin_categories_path)
+ PublicBodyHeading.count.should == n - 1
+ end
+ end
+
+ context 'when reordering public body headings' do
+
+ render_views
+
+ before do
+ @first = FactoryGirl.create(:public_body_heading, :display_order => 0)
+ @second = FactoryGirl.create(:public_body_heading, :display_order => 1)
+ @default_params = { :headings => [@second.id, @first.id] }
+ end
+
+ def make_request(params=@default_params)
+ post :reorder, params
+ end
+
+ context 'when handling valid input' do
+
+ it 'should reorder headings according to their position in the submitted params' do
+ make_request
+ PublicBodyHeading.find(@second.id).display_order.should == 0
+ PublicBodyHeading.find(@first.id).display_order.should == 1
+ end
+
+ it 'should return a "success" status' do
+ make_request
+ response.should be_success
+ end
+ end
+
+ context 'when handling invalid input' do
+
+ before do
+ @params = { :headings => [@second.id, @first.id, @second.id + 1]}
+ end
+
+ it 'should return an "unprocessable entity" status and an error message' do
+ make_request(@params)
+ assert_response :unprocessable_entity
+ response.body.should match("Couldn't find PublicBodyHeading with id")
+ end
+
+ it 'should not reorder headings' do
+ make_request(@params)
+ PublicBodyHeading.find(@first.id).display_order.should == 0
+ PublicBodyHeading.find(@second.id).display_order.should == 1
+ end
+
+ end
+ end
+
+ context 'when reordering public body categories' do
+
+ render_views
+
+ before do
+ @heading = FactoryGirl.create(:public_body_heading)
+ @first_category = FactoryGirl.create(:public_body_category)
+ @first_link = FactoryGirl.create(:public_body_category_link,
+ :public_body_category => @first_category,
+ :public_body_heading => @heading,
+ :category_display_order => 0)
+ @second_category = FactoryGirl.create(:public_body_category)
+ @second_link = FactoryGirl.create(:public_body_category_link,
+ :public_body_category => @second_category,
+ :public_body_heading => @heading,
+ :category_display_order => 1)
+ @default_params = { :categories => [@second_category.id, @first_category.id],
+ :id => @heading }
+ @old_order = [@first_category, @second_category]
+ @new_order = [@second_category, @first_category]
+ end
+
+ def make_request(params=@default_params)
+ post :reorder_categories, params
+ end
+
+ context 'when handling valid input' do
+
+ it 'should reorder categories for the heading according to their position \
+ in the submitted params' do
+
+ @heading.public_body_categories.should == @old_order
+ make_request
+ @heading.public_body_categories(reload=true).should == @new_order
+ end
+
+ it 'should return a success status' do
+ make_request
+ response.should be_success
+ end
+ end
+
+ context 'when handling invalid input' do
+
+ before do
+ @new_category = FactoryGirl.create(:public_body_category)
+ @params = @default_params.merge(:categories => [@second_category.id,
+ @first_category.id,
+ @new_category.id])
+ end
+
+ it 'should return an "unprocessable entity" status and an error message' do
+ make_request(@params)
+ assert_response :unprocessable_entity
+ response.body.should match("Couldn't find PublicBodyCategoryLink")
+ end
+
+ it 'should not reorder the categories for the heading' do
+ make_request(@params)
+ @heading.public_body_categories(reload=true).should == @old_order
+ end
+ end
+
+ end
+end
diff --git a/spec/controllers/admin_spam_addresses_controller_spec.rb b/spec/controllers/admin_spam_addresses_controller_spec.rb
index da1e9bb5a..a1e434159 100644
--- a/spec/controllers/admin_spam_addresses_controller_spec.rb
+++ b/spec/controllers/admin_spam_addresses_controller_spec.rb
@@ -37,7 +37,7 @@ describe AdminSpamAddressesController do
it 'redirects to the index action if successful' do
SpamAddress.any_instance.stub(:save).and_return(true)
post :create, :spam_address => spam_params
- expect(response).to redirect_to(spam_addresses_path)
+ expect(response).to redirect_to(admin_spam_addresses_path)
end
it 'notifies the admin the spam address has been created' do
@@ -83,7 +83,7 @@ describe AdminSpamAddressesController do
end
it 'redirects to the index action' do
- expect(response).to redirect_to(spam_addresses_path)
+ expect(response).to redirect_to(admin_spam_addresses_path)
end
end
diff --git a/spec/controllers/health_checks_controller_spec.rb b/spec/controllers/health_checks_controller_spec.rb
new file mode 100644
index 000000000..f7ad6d6a4
--- /dev/null
+++ b/spec/controllers/health_checks_controller_spec.rb
@@ -0,0 +1,30 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe HealthChecksController do
+
+ describe :index do
+
+ describe :index do
+
+ it 'returns a 200 if all health checks pass' do
+ HealthChecks.stub(:ok? => true)
+ get :index
+ expect(response.status).to eq(200)
+ end
+
+ it 'returns a 500 if the health check fails' do
+ HealthChecks.stub(:ok? => false)
+ get :index
+ expect(response.status).to eq(500)
+ end
+
+ it 'does not render a layout' do
+ get :index
+ expect(response).to render_template(:layout => false)
+ end
+
+ end
+
+ end
+
+end
diff --git a/spec/controllers/public_body_controller_spec.rb b/spec/controllers/public_body_controller_spec.rb
index f64975580..fc7143522 100644
--- a/spec/controllers/public_body_controller_spec.rb
+++ b/spec/controllers/public_body_controller_spec.rb
@@ -7,6 +7,7 @@ describe PublicBodyController, "when showing a body" do
render_views
before(:each) do
+ PublicBodyCategory.stub!(:load_categories)
load_raw_emails_data
get_fixtures_xapian_index
end
@@ -75,6 +76,10 @@ end
describe PublicBodyController, "when listing bodies" do
render_views
+ before(:each) do
+ PublicBodyCategory.stub!(:load_categories)
+ end
+
it "should be successful" do
get :list
response.should be_success
@@ -204,16 +209,19 @@ describe PublicBodyController, "when listing bodies" do
end
end
- it "should list a tagged thing on the appropriate list page, and others on the other page, and all still on the all page" do
- load_test_categories
-
- public_bodies(:humpadink_public_body).tag_string = "foo local_council"
+ it "should list a tagged thing on the appropriate list page, and others on the other page,
+ and all still on the all page" do
+ category = FactoryGirl.create(:public_body_category)
+ heading = FactoryGirl.create(:public_body_heading)
+ PublicBodyCategoryLink.create(:public_body_heading_id => heading.id,
+ :public_body_category_id => category.id)
+ public_bodies(:humpadink_public_body).tag_string = category.category_tag
- get :list, :tag => "local_council"
+ get :list, :tag => category.category_tag
response.should render_template('list')
assigns[:public_bodies].should == [ public_bodies(:humpadink_public_body) ]
- assigns[:tag].should == "local_council"
- assigns[:description].should == "in the category ‘Local councils’"
+ assigns[:tag].should == category.category_tag
+ assigns[:description].should == "in the category ‘#{category.title}’"
get :list, :tag => "other"
response.should render_template('list')
diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb
index f7c935af3..6c0f4573e 100644
--- a/spec/controllers/request_controller_spec.rb
+++ b/spec/controllers/request_controller_spec.rb
@@ -1827,7 +1827,15 @@ describe RequestController, "when sending a followup message" do
# make the followup
session[:user_id] = users(:bob_smith_user).id
- post :show_response, :outgoing_message => { :body => "What a useless response! You suck.", :what_doing => 'normal_sort' }, :id => info_requests(:fancy_dog_request).id, :incoming_message_id => incoming_messages(:useless_incoming_message), :submitted_followup => 1
+
+ post :show_response,
+ :outgoing_message => {
+ :body => "What a useless response! You suck.",
+ :what_doing => 'normal_sort'
+ },
+ :id => info_requests(:fancy_dog_request).id,
+ :incoming_message_id => incoming_messages(:useless_incoming_message),
+ :submitted_followup => 1
# check it worked
deliveries = ActionMailer::Base.deliveries
@@ -1982,7 +1990,15 @@ describe RequestController, "sending overdue request alerts" do
:info_request_id => chicken_request.id,
:body => 'Some text',
:what_doing => 'normal_sort')
- outgoing_message.send_message
+
+ outgoing_message.sendable?
+ mail_message = OutgoingMailer.followup(
+ outgoing_message.info_request,
+ outgoing_message,
+ outgoing_message.incoming_message_followup
+ ).deliver
+ outgoing_message.record_email_delivery(mail_message.to_addrs.join(', '), mail_message.message_id)
+
outgoing_message.save!
chicken_request = InfoRequest.find(chicken_request.id)
diff --git a/spec/controllers/user_controller_spec.rb b/spec/controllers/user_controller_spec.rb
index e4854fe6b..413d395c5 100644
--- a/spec/controllers/user_controller_spec.rb
+++ b/spec/controllers/user_controller_spec.rb
@@ -21,7 +21,8 @@ describe UserController, "when redirecting a show request to a canonical url" do
it 'should not redirect a long canonical name that has a numerical suffix' do
User.stub!(:find).with(:first, anything()).and_return(mock_model(User,
:url_name => 'bob_smithbob_smithbob_smithbob_s_2',
- :name => 'Bob Smith Bob Smith Bob Smith Bob Smith'))
+ :name => 'Bob Smith Bob Smith Bob Smith Bob Smith',
+ :info_requests => []))
User.stub!(:find).with(:all, anything()).and_return([])
get :show, :url_name => 'bob_smithbob_smithbob_smithbob_s_2'
response.should be_success
@@ -107,6 +108,15 @@ describe UserController, "when showing a user" do
]
end
+ it 'filters by the given request status' do
+ get :show, :url_name => 'bob_smith',
+ :user_query => 'money',
+ :request_latest_status => 'waiting_response'
+ assigns[:xapian_requests].results.map{|x|x[:model].info_request}.should =~ [
+ info_requests(:naughty_chicken_request)
+ ]
+ end
+
it "should not show unconfirmed users" do
begin
get :show, :url_name => "unconfirmed_user"
diff --git a/spec/factories/outgoing_messages.rb b/spec/factories/outgoing_messages.rb
index d1ed25093..e11cbdfb9 100644
--- a/spec/factories/outgoing_messages.rb
+++ b/spec/factories/outgoing_messages.rb
@@ -1,6 +1,8 @@
FactoryGirl.define do
factory :outgoing_message do
+ info_request
+
factory :initial_request do
ignore do
status 'ready'
@@ -8,7 +10,9 @@ FactoryGirl.define do
body 'Some information please'
what_doing 'normal_sort'
end
+
end
+
factory :internal_review_request do
ignore do
status 'ready'
@@ -16,14 +20,27 @@ FactoryGirl.define do
body 'I want a review'
what_doing 'internal_review'
end
+
end
+
+ # FIXME: This here because OutgoingMessage has an after_initialize,
+ # which seems to call everything in the app! FactoryGirl calls new with
+ # no parameters and then uses the assignment operator of each attribute
+ # to update it. Because after_initialize executes before assigning the
+ # attributes, loads of stuff fails because whatever after_initialize is
+ # doing expects some of the attributes to be there.
initialize_with { OutgoingMessage.new({ :status => status,
:message_type => message_type,
:body => body,
:what_doing => what_doing }) }
+
after_create do |outgoing_message|
- outgoing_message.send_message
+ outgoing_message.sendable?
+ outgoing_message.record_email_delivery(
+ 'test@example.com',
+ 'ogm-14+537f69734b97c-1ebd@localhost')
end
+
end
end
diff --git a/spec/factories/public_body_categories.rb b/spec/factories/public_body_categories.rb
new file mode 100644
index 000000000..baa474c6b
--- /dev/null
+++ b/spec/factories/public_body_categories.rb
@@ -0,0 +1,8 @@
+
+FactoryGirl.define do
+ factory :public_body_category do
+ sequence(:title) { |n| "Example Public Body Category #{n}" }
+ sequence(:category_tag) { |n| "example_tag_#{n}" }
+ sequence(:description) { |n| "Example Public body Description #{n}" }
+ end
+end
diff --git a/spec/factories/public_body_category_links.rb b/spec/factories/public_body_category_links.rb
new file mode 100644
index 000000000..7663b1f52
--- /dev/null
+++ b/spec/factories/public_body_category_links.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :public_body_category_link do
+ association :public_body_category
+ association :public_body_heading
+ end
+end
diff --git a/spec/factories/public_body_headings.rb b/spec/factories/public_body_headings.rb
new file mode 100644
index 000000000..ed54ddada
--- /dev/null
+++ b/spec/factories/public_body_headings.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+ factory :public_body_heading do
+ sequence(:name) { |n| "Example Public Body Heading #{n}" }
+ end
+end
diff --git a/spec/fixtures/info_request_events.yml b/spec/fixtures/info_request_events.yml
index b2f40cc37..23ef80cc2 100644
--- a/spec/fixtures/info_request_events.yml
+++ b/spec/fixtures/info_request_events.yml
@@ -31,8 +31,10 @@ silly_outgoing_message_event:
info_request_id: 103
event_type: sent
created_at: 2007-10-14 10:41:12.686264
- described_state:
outgoing_message_id: 2
+ calculated_state: waiting_response
+ described_state: waiting_response
+ last_described_at: 2007-10-14 10:41:12.686264
useless_incoming_message_event:
id: 902
params_yaml: "--- \n\
diff --git a/spec/helpers/health_checks_helper_spec.rb b/spec/helpers/health_checks_helper_spec.rb
new file mode 100644
index 000000000..7d4083da5
--- /dev/null
+++ b/spec/helpers/health_checks_helper_spec.rb
@@ -0,0 +1,15 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe HealthChecksHelper do
+ include HealthChecksHelper
+
+ describe :check_status do
+
+ it 'warns that the check is failing' do
+ check = double(:message => 'Failed', :ok? => false)
+ expect(check_status(check)).to include('red')
+ end
+
+ end
+
+end
diff --git a/spec/integration/localisation_spec.rb b/spec/integration/localisation_spec.rb
index 4f6b61ae1..037603ad5 100644
--- a/spec/integration/localisation_spec.rb
+++ b/spec/integration/localisation_spec.rb
@@ -24,14 +24,29 @@ describe "when generating urls" do
response.should_not contain @home_link_regex
end
- it 'should redirect requests for a public body in a locale to the canonical name in that locale' do
- get('/es/body/dfh')
- response.should redirect_to "/es/body/edfh"
- end
+ context 'when handling public body requests' do
+
+ before do
+ AlaveteliLocalization.set_locales(available_locales='es en', default_locale='en')
+ body = FactoryGirl.create(:public_body, :short_name => 'english_short')
+ I18n.with_locale(:es) do
+ body.short_name = 'spanish_short'
+ body.save!
+ end
+ end
+
+ it 'should redirect requests for a public body in a locale to the
+ canonical name in that locale' do
+ get('/es/body/english_short')
+ response.should redirect_to "/es/body/spanish_short"
+ end
- it 'should remember a filter view when redirecting a public body request to the canonical name' do
- get('/es/body/tgq/successful')
- response.should redirect_to "/es/body/etgq/successful"
+ it 'should remember a filter view when redirecting a public body
+ request to the canonical name' do
+ AlaveteliLocalization.set_locales(available_locales='es en', default_locale='en')
+ get('/es/body/english_short/successful')
+ response.should redirect_to "/es/body/spanish_short/successful"
+ end
end
describe 'when there is more than one locale' do
diff --git a/spec/lib/health_checks/checks/days_ago_check_spec.rb b/spec/lib/health_checks/checks/days_ago_check_spec.rb
new file mode 100644
index 000000000..33b4642cd
--- /dev/null
+++ b/spec/lib/health_checks/checks/days_ago_check_spec.rb
@@ -0,0 +1,66 @@
+require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper')
+
+describe HealthChecks::Checks::DaysAgoCheck do
+ include HealthChecks::Checks
+
+ it { should be_kind_of(HealthChecks::HealthCheckable) }
+
+ it 'defaults to comparing to one day ago' do
+ check = HealthChecks::Checks::DaysAgoCheck.new
+ expect(check.days).to eq(1)
+ end
+
+ it 'accepts a custom number of days' do
+ check = HealthChecks::Checks::DaysAgoCheck.new(:days => 4)
+ expect(check.days).to eq(4)
+ end
+
+ describe :check do
+
+ it 'is successful if the subject is in the last day' do
+ check = HealthChecks::Checks::DaysAgoCheck.new { Time.now }
+ expect(check.check).to be_true
+ end
+
+ it 'fails if the subject is over a day ago' do
+ check = HealthChecks::Checks::DaysAgoCheck.new { 2.days.ago }
+ expect(check.check).to be_false
+ end
+
+ end
+
+ describe :failure_message do
+
+ it 'includes the check subject in the default message' do
+ subject = 2.days.ago
+ check = HealthChecks::Checks::DaysAgoCheck.new { subject }
+ expect(check.failure_message).to include(subject.to_s)
+ end
+
+ it 'includes the check subject in a custom message' do
+ params = { :failure_message => 'This check failed' }
+ subject = 2.days.ago
+ check = HealthChecks::Checks::DaysAgoCheck.new(params) { subject }
+ expect(check.failure_message).to include(subject.to_s)
+ end
+
+ end
+
+ describe :success_message do
+
+ it 'includes the check subject in the default message' do
+ subject = Time.now
+ check = HealthChecks::Checks::DaysAgoCheck.new { subject }
+ expect(check.failure_message).to include(subject.to_s)
+ end
+
+ it 'includes the check subject in a custom message' do
+ params = { :success_message => 'This check succeeded' }
+ subject = Time.now
+ check = HealthChecks::Checks::DaysAgoCheck.new(params) { subject }
+ expect(check.success_message).to include(subject.to_s)
+ end
+
+ end
+
+end
diff --git a/spec/lib/health_checks/health_checkable_spec.rb b/spec/lib/health_checks/health_checkable_spec.rb
new file mode 100644
index 000000000..abfeb5c21
--- /dev/null
+++ b/spec/lib/health_checks/health_checkable_spec.rb
@@ -0,0 +1,128 @@
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
+
+describe HealthChecks::HealthCheckable do
+
+ before(:each) do
+ class MockCheck
+ include HealthChecks::HealthCheckable
+ end
+ @subject = MockCheck.new
+ end
+
+ describe :initialize do
+
+ it 'allows a custom failure message to be set' do
+ @subject = MockCheck.new(:failure_message => 'F')
+ expect(@subject.failure_message).to eq('F')
+ end
+
+ it 'allows a custom success message to be set' do
+ @subject = MockCheck.new(:success_message => 'S')
+ expect(@subject.success_message).to eq('S')
+ end
+
+ end
+
+ describe :name do
+
+ it 'returns the name of the check' do
+ expect(@subject.name).to eq('MockCheck')
+ end
+
+ end
+
+ describe :check do
+
+ it 'is intended to be overridden by the includer' do
+ expect{ @subject.check }.to raise_error(NotImplementedError)
+ end
+
+ end
+
+ describe :ok? do
+
+ it 'returns true if the check was successful' do
+ @subject.stub(:check => true)
+ expect(@subject.ok?).to be_true
+ end
+
+ it 'returns false if the check failed' do
+ @subject.stub(:check => false)
+ expect(@subject.ok?).to be_false
+ end
+
+ end
+
+ describe :failure_message do
+
+ it 'returns a default message if one has not been set' do
+ expect(@subject.failure_message).to eq('Failed')
+ end
+
+ end
+
+ describe :failure_message= do
+
+ it 'allows a custom failure message to be set' do
+ @subject.failure_message = 'F'
+ expect(@subject.failure_message).to eq('F')
+ end
+
+ end
+
+ describe :success_message do
+
+ it 'returns a default message if one has not been set' do
+ expect(@subject.success_message).to eq('Success')
+ end
+
+ end
+
+ describe :success_message= do
+
+ it 'allows a custom success message to be set' do
+ @subject.success_message = 'S'
+ expect(@subject.success_message).to eq('S')
+ end
+
+ end
+
+ describe :message do
+
+ context 'if the check succeeds' do
+
+ before(:each) do
+ @subject.stub(:check => true)
+ end
+
+ it 'returns the default success message' do
+ expect(@subject.message).to eq('Success')
+ end
+
+ it 'returns a custom success message if one has been set' do
+ @subject.success_message = 'Custom Success'
+ expect(@subject.message).to eq('Custom Success')
+ end
+
+ end
+
+ context 'if the check fails' do
+
+ before(:each) do
+ @subject.stub(:check => false)
+ end
+
+ it 'returns the default failure message' do
+ expect(@subject.message).to eq('Failed')
+ end
+
+ it 'returns a custom failure message if one has been set' do
+ @subject.failure_message = 'Custom Failed'
+ expect(@subject.message).to eq('Custom Failed')
+ end
+
+ end
+
+ end
+
+end
diff --git a/spec/lib/health_checks/health_checks_spec.rb b/spec/lib/health_checks/health_checks_spec.rb
new file mode 100644
index 000000000..c7037b813
--- /dev/null
+++ b/spec/lib/health_checks/health_checks_spec.rb
@@ -0,0 +1,77 @@
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
+
+describe HealthChecks do
+ include HealthChecks
+
+ describe :add do
+
+ it 'adds a check to the collection and returns the check' do
+ check = double('MockCheck', :check => true)
+ expect(add(check)).to eq(check)
+ end
+
+ it 'does not add checks that do not define the check method' do
+ check = double('BadCheck')
+ expect(add(check)).to eq(false)
+ end
+
+ end
+
+ describe :all do
+
+ it 'returns all the checks' do
+ check1 = double('MockCheck', :check => true)
+ check2 = double('AnotherCheck', :check => false)
+ add(check1)
+ add(check2)
+ expect(all).to include(check1, check2)
+ end
+
+ end
+
+ describe :each do
+
+ it 'iterates over each check' do
+ expect(subject).to respond_to(:each)
+ end
+
+ end
+
+ describe :ok? do
+
+ it 'returns true if all checks are ok' do
+ checks = [
+ double('MockCheck', :ok? => true),
+ double('FakeCheck', :ok? => true),
+ double('TestCheck', :ok? => true)
+ ]
+ HealthChecks.stub(:all => checks)
+
+ expect(HealthChecks.ok?).to be_true
+ end
+
+ it 'returns false if all checks fail' do
+ checks = [
+ double('MockCheck', :ok? => false),
+ double('FakeCheck', :ok? => false),
+ double('TestCheck', :ok? => false)
+ ]
+ HealthChecks.stub(:all => checks)
+
+ expect(HealthChecks.ok?).to be_false
+ end
+
+ it 'returns false if a single check fails' do
+ checks = [
+ double('MockCheck', :ok? => true),
+ double('FakeCheck', :ok? => false),
+ double('TestCheck', :ok? => true)
+ ]
+ HealthChecks.stub(:all => checks)
+
+ expect(HealthChecks.ok?).to be_false
+ end
+
+ end
+
+end
diff --git a/spec/lib/public_body_categories_spec.rb b/spec/lib/public_body_categories_spec.rb
deleted file mode 100644
index e53d9a028..000000000
--- a/spec/lib/public_body_categories_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
-
-describe PublicBodyCategories do
-
- before do
- load_test_categories
- end
-
- describe 'when asked for categories with headings' do
-
- it 'should return a list of headings as plain strings, each followed by n tag specifications as
- lists in the form:
- ["tag_to_use_as_category", "Sub category title", "Instance description"]' do
- expected_categories = ["Local and regional", ["local_council",
- "Local councils",
- "a local council"],
- "Miscellaneous", ["other",
- "Miscellaneous",
- "miscellaneous"]]
- PublicBodyCategories::get().with_headings().should == expected_categories
- end
-
- end
-
- describe 'when asked for headings' do
-
- it 'should return a list of headings' do
- PublicBodyCategories::get().headings().should == ['Local and regional', 'Miscellaneous']
- end
-
- end
-
- describe 'when asked for tags by headings' do
-
- it 'should return a hash of tags keyed by heading' do
- PublicBodyCategories::get().by_heading().should == {'Local and regional' => ['local_council'],
- 'Miscellaneous' => ['other']}
- end
-
- end
-
-end \ No newline at end of file
diff --git a/spec/models/censor_rule_spec.rb b/spec/models/censor_rule_spec.rb
index 5b41cc0d4..4ecd2d3e1 100644
--- a/spec/models/censor_rule_spec.rb
+++ b/spec/models/censor_rule_spec.rb
@@ -90,17 +90,32 @@ end
describe 'when validating rules' do
- it 'should be invalid without text' do
+ it 'must have the text to redact' do
censor_rule = CensorRule.new
- censor_rule.valid?.should == false
- censor_rule.errors[:text].should == ["can't be blank"]
+ expect(censor_rule).to have(1).error_on(:text)
+ expect(censor_rule.errors[:text]).to eql(["can't be blank"])
+ end
+
+ it 'must have a replacement' do
+ expect(CensorRule.new).to have(1).error_on(:replacement)
+ end
+
+ it 'must have a last_edit_editor' do
+ expect(CensorRule.new).to have(1).error_on(:last_edit_editor)
+ end
+
+ it 'must have a last_edit_comment' do
+ expect(CensorRule.new).to have(1).error_on(:last_edit_comment)
end
describe 'when validating a regexp rule' do
before do
@censor_rule = CensorRule.new(:regexp => true,
- :text => '*')
+ :text => '*',
+ :replacement => '---',
+ :last_edit_comment => 'test',
+ :last_edit_editor => 'rspec')
end
it 'should try to create a regexp from the text' do
@@ -133,7 +148,10 @@ describe 'when validating rules' do
describe 'when the allow_global flag has been set' do
before do
- @censor_rule = CensorRule.new(:text => 'some text')
+ @censor_rule = CensorRule.new(:text => 'some text',
+ :replacement => '---',
+ :last_edit_comment => 'test',
+ :last_edit_editor => 'rspec')
@censor_rule.allow_global = true
end
@@ -146,7 +164,10 @@ describe 'when validating rules' do
describe 'when the allow_global flag has not been set' do
before do
- @censor_rule = CensorRule.new(:text => '/./')
+ @censor_rule = CensorRule.new(:text => '/./',
+ :replacement => '---',
+ :last_edit_comment => 'test',
+ :last_edit_editor => 'rspec')
end
it 'should not allow a global text censor rule (without user_id, request_id or public_body_id)' do
diff --git a/spec/models/change_email_validator_spec.rb b/spec/models/change_email_validator_spec.rb
new file mode 100644
index 000000000..b667a23d1
--- /dev/null
+++ b/spec/models/change_email_validator_spec.rb
@@ -0,0 +1,124 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+def validator_with_user_and_params(user, params = {})
+ validator = ChangeEmailValidator.new(params)
+ validator.logged_in_user = user
+ validator
+end
+
+describe ChangeEmailValidator do
+
+ let(:user) { FactoryGirl.create(:user) }
+
+ describe :old_email do
+
+ it 'must have an old email' do
+ params = { :old_email => nil,
+ :new_email => 'new@example.com',
+ :user_circumstance => 'change_email',
+ :password => 'jonespassword' }
+ validator = validator_with_user_and_params(user, params)
+
+ msg = 'Please enter your old email address'
+ expect(validator.errors_on(:old_email)).to include(msg)
+ end
+
+ it 'must be a valid email' do
+ params = { :old_email => 'old',
+ :new_email => 'new@example.com',
+ :user_circumstance => 'change_email',
+ :password => 'jonespassword' }
+ validator = validator_with_user_and_params(user, params)
+
+ msg = "Old email doesn't look like a valid address"
+ expect(validator.errors_on(:old_email)).to include(msg)
+ end
+
+ it 'must have the same email as the logged in user' do
+ params = { :old_email => user.email,
+ :new_email => 'new@example.com',
+ :user_circumstance => 'change_email',
+ :password => 'jonespassword' }
+ validator = validator_with_user_and_params(user, params)
+ validator.logged_in_user = FactoryGirl.build(:user)
+
+ msg = "Old email address isn't the same as the address of the account you are logged in with"
+ expect(validator.errors_on(:old_email)).to include(msg)
+ end
+
+ end
+
+ describe :new_email do
+
+ it 'must have a new email' do
+ params = { :old_email => user.email,
+ :new_email => nil,
+ :user_circumstance => 'change_email',
+ :password => 'jonespassword' }
+ validator = validator_with_user_and_params(user, params)
+
+ msg = 'Please enter your new email address'
+ expect(validator.errors_on(:new_email)).to include(msg)
+ end
+
+ it 'must be a valid email' do
+ params = { :old_email => user.email,
+ :new_email => 'new',
+ :user_circumstance => 'change_email',
+ :password => 'jonespassword' }
+ validator = validator_with_user_and_params(user, params)
+
+ msg = "New email doesn't look like a valid address"
+ expect(validator.errors_on(:new_email)).to include(msg)
+ end
+
+ end
+
+ describe :password do
+
+ pending 'password_and_format_of_email validation fails when password is nil' do
+ it 'must have a password' do
+ params = { :old_email => user.email,
+ :new_email => 'new@example.com',
+ :password => nil }
+ validator = validator_with_user_and_params(user, params)
+
+ msg = 'Please enter your password'
+ expect(validator.errors_on(:password)).to include(msg)
+ end
+ end
+
+ it 'does not require a password if changing email' do
+ params = { :old_email => user.email,
+ :new_email => 'new@example.com',
+ :user_circumstance => 'change_email',
+ :password => '' }
+ validator = validator_with_user_and_params(user, params)
+
+ expect(validator).to have(0).errors_on(:password)
+ end
+
+ it 'must have a password if not changing email' do
+ params = { :old_email => user.email,
+ :new_email => 'new@example.com',
+ :user_circumstance => 'unknown',
+ :password => '' }
+ validator = validator_with_user_and_params(user, params)
+
+ msg = 'Please enter your password'
+ expect(validator.errors_on(:password)).to include(msg)
+ end
+
+ it 'must be the correct password' do
+ params = { :old_email => user.email,
+ :new_email => 'new@example.com',
+ :password => 'incorrectpass' }
+ validator = validator_with_user_and_params(user, params)
+
+ msg = 'Password is not correct'
+ expect(validator.errors_on(:password)).to include(msg)
+ end
+
+ end
+
+end
diff --git a/spec/models/info_request_spec.rb b/spec/models/info_request_spec.rb
index afb8e0949..9ad616ea5 100644
--- a/spec/models/info_request_spec.rb
+++ b/spec/models/info_request_spec.rb
@@ -848,9 +848,11 @@ describe InfoRequest do
context "a series of events on a request" do
it "should have sensible events after the initial request has been made" do
# An initial request is sent
- # The logic that changes the status when a message is sent is mixed up
- # in OutgoingMessage#send_message. So, rather than extract it (or call it)
- # let's just duplicate what it does here for the time being.
+ # FIXME: The logic that changes the status when a message
+ # is sent is mixed up in
+ # OutgoingMessage#record_email_delivery. So, rather than
+ # extract it (or call it) let's just duplicate what it does
+ # here for the time being.
request.log_event('sent', {})
request.set_described_state('waiting_response')
@@ -919,7 +921,8 @@ describe InfoRequest do
request.log_event("status_update", {})
request.set_described_state("waiting_response")
# A normal follow up is sent
- # This is normally done in OutgoingMessage#send_message
+ # This is normally done in
+ # OutgoingMessage#record_email_delivery
request.log_event('followup_sent', {})
request.set_described_state('waiting_response')
diff --git a/spec/models/public_body_category/category_collection_spec.rb b/spec/models/public_body_category/category_collection_spec.rb
new file mode 100644
index 000000000..1fbcbe739
--- /dev/null
+++ b/spec/models/public_body_category/category_collection_spec.rb
@@ -0,0 +1,81 @@
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
+
+describe PublicBodyCategory::CategoryCollection do
+ context "requesting data" do
+
+ before do
+ data = [ "Local and regional",
+ [ "local_council", "Local councils", "a local council" ],
+ "Miscellaneous",
+ [ "other", "Miscellaneous", "miscellaneous" ] ]
+ @categories = PublicBodyCategory::CategoryCollection.new
+ data.each { |item| @categories << item }
+ end
+
+ describe 'when asked for headings' do
+
+ it 'should return a list of headings' do
+ @categories.headings().should == ['Local and regional', 'Miscellaneous']
+ end
+
+ end
+
+ describe 'when asked for categories with headings' do
+ it 'should return a list of headings as plain strings, each followed by n tag specifications as
+ lists in the form:
+ ["tag_to_use_as_category", "Sub category title", "Instance description"]' do
+ expected_categories = ["Local and regional", ["local_council",
+ "Local councils",
+ "a local council"],
+ "Miscellaneous", ["other",
+ "Miscellaneous",
+ "miscellaneous"]]
+ @categories.with_headings().should == expected_categories
+ end
+ end
+
+
+
+ describe 'when asked for tags by headings' do
+ it 'should return a hash of tags keyed by heading' do
+ @categories.by_heading().should == {'Local and regional' => ['local_council'],
+ 'Miscellaneous' => ['other']}
+ end
+ end
+
+ describe 'when asked for categories with description' do
+ it 'should return a list of tag specifications as lists in the form:
+ ["tag_to_use_as_category", "Sub category title", "Instance description"]' do
+ expected_categories = [
+ ["local_council", "Local councils", "a local council"],
+ ["other", "Miscellaneous", "miscellaneous"]
+ ]
+ @categories.with_description().should == expected_categories
+ end
+ end
+
+ describe 'when asked for tags' do
+ it 'should return a list of tags' do
+ @categories.tags().should == ["local_council", "other"]
+ end
+ end
+
+ describe 'when asked for categories by tag' do
+ it 'should return a hash of categories keyed by tag' do
+ @categories.by_tag().should == {
+ "local_council" => "Local councils",
+ "other" => "Miscellaneous"
+ }
+ end
+ end
+
+ describe 'when asked for singular_by_tag' do
+ it 'should return a hash of category descriptions keyed by tag' do
+ @categories.singular_by_tag().should == {
+ "local_council" => "a local council",
+ "other" => "miscellaneous"
+ }
+ end
+ end
+ end
+end
diff --git a/spec/models/public_body_category_link_spec.rb b/spec/models/public_body_category_link_spec.rb
new file mode 100644
index 000000000..8d91f02d5
--- /dev/null
+++ b/spec/models/public_body_category_link_spec.rb
@@ -0,0 +1,53 @@
+# == Schema Information
+#
+# Table name: public_body_category_link
+#
+# public_body_category_id :integer not null
+# public_body_heading_id :integer not null
+# category_display_order :integer
+#
+
+require 'spec_helper'
+
+describe PublicBodyHeading, 'when validating' do
+
+ it 'should set a default display order based on the next available display order' do
+ heading = FactoryGirl.create(:public_body_heading)
+ category = FactoryGirl.create(:public_body_category)
+ category_link = PublicBodyCategoryLink.new(:public_body_heading => heading,
+ :public_body_category => category)
+ category_link.valid?
+ category_link.category_display_order.should == PublicBodyCategoryLink.next_display_order(heading)
+ end
+
+ it 'should be invalid without a category' do
+ category_link = PublicBodyCategoryLink.new
+ category_link.should_not be_valid
+ category_link.errors[:public_body_category].should == ["can't be blank"]
+ end
+
+ it 'should be invalid without a heading' do
+ category_link = PublicBodyCategoryLink.new
+ category_link.should_not be_valid
+ category_link.errors[:public_body_heading].should == ["can't be blank"]
+ end
+
+end
+
+describe PublicBodyCategoryLink, 'when setting a category display order' do
+
+ it 'should return 0 if there are no public body headings' do
+ heading = FactoryGirl.create(:public_body_heading)
+ PublicBodyCategoryLink.next_display_order(heading).should == 0
+ end
+
+ it 'should return one more than the highest display order if there are public body headings' do
+ heading = FactoryGirl.create(:public_body_heading)
+ category = FactoryGirl.create(:public_body_category)
+ category_link = PublicBodyCategoryLink.create(:public_body_heading_id => heading.id,
+ :public_body_category_id => category.id)
+
+ PublicBodyCategoryLink.next_display_order(heading).should == 1
+ end
+
+end
diff --git a/spec/models/public_body_category_spec.rb b/spec/models/public_body_category_spec.rb
new file mode 100644
index 000000000..2d39a7376
--- /dev/null
+++ b/spec/models/public_body_category_spec.rb
@@ -0,0 +1,65 @@
+# == Schema Information
+#
+# Table name: public_body_categories
+#
+# id :integer not null, primary key
+# locale :string
+# title :text not null
+# category_tag :text not null
+# description :text not null
+# display_order :integer
+#
+
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe PublicBodyCategory do
+ describe 'when loading the data' do
+ it 'should use the display_order field to preserve the original data order' do
+ PublicBodyCategory.add(:en, [
+ "Local and regional",
+ [ "local_council", "Local councils", "a local council" ],
+ "Miscellaneous",
+ [ "other", "Miscellaneous", "miscellaneous" ],
+ [ "aardvark", "Aardvark", "daft test"],])
+
+ headings = PublicBodyHeading.all
+ cat_group1 = headings[0].public_body_categories
+ cat_group1.count.should eq 1
+ cat_group1[0].title.should eq "Local councils"
+
+ cat_group2 = headings[1].public_body_categories
+ cat_group2.count.should eq 2
+ cat_group2[0].title.should eq "Miscellaneous"
+ cat_group2[0].public_body_category_links.where(
+ :public_body_heading_id => headings[1].id).
+ first.
+ category_display_order.should eq 0
+
+ cat_group2[1].title.should eq "Aardvark"
+ cat_group2[1].public_body_category_links.where(
+ :public_body_heading_id => headings[1].id).
+ first.
+ category_display_order.should eq 1
+ end
+ end
+
+ context 'when validating' do
+
+ it 'should require a title' do
+ category = PublicBodyCategory.new
+ category.should_not be_valid
+ category.errors[:title].should == ["Title can't be blank"]
+ end
+
+ it 'should require a category tag' do
+ category = PublicBodyCategory.new
+ category.should_not be_valid
+ category.errors[:category_tag].should == ["Tag can't be blank"]
+ end
+
+ it 'should require a unique tag' do
+ existing = FactoryGirl.create(:public_body_category)
+ PublicBodyCategory.new(:email => existing.category_tag).should_not be_valid
+ end
+ end
+end
diff --git a/spec/models/public_body_heading_spec.rb b/spec/models/public_body_heading_spec.rb
new file mode 100644
index 000000000..add2cac60
--- /dev/null
+++ b/spec/models/public_body_heading_spec.rb
@@ -0,0 +1,68 @@
+# == Schema Information
+#
+# Table name: public_body_headings
+#
+# id :integer not null, primary key
+# locale :string
+# name :text not null
+# display_order :integer
+#
+
+require 'spec_helper'
+
+describe PublicBodyHeading do
+
+ context 'when loading the data' do
+
+ before do
+ PublicBodyCategory.add(:en, [
+ "Local and regional",
+ [ "local_council", "Local councils", "a local council" ],
+ "Miscellaneous",
+ [ "other", "Miscellaneous", "miscellaneous" ],])
+ end
+
+ it 'should use the display_order field to preserve the original data order' do
+ headings = PublicBodyHeading.all
+ headings[0].name.should eq 'Local and regional'
+ headings[0].display_order.should eq 0
+ headings[1].name.should eq 'Miscellaneous'
+ headings[1].display_order.should eq 1
+ end
+
+ end
+
+ context 'when validating' do
+
+ it 'should require a name' do
+ heading = PublicBodyHeading.new
+ heading.should_not be_valid
+ heading.errors[:name].should == ["Name can't be blank"]
+ end
+
+ it 'should require a unique name' do
+ heading = FactoryGirl.create(:public_body_heading)
+ new_heading = PublicBodyHeading.new(:name => heading.name)
+ new_heading.should_not be_valid
+ new_heading.errors[:name].should == ["Name is already taken"]
+ end
+
+ it 'should set a default display order based on the next available display order' do
+ heading = PublicBodyHeading.new
+ heading.valid?
+ heading.display_order.should == PublicBodyHeading.next_display_order
+ end
+ end
+
+ context 'when setting a display order' do
+
+ it 'should return 0 if there are no public body headings' do
+ PublicBodyHeading.next_display_order.should == 0
+ end
+
+ it 'should return one more than the highest display order if there are public body headings' do
+ heading = FactoryGirl.create(:public_body_heading)
+ PublicBodyHeading.next_display_order.should == 1
+ end
+ end
+end
diff --git a/spec/models/public_body_spec.rb b/spec/models/public_body_spec.rb
index a7544c218..225958cac 100644
--- a/spec/models/public_body_spec.rb
+++ b/spec/models/public_body_spec.rb
@@ -546,6 +546,58 @@ CSV
errors.should include("error: line 3: Url name URL name is already taken for authority 'Foobar Test'")
end
+ it 'has a default list of fields to import' do
+ expected_fields = [
+ ['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)'],
+ ]
+
+ expect(PublicBody.csv_import_fields).to eq(expected_fields)
+ end
+
+ it 'allows you to override the default list of fields to import' do
+ old_csv_import_fields = PublicBody.csv_import_fields.clone
+ expected_fields = [
+ ['name', '(i18n)<strong>Existing records cannot be renamed</strong>'],
+ ['short_name', '(i18n)'],
+ ]
+
+ PublicBody.csv_import_fields = expected_fields
+
+ expect(PublicBody.csv_import_fields).to eq(expected_fields)
+
+ # Reset our change so that we don't affect other specs
+ PublicBody.csv_import_fields = old_csv_import_fields
+ end
+
+ it 'allows you to append to the default list of fields to import' do
+ old_csv_import_fields = PublicBody.csv_import_fields.clone
+ expected_fields = [
+ ['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)'],
+ ['a_new_field', ''],
+ ]
+
+ PublicBody.csv_import_fields << ['a_new_field', '']
+
+ expect(PublicBody.csv_import_fields).to eq(expected_fields)
+
+ # Reset our change so that we don't affect other specs
+ PublicBody.csv_import_fields = old_csv_import_fields
+ end
+
end
describe PublicBody do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 0e3fe35c7..74a4891c2 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -201,14 +201,6 @@ Spork.prefork do
I18n.default_locale = original_default_locale
end
- def load_test_categories
- PublicBodyCategories.add(:en, [
- "Local and regional",
- [ "local_council", "Local councils", "a local council" ],
- "Miscellaneous",
- [ "other", "Miscellaneous", "miscellaneous" ],])
- end
-
def basic_auth_login(request, username = nil, password = nil)
username = AlaveteliConfiguration::admin_username if username.nil?
password = AlaveteliConfiguration::admin_password if password.nil?