aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/controllers/request_controller.rb67
-rw-r--r--app/models/incoming_message.rb45
-rw-r--r--app/views/layouts/default.rhtml3
-rw-r--r--app/views/request/_after_actions.rhtml6
-rw-r--r--app/views/request/simple_correspondence.rhtml45
-rw-r--r--config/environment.rb1
-rw-r--r--config/general.yml-example8
-rw-r--r--config/routes.rb2
-rw-r--r--doc/CHANGES.md10
-rw-r--r--doc/INSTALL.md11
-rw-r--r--lib/alaveteli_external_command.rb33
-rw-r--r--public/stylesheets/print.css4
-rw-r--r--public/stylesheets/theme.css5
-rw-r--r--spec/controllers/request_controller_spec.rb23
-rw-r--r--spec/integration/search_request_spec.rb3
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