diff options
Diffstat (limited to 'app')
61 files changed, 1156 insertions, 411 deletions
diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 7f8cfbd67..ca5538e03 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -19,19 +19,9 @@ class AdminController < ApplicationController # Expire cached attachment files for a request def expire_for_request(info_request) - # Clear out cached entries - use low level disk removal, even though we - # are clearing results from caches_action, for several reasons: - # * We can't use expire_action here, as it doesn't seem to be - # compatible with the :only_path we used in the caches_action - # call. - # * Removing everything is simpler than having to get all the - # parameters right for the path, and calling for HTML version vs. raw - # attachment version. - # * We cope properly with filenames changed by censor rules, which - # change the URL. - # * We could use expire_fragment with a Regexp, but it walks the whole - # cache which is insanely slow - cache_subpath = File.join(self.cache_store.cache_path, "views/request/#{info_request.id}") + # Clear out cached entries, by removing files from disk (the built in + # Rails fragment cache made doing this and other things too hard) + cache_subpath = foi_fragment_cache_all_for_request(info_request) FileUtils.rm_rf(cache_subpath) # Remove the database caches of body / attachment text (the attachment text diff --git a/app/controllers/admin_public_body_controller.rb b/app/controllers/admin_public_body_controller.rb index 74a3a86c6..bce04ff98 100644 --- a/app/controllers/admin_public_body_controller.rb +++ b/app/controllers/admin_public_body_controller.rb @@ -120,7 +120,15 @@ class AdminPublicBodyController < AdminController def import_csv if params[:csv_file] - if not params[:tag].empty? + if !params[:tag].empty? + if params['commit'] == 'Dry run' + dry_run_only = true + elsif params['commit'] == 'Upload' + dry_run_only = false + else + raise "internal error, unknown button label" + end + # Try with dry run first csv_contents = params[:csv_file].read en = PublicBody.import_csv(csv_contents, params[:tag], true, admin_http_auth_user()) @@ -128,14 +136,18 @@ class AdminPublicBodyController < AdminController notes = en[1] if errors.size == 0 - # And if OK, with real run - en = PublicBody.import_csv(csv_contents, params[:tag], false, admin_http_auth_user()) - errors = en[0] - notes = en[1] - if errors.size != 0 - raise "dry run mismatched real run" + if dry_run_only + notes.push("Dry run was successful, real run would do as above.") + else + # And if OK, with real run + en = PublicBody.import_csv(csv_contents, params[:tag], false, admin_http_auth_user()) + errors = en[0] + notes = en[1] + if errors.size != 0 + raise "dry run mismatched real run" + end + notes.push("Import was successful.") end - notes.push("Import was successful.") end @errors = errors.join("\n") @notes = notes.join("\n") diff --git a/app/controllers/admin_request_controller.rb b/app/controllers/admin_request_controller.rb index ff2772b0e..f077691ff 100644 --- a/app/controllers/admin_request_controller.rb +++ b/app/controllers/admin_request_controller.rb @@ -215,10 +215,10 @@ class AdminRequestController < AdminController # Bejeeps, look, sometimes a URL is something that belongs in a controller, jesus. # XXX hammer this square peg into the round MVC hole - should be calling main_url(upload_response_url()) post_redirect = PostRedirect.new( - :uri => upload_response_url(:url_title => info_request.url_title), + :uri => main_url(upload_response_url(:url_title => info_request.url_title, :only_path => true)), :user_id => user.id) post_redirect.save! - url = confirm_url(:email_token => post_redirect.email_token) + url = main_url(confirm_url(:email_token => post_redirect.email_token, :only_path => true)) flash[:notice] = 'Send "' + name + '" <<a href="mailto:' + email + '">' + email + '</a>> this URL: <a href="' + url + '">' + url + "</a> - it will log them in and let them upload a response to this request." redirect_to request_admin_url(info_request) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5055519ec..9ee1c250b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -40,21 +40,12 @@ class ApplicationController < ActionController::Base before_filter :session_remember_me def session_remember_me # Reset the "sliding window" session expiry time. - if session[:remember_me] - expire_time = 1.month.from_now - # "Why is session[:force_new_cookie] set to Time.now? In order for the “sliding window” - # concept to work, a fresh cookie must be sent with every response. Rails only - # sends a cookie when the session data has changed so using a value like Time.now - # ensures that it changes every time. What I have actually found is that some - # internal voodoo causes the session data to change slightly anyway but it’s best - # to be sure!" - session[:force_new_cookie] = Time.now - else - expire_time = nil - end - # if statement here is so test code runs - if session.instance_variable_get(:@dbman) - session.instance_variable_get(:@dbman).instance_variable_get(:@cookie_options)['expires'] = expire_time + if request.env['rack.session.options'] + if session[:remember_me] + request.env['rack.session.options'][:expire_after] = 1.month + else + request.env['rack.session.options'][:expire_after] = nil + end end end @@ -100,6 +91,27 @@ class ApplicationController < ActionController::Base controller_example_group.get params[:action], params end + # Used to work out where to cache fragments. We add an extra path to the + # URL using the first three digits of the info request id, because we can't + # have more than 32,000 entries in one directory on an ext3 filesystem. + def foi_fragment_cache_part_path(param) + path = url_for(param) + id = param['id'] || param[:id] + first_three_digits = id.to_s()[0..2] + path = path.sub("/request/", "/request/" + first_three_digits + "/") + return path + end + def foi_fragment_cache_path(param) + path = foi_fragment_cache_part_path(param) + path = "/views" + path + return File.join(self.cache_store.cache_path, path) + end + def foi_fragment_cache_all_for_request(info_request) + first_three_digits = info_request.id.to_s()[0..2] + path = "views/request/#{first_three_digits}/#{info_request.id}" + return File.join(self.cache_store.cache_path, path) + end + private # Check the user is logged in @@ -184,6 +196,16 @@ class ApplicationController < ActionController::Base end end + # + def check_read_only + read_only = MySociety::Config.get('READ_ONLY') + if !read_only.empty? + flash[:notice] = "<p>WhatDoTheyKnow is currently in maintenance. You can only view existing requests. You cannot make new ones, add followups or annotations, or otherwise change the database.</p> <p>" + read_only + "</p>" + redirect_to frontpage_url + end + + end + # For administration interface, return display name of authenticated user def admin_http_auth_user # This needs special magic in mongrel: http://www.ruby-forum.com/topic/83067 diff --git a/app/controllers/comment_controller.rb b/app/controllers/comment_controller.rb index dfa31f9ef..d5f8f89fb 100644 --- a/app/controllers/comment_controller.rb +++ b/app/controllers/comment_controller.rb @@ -7,6 +7,7 @@ # $Id: comment_controller.rb,v 1.9 2009-03-09 01:17:04 francis Exp $ class CommentController < ApplicationController + before_filter :check_read_only, :only => [ :new ] def new if params[:type] == 'request' diff --git a/app/controllers/general_controller.rb b/app/controllers/general_controller.rb index 5c4103616..efab26fad 100644 --- a/app/controllers/general_controller.rb +++ b/app/controllers/general_controller.rb @@ -7,7 +7,7 @@ # # $Id: general_controller.rb,v 1.57 2009-10-03 10:23:43 francis Exp $ -require 'xmlsimple' +require 'lib/xmlsimple' require 'open-uri' class GeneralController < ApplicationController diff --git a/app/controllers/public_body_controller.rb b/app/controllers/public_body_controller.rb index 1ca82e463..591081fe9 100644 --- a/app/controllers/public_body_controller.rb +++ b/app/controllers/public_body_controller.rb @@ -6,6 +6,8 @@ # # $Id: public_body_controller.rb,v 1.8 2009-09-14 13:27:00 francis Exp $ +require 'csv' + class PublicBodyController < ApplicationController # XXX tidy this up with better error messages, and a more standard infrastructure for the redirect to canonical URL def show @@ -93,5 +95,51 @@ class PublicBodyController < ApplicationController cache_in_squid end + + # Used so URLs like /local/islington work, for use e.g. writing to a local paper. + def list_redirect + @tag = params[:tag] + redirect_to list_public_bodies_url(:tag => @tag) + end + + def list_all_csv + public_bodies = PublicBody.find(:all, :order => 'url_name') + report = StringIO.new + CSV::Writer.generate(report, ',') do |title| + title << [ + 'Name', + 'Short name', + # deliberately not including 'Request email' + 'URL name', + 'Tags', + 'Home page', + 'Publication scheme', + 'Charity number', + 'Created at', + 'Updated at', + 'Version', + ] + public_bodies.each do |public_body| + title << [ + public_body.name, + public_body.short_name, + # DO NOT include request_email (we don't want to make it + # easy to spam all authorities with requests) + public_body.url_name, + public_body.tag_string, + public_body.calculated_home_page, + public_body.publication_scheme, + public_body.charity_number, + public_body.created_at, + public_body.updated_at, + public_body.version, + ] + end + end + report.rewind + send_data(report.read, :type=> 'text/csv; charset=utf-8; header=present', + :filename => 'all-authorities.csv', + :disposition =>'attachment', :encoding => 'utf8') + end end diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 2606a9609..0664093c3 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -7,6 +7,7 @@ # $Id: request_controller.rb,v 1.192 2009-10-19 19:26:40 francis Exp $ class RequestController < ApplicationController + before_filter :check_read_only, :only => [ :new, :show_response, :describe_state ] def show # Look up by old style numeric identifiers @@ -64,11 +65,26 @@ class RequestController < ApplicationController @last_response = @info_request.get_last_response end + # Extra info about a request, such as event history + def details + @info_request = InfoRequest.find_by_url_title(params[:url_title]) + if !@info_request.user_can_view?(authenticated_user) + render :template => 'request/hidden', :status => 410 # gone + return + end + + @columns = ['id', 'event_type', 'created_at', 'described_state', 'last_described_at', 'calculated_state' ] + end + # Requests similar to this one def similar @per_page = 25 @page = (params[:page] || "1").to_i @info_request = InfoRequest.find_by_url_title(params[:url_title]) + if !@info_request.user_can_view?(authenticated_user) + render :template => 'request/hidden', :status => 410 # gone + return + end @xapian_object = ::ActsAsXapian::Similar.new([InfoRequestEvent], @info_request.info_request_events, :offset => (@page - 1) * @per_page, :limit => @per_page, :collapse_by_prefix => 'request_collapse') @@ -148,27 +164,27 @@ class RequestController < ApplicationController # First time we get to the page, just display it if params[:submitted_new_request].nil? || params[:reedit] # Read parameters in - public body must be passed in - if params[:public_body_id] - params[:info_request] = { :public_body_id => params[:public_body_id] } + params[:info_request] = { :public_body_id => params[:public_body_id] } if !params[:info_request] + if !params[:info_request][:public_body_id] + redirect_to frontpage_url + return end @info_request = InfoRequest.new(params[:info_request]) params[:info_request_id] = @info_request.id + params[:outgoing_message] = {} if !params[:outgoing_message] + params[:outgoing_message][:info_request] = @info_request @outgoing_message = OutgoingMessage.new(params[:outgoing_message]) @outgoing_message.set_signature_name(@user.name) if !@user.nil? - if @info_request.public_body.nil? - redirect_to frontpage_url - else - if @info_request.public_body.is_requestable? - render :action => 'new' + if @info_request.public_body.is_requestable? + render :action => 'new' + else + if @info_request.public_body.not_requestable_reason == 'bad_contact' + render :action => 'new_bad_contact' else - if @info_request.public_body.not_requestable_reason == 'bad_contact' - render :action => 'new_bad_contact' - else - # if not requestable because defunct or not_apply, redirect to main page - # (which doesn't link to the /new/ URL) - redirect_to public_body_url(@info_request.public_body) - end + # if not requestable because defunct or not_apply, redirect to main page + # (which doesn't link to the /new/ URL) + redirect_to public_body_url(@info_request.public_body) end end return @@ -318,11 +334,14 @@ class RequestController < ApplicationController # Display advice for requester on what to do next, as appropriate if @info_request.calculate_status == 'waiting_response' - flash[:notice] = "<p>Thank you! Hopefully your wait isn't too long.</p> <p>By law, you should get a response promptly, and normally before the end of <strong>" + simple_date(@info_request.date_response_required_by) + "</strong>.</p>" + flash[:notice] = "<p>Thank you! Hopefully your wait isn't too long.</p> <p>By law, you should get a response promptly, and " + (@info_request.public_body.is_school? ? "in term time" : "") + " normally before the end of <strong>" + simple_date(@info_request.date_response_required_by) + "</strong>.</p>" redirect_to request_url(@info_request) elsif @info_request.calculate_status == 'waiting_response_overdue' - flash[:notice] = "<p>Thank you! Hope you don't have to wait much longer.</p> <p>By law, you should have got a response promptly, and normally before the end of <strong>" + simple_date(@info_request.date_response_required_by) + "</strong>.</p>" + flash[:notice] = "<p>Thank you! Hope you don't have to wait much longer.</p> <p>By law, you should have got a response promptly, and " + (@info_request.public_body.is_school? ? "in term time" : "") + " normally before the end of <strong>" + simple_date(@info_request.date_response_required_by) + "</strong>.</p>" redirect_to request_url(@info_request) + elsif @info_request.calculate_status == 'waiting_response_very_overdue' + flash[:notice] = "<p>Thank you! Your request is long overdue, by more than 40 working days. Most requests should be answered within 20 working days. You might like to complain about this, see below.</p>" + redirect_to unhappy_url(@info_request) elsif @info_request.calculate_status == 'not_held' flash[:notice] = "<p>Thank you! Here are some ideas on what to do next:</p> <ul> @@ -337,7 +356,7 @@ class RequestController < ApplicationController " redirect_to request_url(@info_request) elsif @info_request.calculate_status == 'rejected' - flash[:notice] = "Oh no! Sorry to hear that your request was rejected. Here is what to do now." + flash[:notice] = "Oh no! Sorry to hear that your request was refused. Here is what to do now." redirect_to unhappy_url(@info_request) elsif @info_request.calculate_status == 'successful' flash[:notice] = "<p>We're glad you got all the information that you wanted. If you write about or make use of the information, please come back and add an annotation below saying what you did.</p><p>If you found WhatDoTheyKnow useful, <a href=\"http://www.mysociety.org/donate/\">make a donation</a> to the charity which runs it.</p>" @@ -360,8 +379,8 @@ class RequestController < ApplicationController flash[:notice] = "Please use the form below to tell us more." redirect_to help_general_url(:action => 'contact') elsif @info_request.calculate_status == 'user_withdrawn' - flash[:notice] = "Thanks for letting us know that you've withdrawn your request. Please add an annotation below to let other people know why you withdrew it." - redirect_to request_url(@info_request) + flash[:notice] = "If you have not done so already, please write a message below telling the authority that you have withdrawn your request. Otherwise they will not know it has been withdrawn." + redirect_to respond_to_last_url(@info_request) else raise "unknown calculate_status " + @info_request.calculate_status end @@ -519,7 +538,7 @@ class RequestController < ApplicationController # Test for hidden incoming_message = IncomingMessage.find(params[:incoming_message_id]) if !incoming_message.info_request.user_can_view?(authenticated_user) - render :template => 'request/hidden' + render :template => 'request/hidden', :status => 410 # gone end end @@ -527,8 +546,10 @@ class RequestController < ApplicationController around_filter :cache_attachments, :only => [ :get_attachment, :get_attachment_as_html ] def cache_attachments key = params.merge(:only_path => true) - if cached = read_fragment(key) - #if cached = 'zzz***zzz' + key_path = foi_fragment_cache_path(key) + + if File.exists?(key_path) + cached = File.read(key_path) IncomingMessage # load global filename_to_mimetype XXX should move filename_to_mimetype to proper namespace response.content_type = filename_to_mimetype(params[:file_name].join("/")) or 'application/octet-stream' render_for_text(cached) @@ -537,7 +558,14 @@ class RequestController < ApplicationController yield - write_fragment(key, response.body) + # write it to the fileystem ourselves, so is just a plain file. (The + # various fragment cache functions using Ruby Marshall to write the file + # which adds a header, so isnt compatible with images that have been + # extracted elsewhere from PDFs) + FileUtils.mkdir_p(File.dirname(key_path)) + File.atomic_write(key_path) do |f| + f.write(response.body) + end end def get_attachment @@ -558,14 +586,16 @@ class RequestController < ApplicationController # images made during conversion (e.g. images in PDF files) are put in the cache directory, so # the same cache code in cache_attachments above will display them. - image_dir = File.dirname(ActionController::Base.cache_store.cache_path + "/views" + url_for(params.merge(:only_path => true))) + key = params.merge(:only_path => true) + key_path = foi_fragment_cache_path(key) + image_dir = File.dirname(key_path) FileUtils.mkdir_p(image_dir) - html = @attachment.body_as_html(image_dir) + html, wrapper_id = @attachment.body_as_html(image_dir) view_html_stylesheet = render_to_string :partial => "request/view_html_stylesheet" html.sub!(/<head>/i, "<head>" + view_html_stylesheet) - html.sub!(/<body[^>]*>/i, '<body><prefix-here><div id="wrapper"><div id="view_html_content">' + view_html_stylesheet) - html.sub!(/<\/body[^>]*>/i, '</div></div></body>' + view_html_stylesheet) + html.sub!(/<body[^>]*>/i, '<body><prefix-here><div id="' + wrapper_id + '"><div id="view_html_content">') + html.sub!(/<\/body[^>]*>/i, '</div></div></body>') view_html_prefix = render_to_string :partial => "request/view_html_prefix" html.sub!("<prefix-here>", view_html_prefix) @@ -595,6 +625,7 @@ class RequestController < ApplicationController raise "internal error, pre-auth filter should have caught this" if !@info_request.user_can_view?(authenticated_user) @attachment = IncomingMessage.get_attachment_by_url_part_number(@incoming_message.get_attachments_for_display, @part_number) + raise "attachment not found part number " + @part_number.to_s + " incoming_message " + @incoming_message.id.to_s if @attachment.nil? # check filename in URL matches that in database (use a censor rule if you want to change a filename) raise "please use same filename as original file has, display: '" + @attachment.display_filename + "' old_display: '" + @attachment.old_display_filename + "' original: '" + @original_filename + "'" if @attachment.display_filename != @original_filename && @attachment.old_display_filename != @original_filename @@ -631,7 +662,7 @@ class RequestController < ApplicationController if params[:submitted_upload_response] file_name = nil file_content = nil - if params[:file_1].class.to_s == "ActionController::UploadedTempfile" + if !params[:file_1].nil? file_name = params[:file_1].original_filename file_content = params[:file_1].read end diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index bc117ce2e..b3f9511b5 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -147,10 +147,13 @@ class UserController < ApplicationController end # Logout form - def signout + def _do_signout session[:user_id] = nil session[:user_circumstance] = nil session[:remember_me] = false + end + def signout + self._do_signout if params[:r] redirect_to params[:r] else @@ -159,24 +162,24 @@ class UserController < ApplicationController end # Change password (XXX and perhaps later email) - requires email authentication - def signchange + def signchangepassword if @user and ((not session[:user_circumstance]) or (session[:user_circumstance] != "change_password")) # Not logged in via email, so send confirmation - params[:submitted_signchange_send_confirm] = true - params[:signchange] = { :email => @user.email } + params[:submitted_signchangepassword_send_confirm] = true + params[:signchangepassword] = { :email => @user.email } end - if params[:submitted_signchange_send_confirm] + if params[:submitted_signchangepassword_send_confirm] # They've entered the email, check it is OK and user exists - if not MySociety::Validate.is_valid_email(params[:signchange][:email]) + if not MySociety::Validate.is_valid_email(params[:signchangepassword][:email]) flash[:error] = "That doesn't look like a valid email address. Please check you have typed it correctly." - render :action => 'signchange_send_confirm' + render :action => 'signchangepassword_send_confirm' return end - user_signchange = User.find_user_by_email(params[:signchange][:email]) - if user_signchange - # Send email with login link to go to signchange page - url = signchange_url + user_signchangepassword = User.find_user_by_email(params[:signchangepassword][:email]) + if user_signchangepassword + # Send email with login link to go to signchangepassword page + url = signchangepassword_url if params[:pretoken] url += "?pretoken=" + params[:pretoken] end @@ -188,27 +191,27 @@ class UserController < ApplicationController }, :circumstance => "change_password" # special login that lets you change your password ) - post_redirect.user = user_signchange + post_redirect.user = user_signchangepassword post_redirect.save! url = confirm_url(:email_token => post_redirect.email_token) - UserMailer.deliver_confirm_login(user_signchange, post_redirect.reason_params, url) + UserMailer.deliver_confirm_login(user_signchangepassword, post_redirect.reason_params, url) else # User not found, but still show confirm page to not leak fact user exists end - render :action => 'signchange_confirm' + render :action => 'signchangepassword_confirm' elsif not @user # Not logged in, prompt for email - render :action => 'signchange_send_confirm' + render :action => 'signchangepassword_send_confirm' else # Logged in via special email change password link, so can offer form to change password raise "internal error" unless (session[:user_circumstance] == "change_password") - if params[:submitted_signchange_password] + if params[:submitted_signchangepassword_do] @user.password = params[:user][:password] @user.password_confirmation = params[:user][:password_confirmation] if not @user.valid? - render :action => 'signchange' + render :action => 'signchangepassword' else @user.save! flash[:notice] = "Your password has been changed." @@ -220,11 +223,71 @@ class UserController < ApplicationController end end else - render :action => 'signchange' + render :action => 'signchangepassword' end end end + # Change your email + def signchangeemail + if not authenticated?( + :web => "To change your email address used on WhatDoTheyKnow.com", + :email => "Then you can change your email address used on WhatDoTheyKnow.com", + :email_subject => "Change your email address used on WhatDoTheyKnow.com" + ) + # "authenticated?" has done the redirect to signin page for us + return + end + + if !params[:submitted_signchangeemail_do] + render :action => 'signchangeemail' + return + end + + @signchangeemail = ChangeEmailValidator.new(params[:signchangeemail]) + @signchangeemail.logged_in_user = @user + + if !@signchangeemail.valid? + render :action => 'signchangeemail' + return + end + + # if new email already in use, send email there saying what happened + user_alreadyexists = User.find_user_by_email(@signchangeemail.new_email) + if user_alreadyexists + UserMailer.deliver_changeemail_already_used(@user.email, @signchangeemail.new_email) + # it is important this screen looks the same as the one below, so + # you can't change to someone's email in order to tell if they are + # registered with that email on the site + render :action => 'signchangeemail_confirm' + return + end + + # if not already, send a confirmation link to the new email address which logs + # them into the old email's user account, but with special user_circumstance + if (not session[:user_circumstance]) or (session[:user_circumstance] != "change_email") + post_redirect = PostRedirect.new(:uri => signchangeemail_url(), :post_params => params, + :circumstance => "change_email" # special login that lets you change your email + ) + post_redirect.user = @user + post_redirect.save! + + url = confirm_url(:email_token => post_redirect.email_token) + UserMailer.deliver_changeemail_confirm(@user, @signchangeemail.new_email, url) + # it is important this screen looks the same as the one above, so + # you can't change to someone's email in order to tell if they are + # registered with that email on the site + render :action => 'signchangeemail_confirm' + return + end + + # circumstance is 'change_email', so can actually change the email + @user.email = @signchangeemail.new_email + @user.save! + flash[:notice] = "You have now changed your email address used on WhatDoTheyKnow.com" + redirect_to user_url(@user) + end + # Send a message to another user def contact @recipient_user = User.find(params[:id]) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 7cc0b0e5d..08908abee 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -59,5 +59,11 @@ module ApplicationHelper t = highlight_words(t, words, html) return t end + + # Use our own algorithm for finding path of cache + def foi_cache(name = {}, options = nil, &block) + name = @controller.foi_fragment_cache_part_path(name) + @controller.fragment_for(output_buffer, name, options, &block) + end end diff --git a/app/helpers/link_to_helper.rb b/app/helpers/link_to_helper.rb index 6ee0edb2b..aa63ef65d 100644 --- a/app/helpers/link_to_helper.rb +++ b/app/helpers/link_to_helper.rb @@ -33,6 +33,10 @@ module LinkToHelper return similar_request_url(:url_title => info_request.url_title, :only_path => true) end + def request_details_url(info_request) + return details_request_url(:url_title => info_request.url_title, :only_path => true) + end + # Incoming / outgoing messages def incoming_message_url(incoming_message) return request_url(incoming_message.info_request)+"#incoming-"+incoming_message.id.to_s diff --git a/app/models/change_email_validator.rb b/app/models/change_email_validator.rb new file mode 100644 index 000000000..ff7f2f931 --- /dev/null +++ b/app/models/change_email_validator.rb @@ -0,0 +1,42 @@ +# models/changeemail_validator.rb: +# Validates email change form submissions. +# +# Copyright (c) 2010 UK Citizens Online Democracy. All rights reserved. +# Email: francis@mysociety.org; WWW: http://www.mysociety.org/ +# +# $Id: contact_validator.rb,v 1.32 2009-09-17 21:10:05 francis Exp $ + +class ChangeEmailValidator < ActiveRecord::BaseWithoutTable + strip_attributes! + + column :old_email, :string + column :new_email, :string + column :password, :string + + attr_accessor :logged_in_user + + validates_presence_of :old_email, :message => "^Please enter your old email address" + validates_presence_of :new_email, :message => "^Please enter your new email address" + validates_presence_of :password, :message => "^Please enter your password" + + def validate + if !self.old_email.blank? && !MySociety::Validate.is_valid_email(self.old_email) + errors.add(:old_email, "doesn't look like a valid address") + end + + if !errors[:old_email] + if self.old_email.downcase != self.logged_in_user.email.downcase + errors.add(:old_email, "address isn't the same as the address of the account you are logged in with") + elsif !self.logged_in_user.has_this_password?(self.password) + if !errors[:password] + errors.add(:password, "is not correct") + end + end + end + + if !self.new_email.blank? && !MySociety::Validate.is_valid_email(self.new_email) + errors.add(:new_email, "doesn't look like a valid address") + end + end + +end diff --git a/app/models/comment.rb b/app/models/comment.rb index 0da6c1ce3..4e9976373 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -45,7 +45,7 @@ class Comment < ActiveRecord::Base read_attribute(:body) end - # So when made invisble it vanishes + # 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 diff --git a/app/models/incoming_message.rb b/app/models/incoming_message.rb index 2348c17b5..316f2683a 100644 --- a/app/models/incoming_message.rb +++ b/app/models/incoming_message.rb @@ -29,6 +29,8 @@ require 'htmlentities' require 'rexml/document' require 'zip/zip' require 'mahoro' +require 'mapi/msg' +require 'mapi/convert' # Monkeypatch! Adding some extra members to store extra info in. module TMail @@ -51,6 +53,9 @@ $file_extension_to_mime_type = { "xlsx" => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', "ppt" => 'application/vnd.ms-powerpoint', "pptx" => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + "oft" => 'application/vnd.ms-outlook', + "msg" => 'application/vnd.ms-outlook', + "tnef" => 'application/ms-tnef', "tif" => 'image/tiff', "gif" => 'image/gif', "jpg" => 'image/jpeg', # XXX add jpeg @@ -303,10 +308,54 @@ class FOIAttachment end end + # Whether this type has a "View as HTML" + def has_body_as_html? + if self.content_type == 'text/plain' + return true + elsif self.content_type == 'application/vnd.ms-word' + return true + elsif self.content_type == 'application/vnd.ms-excel' + return true + elsif self.content_type == 'application/pdf' + return true + elsif self.content_type == 'application/rtf' + return true + end + return false + end + + # Name of type of attachment type - only valid for things that has_body_as_html? + def name_of_content_type + if self.content_type == 'text/plain' + return "Text file" + elsif self.content_type == 'application/vnd.ms-word' + return "Word document" + elsif self.content_type == 'application/vnd.ms-excel' + return "Excel spreadsheet" + elsif self.content_type == 'application/pdf' + return "PDF file" + elsif self.content_type == 'application/rtf' + return "RTF file" + end + end + # For "View as HTML" of attachment def body_as_html(dir) html = nil + wrapper_id = "wrapper" + + # simple cases, can never fail + if self.content_type == 'text/plain' + text = self.body.strip + text = CGI.escapeHTML(text) + text = MySociety::Format.make_clickable(text) + html = text.gsub(/\n/, '<br>') + return "<html><head></head><body>" + html + "</body></html>", wrapper_id + end + # the extractions will also produce image files, which go in the + # current directory, so change to the directory the function caller + # wants everything in Dir.chdir(dir) do tempfile = Tempfile.new('foiextract', '.') tempfile.print self.body @@ -317,10 +366,22 @@ class FOIAttachment system("/usr/bin/wvHtml --charset=UTF-8 " + tempfile.path + " " + tempfile.path + ".html") html = File.read(tempfile.path + ".html") File.unlink(tempfile.path + ".html") + elsif self.content_type == 'application/vnd.ms-excel' + # Don't colorise, e.g. otherwise this one comes out with white + # text which is nasty: + # http://www.whatdotheyknow.com/request/30485/response/74705/attach/html/2/Empty%20premises%20Sefton.xls.html + IO.popen("/usr/bin/xlhtml -nc -a " + tempfile.path + "", "r") do |child| + html = child.read() + wrapper_id = "wrapper_xlhtml" + end elsif self.content_type == 'application/pdf' IO.popen("/usr/bin/pdftohtml -nodrm -zoom 1.0 -stdout -enc UTF-8 -noframes " + tempfile.path + "", "r") do |child| html = child.read() end + elsif self.content_type == 'application/rtf' + IO.popen("/usr/bin/unrtf --html " + tempfile.path + "", "r") do |child| + html = child.read() + end else raise "No HTML conversion available for type " + self.content_type end @@ -341,30 +402,12 @@ class FOIAttachment body_without_tags = body.gsub(/\s+/,"").gsub(/\<[^\>]*\>/, "") contains_images = html.match(/<img/mi) ? true : false if !$?.success? || html.size == 0 || (body_without_tags.size == 0 && !contains_images) - return "<html><head></head><body><p>Sorry, the conversion to HTML failed. Please use the download link at the top right.</p></body></html>" + return "<html><head></head><body><p>Sorry, the conversion to HTML failed. Please use the download link at the top right.</p></body></html>", wrapper_id end - return html - end - - # Whether this type has a "View as HTML" - def has_body_as_html? - if self.content_type == 'application/vnd.ms-word' - return true - elsif self.content_type == 'application/pdf' - return true - end - return false + return html, wrapper_id end - # Name of type of attachment type - only valid for things that has_body_as_html? - def name_of_content_type - if self.content_type == 'application/vnd.ms-word' - return "Word document" - elsif self.content_type == 'application/pdf' - return "PDF file" - end - end end class IncomingMessage < ActiveRecord::Base @@ -419,20 +462,30 @@ class IncomingMessage < ActiveRecord::Base _count_parts_recursive(p) end else - if part.content_type == 'message/rfc822' - # An email attached as text - # e.g. http://www.whatdotheyknow.com/request/64/response/102 - begin + part_filename = TMail::Mail.get_part_file_name(part) + begin + if part.content_type == 'message/rfc822' + # An email attached as text + # e.g. http://www.whatdotheyknow.com/request/64/response/102 part.rfc822_attachment = TMail::Mail.parse(part.body) - rescue - # If attached mail doesn't parse, treat it as text part - part.rfc822_attachment = nil - @count_parts_count += 1 - part.url_part_number = @count_parts_count - else - _count_parts_recursive(part.rfc822_attachment) + elsif part.content_type == 'application/vnd.ms-outlook' || part_filename && filename_to_mimetype(part_filename) == 'application/vnd.ms-outlook' + # An email attached as an Outlook file + # e.g. http://www.whatdotheyknow.com/request/chinese_names_for_british_politi + msg = Mapi::Msg.open(StringIO.new(part.body)) + part.rfc822_attachment = TMail::Mail.parse(msg.to_mime.to_s) + elsif part.content_type == 'application/ms-tnef' + # A set of attachments in a TNEF file + part.rfc822_attachment = TNEF.as_tmail(part.body) end + rescue + # If attached mail doesn't parse, treat it as text part + part.rfc822_attachment = nil else + unless part.rfc822_attachment.nil? + _count_parts_recursive(part.rfc822_attachment) + end + end + if part.rfc822_attachment.nil? @count_parts_count += 1 part.url_part_number = @count_parts_count end @@ -486,7 +539,7 @@ class IncomingMessage < ActiveRecord::Base uncompressed_text = child.read() end # if we managed to uncompress the PDF... - if !uncompressed_text.nil? + if !uncompressed_text.nil? && !uncompressed_text.empty? # then censor stuff (making a copy so can compare again in a bit) censored_uncompressed_text = uncompressed_text.dup self._binary_mask_stuff_internal!(censored_uncompressed_text) @@ -499,7 +552,7 @@ class IncomingMessage < ActiveRecord::Base child.close_write() recompressed_text = child.read() end - if !recompressed_text.nil? + if !recompressed_text.nil? && !recompressed_text.empty? text[0..-1] = recompressed_text # [0..-1] makes it change the 'text' string in place end end @@ -707,15 +760,22 @@ class IncomingMessage < ActiveRecord::Base if curr_mail.sub_type == 'alternative' # Choose best part from alternatives best_part = nil + # Take the last text/plain one, or else the first one curr_mail.parts.each do |m| - # Take the first one, or the last text/plain one - # XXX - could do better! if not best_part best_part = m elsif m.content_type == 'text/plain' best_part = m end end + # Take an HTML one as even higher priority. (They tend + # to render better than text/plain, e.g. don't wrap links here: + # http://www.whatdotheyknow.com/request/amount_and_cost_of_freedom_of_in#incoming-72238 ) + curr_mail.parts.each do |m| + if m.content_type == 'text/html' + best_part = m + end + end leaves_found += _get_attachment_leaves_recursive(best_part, within_rfc822_attachment) else # Add all parts @@ -724,6 +784,11 @@ class IncomingMessage < ActiveRecord::Base end end else + # XXX Yuck. this section alters various content_type's. That puts + # it into conflict with ensure_parts_counted which it has to be + # called both before and after. It will fail with cases of + # attachments of attachments etc. + # Don't allow nil content_types if curr_mail.content_type.nil? curr_mail.content_type = 'application/octet-stream' @@ -746,9 +811,16 @@ class IncomingMessage < ActiveRecord::Base curr_mail.content_type = 'text/plain' end end + if curr_mail.content_type == 'application/vnd.ms-outlook' || curr_mail.content_type == 'application/ms-tnef' + ensure_parts_counted # fills in rfc822_attachment variable + if curr_mail.rfc822_attachment.nil? + # Attached mail didn't parse, so treat as binary + curr_mail.content_type = 'application/octet-stream' + end + end - # If the part is an attachment of email in text form - if curr_mail.content_type == 'message/rfc822' + # If the part is an attachment of email + if curr_mail.content_type == 'message/rfc822' || curr_mail.content_type == 'application/vnd.ms-outlook' || curr_mail.content_type == 'application/ms-tnef' ensure_parts_counted # fills in rfc822_attachment variable leaves_found += _get_attachment_leaves_recursive(curr_mail.rfc822_attachment, curr_mail.rfc822_attachment) else @@ -863,9 +935,13 @@ class IncomingMessage < ActiveRecord::Base def get_main_body_text_part leaves = get_attachment_leaves - # Find first part which is text/plain + # Find first part which is text/plain or text/html + # (We have to include HTML, as increasingly there are mail clients that + # include no text alternative for the main part, and we don't want to + # instead use the first text attachment + # e.g. http://www.whatdotheyknow.com/request/list_of_public_authorties) leaves.each do |p| - if p.content_type == 'text/plain' + if p.content_type == 'text/plain' or p.content_type == 'text/html' return p end end @@ -898,7 +974,7 @@ class IncomingMessage < ActiveRecord::Base # e.g. for https://secure.mysociety.org/admin/foi/request/show_raw_email/24550 main_part = get_main_body_text_part if main_part.nil? - return + return [] end text = main_part.body @@ -936,10 +1012,13 @@ class IncomingMessage < ActiveRecord::Base # Returns all attachments for use in display code # XXX is this called multiple times and should be cached? def get_attachments_for_display - ensure_parts_counted - main_part = get_main_body_text_part leaves = get_attachment_leaves + + # XXX we have to call ensure_parts_counted after get_attachment_leaves + # which is really messy. + ensure_parts_counted + attachments = [] for leaf in leaves if leaf != main_part @@ -965,7 +1044,12 @@ class IncomingMessage < ActiveRecord::Base headers = "" for header in [ 'Date', 'Subject', 'From', 'To', 'Cc' ] if leaf.within_rfc822_attachment.header.include?(header.downcase) - headers = headers + header + ": " + leaf.within_rfc822_attachment.header[header.downcase].to_s + "\n" + header_value = leaf.within_rfc822_attachment.header[header.downcase] + # Example message which has a blank Date header: + # http://www.whatdotheyknow.com/request/30747/response/80253/attach/html/17/Common%20Purpose%20Advisory%20Group%20Meeting%20Tuesday%202nd%20March.txt.html + if !header_value.blank? + headers = headers + header + ": " + header_value.to_s + "\n" + end end end # XXX call _convert_part_body_to_text here, but need to get charset somehow @@ -1099,11 +1183,14 @@ class IncomingMessage < ActiveRecord::Base File.unlink(tempfile.path + ".txt") end elsif content_type == 'application/rtf' + # catdoc on RTF prodcues less comments and extra bumf than --text option to unrtf IO.popen("/usr/bin/catdoc " + tempfile.path, "r") do |child| text += child.read() + "\n\n" end elsif content_type == 'text/html' - IO.popen("/usr/bin/lynx -display_charset=UTF-8 -force_html -dump " + tempfile.path, "r") do |child| + # lynx wordwraps links in its output, which then don't get formatted properly + # by WhatDoTheyKnow. We use elinks instead, which doesn't do that. + IO.popen("/usr/bin/elinks -dump-charset utf-8 -force-html -dump " + tempfile.path, "r") do |child| text += child.read() + "\n\n" end elsif content_type == 'application/vnd.ms-excel' @@ -1273,7 +1360,7 @@ class IncomingMessage < ActiveRecord::Base prefix = email prefix =~ /^(.*)@/ prefix = $1 - if !prefix.nil? && prefix.downcase.match(/^(postmaster|mailer-daemon|auto_reply|donotreply)$/) + if !prefix.nil? && prefix.downcase.match(/^(postmaster|mailer-daemon|auto_reply|donotreply|no-reply)$/) return false end diff --git a/app/models/info_request.rb b/app/models/info_request.rb index 7cee3fe1c..f6eec0601 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -56,7 +56,7 @@ class InfoRequest < ActiveRecord::Base 'waiting_clarification', 'gone_postal', 'not_held', - 'rejected', + 'rejected', # this is called 'refused' in UK FOI law and the user interface, but 'rejected' internally for historic reasons 'successful', 'partially_successful', 'internal_review', @@ -83,7 +83,7 @@ class InfoRequest < ActiveRecord::Base 'authority_only', # only people from authority domains 'nobody' ] - # what to do with rejected new responses + # what to do with refused new responses validates_inclusion_of :handle_rejected_responses, :in => [ 'bounce', # return them to sender 'holding_pen', # put them in the holding pen @@ -95,6 +95,9 @@ class InfoRequest < ActiveRecord::Base if !self.title.nil? && !MySociety::Validate.uses_mixed_capitals(self.title, 10) errors.add(:title, '^Please write the summary using a mixture of capital and lower case letters. This makes it easier for others to read.') end + if !self.title.nil? && title.size > 200 + errors.add(:title, '^Please keep the summary short, like in the subject of an email. You can use a phrase, rather than a full sentence.') + end end OLD_AGE_IN_DAYS = 21.days @@ -132,7 +135,7 @@ class InfoRequest < ActiveRecord::Base # we update index for every event. Also reindex if prominence changes. after_update :reindex_some_request_events def reindex_some_request_events - if self.changes.include?('url_title') || self.changes.include?('prominence') + if self.changes.include?('url_title') || self.changes.include?('prominence') || self.changes.include?('user_id') self.reindex_request_events end end @@ -199,6 +202,8 @@ public # Subject lines for emails about the request def email_subject_request + # XXX pull out this general_register_office specialisation + # into some sort of separate jurisdiction dependent file if self.public_body.url_name == 'general_register_office' # without GQ in the subject, you just get an auto response self.law_used_full + ' request GQ - ' + self.title @@ -206,11 +211,15 @@ public self.law_used_full + ' request - ' + self.title end end - def email_subject_followup - if self.public_body.url_name == 'general_register_office' - 'Re: ' + self.law_used_full + ' request GQ - ' + self.title + def email_subject_followup(incoming_message = nil) + if incoming_message.nil? || !incoming_message.valid_to_reply_to? + 'Re: ' + self.email_subject_request else - 'Re: ' + self.law_used_full + ' request - ' + self.title + if incoming_message.mail.subject.match(/^Re:/i) + incoming_message.mail.subject + else + 'Re: ' + incoming_message.mail.subject + end end end @@ -487,10 +496,13 @@ public # self.described_state, can take these two values: # waiting_classification # waiting_response_overdue + # waiting_response_very_overdue def calculate_status return 'waiting_classification' if self.awaiting_description return described_state unless self.described_state == "waiting_response" # Compare by date, so only overdue on next day, not if 1 second late + return 'waiting_response_very_overdue' if + Time.now.strftime("%Y-%m-%d") > self.date_very_overdue_after.strftime("%Y-%m-%d") return 'waiting_response_overdue' if Time.now.strftime("%Y-%m-%d") > self.date_response_required_by.strftime("%Y-%m-%d") return 'waiting_response' @@ -568,24 +580,35 @@ public end end if last_sent.nil? - raise "internal error, date_response_required_by gets nil for request " + self.id.to_s + " outgoing messages count " + self.outgoing_messages.size.to_s + " all events: " + self.info_request_events.to_yaml + raise "internal error, last_event_forming_initial_request gets nil for request " + self.id.to_s + " outgoing messages count " + self.outgoing_messages.size.to_s + " all events: " + self.info_request_events.to_yaml end return last_sent end + # The last time that the initial request was sent/resent + def date_initial_request_last_sent_at + last_sent = last_event_forming_initial_request + return last_sent.outgoing_message.last_sent_at + end # How do we cope with case where extra info was required from the requester # by the public body in order to fulfill the request, as per sections 1(3) # and 10(6b) ? For clarifications this is covered by # last_event_forming_initial_request. There may be more obscure # things, e.g. fees, not properly covered. def date_response_required_by - last_sent = last_event_forming_initial_request - return Holiday.due_date_from(last_sent.outgoing_message.last_sent_at, 20) + return Holiday.due_date_from(self.date_initial_request_last_sent_at, 20) end - - # Are we more than 20 working days overdue? - def working_days_20_overdue? - return Holiday.due_date_from(date_response_required_by.to_date, 20) <= Time.now.to_date + # This is a long stop - even with UK public interest test extensions, 40 + # days is a very long time. + def date_very_overdue_after + last_sent = last_event_forming_initial_request + if self.public_body.is_school? + # schools have 60 working days maximum (even over a long holiday) + return Holiday.due_date_from(self.date_initial_request_last_sent_at, 60) + else + # public interest test ICO guidance gives 40 working maximum + return Holiday.due_date_from(self.date_initial_request_last_sent_at, 40) + end end # Where the initial request is sent to @@ -715,11 +738,13 @@ public elsif status == 'waiting_response' "Awaiting response." elsif status == 'waiting_response_overdue' - "Response overdue." + "Delayed." + elsif status == 'waiting_response_very_overdue' + "Long overdue." elsif status == 'not_held' "Information not held." elsif status == 'rejected' - "Rejected." + "Refused." elsif status == 'partially_successful' "Partially successful." elsif status == 'successful' @@ -894,9 +919,9 @@ public # This is called from cron regularly. def InfoRequest.stop_new_responses_on_old_requests # 6 months since last change to request, only allow new incoming messages from authority domains - InfoRequest.update_all "allow_new_responses_from = 'authority_only' where updated_at < (now() - interval '6 months') and allow_new_responses_from = 'anybody'" + InfoRequest.update_all "allow_new_responses_from = 'authority_only' where updated_at < (now() - interval '6 months') and allow_new_responses_from = 'anybody' and url_title <> 'holding_pen'" # 1 year since last change requests, don't allow any new incoming messages - InfoRequest.update_all "allow_new_responses_from = 'nobody' where updated_at < (now() - interval '1 year') and allow_new_responses_from in ('anybody', 'authority_only')" + InfoRequest.update_all "allow_new_responses_from = 'nobody' where updated_at < (now() - interval '1 year') and allow_new_responses_from in ('anybody', 'authority_only') and url_title <> 'holding_pen'" end # Returns a random FOI request diff --git a/app/models/info_request_event.rb b/app/models/info_request_event.rb index 4e2a10a1f..1422431dc 100644 --- a/app/models/info_request_event.rb +++ b/app/models/info_request_event.rb @@ -282,7 +282,7 @@ class InfoRequestEvent < ActiveRecord::Base elsif status == 'not_held' return "Information not held" elsif status == 'rejected' - return "Rejection" + return "Refused" elsif status == 'partially_successful' return "Some information sent" elsif status == 'successful' diff --git a/app/models/outgoing_mailer.rb b/app/models/outgoing_mailer.rb index ae7e86f4e..1546d3fd0 100644 --- a/app/models/outgoing_mailer.rb +++ b/app/models/outgoing_mailer.rb @@ -73,7 +73,7 @@ class OutgoingMailer < ApplicationMailer if outgoing_message.what_doing == 'internal_review' return "Internal review of " + info_request.email_subject_request else - return info_request.email_subject_followup + return info_request.email_subject_followup(outgoing_message.incoming_message_followup) end end # Whether we have a valid email address for a followup diff --git a/app/models/outgoing_message.rb b/app/models/outgoing_message.rb index 36190ce2f..40abe0b0f 100644 --- a/app/models/outgoing_message.rb +++ b/app/models/outgoing_message.rb @@ -39,13 +39,23 @@ class OutgoingMessage < ActiveRecord::Base # contact address changed has_many :info_request_events + # 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 + # How the default letter starts and ends def get_salutation ret = "Dear " 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 - ret = ret + "Sir or Madam" + ret = ret + self.info_request.public_body.name end return ret + "," end @@ -56,6 +66,9 @@ class OutgoingMessage < ActiveRecord::Base return "Yours faithfully," end end + def get_internal_review_insert_here_note + return "GIVE DETAILS ABOUT YOUR COMPLAINT HERE" + end def get_default_letter if self.what_doing == 'internal_review' "Please pass this on to the person who conducts Freedom of Information reviews." + @@ -64,7 +77,7 @@ class OutgoingMessage < ActiveRecord::Base self.info_request.public_body.name + "'s handling of my FOI request " + "'" + self.info_request.title + "'." + - "\n\n" + + "\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:\n" + "http://" + MySociety::Config.get("DOMAIN", '127.0.0.1:3000') + "/request/" + self.info_request.url_title else @@ -119,9 +132,13 @@ class OutgoingMessage < ActiveRecord::Base # Check have edited letter def validate - if self.body.empty? || self.body =~ /\A#{get_salutation}\s+#{get_signoff}/ + if self.body.empty? || self.body =~ /\A#{get_salutation}\s+#{get_signoff}/ || self.body =~ /#{get_internal_review_insert_here_note}/ if self.message_type == 'followup' - errors.add(:body, "^Please enter your follow up message") + if self.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") + end elsif errors.add(:body, "^Please enter your letter requesting information") else diff --git a/app/models/post_redirect.rb b/app/models/post_redirect.rb index edd151730..a17b24d16 100644 --- a/app/models/post_redirect.rb +++ b/app/models/post_redirect.rb @@ -61,11 +61,7 @@ class PostRedirect < ActiveRecord::Base # Makes a random token, suitable for using in URLs e.g confirmation messages. def self.generate_random_token - bits = 12 * 8 - # Make range from value to double value, so number of digits in base 36 - # encoding is quite long always. - rand_num = rand(max = 2**(bits+1)) + 2**bits - rand_num.to_s(base=36) + MySociety::Util.generate_token end # Make the token @@ -90,7 +86,7 @@ class PostRedirect < ActiveRecord::Base return post_redirects[0] end - # Called from cron job delete-old-post-redirects + # Called from cron job delete-old-things def self.delete_old_post_redirects PostRedirect.delete_all "updated_at < (now() - interval '6 months')" end diff --git a/app/models/request_mailer.rb b/app/models/request_mailer.rb index 1bebb5181..7e2041b4f 100644 --- a/app/models/request_mailer.rb +++ b/app/models/request_mailer.rb @@ -85,7 +85,25 @@ class RequestMailer < ApplicationMailer headers 'Return-Path' => blackhole_email, 'Reply-To' => @from, # not much we can do if the user's email is broken 'Auto-Submitted' => 'auto-generated' # http://tools.ietf.org/html/rfc3834 @recipients = user.name_and_email - @subject = "You're overdue a response to your FOI request - " + info_request.title + @subject = "Delayed response to your FOI request - " + info_request.title + @body = { :info_request => info_request, :url => url } + end + + # Tell the requester that the public body is very late in replying + def very_overdue_alert(info_request, user) + respond_url = respond_to_last_url(info_request) + "#followup" + + post_redirect = PostRedirect.new( + :uri => respond_url, + :user_id => user.id) + post_redirect.save! + url = confirm_url(:email_token => post_redirect.email_token) + + @from = contact_from_name_and_email + headers 'Return-Path' => blackhole_email, 'Reply-To' => @from, # not much we can do if the user's email is broken + 'Auto-Submitted' => 'auto-generated' # http://tools.ietf.org/html/rfc3834 + @recipients = user.name_and_email + @subject = "You're long overdue a response to your FOI request - " + info_request.title @body = { :info_request => info_request, :url => url } end @@ -209,21 +227,35 @@ class RequestMailer < ApplicationMailer for info_request in info_requests alert_event_id = info_request.last_event_forming_initial_request.id # Only overdue requests - if info_request.calculate_status == 'waiting_response_overdue' + if ['waiting_response_overdue', 'waiting_response_very_overdue'].include?(info_request.calculate_status) + if info_request.calculate_status == 'waiting_response_overdue' + alert_type = 'overdue_1' + elsif info_request.calculate_status == 'waiting_response_very_overdue' + alert_type = 'very_overdue_1' + else + raise "unknown request status" + end + # For now, just to the user who created the request - sent_already = UserInfoRequestSentAlert.find(:first, :conditions => [ "alert_type = 'overdue_1' and user_id = ? and info_request_id = ? and info_request_event_id = ?", info_request.user_id, info_request.id, alert_event_id]) + sent_already = UserInfoRequestSentAlert.find(:first, :conditions => [ "alert_type = ? and user_id = ? and info_request_id = ? and info_request_event_id = ?", alert_type, info_request.user_id, info_request.id, alert_event_id]) if sent_already.nil? # Alert not yet sent for this user, so send it #STDERR.puts "sending overdue alert to info_request " + info_request.id.to_s + " user " + info_request.user_id.to_s + " event " + alert_event_id store_sent = UserInfoRequestSentAlert.new store_sent.info_request = info_request store_sent.user = info_request.user - store_sent.alert_type = 'overdue_1' + store_sent.alert_type = alert_type store_sent.info_request_event_id = alert_event_id # Only send the alert if the user can act on it by making a followup # (otherwise they are banned, and there is no point sending it) if info_request.user.can_make_followup? - RequestMailer.deliver_overdue_alert(info_request, info_request.user) + if info_request.calculate_status == 'waiting_response_overdue' + RequestMailer.deliver_overdue_alert(info_request, info_request.user) + elsif info_request.calculate_status == 'waiting_response_very_overdue' + RequestMailer.deliver_very_overdue_alert(info_request, info_request.user) + else + raise "unknown request status" + end end store_sent.save! #STDERR.puts "sent " + info_request.user.email diff --git a/app/models/track_mailer.rb b/app/models/track_mailer.rb index 9d8e8348d..6c9d9949b 100644 --- a/app/models/track_mailer.rb +++ b/app/models/track_mailer.rb @@ -33,16 +33,23 @@ class TrackMailer < ApplicationMailer now = Time.now() users = User.find(:all, :conditions => [ "last_daily_track_email < ?", now - 1.day ]) for user in users - #STDERR.puts "user " + user.url_name + #STDERR.puts Time.now.to_s + " user " + user.url_name email_about_things = [] track_things = TrackThing.find(:all, :conditions => [ "tracking_user_id = ? and track_medium = ?", user.id, 'email_daily' ]) for track_thing in track_things - #STDERR.puts " track " + track_thing.track_query + #STDERR.puts Time.now.to_s + " track " + track_thing.track_query # What have we alerted on already? + # + # We only use track_things_sent_emails records which are less than 14 days old. + # In the search query loop below, we also only use items described in last 7 days. + # An item described that recently definitely can't appear in track_things_sent_emails + # earlier, so this is safe (with a week long margin of error). If the alerts break + # for a whole week, then they will miss some items. Tough. done_info_request_events = {} - for t in track_thing.track_things_sent_emails + tt_sent = track_thing.track_things_sent_emails.find(:all, :conditions => ['created_at > ?', now - 14.days]) + for t in tt_sent if not t.info_request_event_id.nil? done_info_request_events[t.info_request_event_id] = 1 end @@ -51,19 +58,20 @@ class TrackMailer < ApplicationMailer # Query for things in this track. We use described_at for the # ordering, so we catch anything new (before described), or # anything whose new status has been described. - xapian_object = InfoRequest.full_search([InfoRequestEvent], track_thing.track_query, 'described_at', true, nil, 200, 1) - + xapian_object = InfoRequest.full_search([InfoRequestEvent], track_thing.track_query, 'described_at', true, nil, 100, 1) # Go through looking for unalerted things alert_results = [] for result in xapian_object.results - if result[:model].class.to_s == "InfoRequestEvent" - if not done_info_request_events.include?(result[:model].id) and track_thing.created_at < result[:model].described_at - # OK alert this one - alert_results.push(result) - end - else + if result[:model].class.to_s != "InfoRequestEvent" raise "need to add other types to TrackMailer.alert_tracks (unalerted)" end + + next if track_thing.created_at >= result[:model].described_at # made before the track was created + next if result[:model].described_at < now - 7.days # older than 1 week (see 14 days / 7 days in comment above) + next if done_info_request_events.include?(result[:model].id) # definitely already done + + # OK alert this one + alert_results.push(result) end # If there were more alerts for this track, then store them if alert_results.size > 0 diff --git a/app/models/track_thing.rb b/app/models/track_thing.rb index 3392a4210..c2036118f 100644 --- a/app/models/track_thing.rb +++ b/app/models/track_thing.rb @@ -215,7 +215,7 @@ class TrackThing < ActiveRecord::Base # RSS sorting - XXX 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 rejected requests in which case want to sort by + # 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. diff --git a/app/models/track_things_sent_email.rb b/app/models/track_things_sent_email.rb index 35229371c..b39dad932 100644 --- a/app/models/track_things_sent_email.rb +++ b/app/models/track_things_sent_email.rb @@ -25,6 +25,12 @@ class TrackThingsSentEmail < ActiveRecord::Base belongs_to :user belongs_to :public_body belongs_to :track_thing + + # Called from cron job delete-old-things + def self.delete_old_track_things_sent_email + TrackThingsSentEmail.delete_all "updated_at < (now() - interval '1 month')" + end + end diff --git a/app/models/user.rb b/app/models/user.rb index bcad6229f..eb8089cf1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -134,8 +134,7 @@ class User < ActiveRecord::Base user = self.find_user_by_email(params[:email]) if user # There is user with email, check password - expected_password = encrypted_password(params[:password], user.salt) - if user.hashed_password != expected_password + if !user.has_this_password?(params[:password]) user.errors.add_to_base(auth_fail_message) end else @@ -184,7 +183,12 @@ class User < ActiveRecord::Base self.hashed_password = User.encrypted_password(self.password, self.salt) end - # For use in to/from in email messages + def has_this_password?(password) + expected_password = User.encrypted_password(password, self.salt) + return self.hashed_password == expected_password + end + +# For use in to/from in email messages def name_and_email return TMail::Address.address_from_name_and_email(self.name, self.email).to_s end diff --git a/app/models/user_info_request_sent_alert.rb b/app/models/user_info_request_sent_alert.rb index 309466792..dde6fd339 100644 --- a/app/models/user_info_request_sent_alert.rb +++ b/app/models/user_info_request_sent_alert.rb @@ -25,6 +25,7 @@ class UserInfoRequestSentAlert < ActiveRecord::Base validates_inclusion_of :alert_type, :in => [ '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 diff --git a/app/models/user_mailer.rb b/app/models/user_mailer.rb index 06a115c5b..70ca42675 100644 --- a/app/models/user_mailer.rb +++ b/app/models/user_mailer.rb @@ -27,5 +27,26 @@ class UserMailer < ApplicationMailer @body[:url] = url end + def changeemail_confirm(user, new_email, url) + @from = contact_from_name_and_email + headers 'Return-Path' => blackhole_email, 'Reply-To' => @from # we don't care about bounces when people are fiddling with their account + @recipients = new_email + @subject = "Confirm your new email address on WhatDoTheyKnow.com" + @body[:name] = user.name + @body[:url] = url + @body[:old_email] = user.email + @body[:new_email] = new_email + end + + def changeemail_already_used(old_email, new_email) + @from = contact_from_name_and_email + headers 'Return-Path' => blackhole_email, 'Reply-To' => @from # we don't care about bounces when people are fiddling with their account + @recipients = new_email + @subject = "Unable to change email address on WhatDoTheyKnow.com" + @body[:old_email] = old_email + @body[:new_email] = new_email + end + + end diff --git a/app/views/admin_public_body/import_csv.rhtml b/app/views/admin_public_body/import_csv.rhtml index 3ca1f4d59..75981a501 100644 --- a/app/views/admin_public_body/import_csv.rhtml +++ b/app/views/admin_public_body/import_csv.rhtml @@ -12,15 +12,7 @@ <% form_tag 'import_csv', :multipart => true do %> <p> - <label for="tag">Tag to add entries to (maybe you want: - <% for category, description in PublicBodyCategories::CATEGORIES_BY_TAG %> - <% if category != "other" %> - <strong><%= category %></strong>=<%= description %>; - <% end %> - <% end %> - ) - </label> - <br/> + <label for="tag">Tag to add entries to / alter entries for:</label> <%= text_field_tag 'tag', params[:tag] %> </p> @@ -30,10 +22,23 @@ <%= file_field_tag :csv_file, :size => 60 %> </p> - <p><strong>Note:</strong> This will report errors in the CSV file. If there are no errors, it will make updates. - Any changes since last import will be overwritten - e.g. email addresses changed back. Changes and additions - are reported on a successful import for you to check. + <p><strong>Note:</strong> Choose <strong>dry run</strong> to test, without + actually altering the database. Choose <strong>upload</strong> to actually + make the changes. In either case, you will be shown any errors, or details + of the changes. When uploading, any changes since last import will be + overwritten - e.g. email addresses changed back. + </p> - <p><%= submit_tag 'Upload' %></p> + <p><%= submit_tag 'Dry run' %> <%= submit_tag 'Upload' %></p> <% end %> +<hr> + +<p>Standard tags: + <% for category, description in PublicBodyCategories::CATEGORIES_BY_TAG %> + <% if category != "other" %> + <strong><%= category %></strong>=<%= description %>; + <% end %> + <% end %> + </p> + diff --git a/app/views/admin_request/show.rhtml b/app/views/admin_request/show.rhtml index b9115dd59..a6256478a 100644 --- a/app/views/admin_request/show.rhtml +++ b/app/views/admin_request/show.rhtml @@ -6,7 +6,15 @@ <p> <% for column in InfoRequest.content_columns %> <strong><%= column.human_name %>:</strong> <%=h @info_request.send(column.name) %> - <br/> + <% if column.name == 'described_state' %> + <strong>Calculated status:</strong> <%= @info_request.calculate_status %> + <br/><strong>Initial request last sent at:</strong> <%=@info_request.date_initial_request_last_sent_at.to_date %> + <strong>Date response required by:</strong> <%= @info_request.date_response_required_by %> + <strong>Very overdue after:</strong> <%= @info_request.date_very_overdue_after %> + <% end %> + <% if ![ 'allow_new_responses_from' ].include?(column.name) %> + <br/> + <% end %> <% end %> <strong>Created by:</strong> <%= user_both_links(@info_request.user) %> <br> <strong>Public authority:</strong> <%= public_body_both_links(@info_request.public_body) %> <br> diff --git a/app/views/admin_track/_some_tracks.rhtml b/app/views/admin_track/_some_tracks.rhtml index 8aaae7048..72ee5fd95 100644 --- a/app/views/admin_track/_some_tracks.rhtml +++ b/app/views/admin_track/_some_tracks.rhtml @@ -5,7 +5,7 @@ <% for column in TrackThing.content_columns %> <th><%= column.human_name %></th> <% end %> - <th>Items sent by email</th> + <th>Items sent by email (in last month)</th> <th>Actions</th> </tr> diff --git a/app/views/admin_track/list.rhtml b/app/views/admin_track/list.rhtml index 1d076edf5..58c87ddba 100644 --- a/app/views/admin_track/list.rhtml +++ b/app/views/admin_track/list.rhtml @@ -16,7 +16,7 @@ <% for column in TrackThing.content_columns %> <th><%= column.human_name %></th> <% end %> - <th>Items sent by email</th> + <th>Items sent by email (in last month)</th> </tr> <% for track_thing in @admin_tracks %> diff --git a/app/views/comment/new.rhtml b/app/views/comment/new.rhtml index c0e7e3a22..7d7dfee6f 100644 --- a/app/views/comment/new.rhtml +++ b/app/views/comment/new.rhtml @@ -49,7 +49,7 @@ Annotations are so anyone, including you, can help the requester with their requ <li> Ideas on what <strong>other documents to request</strong> which the authority may hold. </li> <% end %> <% if [ 'rejected' ].include?(@info_request.described_state) %> - <li> Advise on whether the <strong>rejection is legal</strong>, and how to complain about it if not. </li> + <li> Advise on whether the <strong>refusal is legal</strong>, and how to complain about it if not. </li> <% end %> <% if [ 'internal_review' ].include?(@info_request.described_state) %> diff --git a/app/views/general/frontpage.rhtml b/app/views/general/frontpage.rhtml index 00b758e9f..b3fb3f12c 100644 --- a/app/views/general/frontpage.rhtml +++ b/app/views/general/frontpage.rhtml @@ -17,7 +17,7 @@ <br> <br> - OR, <strong>search</strong> for information others have requested. + OR, <strong>search</strong> for information others have requested using WhatDoTheyKnow.com </p> <% end %> </div> diff --git a/app/views/general/search.rhtml b/app/views/general/search.rhtml index 1fc93b099..9993e733a 100644 --- a/app/views/general/search.rhtml +++ b/app/views/general/search.rhtml @@ -127,7 +127,7 @@ <table class="status_table"> <tr><td><strong><%=search_link('status:waiting_response')%></strong></td><td> Waiting for the public authority to reply </td></tr> <tr><td><strong><%=search_link('status:not_held')%></strong></td><td> The public authority does not have the information requested </td></tr> - <tr><td><strong><%=search_link('status:rejected')%></strong></td><td> The request was rejected by the public authority </td></tr> + <tr><td><strong><%=search_link('status:rejected')%></strong></td><td> The request was refused by the public authority </td></tr> <tr><td><strong><%=search_link('status:partially_successful')%></strong></td><td> Some of the information requested has been received </td></tr> <tr><td><strong><%=search_link('status:successful')%></strong></td><td> All of the information requested has been received </td></tr> <tr><td><strong><%=search_link('status:waiting_clarification')%></strong></td><td> The public authority would like part of the request explained </td></tr> diff --git a/app/views/help/about.rhtml b/app/views/help/about.rhtml index a67c65009..5ff22b23f 100644 --- a/app/views/help/about.rhtml +++ b/app/views/help/about.rhtml @@ -58,6 +58,28 @@ If you like what we're doing, then you can <h1 id="making_requests">Making requests <a href="#making_requests">#</a> </h1> <dl> +<dt id="which_authority">I'm not sure which authority to make my request to, how can I find out? <a href="#which_authority">#</a> </dt> + +<dd> +<p>It can be hard to untangle government's complicated structured, and work out +who knows the information that you want. Here are a few tips: +<ul> +<li>Browse or search WhatDoTheyKnow looking for similar requests to yours.</li> +<li>When you've found an authority you think might have the information, use +the "home page" link on the right hand side of their page to check what they do +on their website.</li> +<li>Contact the authority by phone or email to ask if they hold the kind of +information you're after.</li> +<li>Don't worry excessively about getting the right authority. If you get it +wrong, they ought to advise you who to make the request to instead. +</li> +<li>If you've got a thorny case, please <a href="/help/contact">contact us</a> for help.</li> +</ul> + +</dd> + + + <dt id="missing_body">You're missing the public authority that I want to request from! <a href="#missing_body">#</a> </dt> <dd> @@ -70,6 +92,29 @@ to hear from you too. </dd> +<dt id="authorities">Why do you include some authorities that aren't formally subject to FOI?<a href="#authorities">#</a> </dt> + +<dd> +<p>WhatDoTheyKnow lets you make requests for information to a range of +organisations:</p> + +<ul> + <li> Those formally subject to the FOI Act</li> + <li> Those formally subject to the Environmental Regulations (a less well + defined group)</li> + <li> Those which voluntarily comply with the FOI Act</li> + <li> Those which aren't subject to the Act but we think should be, on grounds + such as them having significant public responsibilities. + </li> +</ul> + +<p>In the last case, we're using the site to lobby for expansion of the +scope of the FOI Act. Even if an organisation is not legally obliged to respond +to an FOI request, they can still do so voluntarily. +</p> + +</dd> + <dt id="focused">Why must I keep my request focused?<a href="#focused">#</a> </dt> <dd> @@ -111,18 +156,20 @@ annotations after submitting the request). <p>Making an FOI request is nearly always free.</p> -<p>Sometimes an authority will reject your request, saying that the cost +<p>Authorities often include unnecessary, scary, boilerplate in +acknowledgement messages saying they "may" charge a fee. Ignore such notices. +They hardly ever will actually charge a fee. If they do, they can only charge you if +you have specifically agreed in advance to pay. <a + href="http://www.ico.gov.uk/upload/documents/library/freedom_of_information/practical_application/chargingafee.pdf">More + details</a> from the Information Commissioner. +</p> + +<p>Sometimes an authority will refuse your request, saying that the cost of handling it exceeds £600 (for central government) or £450 (for all other public authorities). At this point you can refine your request. e.g. it would be much cheaper for an authority to tell you the amount spent on marshmallows in the past year than in the past ten years. -.</p> - -<p>There are other rare cases where an authority may say that they want to charge you, such as for postage -or photocopying. Either way, don't worry, the authority cannot make a charge unless you have -specifically agreed in advance to pay it. <a -href="http://www.ico.gov.uk/upload/documents/library/freedom_of_information/practical_application/chargingafee.pdf -">More details</a> from the Information Commissioner. </p> +</p> </dd> @@ -131,21 +178,18 @@ href="http://www.ico.gov.uk/upload/documents/library/freedom_of_information/prac <dt id="quickly_response">How quickly will I get a response? <a href="#quickly_response">#</a> </dt> <dd> -<p>By law public authorities must respond "promptly", and in most cases not -later than 20 working days after receiving your request. That date -is shown on the page for your request. </p> - -<p>You will be emailed if this date goes by without a response, so you can send -the public authority another note to remind them if they are breaking the -law.</p> +<p>By law, public authorities must respond <strong>promptly</strong> to +requests. +</p> -<p>There are some cases where the hard deadline is allowed to go beyond the 20 -day period, such as if you had to clarify your request, or if they are a -school. They will normally say if they are invoking such a reason. -See '<a href="#days">You've calculated our deadline wrongly!</a>' for -details about what is allowed. </p> +<p>Even if they are not prompt, in nearly all cases they must respond within +20 working days. If you had to clarify your request, or they are a school, +or one or two other cases, then they may have more time +(<a href="#days">full details</a>). -<p>Remember though, they should anyway have responded promptly.</p> +<p>WhatDoTheyKnow will email you if you don't get a timely response. You can +then send the public authority a message to remind them, and tell them if they +are breaking the law.</p> </dd> @@ -174,6 +218,21 @@ details about what is allowed. </p> then read our page '<a href="/help/unhappy">Unhappy about the response you got?</a>'. </dd> +<dt id="reuse">It says I can't re-use the information I got!<a href="#reuse">#</a> </dt> +<dd> +<p>Authorities often add legal boilerplate about the +"<a href="http://www.opsi.gov.uk/si/si2005/20051515">Re-Use of Public Sector +Information Regulations 2005</a>", which at first glance implies you may not +be able do anything with the information. +</p> + +<p>You can, of course, write articles about the information or summarise it, or +quote parts of it. We also think you should feel free to republish the +information in full, just as we do, even though in theory you might not be +allowed to do so. See <a href="#copyright">our policy on copyright</a>.</p> + +</dd> + <dt id="ico_help">Can you tell me more of the nitty gritty about the process of making requests? <a href="#ico_help">#</a> </dt> <dd> @@ -209,28 +268,35 @@ immediately so we can remove it.</p> <dt id="private_requests">I'd like to keep my request secret! (At least until I publish my story) <a href="#private_requests">#</a> </dt> -<dd>WhatDoTheyKnow is currently only designed for public requests. All +<dd><p>WhatDoTheyKnow is currently only designed for public requests. All responses that we receive are automatically published on the website for anyone -to read. You should contact the public authority directly if you would like to +to read. </p> +<p>You should contact the public authority directly if you would like to make a request in private. If you're interested in buying a system which helps you manage FOI requests in secret, then <a href="/help/contact">contact us</a>. +</p> </dd> <dt id="eir">Why can I only request information about the environment from some authorities? <a href="#eir">#</a> </dt> -<dd>Some public authorities, such as <a href="http://www.whatdotheyknow.com/body/south_east_water">South East Water</a>, +<dd> +<p>Some public authorities, such as <a href="http://www.whatdotheyknow.com/body/south_east_water">South East Water</a>, don't come under the Freedom of Information Act, but do come under another law called -the Environmental Information Regulations (EIR). It's a very similar law, so you make a request -to them using WhatDoTheyKnow in just the same way as an FOI request. The only difference -is that on the page where you write you request, it reminds you that you can only -request "environmental information" and tells you what that means. It is quite broad. -</dd> +the Environmental Information Regulations (EIR). +</p> -<dt id="eir_2">So can I request information using EIR from other authorities? <a href="#eir_2">#</a> </dt> +<p>It's a very similar law, so you make a request +to them using WhatDoTheyKnow in just the same way as an FOI request. The only +difference is that on the page where your write you request, it reminds you +that you can only request "environmental information" and tells you what that +means. It is quite broad. +</p> -<dd>Yes, just make a Freedom of Information (FOI) request as normal. The +<p>You can, of course, request environmental information from other +authorities. Just make a Freedom of Information (FOI) request as normal. The authority has a duty to work out if the Environmental Information Regulations (EIR) is the more appropriate legislation to reply under. +</p> </dd> <dt id="multiple">Can I make the same to request to lots of authorities, e.g. all councils? <a href="#multiple">#</a> </dt> @@ -280,7 +346,8 @@ you ask us to. <dd> <p>We publish your request on the Internet so that anybody can read it and -make use of the information that you have found. +make use of the information that you have found. We do not normally delete +requests (<a href="#delete_requests">more details</a>). </p> <p> Your name is tangled up with your request, so has to be published as well. @@ -317,7 +384,7 @@ Information Commissioner later about the handling of your request. <ul> <li>Use a different form of your name. The guidance says that "Mr Arthur Thomas Roberts" can make a valid request as "Arthur Roberts", -"A. T. Roberts", or "Mr Roberts", but not as "Arthur" or "A.T.R.". +"A. T. Roberts", or "Mr Roberts", but <strong>not</strong> as "Arthur" or "A.T.R.". </li> <li>Women may use their maiden name.</li> <li>In most cases, you may use any name by which you are "widely known and/or @@ -326,7 +393,7 @@ is regularly used". a company, or the trading name of a sole trader. <li>Ask someone else to make the request on your behalf. <li>You may, if you are really stuck, ask us to make the request on -your behalf. Please <a href="/help/about">contact us</a> with +your behalf. Please <a href="/help/contact">contact us</a> with a good reason why you cannot make the request yourself and cannot ask a friend to. We don't have the resources to do this for everyone. </ul> @@ -335,6 +402,28 @@ ask a friend to. We don't have the resources to do this for everyone. </dd> +<dt id="delete_requests">Can you delete my requests, or alter my name? <a href="#delete_requests">#</a> </dt> + +<dd> + +<p>WhatDoTheyKnow is a permanent, public archive of Freedom of +Information requests. Even though you may not find the response to +a request useful any more, it may be of interest to others. For this +reason, we will not delete requests. +</p> + +<p>Under exceptional circumstances we may remove or change your name +on the website, following similar policy as for the names of +public servants. Similarly, we may also remove other personal information. See +'<a href="#takedown">Can you take down personal information about me?</a>'. +</p> + +<p>If you're worried about this before you make your request, +see the section on <a href="#real_name">pseudonyms</a>.</p> + +</dd> + + <dt id="full_address">They've asked for my postal address! <a href="#full_address">#</a> </dt> <dd> @@ -470,7 +559,7 @@ needing any email, using the "respond to request" link at the bottom of each request page. </dd> -<dt id="days">You've calculated our deadline wrongly!<a href="#days">#</a> </dt> +<dt id="days">How do you calculate the deadline shown on request pages?<a href="#days">#</a> </dt> <dd> <p>The Freedom of Information Act says:</p> @@ -493,7 +582,7 @@ to have more of that complexity visible.</p> </dd> -<dt id="days2">But really, you calculated it wrong!<a href="#days2">#</a> </dt> +<dt id="days2">But really, how do you calculate the deadline?<a href="#days2">#</a> </dt> <dd> @@ -503,45 +592,78 @@ it is best if they show the hard work they are doing by explaining what is taking the extra time to do. </p> -<p>That said, WhatDoTheyKnow does attempt to show the maximum legal deadline -for response to each request. Here is the complex detail of how we calculate -it.</p> +<p>That said, WhatDoTheyKnow does show the maximum legal deadline +for response on each request. Here's how we calculate it.</p> <ul> <li>If the day we deliver the request by email is a working day, we count that -as "day zero", even if it was delivered late in the evening. Days end at midnight. -We then count the next working day as "day one", and so on up to 20 days.</li> +as "day zero", even if it was delivered late in the evening. Days end at +midnight. We then count the next working day as "day one", and so on up to +<strong>20 working days</strong>.</li> <li>If the day the request email was delivered was a non-working day, we count the next working day as "day one". Delivery is delivery, even if it happened on the weekend. Some authorities <a href="http://www.whatdotheyknow.com/request/policy_regarding_body_scans#incoming-1100">disagree with this</a>, our lawyer disagrees with them. </li> -<li>In theory, authorities can claim a time extension for applying a public -interest test. We don't think this should be a special reason for delay. There -are lots of other good reasons the authority might want more time, such as if -somebody is on holiday and they can't find the information. When -there's going to be any delay at all, we prefer it if authorities simply -apologise and explain what they are doing that is taking the extra time, rather -than resorting to legal minutiae. -</li> +<li>Requesters are encouraged to mark when they have <strong>clarified</strong> +their request so the clock resets, but sometimes they get this wrong. If you +see a problem with a particular request, let us know and we'll fix it.</li> +</ul> -<li>Since June 2009, schools have "20 working days disregarding any working -day which is not a school day, or 60 working days, whichever is first". Basically, -cut them some slack if it is holiday time. -</li> +<p>The date thus calculated is shown on requests with the text "By law, +Liverpool City Council should normally have responded by...". There is only +one case which is not normal, see the next question about +<a href="#public_interest_test">public interest test time extensions</a>. +</p> -<li>Requesters are encouraged to mark when they have clarified their request so -the clock resets, but sometimes they get this wrong. If you see a problem with -a particular request, let us know and we'll fix it.</li> +<p>Schools are also a special case, which WhatDoTheyKnow displays differently. +</p> +<ul> +<li>Since June 2009, <strong>schools</strong> have "20 working days +disregarding any working day which is not a school day, or 60 working days, +whichever is first" (<a href="http://www.opsi.gov.uk/si/si2009/draft/ukdsi_9780111477632_en_1">FOI (Time for Compliance with Request) Regulations 2009</a>). WhatDoTheyKnow indicates on requests to schools that the 20 day deadline is only +during term time, and shows them as definitely overdue after 60 working days +</li> </ul> -<p>If you're getting really nerdy about this, read the <a href="http://www.ico.gov.uk/upload/documents/library/freedom_of_information/detailed_specialist_guides/timeforcompliance.pdf">detailed ICO guidance</a>. Meanwhile, -remember that the law says authorities must respond <strong>promptly</strong>. +<p>If you're getting really nerdy about all this, read the <a href="http://www.ico.gov.uk/upload/documents/library/freedom_of_information/detailed_specialist_guides/timeforcompliance.pdf">detailed ICO guidance</a>. +Meanwhile, remember that the law says authorities must respond +<strong>promptly</strong>. That's really what matters.</p> </dd> +<dt id="public_interest_test">How do you reflect time extensions for public interest tests?<a href="#public_interest_test">#</a> </dt> + +<dd> + +<p>The Freedom of Information Act lets authorities claim an indefinite time +extension when applying a <strong>public interest test</strong>. Information +Commissioner guidance says that it should only be used in "exceptionally +complex" cases +(<a href="http://www.ico.gov.uk/upload/documents/library/freedom_of_information/detailed_specialist_guides/foi_good_practice_guidance_4.pdf">FOI Good Practice Guidance No. 4</a>). +WhatDoTheyKnow doesn't specifically handle this case, which is why we use the +phrase "should normally have responded by" when the 20 working day time is +exceeded. +</p> + +<p>The same guidance says that, even in exceptionally complex cases, no +Freedom of Information request should take more than <strong>40 working days</strong> +to answer. WhatDoTheyKnow displays requests which are overdue by that much +with stronger wording to indicate they are definitely late. +</p> + +<p>The Freedom of Information (Scotland) Act does not allow such a public +interest extension. WhatDoTheyKnow would like to see the law changed to either +remove the extension from the UK Act, or to reintroduce an absolute time limit +of 40 working days even with the extension (the House of Lords <a +href="http://www.publicwhip.org.uk/division.php?date=2000-10-17&number=1&house=lords">voted +to remove</a> provision for such a time limit during the initial passage +of the UK Act through Parliament). +</p> +</dd> + <dt id="large_file">How can I send a large file, which won't go by email?<a href="#large_file">#</a> </dt> <dd>Instead of email, you can respond to a request directly from your web @@ -592,9 +714,14 @@ that authorities resend these with the personal information removed.</p> <dt id="mobiles">Do you publish email addresses or mobile phone numbers? <a href="#mobiles">#</a> </dt> -<dd>We automatically remove some emails and mobile numbers from responses to requests. -Please <a href="/help/contact">contact us</a> if we've missed one. -For technical reasons we don't remove them all from attachments, such as PDFs. +<dd><p>To prevent spam, we automatically remove most emails and some mobile numbers from +responses to requests. Please <a href="/help/contact">contact us</a> if we've +missed one. +For technical reasons we don't always remove them from attachments, such as certain PDFs.</p> +<p>If you need to know what an address was that we've removed, please <a + href="/help/contact">get in touch with us</a>. Occasionally, an email address +forms an important part of a response and we will post it up in an obscured +form in an annotation. </dd> <dt id="copyright"><a name="commercial"></a>What is your policy on copyright of documents?<a href="#copyright">#</a> </dt> @@ -650,8 +777,14 @@ requests, and for good public relations, we'd advise you not to do that. </li> <li> The amazing team of volunteers who run the site, answer your support - emails, maintain the database of public authorities and so much more. - Thanks to Tony Bowden, John Cross, Adam McGreggor, Alex Skene, Richard Taylor. + emails, maintain the database of public authorities and + <a href="http://www.mysociety.org/2009/10/13/behind-whatdotheyknow/">so much more</a>. + Thanks to John Cross, Ben Harris, Adam McGreggor, Alex Skene, + Richard Taylor. +</li> +<li> + Volunteers who have provided patches to the code - thanks Peter Collingbourne + and Tony Bowden. </li> <li> Everyone who has helped look up FOI email addresses. diff --git a/app/views/help/unhappy.rhtml b/app/views/help/unhappy.rhtml index cd302a81a..432c00f2e 100644 --- a/app/views/help/unhappy.rhtml +++ b/app/views/help/unhappy.rhtml @@ -14,7 +14,7 @@ to your request '<%=request_link(@info_request) %>'? <ul> <li>You didn't get a reply within 20 working days</li> <li>You did not get all of the information that you requested <strong>or</strong></li> -<li>Your request was rejected, but without a reason valid under the law</li> +<li>Your request was refused, but without a reason valid under the law</li> </ul> <p>... you can</p> @@ -86,7 +86,7 @@ get the information by <strong>other means...</strong></p> <ul> <li>Make a <strong>new FOI request</strong> for summary information, or for -documentation relating indirectly to matters in your rejected request. +documentation relating indirectly to matters in your refused request. <a href="/help/contact">Ask us for ideas</a> if you're stuck.</li> <li>If any <strong>other public authorities</strong> or publicly owned companies are involved, then make FOI requests to them.</li> diff --git a/app/views/layouts/default.rhtml b/app/views/layouts/default.rhtml index aad13dc97..0db75380f 100644 --- a/app/views/layouts/default.rhtml +++ b/app/views/layouts/default.rhtml @@ -3,14 +3,17 @@ <head> <!-- <%= javascript_include_tag :defaults %> --> <script type="text/javascript" src="/jslib/spell/spellChecker.js"></script> + <title> <% if @title %> <%=@title%> - WhatDoTheyKnow <% else %> - WhatDoTheyKnow - file and browse Freedom of Information (FOI) requests + WhatDoTheyKnow - make and browse Freedom of Information (FOI) requests <% end %> </title> + <link rel="shortcut icon" href="/favicon.ico"> + <%= stylesheet_link_tag 'main', :title => "Main", :rel => "stylesheet" %> <%= stylesheet_link_tag 'yucky-green', :title => "Yucky Green", :rel => "alternate stylesheet" %> <!--[if LT IE 7]> @@ -159,17 +162,14 @@ <script type="text/javascript"> var pkBaseURL = (("https:" == document.location.protocol) ? "https://piwik.mysociety.org/" : "http://piwik.mysociety.org/"); document.write(unescape("%3Cscript src='" + pkBaseURL + "piwik.js' type='text/javascript'%3E%3C/script%3E")); -</script> -<script type="text/javascript"> -<!-- -piwik_action_name = ''; -piwik_idsite = 4; -piwik_url = pkBaseURL + "piwik.php"; -piwik_log(piwik_action_name, piwik_idsite, piwik_url); -//--> -</script> -<noscript><p><img src="http://piwik.mysociety.org/piwik.php?idsite=4" style="border:0" alt=""></p></noscript> -<!-- /Piwik --> +</script><script type="text/javascript"> +try { +var piwikTracker = Piwik.getTracker(pkBaseURL + "piwik.php", 4); +piwikTracker.trackPageView(); +piwikTracker.enableLinkTracking(); +} catch( err ) {} +</script><noscript><p><img src="http://piwik.mysociety.org/piwik.php?idsite=4" style="border:0" alt=""/></p></noscript> +<!-- End Piwik Tag --> <% end %> </body> diff --git a/app/views/public_body/list.rhtml b/app/views/public_body/list.rhtml index 9b03e65d0..b1be9272a 100644 --- a/app/views/public_body/list.rhtml +++ b/app/views/public_body/list.rhtml @@ -1,32 +1,37 @@ <div id="body_sidebar"> -<h1>Show only...</h1> - -<h2>Alphabet</h2> -<ul><li> - <%= render :partial => 'alphabet' %> -</li></ul> - -<% first_row = true %> -<% for row in PublicBodyCategories::CATEGORIES_WITH_HEADINGS %> - <% if row.instance_of?(Array) %> - <li> - <%= link_to_unless (@tag == row[0]), row[1], list_public_bodies_url(:tag => row[0]) %> - </li> - <% else %> - <% if not first_row %> - </ul> + <h1>Show only...</h1> + + <h2>Alphabet</h2> + <ul><li> + <%= render :partial => 'alphabet' %> + </li></ul> + + <% first_row = true %> + <% for row in PublicBodyCategories::CATEGORIES_WITH_HEADINGS %> + <% if row.instance_of?(Array) %> + <li> + <%= link_to_unless (@tag == row[0]), row[1], list_public_bodies_url(:tag => row[0]) %> + </li> <% else %> - <% first_row = false %> + <% if not first_row %> + </ul> + <% else %> + <% first_row = false %> + <% end %> + <h2><%=h row%></h2> + <ul> <% end %> - <h2><%=h row%></h2> - <ul> <% end %> -<% end %> -</ul> -<p> -<a href="/help/about#missing_body">Are we missing a public authority?</a> -</p> + </ul> + + <p> + <a href="/help/about#missing_body">Are we missing a public authority?</a> + </p> + <p> + <%= link_to "List of all authorities (CSV)", all_public_bodies_csv_url() %> + </p> + </div> <% @title = "Public authorities - " + @description %> diff --git a/app/views/public_body/show.rhtml b/app/views/public_body/show.rhtml index f28581cdd..56d4cd75c 100644 --- a/app/views/public_body/show.rhtml +++ b/app/views/public_body/show.rhtml @@ -5,15 +5,19 @@ <%= 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", @public_body.calculated_home_page %><br> + <%= 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 %> <% if !@public_body.charity_number.empty? %> - <%= link_to "Charity overview", 'http://www.charity-commission.gov.uk/SHOWCHARITY/RegisterOfCharities/CharityFramework.aspx?RegisteredCharityNumber=' + @public_body.charity_number %><br> + <% if @public_body.charity_number.match(/^SC/) %> + <%= link_to "Charity overview", "http://www.oscr.org.uk/CharityIndexDetails.aspx?id=" + @public_body.charity_number %><br> + <% else %> + <%= link_to "Charity overview", "http://www.charity-commission.gov.uk/SHOWCHARITY/RegisterOfCharities/CharityFramework.aspx?RegisteredCharityNumber=" + @public_body.charity_number %><br> + <% end %> <% end %> - <%= link_to "View FOI email address", view_public_body_email_url(@public_body.url_name) %> + <%= link_to "View FOI email address", view_public_body_email_url(@public_body.url_name) %><br> </div> <h1><%=h(@public_body.name)%></h1> @@ -63,18 +67,18 @@ <% if !@xapian_requests.nil? %> <% if @xapian_requests.results.empty? %> <% if @public_body.eir_only? %> - <h2>Environmental Information Regulations requests made</h2> + <h2>Environmental Information Regulations requests made using this site</h2> <p>Nobody has made any Environmental Information Regulations requests to <%=h(@public_body.name)%> using this site yet.</p> <% else %> - <h2>Freedom of Information requests made</h2> + <h2>Freedom of Information requests made using this site</h2> <p>Nobody has made any Freedom of Information requests to <%=h(@public_body.name)%> using this site yet.</p> <% end %> <% else %> <h2> <% if @public_body.eir_only? %> - <%=pluralize(@public_body.info_requests.size, "Environmental Information Regulations request") %> made + <%=pluralize(@public_body.info_requests.size, "Environmental Information Regulations request") %> made using this site <% else %> - <%=pluralize(@public_body.info_requests.size, "Freedom of Information request") %> made + <%=pluralize(@public_body.info_requests.size, "Freedom of Information request") %> made using this site <% end %> <%= @page_desc %> </h2> diff --git a/app/views/request/_after_actions.rhtml b/app/views/request/_after_actions.rhtml index d51c6bfb3..9bef04ce4 100644 --- a/app/views/request/_after_actions.rhtml +++ b/app/views/request/_after_actions.rhtml @@ -24,7 +24,7 @@ <% if @last_response.nil? %> <%= link_to "Send follow up to " + OutgoingMailer.name_for_followup(@info_request, @last_response), show_response_no_followup_url(:id => @info_request.id, :incoming_message_id => nil) + "#followup" %> <% else %> - <% cache(:controller => "request", :action => "show_response", :id => @info_request.id, :incoming_message_id => @last_response.id, :only_path => true, :template => "_after_actions", :section => "reply_to_link") do %> + <% foi_cache(:controller => "request", :action => "show_response", :id => @info_request.id, :incoming_message_id => @last_response.id, :only_path => true, :template => "_after_actions", :section => "reply_to_link") do %> <%= link_to "Reply to " + OutgoingMailer.name_for_followup(@info_request, @last_response), show_response_url(:id => @info_request.id, :incoming_message_id => @last_response.id) + "#followup" %> <% end %> <% end %> diff --git a/app/views/request/_correspondence.rhtml b/app/views/request/_correspondence.rhtml index 90beb4050..0756b0797 100644 --- a/app/views/request/_correspondence.rhtml +++ b/app/views/request/_correspondence.rhtml @@ -6,7 +6,7 @@ end if not incoming_message.nil? %> <div class="correspondence" id="incoming-<%=incoming_message.id.to_s%>"> - <% cache(:controller => "request", :action => "show_response", :id => @info_request.id, :incoming_message_id => incoming_message.id, :only_path => true, :template => "_correspondence", :section => "incoming_message_bubble", :collapse => @collapse_quotes ? nil : 'no' ) do %> + <% foi_cache(:controller => "request", :action => "show_response", :id => @info_request.id, :incoming_message_id => incoming_message.id, :only_path => true, :template => "_correspondence", :section => "incoming_message_bubble", :collapse => @collapse_quotes ? nil : 'no' ) do %> <h2> <% if !incoming_message.safe_mail_from.nil? && incoming_message.safe_mail_from.strip != @info_request.public_body.name.strip %> <%=h incoming_message.safe_mail_from %><br> diff --git a/app/views/request/_describe_state.rhtml b/app/views/request/_describe_state.rhtml index 4496fddb6..a91231ae8 100644 --- a/app/views/request/_describe_state.rhtml +++ b/app/views/request/_describe_state.rhtml @@ -58,7 +58,7 @@ </div> <div> <%= radio_button "incoming_message", "described_state", "rejected", :id => 'rejected' + id_suffix %> - <label for="rejected<%=id_suffix%>">My request has been <strong>rejected</strong></label> + <label for="rejected<%=id_suffix%>">My request has been <strong>refused</strong></label> </div> <hr> <!------------------------------------------------> diff --git a/app/views/request/_followup.rhtml b/app/views/request/_followup.rhtml index 2429c63b2..b375befd1 100644 --- a/app/views/request/_followup.rhtml +++ b/app/views/request/_followup.rhtml @@ -19,46 +19,42 @@ <a href="/help/contact">contact us</a> if you are <%= user_link(@info_request.user) %> and need to send a follow up.</p> <% else %> - -<!-- <p>Please do <strong>not</strong> make new requests here. - If you are asking for information which was not in your original request, then - <%= link_to "file a new request", new_request_to_body_url(:public_body_id => @info_request.public_body.id.to_s) %> - instead. - </p> - --> - <% if @internal_review %> <p> If you are dissatisfied by the response you got from the public authority, you have the right to complain (<a href="http://foiwiki.com/foiwiki/index.php/Internal_reviews">details</a>). </p> - <% end %> - <% if @info_request.calculate_status == 'waiting_response_overdue' %> - <p> - <% if @info_request.working_days_20_overdue? %> - This request is <strong>long overdue a response</strong>. - <% else %> - This request is <strong>overdue a response</strong>. - <% end %> - You can say that, by law, the authority should have answered - <strong>promptly</strong>. If they have not given you a legal - reason why they need extra time - (<%= link_to "more details", about_url + "#quickly_response" %>), then - you can say they are breaking the law to have not replied by - <strong><%= simple_date(@info_request.date_response_required_by) %></strong>. + <p>Please <strong>only</strong> write messages directly relating to your + request '<%= request_link(@info_request) %>'. If you would like to ask for information + that was not in your original request, then + <%= link_to "file a new request", new_request_to_body_url(:public_body_id => @info_request.public_body.id.to_s) %>. + </p> + + <% status = @info_request.calculate_status %> + <% if status == 'waiting_response_overdue' %> + <p>The response to your request has been <strong>delayed</strong>. You can say that, + by law, the authority should normally have responded + <strong>promptly</strong> and <% if @info_request.public_body.is_school? %> - This is a school, so legally they get lots of extra slack if it is - holiday time. + in term time <% end %> - </p> + by <strong><%= simple_date(@info_request.date_response_required_by) %></strong> + (<%= link_to "details", about_url + "#quickly_response" %>). + </p> + <% elsif status == 'waiting_response_very_overdue' %> + <p> + The response to your request is <strong>long overdue</strong>. You can say that, by + law, under all circumstances, the authority should have responded + by now (<%= link_to "details", about_url + "#quickly_response" %>). + </p> <% end %> <% form_for(:outgoing_message, @outgoing_message, :html => { :id => 'followup_form' }, :url => incoming_message.nil? ? show_response_no_followup_url(:id => @info_request.id) : show_response_url(:id => @info_request.id, :incoming_message_id => incoming_message.id)) do |o| %> <p> - <%= o.text_area :body, :rows => 10, :cols => 55 %> + <%= o.text_area :body, :rows => 15, :cols => 55 %> <br><script type="text/javascript">document.write('<input name="doSpell" type="button" value="Check spelling" onClick="openSpellChecker(document.getElementById(\'followup_form\').body);"/> (optional)')</script> </p> @@ -93,12 +89,7 @@ <% if @internal_review %> <p>Edit and add <strong>more details</strong> to the message above, - explaining why you would like a review. - <ul> - <li>Say that you are dissatisfied by their response</li> - <li>Set out your reasons why</li> - <li>Ask them to review their response</li> - </ul> + explaining why you are dissatisfied with their response. </p> <% end %> diff --git a/app/views/request/_hidden_correspondence.rhtml b/app/views/request/_hidden_correspondence.rhtml new file mode 100644 index 000000000..2c168c3fb --- /dev/null +++ b/app/views/request/_hidden_correspondence.rhtml @@ -0,0 +1,37 @@ +<% if info_request_event.prominence == 'requester_only' %> + <% + if !info_request_event.nil? && info_request_event.event_type == 'response' + incoming_message = info_request_event.incoming_message + end + if not incoming_message.nil? + %> + <div class="correspondence" id="incoming-<%=incoming_message.id.to_s%>"> + <p>This response has been hidden. See annotations to find out why. + If you are the requester, then you may + <%= link_to "sign in", signin_url(:r => request.request_uri) %> + to view the response. + </p> + </div> + <% elsif [ 'sent', 'followup_sent', 'resent', 'followup_resent' ].include?(info_request_event.event_type) %> + <div class="correspondence" id="outgoing-<%=outgoing_message.id.to_s%>"> + <p>This outgoing message has been hidden. See annotations to + find out why. If you are the requester, then you may <%= link_to + "sign in", signin_url(:r => request.request_uri) %> to view the + response. + </p> + </div> + <% elsif info_request_event.event_type == 'comment' %> + <div class="comment_in_request" id="comment-<%=comment.id.to_s%>"> + <p>This comment has been hidden. See annotations to + find out why. If you are the requester, then you may <%= link_to + "sign in", signin_url(:r => request.request_uri) %> to view the + response. + </p> + </div> + <% end %> + +<% elsif info_request_event.prominence == 'hidden' %> + <% # show nothing when hidden %> +<% else %> + <% raise "unexpected prominence on request event" %> +<% end %> diff --git a/app/views/request/_other_describe_state.rhtml b/app/views/request/_other_describe_state.rhtml index 866e90142..66f64c27a 100644 --- a/app/views/request/_other_describe_state.rhtml +++ b/app/views/request/_other_describe_state.rhtml @@ -54,7 +54,7 @@ </div> <div> <%= radio_button "incoming_message", "described_state", "rejected", :id => 'rejected' + id_suffix %> - <label for="rejected<%=id_suffix%>">The request has been <strong>rejected</strong></label> + <label for="rejected<%=id_suffix%>">The request has been <strong>refused</strong></label> </div> <hr> <!------------------------------------------------> diff --git a/app/views/request/_sidebar.rhtml b/app/views/request/_sidebar.rhtml index 0cb9207d8..1509bf494 100644 --- a/app/views/request/_sidebar.rhtml +++ b/app/views/request/_sidebar.rhtml @@ -31,6 +31,7 @@ <!-- Important terms: <%= @xapian_similar.important_terms.join(" ") %> --> <% end %> + <p><%= link_to "Event history details", request_details_url(@info_request) %></p> <p><a href="/help/about#commercial">Are you the owner of any commercial copyright on this page?</a></p> -</div>
\ No newline at end of file +</div> diff --git a/app/views/request/details.rhtml b/app/views/request/details.rhtml new file mode 100644 index 000000000..db7e652f6 --- /dev/null +++ b/app/views/request/details.rhtml @@ -0,0 +1,54 @@ +<% @title = "Details of request '" + h(@info_request.title) + "'" %> +<h1><%="Details of request '" + request_link(@info_request) + "'" %></h1> + +<h2>Event history</h2> + +<p>This table shows the technical details of the internal events that happened +to this request on WhatDoTheyKnow. This could be used to generate information about +the speed with which authorities respond to requests, the number of requests +which require a postal response and much more. +</p> + +<p><strong>Caveat emptor!</strong> To use this data in an honourable way, you will need +a good internal knowledge of user behaviour on WhatDoTheyKnow. How, +why and by whom requests are categorised is not straightforward, and there will +be user error and ambiguity. You will also need to understand FOI law, and the +way authorities use it. Plus you'll need to be an elite statistician. Please +<a href="/help/contact">contact us</a> with questions. +</p> + +<% columns = ['id', 'event_type', 'created_at', 'described_state', 'calculated_state', 'last_described_at' ] %> + +<table> + <tr> + <% for column in @columns%> + <th><%= column %></th> + <% end %> + <th>link</th> + </tr> + +<% for info_request_event in @info_request.info_request_events.find(:all, :order => "created_at, id") %> + <tr class="<%= cycle('odd', 'even') %>"> + <% for column in @columns %> + <td> + <%=h info_request_event.send(column) %> + </td> + <% end %> + <td> + <% if info_request_event.outgoing_message %> + <%= link_to "outgoing", outgoing_message_url(info_request_event.outgoing_message) %> + <% end %> + <% if info_request_event.incoming_message %> + <%= link_to "incoming", incoming_message_url(info_request_event.incoming_message) %> + <% end %> + </td> + </tr> +<% end %> +</table> + +<p>Here <strong>described</strong> means when a user selected a status for the request, and +the most recent event had its status updated to that value. <strong>calculated</strong> is then inferred by +WhatDoTheyKnow for intermediate events, which weren't given an explicit +description by a user. See the <a href="/search">search tips</a> for description of the states.</p> + + diff --git a/app/views/request/new.rhtml b/app/views/request/new.rhtml index aa9bf254d..b48966e2f 100644 --- a/app/views/request/new.rhtml +++ b/app/views/request/new.rhtml @@ -124,7 +124,7 @@ <p class="form_note"> Everything that you enter on this page, including <strong>your name</strong>, will be <strong>displayed publicly</strong> on - this website (<a href="/help/about/#public_request">why?</a>). + this website forever (<a href="/help/about/#public_request">why?</a>). If you are thinking of using a pseudonym, please <a href="/help/about/#real_name">read this first</a>. </p> @@ -132,7 +132,7 @@ <p class="form_note"> Everything that you enter on this page will be <strong>displayed publicly</strong> on - this website (<a href="/help/about/#public_request">why?</a>). + this website forever (<a href="/help/about/#public_request">why?</a>). </p> <% end %> diff --git a/app/views/request/show.rhtml b/app/views/request/show.rhtml index 5b89a6d12..97dc32512 100644 --- a/app/views/request/show.rhtml +++ b/app/views/request/show.rhtml @@ -56,34 +56,34 @@ <% end %> <% elsif @status == 'waiting_response' %> Currently <strong>waiting for a response</strong> from <%= public_body_link(@info_request.public_body) %>, - they <%= link_to "must respond", about_url + "#quickly_response" %> - promptly but no later than <strong><%= simple_date(@info_request.date_response_required_by) %></strong>. - <% elsif @status == 'waiting_response_overdue' %> - <% if @info_request.working_days_20_overdue? %> - This request is <strong>long overdue a response</strong>. - By law, <%= public_body_link(@info_request.public_body) %> - should normally have answered by - <strong><%= simple_date(@info_request.date_response_required_by) %></strong> - (<%= link_to "more details", about_url + "#quickly_response" %>). - You can <strong>complain</strong> by - <%= link_to "requesting an internal review", show_response_no_followup_url(:id => @info_request.id, :incoming_message_id => nil) + "?internal_review=1#followup" %>. - <% else %> - This request is <strong>overdue a response</strong>. - By law, <%= public_body_link(@info_request.public_body) %> - should normally have answered by - <strong><%= simple_date(@info_request.date_response_required_by) %></strong>. - If they need extra time they should have told you - why (<%= link_to "more details", about_url + "#quickly_response" %>). - <% end %> + they must respond promptly and <% if @info_request.public_body.is_school? %> - This is a school, so legally they get lots of extra slack if it is - holiday time. + in term time + <% else %> + normally <% end %> - + no later than <strong><%= simple_date(@info_request.date_response_required_by) %></strong> + (<%= link_to "details", about_url + "#quickly_response" %>). + <% elsif @status == 'waiting_response_overdue' %> + Response to this request is <strong>delayed</strong>. + By law, <%= public_body_link(@info_request.public_body) %> should + normally have responded <strong>promptly</strong> and + <% if @info_request.public_body.is_school? %> + in term time + <% end %> + by <strong><%= simple_date(@info_request.date_response_required_by) %></strong> + (<%= link_to "details", about_url + "#quickly_response" %>). + <% elsif @status == 'waiting_response_very_overdue' %> + Response to this request is <strong>long overdue</strong>. + By law, under all circumstances, <%= public_body_link(@info_request.public_body) %> + should have responded by now + (<%= link_to "details", about_url + "#quickly_response" %>). + You can <strong>complain</strong> by + <%= link_to "requesting an internal review", show_response_no_followup_url(:id => @info_request.id, :incoming_message_id => nil) + "?internal_review=1#followup" %>. <% elsif @status == 'not_held' %> <%= public_body_link(@info_request.public_body) %> <strong>did not have</strong> the information requested. <% elsif @status == 'rejected' %> - The request was <strong>rejected</strong> by <%= public_body_link(@info_request.public_body) %>. + The request was <strong>refused</strong> by <%= public_body_link(@info_request.public_body) %>. <% elsif @status == 'successful' %> The request was <strong>successful</strong>. <% elsif @status == 'partially_successful' %> diff --git a/app/views/request/show_response.rhtml b/app/views/request/show_response.rhtml index 1d841c3a8..ed32a1b67 100644 --- a/app/views/request/show_response.rhtml +++ b/app/views/request/show_response.rhtml @@ -10,7 +10,7 @@ <% if @gone_postal %> <div class="gone_postal_help"> - <h1>What exactly is happening?</h1> + <h1>Which of these is happening?</h1> <dl> diff --git a/app/views/request_mailer/overdue_alert.rhtml b/app/views/request_mailer/overdue_alert.rhtml index ab2faf212..29a1a1d68 100644 --- a/app/views/request_mailer/overdue_alert.rhtml +++ b/app/views/request_mailer/overdue_alert.rhtml @@ -1,10 +1,10 @@ -<%= @info_request.public_body.name %> are late. +<%= @info_request.public_body.name %> have delayed. They have not replied to your <%=@info_request.law_used_short%> request '<%= @info_request.title %>' -promptly, as required by law. +promptly, as normally required by law<% if @info_request.public_body.is_school? %> during term time<% end %>. Click on the link below to send a message to <%= @info_request.public_body.name -%> reminding them to reply to your request. <% if @info_request.public_body.is_school? %> This is a school, so legally they get lots of extra slack if it is holiday time. <% end %> +%> reminding them to reply to your request. <%=@url%> diff --git a/app/views/request_mailer/very_overdue_alert.rhtml b/app/views/request_mailer/very_overdue_alert.rhtml new file mode 100644 index 000000000..2393d29e5 --- /dev/null +++ b/app/views/request_mailer/very_overdue_alert.rhtml @@ -0,0 +1,14 @@ +<%= @info_request.public_body.name %> are long overdue. + +They have not replied to your <%=@info_request.law_used_short%> request '<%= @info_request.title %>', +as required by law<% if @info_request.public_body.is_school? %> even during holidays<% end %>. + +Click on the link below to send a message to <%= @info_request.public_body.name +%> telling them to reply to your request. You might like to ask for an internal +review, asking them to find out why response to the request has been so slow. + +<%=@url%> + +-- the WhatDoTheyKnow team + + diff --git a/app/views/user/_signin.rhtml b/app/views/user/_signin.rhtml index 812bf9b4b..52c2a9e71 100644 --- a/app/views/user/_signin.rhtml +++ b/app/views/user/_signin.rhtml @@ -18,7 +18,7 @@ </p> <p class="form_note"> - <%= link_to "Forgotten your password?", signchange_url + "?pretoken=" + h(params[:token]) %> + <%= link_to "Forgotten your password?", signchangepassword_url + "?pretoken=" + h(params[:token]) %> </p> <p class="form_checkbox"> diff --git a/app/views/user/show.rhtml b/app/views/user/show.rhtml index 59a9cfcd4..4d2020cdc 100644 --- a/app/views/user/show.rhtml +++ b/app/views/user/show.rhtml @@ -50,7 +50,8 @@ <%= link_to "Send message to " + h(@display_user.name), contact_user_url(:id => @display_user.id) %> <% if @is_you %> (just to see how it works) - <br><%= link_to "Change your password", signchange_url() %> + <br><%= link_to "Change your password", signchangepassword_url() %> | + <br><%= link_to "Change your email", signchangeemail_url() %> | <br><%= link_to "Set profile photo", profile_photo_url() %> <% end %> </p> diff --git a/app/views/user/signchange_send_confirm.rhtml b/app/views/user/signchange_send_confirm.rhtml deleted file mode 100644 index e1462760c..000000000 --- a/app/views/user/signchange_send_confirm.rhtml +++ /dev/null @@ -1,30 +0,0 @@ -<% @title = "Change password" %> - -<div id="change_password"> - -<% form_tag({:action => "signchange"}, {:id => "signchange_form"}) do %> - <%= foi_error_messages_for :signchange %> - - <div class="form_note"> - <h1>Change your password</h1> - </div> - - <p> - <label class="form_label" for="signchange_email">Your e-mail:</label> - <%= text_field 'signchange', 'email', { :size => 20 } %> - </p> - - <p class="form_note"> - <strong>Note:</strong> - We will send you an email. Follow the instructions in it to change - your password. - </p> - - <div class="form_button"> - <%= hidden_field_tag 'submitted_signchange_send_confirm', 1 %> - <%= hidden_field_tag 'pretoken', params[:pretoken] %> - <%= submit_tag "Submit" %> - </div> -<% end %> - -</div> diff --git a/app/views/user/signchangeemail.rhtml b/app/views/user/signchangeemail.rhtml new file mode 100644 index 000000000..b98dc383c --- /dev/null +++ b/app/views/user/signchangeemail.rhtml @@ -0,0 +1,41 @@ +<% @title = "Change your email address used on WhatDoTheyKnow.com" %> + +<% raise "internal error" if not @user %> + +<div id="change_email"> + +<% form_tag({:action => "signchangeemail"}, {:id => "signchangeemail_form"}) do %> + <%= foi_error_messages_for :signchangeemail %> + + <div class="form_note"> + <h1>Change your email address used on WhatDoTheyKnow.com</h1> + </div> + + <p> + <label class="form_label" for="signchangeemail_old_email">Old e-mail:</label> + <%= text_field 'signchangeemail', 'old_email', { :size => 20 } %> + </p> + + <p> + <label class="form_label" for="signchangeemail_new_email">New e-mail:</label> + <%= text_field 'signchangeemail', 'new_email', { :size => 20 } %> + </p> + + <p> + <label class="form_label" for="signchangeemail_password">Your password:</label> + <%= password_field 'signchangeemail', 'password', { :size => 15 } %> + </p> + + <p class="form_note"> + <strong>Note:</strong> + We will send an email to your new email address. Follow the + instructions in it to confirm changing your email. + </p> + + <div class="form_button"> + <%= hidden_field_tag 'submitted_signchangeemail_do', 1 %> + <%= submit_tag "Change email on WhatDoTheyKnow.com" %> + </div> +<% end %> + +</div> diff --git a/app/views/user/signchangeemail_confirm.rhtml b/app/views/user/signchangeemail_confirm.rhtml new file mode 100644 index 000000000..96acbf424 --- /dev/null +++ b/app/views/user/signchangeemail_confirm.rhtml @@ -0,0 +1,14 @@ +<% @title = h("Now check your email!") %> + +<h1 class="confirmation_heading">Now check your email!</h1> + +<p class="confirmation_message"> +We've sent an email to your new email address. You'll need to click the link in +it before your email address will be changed. +</p> + +<p class="confirmation_message"> +<small>If you use web-based email or have "junk mail" filters, also check your +bulk/spam mail folders. Sometimes, our messages are marked that way.</small> +</p> + diff --git a/app/views/user/signchange.rhtml b/app/views/user/signchangepassword.rhtml index 032d80945..4191344cb 100644 --- a/app/views/user/signchange.rhtml +++ b/app/views/user/signchangepassword.rhtml @@ -1,14 +1,14 @@ -<% @title = "Change password" %> +<% @title = "Change your password on WhatDoTheyKnow.com" %> <% raise "internal error" if not @user %> <div id="change_password"> -<% form_tag({:action => "signchange"}, {:id => "signchange_form"}) do %> +<% form_tag({:action => "signchangepassword"}, {:id => "signchangepassword_form"}) do %> <%= foi_error_messages_for :user %> <div class="form_note"> - <h1>Change your password</h1> + <h1>Change your password on WhatDoTheyKnow.com</h1> </div> <p> @@ -22,9 +22,9 @@ </p> <div class="form_button"> - <%= hidden_field_tag 'submitted_signchange_password', 1 %> + <%= hidden_field_tag 'submitted_signchangepassword_do', 1 %> <%= hidden_field_tag 'pretoken', params[:pretoken] %> - <%= submit_tag "Change password" %> + <%= submit_tag "Change password on WhatDoTheyKnow.com" %> </div> <% end %> diff --git a/app/views/user/signchange_confirm.rhtml b/app/views/user/signchangepassword_confirm.rhtml index baad6729b..baad6729b 100644 --- a/app/views/user/signchange_confirm.rhtml +++ b/app/views/user/signchangepassword_confirm.rhtml diff --git a/app/views/user/signchangepassword_send_confirm.rhtml b/app/views/user/signchangepassword_send_confirm.rhtml new file mode 100644 index 000000000..8b2e4fa91 --- /dev/null +++ b/app/views/user/signchangepassword_send_confirm.rhtml @@ -0,0 +1,30 @@ +<% @title = "Change your password on WhatDoTheyKnow.com" %> + +<div id="change_password"> + +<% form_tag({:action => "signchangepassword"}, {:id => "signchangepassword_form"}) do %> + <%= foi_error_messages_for :signchangepassword %> + + <div class="form_note"> + <h1>Change your password on WhatDoTheyKnow.com</h1> + </div> + + <p> + <label class="form_label" for="signchangepassword_email">Your e-mail:</label> + <%= text_field 'signchangepassword', 'email', { :size => 20 } %> + </p> + + <p class="form_note"> + <strong>Note:</strong> + We will send you an email. Follow the instructions in it to change + your password. + </p> + + <div class="form_button"> + <%= hidden_field_tag 'submitted_signchangepassword_send_confirm', 1 %> + <%= hidden_field_tag 'pretoken', params[:pretoken] %> + <%= submit_tag "Submit" %> + </div> +<% end %> + +</div> diff --git a/app/views/user_mailer/changeemail_already_used.rhtml b/app/views/user_mailer/changeemail_already_used.rhtml new file mode 100644 index 000000000..0f60ad798 --- /dev/null +++ b/app/views/user_mailer/changeemail_already_used.rhtml @@ -0,0 +1,9 @@ +Someone, perhaps you, just tried to change their email address on +WhatDoTheyKnow.com from <%=@old_email%> to <%=@new_email%>. + +This was not possible because there is already an account using +the email address <%=@new_email%>. + +The accounts have been left as they previously were. + +-- the WhatDoTheyKnow team diff --git a/app/views/user_mailer/changeemail_confirm.rhtml b/app/views/user_mailer/changeemail_confirm.rhtml new file mode 100644 index 000000000..9aa288fb0 --- /dev/null +++ b/app/views/user_mailer/changeemail_confirm.rhtml @@ -0,0 +1,12 @@ +<%= @name %>, + +Please click on the link below to confirm that you want to +change the email address that you use for WhatDoTheyKnow +from <%=@old_email%> to <%=@new_email%> + +<%=@url%> + +We will not reveal your email addresses to anybody unless you +or the law tell us to. + +-- the WhatDoTheyKnow team |