diff options
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"> + <%= 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? |