Skip to content

Commit

Permalink
Merge pull request #934 from thecartercenter/12389_export_forms
Browse files Browse the repository at this point in the history
12389 export form ODK XML for easier Collect import
  • Loading branch information
cooperka authored Mar 15, 2023
2 parents cf3b77b + 8a43830 commit 3623013
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 1 deletion.
34 changes: 34 additions & 0 deletions app/controllers/forms_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,31 @@ def clone
redirect_to(index_url_with_context)
end

# Standard CSV export.
def export
exporter = Forms::Export.new(@form)
send_data(exporter.to_csv, filename: "form-#{@form.name.dasherize}-#{Time.zone.today}.csv")
end

# ODK XML export.
def export_xml
send_data(@form.odk_xml.download, filename: "form-#{@form.name.dasherize}-#{Time.zone.today}.xml")
end

# ODK XML export for all published forms.
# Theoretically works for standard forms too, but they have no XML so can't be exported at this time.
def export_all
forms = Form.where(mission: current_mission) # Mission could be nil for standard forms.
forms = forms.published if current_mission.present?
forms_group = current_mission&.compact_name || "standard"
zipfile_path = Rails.root.join("tmp/forms-#{forms_group}-#{Time.zone.today}.zip")
zip_all(zipfile_path, forms)

# Use send_data (not send_file) in order to block until it's finished before deleting.
File.open(zipfile_path, "r") { |f| send_data(f.read, filename: File.basename(zipfile_path)) }
FileUtils.rm(zipfile_path)
end

private

# Decorates questions for choose_questions view.
Expand All @@ -239,6 +259,20 @@ def questions
Draper::CollectionDecorator.decorate(@questions, with: QuestionDecorator)
end

# Given a set of forms, put them all in a zip file for download.
def zip_all(zipfile_path, forms)
Zip::File.open(zipfile_path, Zip::File::CREATE) do |zipfile|
forms.each do |form|
form_name = "form-#{form.name.dasherize}-#{Time.zone.today}.xml"
zipfile.get_output_stream(form_name) { |f| f.write(form.odk_xml.download) }
rescue Zip::EntryExistsError => e
Sentry.add_breadcrumb(Sentry::Breadcrumb.new(message: "Form: #{form.id}"))
notify_admins(e)
next
end
end
end

def setup_condition_computer
@condition_computer = Forms::ConditionComputer.new(@form)
end
Expand Down
6 changes: 5 additions & 1 deletion app/decorators/action_links/form_link_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ module ActionLinks
# Builds a list of action links for a form.
class FormLinkBuilder < LinkBuilder
def initialize(form)
actions = %i[show edit clone export]
actions = %i[show edit clone]
unless new_action?
actions << [:export_csv, {url: h.export_form_path(form)}]
actions << [:export_xml, {url: h.export_xml_form_path(form)}] if form.odk_xml.attached?
end

unless h.admin_mode?
actions << [:go_live, {method: :patch}] unless form.live?
Expand Down
4 changes: 4 additions & 0 deletions app/decorators/application_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ def nbsp
"&nbsp;".html_safe # rubocop:disable Rails/OutputSafety
end

def new_action?
%w[create new].include?(h.controller.action_name)
end

def show_action?
h.controller.action_name == "show"
end
Expand Down
1 change: 1 addition & 0 deletions app/helpers/forms_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ def forms_index_links(_forms)
links = []
links << create_link(Form) if can?(:create, Form)
add_import_standard_link_if_appropriate(links)
links << link_to(t("action_links.models.form.export_all"), export_all_forms_path) unless admin_mode?
links << link_to(t("action_links.models.form.sms_console"), new_sms_test_path) if can?(:create, Sms::Test)
links
end
Expand Down
2 changes: 2 additions & 0 deletions app/helpers/icon_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ module IconHelper
drop_pin: "map-marker",
edit: "pencil",
export: "download",
export_csv: "download",
export_xml: "download",
go_live: "play",
import: "upload",
index: "list",
Expand Down
2 changes: 2 additions & 0 deletions config/locales/en/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -917,12 +917,14 @@ en:
import_standard: "Import Standard"
import_from_csv: "Import from CSV"
export_csv: "Export CSV"
export_xml: "Export XML"
export: "Export"
models:
form:
new: "Create Form"
sms_guide: "SMS Guide"
sms_console: "SMS Test Console"
export_all: "Export XML"
go_live: "Go Live"
pause: "Pause"
return_to_draft: "Return to Draft Status"
Expand Down
5 changes: 5 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,17 @@
mission_name: /[a-z][a-z0-9]*/ do
# the rest of these routes can have admin mode or not
resources :forms, constraints: ->(req) { req.format == :html } do
collection do
get "export_all"
end

member do
post "add-questions", as: "add_questions", action: "add_questions"
put "clone"
get "choose-questions", as: "choose_questions", action: "choose_questions"
get "sms-guide", as: "sms_guide", action: "sms_guide"
get "export"
get "export_xml"
end
end

Expand Down
48 changes: 48 additions & 0 deletions spec/features/forms/form/form_export_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

require "rails_helper"
require "fileutils"
require "zip"

feature "form export" do
context "single form" do
let(:user) { create(:user, role_name: "coordinator") }
let(:form) { create(:form, :live, question_types: %w[text]) }

before do
login(user)
ODK::FormRenderJob.perform_now(form)
end

it "XML exports successfully" do
visit(form_path(form, locale: "en", mode: "m", mission_name: get_mission.compact_name))
click_link("Export XML")
expect(page.body).to match("<h:title>#{form.name}</h:title>")
end
end

context "multiple forms" do
let(:user) { create(:user, role_name: "coordinator") }
let(:form1) { create(:form, :live, question_types: %w[text]) }
let(:form2) { create(:form, :draft, question_types: %w[text]) } # Draft should not be exported
let(:form3) { create(:form, :paused, question_types: %w[text]) }

before do
login(user)
ODK::FormRenderJob.perform_now(form1)
ODK::FormRenderJob.perform_now(form3)
end

it "XML exports successfully" do
visit(forms_path(locale: "en", mode: "m", mission_name: get_mission.compact_name))
click_link("Export XML")

Zip::File.open_buffer(page.body) do |zipfile|
expect(zipfile.count).to be(2)
zipfile.each do |file|
expect(file.get_input_stream.read).to match("<h:title>.+</h:title>")
end
end
end
end
end

0 comments on commit 3623013

Please sign in to comment.