diff options
-rw-r--r-- | app/controllers/request_controller.rb | 67 | ||||
-rw-r--r-- | app/models/incoming_message.rb | 45 | ||||
-rw-r--r-- | app/views/layouts/default.rhtml | 3 | ||||
-rw-r--r-- | app/views/request/_after_actions.rhtml | 6 | ||||
-rw-r--r-- | app/views/request/simple_correspondence.rhtml | 45 | ||||
-rw-r--r-- | config/environment.rb | 1 | ||||
-rw-r--r-- | config/general.yml-example | 8 | ||||
-rw-r--r-- | config/routes.rb | 2 | ||||
-rw-r--r-- | doc/CHANGES.md | 10 | ||||
-rw-r--r-- | doc/INSTALL.md | 11 | ||||
-rw-r--r-- | lib/alaveteli_external_command.rb | 33 | ||||
-rw-r--r-- | public/stylesheets/print.css | 4 | ||||
-rw-r--r-- | public/stylesheets/theme.css | 5 | ||||
-rw-r--r-- | spec/controllers/request_controller_spec.rb | 23 | ||||
-rw-r--r-- | spec/integration/search_request_spec.rb | 3 |
15 files changed, 219 insertions, 47 deletions
diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 12b5247b5..b615cc834 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -7,6 +7,8 @@ # $Id: request_controller.rb,v 1.192 2009-10-19 19:26:40 francis Exp $ require 'alaveteli_file_types' +require 'zip/zip' +require 'open-uri' class RequestController < ApplicationController before_filter :check_read_only, :only => [ :new, :show_response, :describe_state, :upload_response ] @@ -67,7 +69,6 @@ class RequestController < ApplicationController @status = @info_request.calculate_status @collapse_quotes = params[:unfold] ? false : true @update_status = params[:update_status] ? true : false - @is_owning_user = @info_request.is_owning_user?(authenticated_user) @old_unclassified = @info_request.is_old_unclassified? && !authenticated_user.nil? if @update_status @@ -101,7 +102,7 @@ class RequestController < ApplicationController # For send followup link at bottom @last_response = @info_request.get_last_response - + @is_owning_user = @info_request.is_owning_user?(authenticated_user) respond_to do |format| format.html { @has_json = true; render :template => 'request/show'} format.json { render :json => @info_request.json_for_api(true) } @@ -754,5 +755,67 @@ class RequestController < ApplicationController render :partial => "request/search_ahead.rhtml" end + + def download_entire_request + @locale = self.locale_from_params() + PublicBody.with_locale(@locale) do + info_request = InfoRequest.find_by_url_title(params[:url_title]) + if info_request.nil? + raise ActiveRecord::RecordNotFound.new("Request not found") + end + if authenticated?( + :web => _("To download the zip file"), + :email => _("Then you can download a zip file of {{info_request_title}}.",:info_request_title=>info_request.title), + :email_subject => _("Log in to download a zip file of {{info_request_title}}",:info_request_title=>info_request.title) + ) + updated = Digest::SHA1.hexdigest(info_request.get_last_event.created_at.to_s + info_request.updated_at.to_s) + @url_path = "/download/#{updated[0..1]}/#{updated}/#{params[:url_title]}.zip" + file_path = File.join(File.dirname(__FILE__), '../../cache/zips', @url_path) + if !File.exists?(file_path) + FileUtils.mkdir_p(File.dirname(file_path)) + Zip::ZipFile.open(file_path, Zip::ZipFile::CREATE) { |zipfile| + convert_command = MySociety::Config.get("HTML_TO_PDF_COMMAND") + done = false + if File.exists?(convert_command) + domain = MySociety::Config.get("DOMAIN") + url = "http://#{domain}#{request_url(info_request)}?print_stylesheet=1" + tempfile = Tempfile.new('foihtml2pdf') + output = AlaveteliExternalCommand.run(convert_command, url, tempfile.path) + if !output.nil? + zipfile.get_output_stream("correspondence.pdf") { |f| + f.puts(File.open(tempfile.path).read) + } + done = true + else + logger.error("Could not convert info request #{info_request.id} to PDF") + end + tempfile.close + else + logger.warn("No HTML -> PDF converter found at #{convert_command}") + end + if !done + @info_request = info_request + @info_request_events = info_request.info_request_events + template = File.read(File.join(File.dirname(__FILE__), "..", "views", "request", "simple_correspondence.rhtml")) + output = ERB.new(template).result(binding) + zipfile.get_output_stream("correspondence.txt") { |f| + f.puts(output) + } + end + for message in info_request.incoming_messages + attachments = message.get_attachments_for_display + for attachment in attachments + zipfile.get_output_stream(attachment.display_filename) { |f| + f.puts(attachment.body) + } + end + end + } + File.chmod(0644, file_path) + end + redirect_to @url_path + end + end + end end diff --git a/app/models/incoming_message.rb b/app/models/incoming_message.rb index edc395ff4..c19f1b6e3 100644 --- a/app/models/incoming_message.rb +++ b/app/models/incoming_message.rb @@ -29,7 +29,6 @@ # general not specific to IncomingMessage. require 'alaveteli_file_types' -require 'external_command' require 'htmlentities' require 'rexml/document' require 'zip/zip' @@ -1121,38 +1120,38 @@ class IncomingMessage < ActiveRecord::Base tempfile.print body tempfile.flush if content_type == 'application/vnd.ms-word' - external_command("/usr/bin/wvText", tempfile.path, tempfile.path + ".txt") + AlaveteliExternalCommand.run("/usr/bin/wvText", tempfile.path, tempfile.path + ".txt") # Try catdoc if we get into trouble (e.g. for InfoRequestEvent 2701) if not File.exists?(tempfile.path + ".txt") - external_command("/usr/bin/catdoc", tempfile.path, :append_to => text) + AlaveteliExternalCommand.run("/usr/bin/catdoc", tempfile.path, :append_to => text) else text += File.read(tempfile.path + ".txt") + "\n\n" 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 - external_command("/usr/bin/catdoc", tempfile.path, :append_to => text) + AlaveteliExternalCommand.run("/usr/bin/catdoc", tempfile.path, :append_to => text) elsif content_type == 'text/html' # lynx wordwraps links in its output, which then don't get formatted properly # by Alaveteli. We use elinks instead, which doesn't do that. - external_command("/usr/bin/elinks", "-eval", "'set document.codepage.assume = \"utf-8\"'", "-dump-charset", "utf-8", "-force-html", "-dump", + AlaveteliExternalCommand.run("/usr/bin/elinks", "-eval", "'set document.codepage.assume = \"utf-8\"'", "-dump-charset", "utf-8", "-force-html", "-dump", tempfile.path, :append_to => text) elsif content_type == 'application/vnd.ms-excel' # Bit crazy using /usr/bin/strings - but xls2csv, xlhtml and # py_xls2txt only extract text from cells, not from floating # notes. catdoc may be fooled by weird character sets, but will # probably do for UK FOI requests. - external_command("/usr/bin/strings", tempfile.path, :append_to => text) + AlaveteliExternalCommand.run("/usr/bin/strings", tempfile.path, :append_to => text) elsif content_type == 'application/vnd.ms-powerpoint' # ppthtml seems to catch more text, but only outputs HTML when # we want text, so just use catppt for now - external_command("/usr/bin/catppt", tempfile.path, :append_to => text) + AlaveteliExternalCommand.run("/usr/bin/catppt", tempfile.path, :append_to => text) elsif content_type == 'application/pdf' - external_command("/usr/bin/pdftotext", tempfile.path, "-", :append_to => text) + AlaveteliExternalCommand.run("/usr/bin/pdftotext", tempfile.path, "-", :append_to => text) elsif content_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' # This is Microsoft's XML office document format. # Just pull out the main XML file, and strip it of text. - xml = external_command("/usr/bin/unzip", "-qq", "-c", tempfile.path, "word/document.xml") + xml = AlaveteliExternalCommand.run("/usr/bin/unzip", "-qq", "-c", tempfile.path, "word/document.xml") if !xml.nil? doc = REXML::Document.new(xml) text += doc.each_element( './/text()' ){}.join(" ") @@ -1341,34 +1340,6 @@ class IncomingMessage < ActiveRecord::Base end private :normalise_content_type - def self.external_command(program_name, *args) - # Run an external program, and return its output. - # Standard error is suppressed unless the program - # fails (i.e. returns a non-zero exit status). - opts = {} - if !args.empty? && args[-1].is_a?(Hash) - opts = args.pop - end - - xc = ExternalCommand.new(program_name, *args) - if opts.has_key? :append_to - xc.out = opts[:append_to] - end - xc.run() - if xc.status != 0 - # Error - $stderr.puts("Error from #{program_name} #{args.join(' ')}:") - $stderr.print(xc.err) - return nil - else - if opts.has_key? :append_to - opts[:append_to] << "\n\n" - else - return xc.out - end - end - end - private_class_method :external_command end diff --git a/app/views/layouts/default.rhtml b/app/views/layouts/default.rhtml index 8812f50a6..5ad6ccecb 100644 --- a/app/views/layouts/default.rhtml +++ b/app/views/layouts/default.rhtml @@ -21,6 +21,9 @@ <%= stylesheet_link_tag 'fonts', :rel => "stylesheet", :media => "all" %> <%= stylesheet_link_tag 'theme', :rel => "stylesheet", :media => "all" %> <%= stylesheet_link_tag 'print', :rel => "stylesheet", :media => "print" %> + <% if !params[:print_stylesheet].nil? %> + <%= stylesheet_link_tag 'print', :rel => "stylesheet", :media => "all" %> + <% end %> <%= javascript_include_tag 'jquery.js', 'jquery-ui.min','jquery.cookie.js', 'general.js' %> <%= stylesheet_link_tag 'admin-theme/jquery-ui-1.8.15.custom.css', :rel => 'stylesheet'%> <!--[if LT IE 7]> diff --git a/app/views/request/_after_actions.rhtml b/app/views/request/_after_actions.rhtml index 797ecaea5..30bcf3046 100644 --- a/app/views/request/_after_actions.rhtml +++ b/app/views/request/_after_actions.rhtml @@ -36,6 +36,9 @@ <li> <%= link_to _("Request an internal review"), show_response_no_followup_url(:id => @info_request.id, :incoming_message_id => nil) + "?internal_review=1#followup" %> </li> + <li> + <%= link_to _("Download a zip file containing all the above correspondence"), download_entire_request_url(:url_title => @info_request.url_title) %> + </li> </ul> </div> @@ -45,6 +48,9 @@ <li> <%= link_to _("Respond to request"), upload_response_url(:url_title => @info_request.url_title) %> </li> + <li> + <%= link_to _("Download a zip file containing all the above correspondence"), download_entire_request_url(:url_title => @info_request.url_title) %> + </li> </ul> </div> </div> diff --git a/app/views/request/simple_correspondence.rhtml b/app/views/request/simple_correspondence.rhtml new file mode 100644 index 000000000..45b90b84b --- /dev/null +++ b/app/views/request/simple_correspondence.rhtml @@ -0,0 +1,45 @@ +<%= _('This is a plain-text version of the Freedom of Information request "{{request_title}}". The latest, full version is available online at {{full_url}}', :request_title => @info_request.title, :full_url => "http://#{MySociety::Config.get('DOMAIN')}#{show_request_path(:url_title=>@info_request.url_title)}") %>. + +<% for info_request_event in @info_request_events %> +<% + incoming_message = nil + if info_request_event.visible + if !info_request_event.nil? && info_request_event.event_type == 'response' + incoming_message = info_request_event.incoming_message + end + + + if not incoming_message.nil? + if !incoming_message.safe_mail_from.nil? && incoming_message.safe_mail_from.strip != @info_request.public_body.name.strip %> +<%= _('From:') %> <%= incoming_message.safe_mail_from %><% end + if incoming_message.safe_mail_from.nil? || (incoming_message.mail_from_domain == @info_request.public_body.request_email_domain) %>, <%= @info_request.public_body.name %><% end %> +<%= _('To:') %> <%= @info_request.user.name %> +<%= _('Date:') %> <%= simple_date(incoming_message.sent_at) %> + +<%= incoming_message.get_body_for_quoting %> +<% incoming_message.get_attachments_for_display.each do |a| %> + <%= _('Attachment:') %> <%= a.display_filename %> (<%= a.display_size %>) + <% end %> +<% +elsif [ 'sent', 'followup_sent' ].include?(info_request_event.event_type) + outgoing_message = info_request_event.outgoing_message + %> +<%= _('From:') %> <%= @info_request.user.name %> +<%= _('To:') %> <%= @info_request.public_body.name %> +<%= _('Date:') %> <%= simple_date(info_request_event.created_at) %> +<% + text = outgoing_message.body.strip + outgoing_message.remove_privacy_sensitive_things!(text) %> + +<%= text %> +<% elsif [ 'resent', 'followup_resent' ].include?(info_request_event.event_type) %> +<%= _('Date:') %> <%= simple_date(info_request_event.created_at) %> +Sent <% if info_request_event.outgoing_message.message_type == 'initial_request' %> request <% elsif info_request_event.outgoing_message.message_type == 'followup' %> a follow up <% else %> <% raise "unknown message_type" %><% end %> to <%= public_body_link(@info_request.public_body) %> again<% if not info_request_event.same_email_as_previous_send? %>, using a new contact address<% end %>. + +<% elsif info_request_event.event_type == 'comment' + comment = info_request_event.comment +%> +<%= _("{{username}} left an annotation:", :username =>comment.user.name) %> (<%= simple_date(comment.created_at || Time.now) %>) +<%= comment.body.strip %> +<% end %> +-------------------------------<% end %><% end %> diff --git a/config/environment.rb b/config/environment.rb index 2f7967cdc..daeefb615 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -146,3 +146,4 @@ require 'tnef.rb' require 'i18n_fixes.rb' require 'rack_quote_monkeypatch.rb' require 'world_foi_websites.rb' +require 'alaveteli_external_command.rb' diff --git a/config/general.yml-example b/config/general.yml-example index dcaa0d648..8c59b1b0e 100644 --- a/config/general.yml-example +++ b/config/general.yml-example @@ -122,3 +122,11 @@ GAZE_URL: http://gaze.mysociety.org # The email address to which non-bounce responses should be forwarded FORWARD_NONBOUNCE_RESPONSES_TO: user-support@localhost + +# Path to a program that converts a page at a URL to HTML. It should +# take two arguments: the URL, and a path to an output file. A static +# binary of wkhtmltopdf is recommended: +# http://code.google.com/p/wkhtmltopdf/downloads/list +# If the command is not present, a text-only version will be rendered +# instead. +HTML_TO_PDF_COMMAND: /usr/local/bin/wkhtmltopdf-amd64
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 1fa2f8aa0..414a47908 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -61,6 +61,8 @@ ActionController::Routing::Routes.draw do |map| request.info_request_event '/request_event/:info_request_event_id', :action => 'show_request_event' request.upload_response "/upload/request/:url_title", :action => 'upload_response' + request.download_entire_request '/request/:url_title/download', :action => 'download_entire_request' + end # Use /profile for things to do with the currently signed in user. diff --git a/doc/CHANGES.md b/doc/CHANGES.md index c7424c8e9..8299e8e8a 100644 --- a/doc/CHANGES.md +++ b/doc/CHANGES.md @@ -6,14 +6,20 @@ * FORWARD_NONBOUNCE_RESPONSES_TO * TRACK_SENDER_EMAIL * TRACK_SENDER_NAME -* Execute `script/rebuild-xapian-index` to create new xapian index terms used in latest version of search (can take a long time) + * HTML_TO_PDF_COMMAND +* Execute `script/rebuild-xapian-index` to create new xapian index + terms used in latest version of search (can take a long time) +* Install wkhtmltopdf to enable PDFs in downloadable zipfiles. A + static binary is recommended on Linux in order to run the command + headless: http://code.google.com/p/wkhtmltopdf/downloads/list +* Configure your MTA to handle bounce emails from alerts (see INSTALL-exim4.md) ## Highlighted features * Complete overhaul of design, including improved search, modern look and feel, more twitter links, etc * A banner alerts visitors from other countries to existing sites in their country, or exhorts them to make their own * Bounce emails that result from user alerts are automatically processed and hard bouncing accounts do not continue to receive alerts. See the new instructions in INSTALL-exim4.md for details of how to set this up. - +* Logged in users now have the ability to download a zipfile of the entire correspondence for a request # Version 0.3 diff --git a/doc/INSTALL.md b/doc/INSTALL.md index eb0a77dd9..f6317057e 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -36,7 +36,16 @@ code. Run: git submodule update --init -to fetch the contents of the submodules. +to fetch the contents of the submodules. + +Optionally, you may want to install +[wkhtmltopdf](http://code.google.com/p/wkhtmltopdf/downloads/list). +We recommend downloading the latest, statically compiled version from +the project website, as this allows running headless (i.e. without a +graphical interface running) on Linux. If you do install +`wkhtmltopdf`, you need to edit a setting in the config file to point +to it (see below). + # Configure Database diff --git a/lib/alaveteli_external_command.rb b/lib/alaveteli_external_command.rb new file mode 100644 index 000000000..b967c89b5 --- /dev/null +++ b/lib/alaveteli_external_command.rb @@ -0,0 +1,33 @@ +require 'external_command' + +module AlaveteliExternalCommand + class << self + def run(program_name, *args) + # Run an external program, and return its output. + # Standard error is suppressed unless the program + # fails (i.e. returns a non-zero exit status). + opts = {} + if !args.empty? && args[-1].is_a?(Hash) + opts = args.pop + end + + xc = ExternalCommand.new(program_name, *args) + if opts.has_key? :append_to + xc.out = opts[:append_to] + end + xc.run() + if xc.status != 0 + # Error + $stderr.puts("Error from #{program_name} #{args.join(' ')}:") + $stderr.print(xc.err) + return nil + else + if opts.has_key? :append_to + opts[:append_to] << "\n\n" + else + return xc.out + end + end + end + end +end diff --git a/public/stylesheets/print.css b/public/stylesheets/print.css index 129b452b8..02e0e98c0 100644 --- a/public/stylesheets/print.css +++ b/public/stylesheets/print.css @@ -17,8 +17,8 @@ div#content { p.event_actions, div#after_actions, #right_column, -#header_right, #banner, +#header_right, #describe_state_form_1, #describe_state_form_2 input[type=submit], #footer { @@ -28,8 +28,6 @@ div#after_actions, div.correspondence { background: none; border: 1px solid #DDD; - border-radius: 0; - -moz-border-radius: 0; } p#request_status { diff --git a/public/stylesheets/theme.css b/public/stylesheets/theme.css index ca2fc2c34..0a169d072 100644 --- a/public/stylesheets/theme.css +++ b/public/stylesheets/theme.css @@ -62,6 +62,11 @@ body.front { background: url(/images/home-grad.png) repeat-x 0px 160px; } +div.controller_help h1 a +{ + color: #93278F +} + #wrapper { padding-top:160px; } diff --git a/spec/controllers/request_controller_spec.rb b/spec/controllers/request_controller_spec.rb index 63e86b525..b4aef8470 100644 --- a/spec/controllers/request_controller_spec.rb +++ b/spec/controllers/request_controller_spec.rb @@ -216,7 +216,28 @@ describe RequestController, "when showing one request" do response.body.should have_tag("p.attachment strong", /goodbye.txt/m) end - + it "should make a zipfile available, which has a different URL when it changes" do + ir = info_requests(:fancy_dog_request) + session[:user_id] = ir.user.id # bob_smith_user + receive_incoming_mail('incoming-request-two-same-name.email', ir.incoming_email) + title = 'why_do_you_have_such_a_fancy_dog' + get :download_entire_request, :url_title => title + assigns[:url_path].should have_text(/#{title}.zip$/) + old_path = assigns[:url_path] + response.location.should have_text(/#{assigns[:url_path]}$/) + zipfile = Zip::ZipFile.open(File.join(File.dirname(__FILE__), "../../cache/zips", old_path)) { |zipfile| + zipfile.count.should == 2 + } + receive_incoming_mail('incoming-request-attachment-unknown-extension.email', ir.incoming_email) + get :download_entire_request, :url_title => title + assigns[:url_path].should have_text(/#{title}.zip$/) + response.location.should have_text(/#{assigns[:url_path]}/) + assigns[:url_path].should_not == old_path + zipfile = Zip::ZipFile.open(File.join(File.dirname(__FILE__), "../../cache/zips", assigns[:url_path])) { |zipfile| + zipfile.count.should == 4 +zipfile.entries.each {|x| puts x.name} + } + end end end diff --git a/spec/integration/search_request_spec.rb b/spec/integration/search_request_spec.rb index 25c091111..dcd20c7bd 100644 --- a/spec/integration/search_request_spec.rb +++ b/spec/integration/search_request_spec.rb @@ -13,7 +13,8 @@ describe "When searching" do :comments ] before(:each) do - load_raw_emails_data(raw_emails) + emails = raw_emails.clone + load_raw_emails_data(emails) end it "should not strip quotes from quoted query" do |