diff options
Diffstat (limited to 'app')
79 files changed, 1995 insertions, 955 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..104f10c75 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -27,13 +27,16 @@ body.admin { } .admin { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 20px; @import "compass/css3"; @import "bootstrap"; #main { + margin-bottom: 50px; padding-top: 50px; - } .form-inline { @@ -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/main.scss b/app/assets/stylesheets/main.scss index 8aa8010ae..b063b0d77 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -586,26 +586,22 @@ width:40%; width:26em; } -#authority_preview { -width:45%; -float:right; -background-color:#FFFFE0; -padding-left:1em; -padding-right:1em; -overflow:hidden; -margin-top:-67px; -} - -#authority_preview #header_left,#authority_preview.request_left,#authority_preview #stepwise_make_request { -width:95%; -} - #request_advice { float:right; width:250px; margin-top:1em; } +#select_authority_help { +background-color: #FEF1F6; +float: right; +margin-top: 31px; +overflow: hidden; +padding-left: 1em; +padding-right: 1em; +width: 45%; +} + #request_advice ul { margin:0 auto; } @@ -1717,16 +1713,11 @@ width:575px; padding-right:50px; } -#authority_preview .request_left, -#authority_preview #header_left { - width: 100%; -} - #request_sidebar h2,.list-filter-item { margin-bottom:10px; } -div.ff-icon-printfix,.comment_in_request_text img.comment_quote,body.front #other-country-notice,#other-country-notice,#authority_preview .public-body-name-prefix,#authority_preview #list-filter,#authority_preview h2.foi_results,div#show_response_view p.event_actions, div.batch_public_body_toggle { +div.ff-icon-printfix,.comment_in_request_text img.comment_quote,body.front #other-country-notice,#other-country-notice,div#show_response_view p.event_actions, div.batch_public_body_toggle { display:none; } 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..a8b24e1b1 100644 --- a/app/assets/stylesheets/responsive/_new_request_layout.scss +++ b/app/assets/stylesheets/responsive/_new_request_layout.scss @@ -11,6 +11,9 @@ @include lte-ie7 { width: 27.125em; } + #query { + width: 80%; + } } } @@ -18,41 +21,19 @@ @include grid-row($behavior: nest); } -#authority_preview { +#select_authority_help { @include grid-column(12); - @include respond-min( $main_menu-mobile_menu_cutoff ){ + @include respond-min( $main_menu-mobile_menu_cutoff ) { @include grid-column(6); - margin-top:-67px; - @include ie8{ + @include ie8 { padding-right: 0.9375em; } @include lte-ie7 { width: 26.188em; } } - - /* Hide some elements of the public body that aren't appropriate in this - context */ - #list-filter, h2.foi_results, .public-body-name-prefix { - display: none; - } - - /* Compact request list for viewing in authority preview column */ - .request_left, #header_left { - @include grid-column(12, $collapse: true); - } - - .request_right { - @include grid-column(12, $collapse: true); - } - - span.desc { - background:none; - line-height:18px; - padding: 0; - } - } + /* /new/[body_name] page */ #request_header { @include grid-row; @@ -77,6 +58,10 @@ } } +#typeahead_response .close-button { + float: right; +} + /* Advice sits on right hand side */ #request_advice { diff --git a/app/assets/stylesheets/responsive/_new_request_style.scss b/app/assets/stylesheets/responsive/_new_request_style.scss index ab144b39f..86e17cbfe 100644 --- a/app/assets/stylesheets/responsive/_new_request_style.scss +++ b/app/assets/stylesheets/responsive/_new_request_style.scss @@ -1,5 +1,12 @@ /* Styles for pages in making a new request */ +/* /select_authority page */ +#select_authority_help { + .info { + font-style: italic; + } +} + /* /new/[body_name] page */ .new_info_request { label { 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_censor_rule_controller.rb b/app/controllers/admin_censor_rule_controller.rb index 6f79b5ba1..68ca57510 100644 --- a/app/controllers/admin_censor_rule_controller.rb +++ b/app/controllers/admin_censor_rule_controller.rb @@ -8,23 +8,49 @@ class AdminCensorRuleController < AdminController def new if params[:info_request_id] @info_request = InfoRequest.find(params[:info_request_id]) + @censor_rule = @info_request.censor_rules.build + @form_url = admin_info_request_censor_rules_path(@info_request) end + if params[:user_id] @censor_user = User.find(params[:user_id]) + @censor_rule = @censor_user.censor_rules.build + @form_url = admin_user_censor_rules_path(@censor_user) end + + @censor_rule ||= CensorRule.new + @form_url ||= admin_rule_create_path end def create - params[:censor_rule][:last_edit_editor] = admin_current_user() - @censor_rule = CensorRule.new(params[:censor_rule]) + params[:censor_rule][:last_edit_editor] = admin_current_user + + if params[:info_request_id] + @info_request = InfoRequest.find(params[:info_request_id]) + @censor_rule = @info_request.censor_rules.build(params[:censor_rule]) + @form_url = admin_info_request_censor_rules_path(@info_request) + end + + if params[:user_id] + @censor_user = User.find(params[:user_id]) + @censor_rule = @censor_user.censor_rules.build(params[:censor_rule]) + @form_url = admin_user_censor_rules_path(@censor_user) + end + + @censor_rule ||= CensorRule.new(params[:censor_rule]) + @form_url ||= admin_rule_create_path + if @censor_rule.save if !@censor_rule.info_request.nil? expire_for_request(@censor_rule.info_request) end + if !@censor_rule.user.nil? expire_requests_for_user(@censor_rule.user) end + flash[:notice] = 'CensorRule was successfully created.' + if !@censor_rule.info_request.nil? redirect_to admin_request_show_url(@censor_rule.info_request) elsif !@censor_rule.user.nil? @@ -42,16 +68,20 @@ class AdminCensorRuleController < AdminController end def update - params[:censor_rule][:last_edit_editor] = admin_current_user() + params[:censor_rule][:last_edit_editor] = admin_current_user @censor_rule = CensorRule.find(params[:id]) + if @censor_rule.update_attributes(params[:censor_rule]) - if !@censor_rule.info_request.nil? + unless @censor_rule.info_request.nil? expire_for_request(@censor_rule.info_request) end - if !@censor_rule.user.nil? + + unless @censor_rule.user.nil? expire_requests_for_user(@censor_rule.user) end + flash[:notice] = 'CensorRule was successfully updated.' + if !@censor_rule.info_request.nil? redirect_to admin_request_show_url(@censor_rule.info_request) elsif !@censor_rule.user.nil? @@ -65,19 +95,22 @@ class AdminCensorRuleController < AdminController end def destroy - censor_rule = CensorRule.find(params[:censor_rule_id]) - info_request = censor_rule.info_request - user = censor_rule.user + @censor_rule = CensorRule.find(params[:censor_rule_id]) + info_request = @censor_rule.info_request + user = @censor_rule.user - censor_rule.destroy - if !info_request.nil? + @censor_rule.destroy + + unless info_request.nil? expire_for_request(info_request) end - if !user.nil? + + unless user.nil? expire_requests_for_user(user) end flash[:notice] = "CensorRule was successfully destroyed." + if !info_request.nil? redirect_to admin_request_show_url(info_request) elsif !user.nil? 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..5e305dde3 --- /dev/null +++ b/app/controllers/admin_public_body_categories_controller.rb @@ -0,0 +1,86 @@ +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[:error] = "There are authorities associated with this category, so the tag can't be renamed" + render :action => 'edit' + 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.' + redirect_to edit_admin_category_path(@category) + else + render :action => 'edit' + end + end + 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..e893e760d --- /dev/null +++ b/app/controllers/admin_public_body_headings_controller.rb @@ -0,0 +1,108 @@ +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.' + redirect_to edit_admin_heading_path(@heading) + else + render :action => 'edit' + end + 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]) + 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/application_controller.rb b/app/controllers/application_controller.rb index 4d3f40d40..1ccf7e5db 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -17,6 +17,9 @@ class ApplicationController < ActionController::Base # assign our own handler method for non-local exceptions rescue_from Exception, :with => :render_exception + # Add some security-related headers (see config/initializers/secure_headers.rb) + ensure_security_headers + # Standard headers, footers and navigation for whole site layout "default" include FastGettext::Translation # make functions like _, n_, N_ etc available) 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/help_controller.rb b/app/controllers/help_controller.rb index 9033198a0..93215ccad 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -11,6 +11,10 @@ class HelpController < ApplicationController before_filter :long_cache before_filter :catch_spam, :only => [:contact] + def index + redirect_to help_about_path + end + def unhappy @info_request = nil if params[:url_title] 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..346aaf384 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 @@ -873,10 +900,18 @@ class RequestController < ApplicationController # Type ahead search def search_typeahead - # Since acts_as_xapian doesn't support the Partial match flag, we work around it - # by making the last work a wildcard, which is quite the same - query = params[:q] - @xapian_requests = perform_search_typeahead(query, InfoRequestEvent) + # Since acts_as_xapian doesn't support the Partial match flag, we work + # around it by making the last word a wildcard, which is quite the same + @query = '' + + if params.key?(:requested_from) + @query << "requested_from:#{ params[:requested_from] } " + end + + @per_page = (params.fetch(:per_page) { 25 }).to_i + + @query << params[:q] + @xapian_requests = perform_search_typeahead(@query, InfoRequestEvent, @per_page) render :partial => "request/search_ahead" end 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..65f2fd81c 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,43 @@ 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)) + msg_string = check_email_is_present_and_valid_msg_string(email) + errors.add(email, msg_string) end end + + def check_email_is_present_and_valid_msg_string(email) + case email.to_sym + when :old_email then _("Old email doesn't look like a valid address") + when :new_email then _("New email doesn't look like a valid address") + else + raise "Unsupported email type #{ email }" + 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..cc8d0e94b 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/incoming_message.rb b/app/models/incoming_message.rb index 135a6bdaf..db6722976 100644 --- a/app/models/incoming_message.rb +++ b/app/models/incoming_message.rb @@ -693,8 +693,10 @@ class IncomingMessage < ActiveRecord::Base self.reload # get the main body part from the set of attachments we just created, - # not from the self.foi_attachments association - some of the total set of - # self.foi_attachments may now be obsolete + # not from the self.foi_attachments association - some of the total set + # of self.foi_attachments may now be obsolete. Sometimes (e.g. when + # parsing mail from Apple Mail) we can end up with less attachments + # because the hexdigest of an attachment is identical. main_part = get_main_body_text_part(attachments) # we don't use get_main_body_text_internal, as we want to avoid charset # conversions, since /usr/bin/uudecode needs to deal with those. 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..bb83c4c82 --- /dev/null +++ b/app/models/public_body_category.rb @@ -0,0 +1,91 @@ +# == 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 => 'Tag is already taken' + validates_presence_of :title, :message => "Title can't be blank" + validates_presence_of :category_tag, :message => "Tag can't be blank" + validates_presence_of :description, :message => "Description 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..ba3ff1f95 --- /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 => '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..f1916d233 --- /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 => 'Name is already taken' + validates_presence_of :name, :message => 'Name can\'t be blank' + validates :display_order, :numericality => { :only_integer => true, + :message => '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_censor_rule/_form.html.erb b/app/views/admin_censor_rule/_form.html.erb index 5035238d6..3f602d2e4 100644 --- a/app/views/admin_censor_rule/_form.html.erb +++ b/app/views/admin_censor_rule/_form.html.erb @@ -4,11 +4,9 @@ <%=_("Applies to")%> <% unless info_request.nil? %> <%= request_both_links(info_request) %> - <%= hidden_field 'censor_rule', 'info_request_id', { :value => info_request.id } %> <% end %> <% unless user.nil? %> <%= user_both_links(user) %> - <%= hidden_field 'censor_rule', 'user_id', { :value => user.id } %> <% end %> </div> diff --git a/app/views/admin_censor_rule/_show.html.erb b/app/views/admin_censor_rule/_show.html.erb index 0d4cece93..46904b3b9 100644 --- a/app/views/admin_censor_rule/_show.html.erb +++ b/app/views/admin_censor_rule/_show.html.erb @@ -1,18 +1,17 @@ - <% if censor_rules.size > 0 %> <table class="table table-condensed"> <tr> <th>Id</th> - <% for column in CensorRule.content_columns %> + <% CensorRule.content_columns.each do |column| %> <th><%= column.human_name %></th> <% end %> <th>Actions</th> </tr> - <% for censor_rule in censor_rules %> + <% censor_rules.each do |censor_rule| %> <tr class="<%= cycle('odd', 'even') %>"> <td><%=h censor_rule.id %></td> - <% for column in CensorRule.content_columns.map { |c| c.name } %> + <% CensorRule.content_columns.map { |c| c.name }.each do |column| %> <td><%=h censor_rule.send(column) %></td> <% end %> <td> @@ -26,10 +25,11 @@ <% end %> <% if defined? info_request %> - <%= link_to "New censor rule (for this request only)", admin_rule_new_path(:info_request_id => info_request.id), :class => "btn btn-info" %> + <%= link_to "New censor rule", new_admin_info_request_censor_rule_path(info_request), :class => "btn btn-info" %> + <span class="label label-info">for this request only</span> <% end %> <% if defined? user %> - <%= link_to "New censor rule", admin_rule_new_path(:user_id => user.id), :class => "btn btn-info" %> <span class="label label-info">for all requests by this user</span> + <%= link_to "New censor rule", new_admin_user_censor_rule_path(user), :class => "btn btn-info" %> + <span class="label label-info">for all requests by this user</span> <% end %> - diff --git a/app/views/admin_censor_rule/new.html.erb b/app/views/admin_censor_rule/new.html.erb index 77d22990c..26b3212be 100644 --- a/app/views/admin_censor_rule/new.html.erb +++ b/app/views/admin_censor_rule/new.html.erb @@ -1,11 +1,11 @@ <% @title = _('New censor rule') %> -<h1><%=@title%></h1> +<h1><%= @title %></h1> -<%= form_tag admin_rule_create_path, :class => "form form-horizontal" do %> +<%= form_for @censor_rule, :url => @form_url, :class => "form form-horizontal" do %> <%= render :partial => 'form', :locals => { :info_request => @info_request, :user => @censor_user } %> + <div class="form-actions"> <%= submit_tag "Create", :class => "btn btn-primary" %> </div> <% 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..ba9396ceb 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> @@ -198,8 +202,11 @@ <div id="update-authorities" class="accordion-body collapse"> <% for @change_request in @body_update_requests %> <%= render :partial => 'change_request_summary' %> + <%= 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("Make update", admin_body_edit_path(@change_request.public_body, :change_request_id => @change_request.id), :class => 'btn btn-primary') %> + <% end %> <% end %> </div> </div> diff --git a/app/views/admin_general/stats.html.erb b/app/views/admin_general/stats.html.erb index 27dc25ee0..03268cc14 100644 --- a/app/views/admin_general/stats.html.erb +++ b/app/views/admin_general/stats.html.erb @@ -53,8 +53,3 @@ </div> </div> </div> -<div class="row"> - <div class="span12"> - <h2>Web analytics</h2> - </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..1f033ac9b --- /dev/null +++ b/app/views/admin_public_body_categories/_form.html.erb @@ -0,0 +1,73 @@ +<%= 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> + +<h3>Common Fields</h3> + +<div class="control-group"> + <label for="public_body_category_category_tag" class="control-label">Category tag</label> + <div class="controls"> + <% if PublicBody.find_by_tag(@category.category_tag).count == 0 or + @category.errors.messages.keys.include?(:category_tag) %> + <%= f.text_field :category_tag, :class => "span4" %> + <% else %> + <%= f.text_field :category_tag, :class => "span4", :disabled => true %> + <span class="help-block"> + This Category already has authorities assigned to it so the tags + cannot be modified. + </span> + <% end %> + </div> +</div> + +<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..f92f0c9b0 --- /dev/null +++ b/app/views/admin_public_body_categories/_heading_list.html.erb @@ -0,0 +1,29 @@ +<div class="accordion" id="category_list"> + <% category_headings.each do |heading| %> + <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"> + <span class="badge"><%= heading.public_body_categories.size %></span> + <%= 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..f83d0768d --- /dev/null +++ b/app/views/admin_public_body_categories/edit.html.erb @@ -0,0 +1,35 @@ +<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" %> + <%= link_to 'List all', admin_categories_path, :class => "btn" %> + </div> + <% end %> + </div> + </div> +</div> + +<hr /> + +<div class="row"> + <div class="span12"> + <div class="well"> + <%= form_for @category, :url => admin_category_path(@category), :method => 'delete', :class => "form form-inline" do |f| %> + <%= f.submit "Destroy #{ @category.title }", + :title => @category.title, + :class => "btn btn-danger", + :confirm => 'Are you sure?' %> + <span class="help-block"> + Destroying a category does not destroy the public authorities + associated with the category. + </span> + <% end %> + </div> + </div> +</div> 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..ed9f06d7c --- /dev/null +++ b/app/views/admin_public_body_categories/new.html.erb @@ -0,0 +1,17 @@ +<% @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" %> + <%= link_to 'List all', admin_categories_path, :class => "btn" %> + </div> + <% end %> + </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..d4bc02562 --- /dev/null +++ b/app/views/admin_public_body_headings/edit.html.erb @@ -0,0 +1,39 @@ +<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" %> + <%= link_to 'List all', admin_categories_path, :class => "btn" %> + </div> + <% end %> + </div> + </div> +</div> + +<hr /> + +<div class="row"> + <div class="span12"> + <div class="well"> + <%= form_for @heading, :url => admin_heading_path(@heading), :method => 'delete', :class => "form form-inline" do |f| %> + <%= f.submit "Destroy #{ @heading.name }", + :name => @heading.name, + :class => "btn btn-danger", + :confirm => 'Are you sure?' %> + <span class="help-block"> + <ul> + <li>Destroying a category heading only destroys the heading itself.</li> + <li>Child categories assigned to another heading remain assigned to the other heading.</li> + <li>Child categories with no other heading become "Categories with no heading".</li> + <li>Public authorities remain assigned to the categories.</li> + </ul> + </span> + <% end %> + </div> + </div> +</div> 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..c6fe514b0 --- /dev/null +++ b/app/views/admin_public_body_headings/new.html.erb @@ -0,0 +1,17 @@ +<% @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" %> + <%= link_to 'List all', admin_categories_path, :class => "btn" %> + </div> + <% end %> + </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/general/exception_caught.html.erb b/app/views/general/exception_caught.html.erb index 8d78e2e92..21223dc1e 100644 --- a/app/views/general/exception_caught.html.erb +++ b/app/views/general/exception_caught.html.erb @@ -12,13 +12,10 @@ <%= submit_tag _("Search") %> <% end %> </li> - </ul> + </ul> <% else %> <h1><%= _("Sorry, there was a problem processing this page") %></h1> <p><%= _('You have found a bug. Please <a href="{{contact_url}}">contact us</a> to tell us about the problem', :contact_url => help_contact_path) %></p> <% end %> - <h2><%= _('Technical details') %></h2> - <p><strong><%= h(@exception_class ? @exception_class : _("Unknown")) %></strong></p> - <p><strong><%= h(@exception_message) %></strong></p> </div> 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/_search_ahead.html.erb b/app/views/public_body/_search_ahead.html.erb index 2de638034..b5632bccd 100644 --- a/app/views/public_body/_search_ahead.html.erb +++ b/app/views/public_body/_search_ahead.html.erb @@ -2,9 +2,6 @@ <% if !@xapian_requests.nil? %> <% if @xapian_requests.results.size > 0 %> <h3><%= _('Top search results:') %></h3> - <p> - <%= _('Select one to see more information about the authority.')%> - </p> <% else %> <h3><%= _('No results found.') %></h3> <% end %> @@ -14,5 +11,4 @@ <% end %> </div> <%= will_paginate WillPaginate::Collection.new(@page, @per_page, @xapian_requests.matches_estimated), :params => {:controller=>"request", :action => "select_authority"} %> - <p><%= raw(_('<a href="{{browse_url}}">Browse all</a> or <a href="{{add_url}}">ask us to add one</a>.', :browse_url => list_public_bodies_default_path.html_safe, :add_url => (help_requesting_path + '#missing_body').html_safe)) %></p> <% end %> 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/request/_search_ahead.html.erb b/app/views/request/_search_ahead.html.erb index 1e65a5458..4fbe06ebc 100644 --- a/app/views/request/_search_ahead.html.erb +++ b/app/views/request/_search_ahead.html.erb @@ -1,14 +1,20 @@ -<div id="request_search_ahead_results"> - <% if !@xapian_requests.nil? %> - <% if @xapian_requests.results.size > 0 %> +<% unless @xapian_requests.nil? %> + <div id="request_search_ahead_results"> + <% if @xapian_requests.results.any? %> + <span class="close-button">X</span> <h3><%= _("Possibly related requests:") %></h3> - <% end %> - <% for result in @xapian_requests.results %> - <%= render :partial => 'request/request_listing_short_via_event', :locals => { :event => result[:model], :info_request => result[:model].info_request } %> - <% end %> - <p> - <a id="body-site-search-link"><%= _("Or search in their website for this information.") %></a> - </p> - <% end %> -</div> + <% @xapian_requests.results.each do |result| %> + <%= render :partial => 'request/request_listing_short_via_event', + :locals => { :event => result[:model], + :info_request => result[:model].info_request } %> + <% end %> + + <p> + <a id="body-site-search-link"> + <%= _("Search in their website for this information →") %> + </a> + </p> + <% end %> + </div> +<% end %> diff --git a/app/views/request/new.html.erb b/app/views/request/new.html.erb index 7f1332464..51224129e 100644 --- a/app/views/request/new.html.erb +++ b/app/views/request/new.html.erb @@ -1,19 +1,33 @@ <% unless @batch %> <script type="text/javascript"> $(document).ready(function(){ - // Avoid triggering too often (on each keystroke) by using the debounce jQuery plugin: + // Avoid triggering too often (on each keystroke) by using the + // debounce jQuery plugin: // http://benalman.com/projects/jquery-throttle-debounce-plugin/ $("#typeahead_search").keypress($.debounce( 300, function() { - $("#typeahead_response").load("<%=search_ahead_url%>?q="+encodeURI(this.value), function() { - // When following links in typeahead results, open new tab/window - $("#typeahead_response a").attr("target","_blank"); - - // Update the public body site search link - $("#body-site-search-link").attr("href", "http://www.google.com/#q="+encodeURI($("#typeahead_search").val())+ - "+site:<%= @info_request.public_body.calculated_home_page %>"); + if ( $('#request_search_ahead_results').text().trim().length > 0) { + $('#typeahead_response').slideUp('fast'); + } + + $("#typeahead_response").load("<%= search_ahead_url %>?q="+encodeURI(this.value)+ + "&requested_from=<%= @info_request.public_body.url_name %>"+ + "&per_page=3", function() { + + if ( $('#request_search_ahead_results').text().trim().length > 0) { + $('#typeahead_response').hide().slideDown('fast'); + + // When following links in typeahead results, open new + // tab/window + $("#typeahead_response a").attr("target","_blank"); + + // Update the public body site search link + $("#body-site-search-link").attr("href", "http://www.google.com/#q="+encodeURI($("#typeahead_search").val())+ + "+site:<%= @info_request.public_body.calculated_home_page %>"); + + $('.close-button').click(function() { $(this).parent().hide() }); + } }); })); - }); </script> <% end %> diff --git a/app/views/request/select_authority.html.erb b/app/views/request/select_authority.html.erb index ed072cf64..134648264 100644 --- a/app/views/request/select_authority.html.erb +++ b/app/views/request/select_authority.html.erb @@ -1,60 +1,60 @@ <script type="text/javascript"> - $(document).ready(function(){ - $("#authority_preview").hide(); - - // Avoid triggering too often (on each keystroke) by using the debounce jQuery plugin: + $(document).ready(function() { + // Avoid triggering too often (on each keystroke) by using the debounce + // jQuery plugin: // http://benalman.com/projects/jquery-throttle-debounce-plugin/ $("#query").keypress($.debounce( 300, function() { // Do a type ahead search and display results - $("#typeahead_response").load("<%=search_ahead_bodies_url%>?query="+encodeURI(this.value), function() { - $("#authority_preview").hide(); // Hide the preview, since results have changed - - }); + $("#typeahead_response").load("<%= search_ahead_bodies_url %>?query="+encodeURI(this.value)); })); - // We're using the existing body list: we intercept the clicks on the titles to - // display a preview on the right hand side of the screen - $("#typeahead_response .head a").live('click', function() { - $("#authority_preview").load(this.href+" #public_body_show", function() { - $("#authority_preview").show(); - $(window).scrollTop($("#banner").height()); - $("#authority_preview #header_right").hide(); - location.hash = '#header_left'; - }); - return false; - }); }); </script> <% @title = _("Select the authority to write to") %> - <h1 style="clear: left"><%= _('1. Select an authority') %></h1> - - <div id="authority_selection"> - <%= form_tag({:controller => "request", :action => "select_authority"}, {:id => "search_form", :method => "get"}) do %> - <div> - <p> - <%= _('First, type in the <strong>name of the UK public authority</strong> you\'d - like information from. <strong>By law, they have to respond</strong> - (<a href="{{url}}">why?</a>).', :url => (help_about_path + "#whybother_them").html_safe) %> - </p> - <%= text_field_tag 'query', params[:query], { :size => 30, :title => "type your search term here" } %> - <%= hidden_field_tag 'bodies', 1 %> - <%= submit_tag _('Search') %> - </div> - <% if AlaveteliConfiguration.allow_batch_requests && @user && @user.can_make_batch_requests? %> - <div id="batch_request_link"> - <p> - <%= _('Or make a <a href="{{url}}">batch request</a> to <strong>multiple authorities</strong> at once.', :url => select_authorities_path) %> - </p> - </div> - <% end %> +<h1 style="clear: left"><%= _('1. Select an authority') %></h1> + +<div id="authority_selection"> + <%= form_tag select_authority_path, { :id => 'search_form', :method => 'get' } do %> + <div> + <p> + <%= _(%Q(First, type in the <strong>name of the UK public authority</strong> you'd + like information from. <strong>By law, they have to respond</strong> + (<a href="{{url}}">why?</a>).), :url => (help_about_path(:anchor => 'whybother_them')).html_safe) %> + </p> + + <%= text_field_tag :query, + params[:query], + { :size => 30, + :title => _('type your search term here'), + :placeholder => _('e.g. Ministry of Defence') } %> + <%= hidden_field_tag :bodies, 1 %> + <%= submit_tag _('Search') %> + </div> + + <% if AlaveteliConfiguration.allow_batch_requests && @user && @user.can_make_batch_requests? %> + <div id="batch_request_link"> + <p> + <%= _('Or make a <a href="{{url}}">batch request</a> to <strong>multiple authorities</strong> at once.', + :url => select_authorities_path) %> + </p> + </div> <% end %> + <% end %> - <div id="typeahead_response"> - <%= render :partial => 'public_body/search_ahead' %> - </div> + <div id="typeahead_response"> + <%= render :partial => 'public_body/search_ahead' %> </div> - <div id="authority_preview"> - </div> +</div> + +<div id="select_authority_help"> + <p class="info"><%= _("Can't find the one you want?") %></p> + <p class="actions"> + <%= raw _('<a href="{{browse_url}}">Browse all</a> or <a href="{{add_url}}">ask us to add one</a>.', + :browse_url => list_public_bodies_default_path.html_safe, + :add_url => help_requesting_path(:anchor => 'missing_body') + ).html_safe %> + </p> +</div> diff --git a/app/views/user/_signin.html.erb b/app/views/user/_signin.html.erb index afc55d249..864951733 100644 --- a/app/views/user/_signin.html.erb +++ b/app/views/user/_signin.html.erb @@ -14,7 +14,7 @@ <p> <label class="form_label" for="user_signin_password"><%= _('Password:')%></label> - <%= password_field 'user_signin', 'password', { :size => 15, :tabindex => 20 } %> + <%= password_field 'user_signin', 'password', { :size => 15, :tabindex => 20, :autocomplete => 'off' } %> </p> <p class="form_note"> 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/app/views/user/signchangeemail.html.erb b/app/views/user/signchangeemail.html.erb index 7308179f4..a99bcb785 100644 --- a/app/views/user/signchangeemail.html.erb +++ b/app/views/user/signchangeemail.html.erb @@ -23,7 +23,7 @@ <p> <label class="form_label" for="signchangeemail_password"> <%= _('Your password:')%></label> - <%= password_field 'signchangeemail', 'password', { :size => 15 } %> + <%= password_field 'signchangeemail', 'password', { :size => 15, :autocomplete => 'off' } %> </p> <p class="form_note"> diff --git a/app/views/user/signchangepassword.html.erb b/app/views/user/signchangepassword.html.erb index 51bcb466d..60f5d2c62 100644 --- a/app/views/user/signchangepassword.html.erb +++ b/app/views/user/signchangepassword.html.erb @@ -13,12 +13,12 @@ <p> <label class="form_label" for="user_password"><%= _('New password:')%></label> - <%= password_field 'user', 'password', { :size => 15 } %> + <%= password_field 'user', 'password', { :size => 15, :autocomplete => 'off' } %> </p> <p> <label class="form_label" for="user_password_confirmation"><%= _('New password: (again)')%></label> - <%= password_field 'user', 'password_confirmation', { :size => 15 } %> + <%= password_field 'user', 'password_confirmation', { :size => 15, :autocomplete => 'off' } %> </p> <div class="form_button"> |