diff --git a/.travis.yml b/.travis.yml index 13bb3b9..8e969a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,7 @@ apt: packages: - nodejs + - wkhtmltopdf matrix: fast_finish: true @@ -37,6 +38,7 @@ # Main test script script: + - export WICKED_PDF_PATH=./vendor/bundle/ruby/2.4.0/bin/wkhtmltopdf # Copy over config files needed for setup, and create DB - bin/setup # Precompile the assets diff --git a/app/controllers/plan_exports_controller.rb b/app/controllers/plan_exports_controller.rb new file mode 100644 index 0000000..d2e8a3a --- /dev/null +++ b/app/controllers/plan_exports_controller.rb @@ -0,0 +1,100 @@ +class PlanExportsController < ApplicationController + + after_action :verify_authorized + + def show + @plan = Plan.includes(:answers).find(params[:plan_id]) + + if publicly_authorized? + skip_authorization + @show_coversheet = true + @show_sections_questions = true + @show_unanswered = true + @show_custom_sections = true + @public_plan = true + + elsif privately_authorized? + @show_coversheet = export_params[:project_details].present? + @show_sections_questions = export_params[:question_headings].present? + @show_unanswered = export_params[:unanswered_questions].present? + @show_custom_sections = export_params[:custom_sections].present? + @public_plan = false + + else + raise Pundit::NotAuthorizedError + end + + @hash = @plan.as_pdf(@show_coversheet) + @formatting = export_params[:formatting] || @plan.settings(:export).formatting + if params.key?(:phase_id) + @selected_phase = @plan.phases.find(params[:phase_id]) + else + @selected_phase = @plan.phases.order("phases.updated_at DESC") + .detect { |p| p.visibility_allowed?(@plan) } + end + + respond_to do |format| + format.html { show_html } + format.csv { show_csv } + format.text { show_text } + format.docx { show_docx } + format.pdf { show_pdf } + end + end + + private + + def show_html + render layout: false + end + + def show_csv + send_data @plan.as_csv(@show_sections_questions, + @show_unanswered, + @selected_phase, + @show_custom_sections, + @show_coversheet), + filename: "#{file_name}.csv" + end + + def show_text + send_data render_to_string(partial: 'shared/export/plan_txt'), + filename: "#{file_name}.txt" + end + + def show_docx + render docx: "#{file_name}.docx", + content: render_to_string(partial: 'shared/export/plan') + end + + def show_pdf + render pdf: file_name, + margin: @formatting[:margin], + footer: { + center: _("Created using the %{application_name}. Last modified %{date}") % { + application_name: Rails.configuration.branding[:application][:name], + date: l(@plan.updated_at.to_date, formats: :short) + }, + font_size: 8, + spacing: (Integer(@formatting[:margin][:bottom]) / 2) - 4, + right: "[page] of [topage]" + } + end + + def file_name + @plan.title.gsub(/ /, "_") + end + + def publicly_authorized? + PublicPagePolicy.new(@plan, current_user).plan_organisationally_exportable? || + PublicPagePolicy.new(@plan).plan_export? + end + + def privately_authorized? + authorize @plan, :export? + end + + def export_params + params.fetch(:export, {}) + end +end diff --git a/app/controllers/plans_controller.rb b/app/controllers/plans_controller.rb index 6875978..4891ce7 100644 --- a/app/controllers/plans_controller.rb +++ b/app/controllers/plans_controller.rb @@ -297,44 +297,6 @@ render "download" end - def export - @plan = Plan.includes(:answers).find(params[:id]) - authorize @plan - @selected_phase = @plan.phases.find(params[:phase_id]) - @show_coversheet = params[:export][:project_details].present? - @show_sections_questions = params[:export][:question_headings].present? - @show_unanswered = params[:export][:unanswered_questions].present? - @show_custom_sections = params[:export][:custom_sections].present? - @public_plan = false - @hash = @plan.as_pdf(@show_coversheet) - @formatting = params[:export][:formatting] || @plan.settings(:export).formatting - file_name = @plan.title.gsub(/ /, "_") - - # rubocop:disable Metrics/BlockLength - respond_to do |format| - format.html { render layout: false } - format.csv { send_data @plan.as_csv(@show_sections_questions, @show_unanswered, @selected_phase, @show_custom_sections, @show_coversheet), filename: "#{file_name}.csv" } - format.text { send_data render_to_string(partial: 'shared/export/plan_txt'), filename: "#{file_name}.txt" } - format.docx { render docx: "#{file_name}.docx", content: render_to_string(partial: 'shared/export/plan') } - format.pdf do - # rubocop:disable Metrics/LineLength - render pdf: file_name, - margin: @formatting[:margin], - footer: { - center: _("Created using the %{application_name}. Last modified %{date}") % { - application_name: Rails.configuration.branding[:application][:name], - date: l(@plan.updated_at.to_date, formats: :short) - }, - font_size: 8, - spacing: (Integer(@formatting[:margin][:bottom]) / 2) - 4, - right: "[page] of [topage]" - } - # rubocop:enable Metrics/LineLength - end - end - # rubocop:enable Metrics/BlockLength - end - def duplicate plan = Plan.find(params[:id]) authorize plan diff --git a/app/controllers/public_pages_controller.rb b/app/controllers/public_pages_controller.rb index 021b7bf..3cc7ed8 100644 --- a/app/controllers/public_pages_controller.rb +++ b/app/controllers/public_pages_controller.rb @@ -47,14 +47,16 @@ # ------------------------------------------------------------- def plan_export @plan = Plan.includes(:answers).find(params[:id]) - # covers authorization for this action. Pundit dosent support passing objects into scoped policies - raise Pundit::NotAuthorizedError unless PublicPagePolicy.new(@plan, current_user).plan_organisationally_exportable? || PublicPagePolicy.new(@plan).plan_export? - skip_authorization + # covers authorization for this action. Pundit dosent support passing objects into + # scoped policies + if PublicPagePolicy.new(@plan, current_user).plan_organisationally_exportable? || + PublicPagePolicy.new(@plan).plan_export? + skip_authorization + else + raise Pundit::NotAuthorizedError + end - @show_coversheet = true - @show_sections_questions = true - @show_unanswered = true - @public_plan = true + @hash = @plan.as_pdf(@show_coversheet) @formatting = @plan.settings(:export).formatting diff --git a/app/controllers/super_admin/org_swaps_controller.rb b/app/controllers/super_admin/org_swaps_controller.rb new file mode 100644 index 0000000..0b258dd --- /dev/null +++ b/app/controllers/super_admin/org_swaps_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class SuperAdmin::OrgSwapsController < ApplicationController + + after_action :verify_authorized + + def create + # Allows the user to swap their org affiliation on the fly + authorize current_user, :org_swap? + begin + @org = Org.find(org_swap_params[:org_id]) + rescue ActiveRecord::RecordNotFound + redirect_to(:back, alert: _("Please select an organisation from the list")) + return + end + # rubocop:disable Metrics/LineLength + if @org.present? + current_user.org = @org + if current_user.save + redirect_to :back, + notice: _("Your organisation affiliation has been changed. You may now edit templates for %{org_name}.") % { org_name: current_user.org.name } + else + redirect_to :back, + alert: _("Unable to change your organisation affiliation at this time.") + end + else + redirect_to :back, alert: _("Unknown organisation.") + end + # rubocop:enable Metrics/LineLength + end + + private + + def org_swap_params + params.require(:user).permit(:org_id, :org_name) + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2091f6b..b94276b 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -111,33 +111,6 @@ notice: success_message(_("preferences"), _("saved")) end - # PUT /users/:id/org_swap - # ----------------------------------------------------- - def org_swap - # Allows the user to swap their org affiliation on the fly - authorize current_user - begin - org = Org.find(org_swap_params[:org_id]) - rescue ActiveRecord::RecordNotFound - redirect_to(request.referer, - alert: _("Please select an organisation from the list")) and return - end - # rubocop:disable Metrics/LineLength - if org.present? - current_user.org = org - if current_user.save - redirect_to request.referer, - notice: _("Your organisation affiliation has been changed. You may now edit templates for %{org_name}.") % { org_name: current_user.org.name } - else - redirect_to request.referer, - alert: _("Unable to change your organisation affiliation at this time.") - end - else - redirect_to request.referer, alert: _("Unknown organisation.") - end - # rubocop:enable Metrics/LineLength - end - # PUT /users/:id/activate # ----------------------------------------------------- def activate @@ -177,10 +150,6 @@ private - def org_swap_params - params.require(:user).permit(:org_id, :org_name) - end - ## # html forms return our boolean values as strings, this converts them to true/false def booleanize_hash(node) diff --git a/app/helpers/exports_helper.rb b/app/helpers/exports_helper.rb new file mode 100644 index 0000000..d60364b --- /dev/null +++ b/app/helpers/exports_helper.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module ExportsHelper + + PAGE_MARGINS = { + top: '5', + bottom: "10", + left: "12", + right: "12", + } + + def font_face + @formatting[:font_face].presence || 'Arial, Helvetica, Sans-Serif' + end + + def font_size + @formatting[:font_size].presence || '12' + end + + def margin_top + get_margin_value_for_side(:top) + end + + def margin_bottom + get_margin_value_for_side(:bottom) + end + + def margin_left + get_margin_value_for_side(:left) + end + + def margin_right + get_margin_value_for_side(:right) + end + + def plan_attribution(attribution) + attribution = Array(attribution) + prefix = attribution.many? ? _("Creators:") : _("Creator:") + "#{prefix} #{attribution.join(', ')}" + end + + private + + def get_margin_value_for_side(side) + side = side.to_sym + if @formatting.dig(:margin, side).is_a?(Integer) + @formatting[:margin][side] * 4 + else + @formatting.dig(:margin, side).presence || PAGE_MARGINS[side] + end + end +end \ No newline at end of file diff --git a/app/helpers/plans_helper.rb b/app/helpers/plans_helper.rb index f761b11..a3db40d 100644 --- a/app/helpers/plans_helper.rb +++ b/app/helpers/plans_helper.rb @@ -47,7 +47,7 @@ # If there is more than one phase show the plan title and phase title return hash[:phases].many? ? "#{plan.title} - #{phase[:title]}" : plan.title end - + def display_questions_and_section_headings(section, show_sections_questions, show_custom_sections) # Return true if show_sections_questions is true and either section not customised, or section is customised # and show_custom_sections is true diff --git a/app/models/answer.rb b/app/models/answer.rb index 738dc5d..1ee61fd 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # == Schema Information # # Table name: answers @@ -24,27 +26,22 @@ # class Answer < ActiveRecord::Base + include ValidationMessages - after_save do |answer| - if answer.plan_id.present? - plan = answer.plan - complete = plan.no_questions_matches_no_answers? - if plan.complete != complete - plan.complete = complete - plan.save! - else - plan.touch # Force updated_at changes if nothing changed since save only saves if changes were made to the record - end - end - end - ## - # Associations - belongs_to :question - belongs_to :user - belongs_to :plan + # ================ + # = Associations = + # ================ + + belongs_to :question + + belongs_to :user + + belongs_to :plan + has_many :notes, dependent: :destroy + has_and_belongs_to_many :question_options, join_table: "answers_question_options" has_many :notes @@ -59,8 +56,15 @@ validates :user, presence: { message: PRESENCE_MESSAGE } validates :question, presence: { message: PRESENCE_MESSAGE }, - uniqueness: { message: UNIQUENESS_MESSAGE, - scope: :plan_id } + uniqueness: { message: UNIQUENESS_MESSAGE, + scope: :plan_id } + + # ============= + # = Callbacks = + # ============= + + after_save :set_plan_complete + ## # deep copy the given answer @@ -81,7 +85,7 @@ # # Returns Boolean def has_question_option(option_id) - self.question_option_ids.include?(option_id) + question_option_ids.include?(option_id) end # If the answer's question is option_based, it is checked if exist any question_option @@ -90,21 +94,21 @@ # # Returns Boolean def is_valid? - if self.question.present? - if self.question.question_format.option_based? - return !self.question_options.empty? + if question.present? + if question.question_format.option_based? + return question_options.any? else # (e.g. textarea or textfield question formats) - return self.text.present? + return text.present? end end - return false + false end # Answer notes whose archived is blank sorted by updated_at in descending order # # Returns Array def non_archived_notes - return notes.select{ |n| n.archived.blank? }.sort!{ |x,y| y.updated_at <=> x.updated_at } + notes.select { |n| n.archived.blank? }.sort! { |x, y| y.updated_at <=> x.updated_at } end # Returns True if answer text is blank, false otherwise specificly we want to remove @@ -112,11 +116,11 @@ # # Returns Boolean def is_blank? - if self.text.present? - return self.text.gsub(/<\/?p>/, '').gsub(//, '').chomp.blank? + if text.present? + return text.gsub(/<\/?p>/, "").gsub(//, "").chomp.blank? end # no text so blank - return true + true end # The parsed JSON hash for the current answer object. Generates a new hash if none @@ -124,13 +128,13 @@ # # Returns Hash def answer_hash - default = {'standards' => {}, 'text' => ''} + default = { "standards" => {}, "text" => "" } begin - h = self.text.nil? ? default : JSON.parse(self.text) + h = text.nil? ? default : JSON.parse(text) rescue JSON::ParserError => e h = default end - return h + h end ## @@ -141,10 +145,23 @@ # text - A String with option comment text # # Returns String - def update_answer_hash(standards={},text="") + def update_answer_hash(standards = {}, text = "") h = {} - h['standards'] = standards - h['text'] = text + h["standards"] = standards + h["text"] = text self.text = h.to_json end + + def set_plan_complete + return unless plan_id? + complete = plan.no_questions_matches_no_answers? + if plan.complete != complete + plan.update!(complete: complete) + else + # Force updated_at changes if nothing changed since save only saves if changes + # were made to the record + plan.touch + end + end + end diff --git a/app/models/concerns/exportable_plan.rb b/app/models/concerns/exportable_plan.rb index 1701b83..dac15c3 100644 --- a/app/models/concerns/exportable_plan.rb +++ b/app/models/concerns/exportable_plan.rb @@ -71,9 +71,10 @@ def prepare(coversheet = false) hash = coversheet ? prepare_coversheet : {} - template = Template.includes(phases: { sections: {questions: :question_format } }). - joins(phases: { sections: { questions: :question_format } }). - where(id: self.template_id).order('sections.number', 'questions.number').first + template = Template.includes(phases: { sections: {questions: :question_format } }) + .joins(phases: { sections: { questions: :question_format } }) + .where(id: self.template_id) + .order('sections.number', 'questions.number').first hash[:title] = self.title hash[:answers] = self.answers @@ -83,10 +84,17 @@ template.phases.each do |phase| phs = { title: phase.title, number: phase.number, sections: [] } phase.sections.each do |section| - sctn = { title: section.title, number: section.number, questions: [], modifiable: section.modifiable } + sctn = { title: section.title, + number: section.number, + questions: [], + modifiable: section.modifiable } section.questions.each do |question| txt = question.text - sctn[:questions] << { id: question.id, text: txt, format: question.question_format } + sctn[:questions] << { + id: question.id, + text: txt, + format: question.question_format + } end phs[:sections] << sctn end diff --git a/app/models/phase.rb b/app/models/phase.rb index 58a466d..f56edf6 100644 --- a/app/models/phase.rb +++ b/app/models/phase.rb @@ -98,8 +98,7 @@ # TODO: Move this to Plan model as `num_answered_questions(phase=nil)` # Returns the number of answered question for the phase. def num_answered_questions(plan) - return 0 if plan.nil? - sections.to_a.sum { |s| s.num_answered_questions(plan) } + plan&.num_answered_questions.to_i end # Returns the number of questions for a phase. Note, this method becomes useful @@ -111,4 +110,9 @@ end n end + + def visibility_allowed?(plan) + value = Rational(num_answered_questions(plan), plan.num_questions) * 100 + value >= Rails.application.config.default_plan_percentage_answered.to_f + end end diff --git a/app/views/org_admin/templates/index.html.erb b/app/views/org_admin/templates/index.html.erb index 45fa55b..de858a9 100644 --- a/app/views/org_admin/templates/index.html.erb +++ b/app/views/org_admin/templates/index.html.erb @@ -11,8 +11,9 @@

- <%= form_for current_user, url: org_swap_user_path(current_user), + <%= form_for current_user, url: user_org_swaps_path(current_user), namespace: 'superadmin', + method: "post", html: { id: 'super-admin-switch-org' } do |f| %> <%= render partial: "shared/my_org", locals: { f: f, diff --git a/app/views/paginable/plans/_organisationally_or_publicly_visible.html.erb b/app/views/paginable/plans/_organisationally_or_publicly_visible.html.erb index e5b3d7c..e4415ba 100644 --- a/app/views/paginable/plans/_organisationally_or_publicly_visible.html.erb +++ b/app/views/paginable/plans/_organisationally_or_publicly_visible.html.erb @@ -1,33 +1,45 @@ -<% if current_user.org_id.present? %> -
-
-
- - - - - - - - - - - - <% scope.each do |plan| %> - - - - - - - - <% end %> - -
<%= _('Project Title') %> <%= paginable_sort_link('plans.title') %><%= _('Template') %> <%= paginable_sort_link('templates.title') %><%= _('Owner') %><%= _('Updated') %> <%= paginable_sort_link('plans.updated_at') %><%= _('Download') %>
<%= plan.title.length > 40 ? "#{plan.title[0..39]} ..." : plan.title %><%= plan.template.title %><%= plan.owner.present? ? plan.owner.name : _('Unknown') %><%= l(plan.updated_at.to_date, formats: :short) %> - <%= link_to _('PDF'), plan_export_path(plan, format: :pdf), target: '_blank' %> -
-
-
-
+<% if current_user.org_id? %> +
+
+
+ + + + + + + + + + + + <% scope.each do |plan| %> + + + + + + + + <% end %> + +
+ <%= _('Project Title') %>  + <%= paginable_sort_link('plans.title') %> + + <%= _('Template') %>  + <%= paginable_sort_link('templates.title') %> + <%= _('Owner') %> + <%= _('Updated') %>  + <%= paginable_sort_link('plans.updated_at') %> + <%= _('Download') %>
+ <%= truncate plan.title, length: 40 %> + <%= plan.template.title %><%= plan.owner.present? ? plan.owner.name : _('Unknown') %><%= l(plan.updated_at.to_date, formats: :short) %> + <%= link_to _('PDF'), plan_export_path(plan, format: :pdf), + target: '_blank' %> +
+
+
+
<% end %> diff --git a/app/views/paginable/plans/_privately_visible.html.erb b/app/views/paginable/plans/_privately_visible.html.erb index e144d77..50cbea1 100644 --- a/app/views/paginable/plans/_privately_visible.html.erb +++ b/app/views/paginable/plans/_privately_visible.html.erb @@ -16,7 +16,7 @@ <% scope.each do |plan| %> - + <%= link_to "#{plan.title.length > 60 ? "#{plan.title[0..59]} ..." : plan.title}", plan_path(plan) %> diff --git a/app/views/paginable/plans/_publicly_visible.html.erb b/app/views/paginable/plans/_publicly_visible.html.erb index 475f2a2..b29e2ab 100644 --- a/app/views/paginable/plans/_publicly_visible.html.erb +++ b/app/views/paginable/plans/_publicly_visible.html.erb @@ -17,7 +17,7 @@ <%= (plan.owner.nil? || plan.owner.org.nil? ? _('Not Applicable') : plan.owner.org.name) %> <%= (plan.owner.nil? ? _('Unknown') : plan.owner.name(false)) %> - <%= link_to _('PDF'), plan_export_path(plan, format: :pdf), class: "dmp_table_link", target: '_blank' %> + <%= link_to _('PDF'), plan_export_path(plan_id: plan.id, format: :pdf), class: "dmp_table_link", target: '_blank' %> <% end %> diff --git a/app/views/plan_exports/show.erb b/app/views/plan_exports/show.erb new file mode 100644 index 0000000..62f818d --- /dev/null +++ b/app/views/plan_exports/show.erb @@ -0,0 +1,2 @@ + +<%= render partial: 'shared/export/plan', locals: local_assigns %> \ No newline at end of file diff --git a/app/views/plans/_download_form.html.erb b/app/views/plans/_download_form.html.erb index e8f7e5d..db3326c 100644 --- a/app/views/plans/_download_form.html.erb +++ b/app/views/plans/_download_form.html.erb @@ -1,4 +1,4 @@ -<%= form_tag( export_plan_path(@plan), method: :get, target: '_blank', id: 'download_form') do |f| %> +<%= form_tag(plan_export_path(@plan), method: :get, target: '_blank', id: 'download_form') do |f| %>

<%= _("Download settings") %>

<% if @phase_options.length > 1 %> diff --git a/app/views/plans/export.erb b/app/views/plans/export.erb deleted file mode 100644 index 870f3b9..0000000 --- a/app/views/plans/export.erb +++ /dev/null @@ -1,2 +0,0 @@ - -<%= render partial: '/shared/export/plan', locals: local_assigns %> \ No newline at end of file diff --git a/app/views/plans/index.html.erb b/app/views/plans/index.html.erb index 862d276..e3d33cd 100644 --- a/app/views/plans/index.html.erb +++ b/app/views/plans/index.html.erb @@ -1,6 +1,6 @@ <% title _('My Dashboard') %>
-
+

<%= _('My Dashboard') %>

@@ -19,7 +19,7 @@ <%= paginable_renderise( partial: '/paginable/plans/privately_visible', controller: 'paginable/plans', - action: 'privately_visible', + action: 'privately_visible', scope: @plans, query_params: { sort_field: 'plans.updated_at', sort_direction: 'desc' }) %>

@@ -31,8 +31,8 @@
- <% if @organisationally_or_publicly_visible.length > 0 && !current_user.org.is_other? %> -

<%= _('%{org_title} Plans') %{ :org_title => current_user.org.name } %>

+ <% if @organisationally_or_publicly_visible.any? && !current_user.org.is_other? %> +

<%= _('%{org_title} Plans') % { :org_title => current_user.org.name } %>

<%= _('The table below lists the plans that users at your organisation have created and shared within your organisation. This allows you to download a PDF and view their plans as samples or to discover new research data.') %>

<%= paginable_renderise( partial: '/paginable/plans/organisationally_or_publicly_visible', diff --git a/app/views/shared/export/_plan.erb b/app/views/shared/export/_plan.erb index 38b551c..43c7022 100644 --- a/app/views/shared/export/_plan.erb +++ b/app/views/shared/export/_plan.erb @@ -1,25 +1,10 @@ -<% - font_face = (@formatting[:font_face].present? ? "#{@formatting[:font_face]}" : 'Arial, Helvetica, Sans-Serif') - font_size = (@formatting[:font_size].present? ? "#{@formatting[:font_size]}" : '12') - margin_top = '5' - margin_bottom = '10' - margin_left = '12' - margin_right = '12' - - if @formatting[:margin].present? - margin_top = (@formatting[:margin][:top].is_a?(Integer) ? @formatting[:margin][:top] * 4 : @formatting[:margin][:top]) if @formatting[:margin][:top].present? - margin_right = (@formatting[:margin][:right].is_a?(Integer) ? @formatting[:margin][:right] * 4 : @formatting[:margin][:right]) if @formatting[:margin][:right].present? - margin_bottom = (@formatting[:margin][:bottom].is_a?(Integer) ? @formatting[:margin][:bottom] * 4 : @formatting[:margin][:bottom]) if @formatting[:margin][:bottom].present? - margin_left = (@formatting[:margin][:left].is_a?(Integer) ? @formatting[:margin][:left] * 4 : @formatting[:margin][:left]) if @formatting[:margin][:left].present? - end -%> - <%= @plan.title %> - <%= render partial: '/shared/export/plan_styling', + + <%= render partial: 'shared/export/plan_styling', locals: { font_face: font_face, font_size: "#{font_size}pt", @@ -28,13 +13,14 @@ <% if @show_coversheet %> - <%= render partial: '/shared/export/plan_coversheet' %> + <%= render partial: 'shared/export/plan_coversheet' %> <% end %> <% @hash[:phases].each do |phase| %> <%# Only render selected phase %> <% if phase[:title] == @selected_phase.title %> -
+ +

<%= download_plan_page_title(@plan, phase, @hash) %>


<% phase[:sections].each do |section| %> @@ -56,6 +42,7 @@ <% if @show_unanswered && (answer.blank? || (options.blank? && blank))%>

<%= _('Question not answered.') -%>

<% else %> + <%# case where Question has options %> <% if options.any? %>
    diff --git a/app/views/shared/export/_plan_coversheet.erb b/app/views/shared/export/_plan_coversheet.erb index 2a6ce37..4b79f25 100644 --- a/app/views/shared/export/_plan_coversheet.erb +++ b/app/views/shared/export/_plan_coversheet.erb @@ -5,7 +5,9 @@


    -

    <%= @hash[:attribution].length > 1 ? _("Creators: ") : _('Creator:') %> <%= @hash[:attribution].join(', ') %>


    +

    + <%= plan_attribution(@hash[:attribution]) %> +


    <%= _("Affiliation: ") + @hash[:affiliation] %>


    @@ -32,7 +34,11 @@ <% if @public_plan %>

    <%= _("Copyright information:") %>

    -

    <%= _(" The above plan creator(s) have agreed that others may use as much of the text of this plan as they would like in their own plans, and customise it as necessary. You do not need to credit the creator(s) as the source of the language used, but using any of the plan's text does not imply that the creator(s) endorse, or have any relationship to, your project or proposal") %>

    +
    +

    + <%= _(" The above plan creator(s) have agreed that others may use as much of the text of this plan as they would like in their own plans, and customise it as necessary. You do not need to credit the creator(s) as the source of the language used, but using any of the plan's text does not imply that the creator(s) endorse, or have any relationship to, your project or proposal") %> +

    +
    <% end %>
    -
    \ No newline at end of file +
    diff --git a/config/initializers/wicked_pdf.rb.example b/config/initializers/wicked_pdf.rb.example index 1473351..158a274 100644 --- a/config/initializers/wicked_pdf.rb.example +++ b/config/initializers/wicked_pdf.rb.example @@ -1,7 +1,7 @@ module DMPRoadmap class Application < Rails::Application WickedPdf.config = { - :exe_path => '/usr/local/bin/wkhtmltopdf' + exe_path: ENV['WICKED_PDF_PATH'] || '/usr/local/bin/wkhtmltopdf' } end -end \ No newline at end of file +end diff --git a/config/routes.rb b/config/routes.rb index acab4da..2dac984 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -17,11 +17,16 @@ post '/orgs/shibboleth', to: 'orgs#shibboleth_ds_passthru' resources :users, path: 'users', only: [] do + + resources :org_swaps, only: [:create], + controller: "super_admin/org_swaps" + member do put 'update_email_preferences' - put 'org_swap', constraints: {format: [:json]} end + post '/acknowledge_notification', to: 'users#acknowledge_notification' + end #organisation admin area @@ -50,7 +55,6 @@ get "public_plans" => 'public_pages#plan_index' get "public_templates" => 'public_pages#template_index' get "template_export/:id" => 'public_pages#template_export', as: 'template_export' - get "plan_export/:id" => 'public_pages#plan_export', as: 'plan_export' #post 'contact_form' => 'contacts', as: 'localized_contact_creation' #get 'contact_form' => 'contacts#new', as: 'localized_contact_form' @@ -104,12 +108,14 @@ resources :feedback_requests, only: [:create] resources :plans do + + resource :export, controller: "plan_exports" + member do get 'answer' get 'share' get 'download' post 'duplicate' - get 'export' post 'visibility', constraints: {format: [:json]} post 'set_test', constraints: {format: [:json]} get 'overview' diff --git a/spec/factories/plans.rb b/spec/factories/plans.rb index e97fe61..e7ad5d7 100644 --- a/spec/factories/plans.rb +++ b/spec/factories/plans.rb @@ -45,6 +45,7 @@ feedback_requested false complete false transient do + phases 0 answers 0 guidance_groups 0 end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index d6cf3dd..da1a394 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -54,5 +54,22 @@ email { Faker::Internet.unique.safe_email } password { "password" } accept_terms { true } + + trait :org_admin do + after(:create) do |user, evaluator| + %w[modify_templates modify_guidance].each do |perm_name| + user.perms << Perm.find_or_create_by(name: perm_name) + end + end + end + + trait :super_admin do + after(:create) do |user, evaluator| + %w[change_org_affiliation add_organisations + modify_templates modify_guidance].each do |perm_name| + user.perms << Perm.find_or_create_by(name: perm_name) + end + end + end end end diff --git a/spec/features/plans/exports_spec.rb b/spec/features/plans/exports_spec.rb new file mode 100644 index 0000000..14ce66c --- /dev/null +++ b/spec/features/plans/exports_spec.rb @@ -0,0 +1,130 @@ +require "rails_helper" + +RSpec.describe "PlansExports", type: :feature, js: true do + + let!(:template) { create(:template, phases: 2) } + let!(:user) { create(:user) } + let!(:plan) { create(:plan, template: template) } + let!(:org) { user.org } + + before do + template.phases.each { |p| create_list(:section, 2, phase: p) } + template.sections.each { |s| create_list(:question, 2, section: s) } + plan.roles << create(:role, :commenter, user: user) + plan.roles << create(:role, :creator, user: user) + sign_in(user) + end + + scenario "User downloads plan from dashboard" do + new_plan = create(:plan, :publicly_visible, template: template) + new_phase = create(:phase, template: template, sections: 2) + new_phase.sections do |sect| + create_list(:question, 2, section: sect) + end + new_plan.questions.each do |question| + create(:answer, question: question, plan: new_plan) + end + new_user = create(:user, org: org) + create(:role, :creator, :commenter, :administrator, :editor, + plan: new_plan, + user: new_user) + sign_in(user) + click_link "PDF" + end + + scenario "User downloads public plan belonging to other User" do + new_plan = create(:plan, :publicly_visible, template: template) + create(:role, :creator, plan: new_plan) + sign_in(user) + within("#plan_#{plan.id}") do + click_button("Actions") + click_link "Download" + end + select("html") + new_window = window_opened_by { click_button "Download Plan" } + within_window new_window do + expect(page.source).to have_text(plan.title) + end + end + + scenario "User downloads org plan belonging to User in same org" do + new_plan = create(:plan, :organisationally_visible, template: template) + role = create(:role, :creator, plan: new_plan, user: create(:user, org: org)) + sign_in(user) + within("#plan_#{plan.id}") do + click_button("Actions") + click_link "Download" + end + select("html") + new_window = window_opened_by { click_button "Download Plan" } + within_window new_window do + expect(page.source).to have_text(plan.title) + end + end + + scenario "User downloads org plan belonging to User in other org" do + new_plan = create(:plan, :organisationally_visible, template: template) + role = create(:role, :creator, plan: new_plan) + sign_in(create(:user)) + expect(page).not_to have_text(new_plan.title) + end + + scenario "User attempts to download private plan belonging to User in same" do + new_plan = create(:plan, :privately_visible, template: template) + role = create(:role, :creator, plan: new_plan) + sign_in(create(:user)) + expect(page).not_to have_text(new_plan.title) + end + + scenario "User downloads their plan as HTML" do + within("#plan_#{plan.id}") do + click_button("Actions") + click_link "Download" + end + select("html") + new_window = window_opened_by { click_button "Download Plan" } + within_window new_window do + expect(page.source).to have_text(plan.title) + end + end + + scenario "User downloads their plan as PDF" do + within("#plan_#{plan.id}") do + click_button("Actions") + click_link "Download" + end + select("pdf") + click_button "Download Plan" + expect(page.source).to have_text(plan.title) + end + + scenario "User downloads their plan as CSV" do + within("#plan_#{plan.id}") do + click_button("Actions") + click_link "Download" + end + select("csv") + click_button "Download Plan" + expect(page.source).to have_text(plan.title) + end + + scenario "User downloads their plan as text" do + within("#plan_#{plan.id}") do + click_button("Actions") + click_link "Download" + end + select("text") + click_button "Download Plan" + expect(page.source).to have_text(plan.title) + end + + scenario "User downloads their plan as docx" do + within("#plan_#{plan.id}") do + click_button("Actions") + click_link "Download" + end + select("docx") + click_button "Download Plan" + expect(page.source).to have_text(plan.title) + end +end diff --git a/spec/features/super_admins/org_swaps_spec.rb b/spec/features/super_admins/org_swaps_spec.rb new file mode 100644 index 0000000..704b078 --- /dev/null +++ b/spec/features/super_admins/org_swaps_spec.rb @@ -0,0 +1,33 @@ +require "rails_helper" + +RSpec.describe "SuperAdmins OrgSwaps", type: :feature, js: true do + + before do + @org1, @org2 = *create_list(:org, 2) + end + + scenario "Org admin attempts to change to new org" do + @user = create(:user, :org_admin, org: @org1) + sign_in(@user) + click_link "Admin" + click_link "Templates" + expect(page).not_to have_text('Change affiliation') + end + + scenario "Super admin changes to new org" do + @user = create(:user, :super_admin, org: @org1) + sign_in(@user) + click_link "Admin" + click_link "Templates" + find('[aria-describedby="label-id-superadmin_user_org_name"]').click + fill_in(:superadmin_user_org_name, with: @org2.name[0..4]) + find("#suggestion-2-0").click + click_button "Change affiliation" + expect(current_path).to eql(org_admin_templates_path) + expect(page).to have_text(@org2.name) + expect(page).not_to have_text(@org1.name) + expect(@user.reload.org).to eql(@org2) + end + +end +