diff --git a/decidim-core/app/models/decidim/component.rb b/decidim-core/app/models/decidim/component.rb index 04a82f959917..e6e2b7dcf574 100644 --- a/decidim-core/app/models/decidim/component.rb +++ b/decidim-core/app/models/decidim/component.rb @@ -11,6 +11,7 @@ class Component < ApplicationRecord include Loggable include Decidim::ShareableWithToken include ScopableComponent + include Decidim::Templates::Templatable if defined? Decidim::Templates::Templatable belongs_to :participatory_space, polymorphic: true diff --git a/decidim-proposals/app/views/decidim/proposals/admin/proposal_answers/_form.html.erb b/decidim-proposals/app/views/decidim/proposals/admin/proposal_answers/_form.html.erb index 279585ff412b..a26372c01036 100644 --- a/decidim-proposals/app/views/decidim/proposals/admin/proposal_answers/_form.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/admin/proposal_answers/_form.html.erb @@ -5,6 +5,10 @@
+ <% if defined?(Decidim::Templates) %> + <%= render "decidim/templates/admin/proposal_answer_templates/template_chooser", form: f %> + <% end %> +
<%= f.collection_radio_buttons :internal_state, [["not_answered", t(".not_answered")], ["accepted", t(".accepted")], ["rejected", t(".rejected")], ["evaluating", t(".evaluating")]], :first, :last, prompt: true %>
diff --git a/decidim-templates/app/commands/decidim/templates/admin/copy_proposal_answer_template.rb b/decidim-templates/app/commands/decidim/templates/admin/copy_proposal_answer_template.rb new file mode 100644 index 000000000000..1942e4446fd7 --- /dev/null +++ b/decidim-templates/app/commands/decidim/templates/admin/copy_proposal_answer_template.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Decidim + module Templates + # A command with all the business logic when duplicating a proposal's answer template + module Admin + class CopyProposalAnswerTemplate < Rectify::Command + def initialize(template) + @template = template + end + + def call + return broadcast(:invalid) unless @template.valid? + + Template.transaction do + copy_template + end + + broadcast(:ok, @copied_template) + end + + def copy_template + @copied_template = Template.create!( + organization: @template.organization, + name: @template.name, + description: @template.description, + target: :proposal_answer, + field_values: @template.field_values, + templatable: @template.templatable + ) + end + end + end + end +end diff --git a/decidim-templates/app/commands/decidim/templates/admin/copy_questionnaire_template.rb b/decidim-templates/app/commands/decidim/templates/admin/copy_questionnaire_template.rb index d8379d196abf..909dba99c3d3 100644 --- a/decidim-templates/app/commands/decidim/templates/admin/copy_questionnaire_template.rb +++ b/decidim-templates/app/commands/decidim/templates/admin/copy_questionnaire_template.rb @@ -39,7 +39,8 @@ def copy_template @copied_template = Template.create!( organization: @template.organization, name: @template.name, - description: @template.description + description: @template.description, + target: :questionnaire ) @resource = Decidim::Forms::Questionnaire.create!( @template.templatable.dup.attributes.merge( diff --git a/decidim-templates/app/commands/decidim/templates/admin/create_proposal_answer_template.rb b/decidim-templates/app/commands/decidim/templates/admin/create_proposal_answer_template.rb new file mode 100644 index 000000000000..6b79d3fe8d3b --- /dev/null +++ b/decidim-templates/app/commands/decidim/templates/admin/create_proposal_answer_template.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Decidim + module Templates + module Admin + class CreateProposalAnswerTemplate < Rectify::Command + # Initializes the command. + # + # form - The source for this ProposalAnswerTemplate. + def initialize(form) + @form = form + end + + def call + return broadcast(:invalid) unless @form.valid? + + @template = Decidim.traceability.create!( + Template, + @form.current_user, + name: @form.name, + description: @form.description, + organization: @form.current_organization, + field_values: { internal_state: @form.internal_state }, + target: :proposal_answer + ) + + resource = identify_templateable_resource + @template.update!(templatable: resource) + + broadcast(:ok, @template) + end + + private + + def identify_templateable_resource + resource = @form.scope_for_availability.split("-") + case resource.first + when "organizations" + @form.current_organization + when "components" + component = Decidim::Component.find_by(id: resource.last) + component&.participatory_space&.decidim_organization_id == @form.current_organization.id ? component : nil + end + end + end + end + end +end diff --git a/decidim-templates/app/commands/decidim/templates/admin/create_questionnaire_template.rb b/decidim-templates/app/commands/decidim/templates/admin/create_questionnaire_template.rb index 24632e30c6d8..bbf5e10a0530 100644 --- a/decidim-templates/app/commands/decidim/templates/admin/create_questionnaire_template.rb +++ b/decidim-templates/app/commands/decidim/templates/admin/create_questionnaire_template.rb @@ -20,7 +20,8 @@ def call @form.current_user, name: @form.name, description: @form.description, - organization: @form.current_organization + organization: @form.current_organization, + target: :questionnaire ) @questionnaire = Decidim::Forms::Questionnaire.create!(questionnaire_for: @template) diff --git a/decidim-templates/app/commands/decidim/templates/admin/destroy_questionnaire_template.rb b/decidim-templates/app/commands/decidim/templates/admin/destroy_questionnaire_template.rb new file mode 100644 index 000000000000..8df5e09fcd3a --- /dev/null +++ b/decidim-templates/app/commands/decidim/templates/admin/destroy_questionnaire_template.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Decidim + module Templates + module Admin + class DestroyQuestionnaireTemplate < DestroyTemplate + protected + + def destroy_template + Decidim.traceability.perform_action!( + :delete, + template, + current_user + ) do + template.destroy! + template.templatable.destroy + end + end + end + end + end +end diff --git a/decidim-templates/app/commands/decidim/templates/admin/destroy_template.rb b/decidim-templates/app/commands/decidim/templates/admin/destroy_template.rb index 1d692955ce57..000ef22dd968 100644 --- a/decidim-templates/app/commands/decidim/templates/admin/destroy_template.rb +++ b/decidim-templates/app/commands/decidim/templates/admin/destroy_template.rb @@ -22,7 +22,7 @@ def call broadcast(:ok) end - private + protected attr_reader :template, :current_user diff --git a/decidim-templates/app/commands/decidim/templates/admin/update_proposal_answer_template.rb b/decidim-templates/app/commands/decidim/templates/admin/update_proposal_answer_template.rb new file mode 100644 index 000000000000..064c9e4c8c3a --- /dev/null +++ b/decidim-templates/app/commands/decidim/templates/admin/update_proposal_answer_template.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Decidim + module Templates + module Admin + class UpdateProposalAnswerTemplate < Rectify::Command + # Initializes the command. + # + # template - The Template to update. + # form - The form object containing the data to update. + # user - The user that updates the template. + def initialize(template, form, user) + @template = template + @form = form + @user = user + end + + def call + return broadcast(:invalid) unless @form.valid? + return broadcast(:invalid) unless @user.organization == @template.organization + + @template = Decidim.traceability.update!( + @template, + @user, + name: @form.name, + description: @form.description, + field_values: { internal_state: @form.internal_state }, + target: :proposal_answer + ) + + resource = identify_templateable_resource + @template.update!(templatable: resource) + + broadcast(:ok, @template) + end + + private + + def identify_templateable_resource + resource = @form.scope_for_availability.split("-") + case resource.first + when "organizations" + @form.current_organization + when "components" + component = Decidim::Component.find_by(id: resource.last) + component&.participatory_space&.decidim_organization_id == @form.current_organization.id ? component : nil + end + end + end + end + end +end diff --git a/decidim-templates/app/commands/decidim/templates/admin/update_template.rb b/decidim-templates/app/commands/decidim/templates/admin/update_questionnaire_template.rb similarity index 93% rename from decidim-templates/app/commands/decidim/templates/admin/update_template.rb rename to decidim-templates/app/commands/decidim/templates/admin/update_questionnaire_template.rb index 94ba7be7a19c..8ac029baa5cb 100644 --- a/decidim-templates/app/commands/decidim/templates/admin/update_template.rb +++ b/decidim-templates/app/commands/decidim/templates/admin/update_questionnaire_template.rb @@ -4,7 +4,7 @@ module Decidim module Templates module Admin # Updates the questionnaire template given form data. - class UpdateTemplate < Rectify::Command + class UpdateQuestionnaireTemplate < Rectify::Command # Initializes the command. # # template - The Template to update. diff --git a/decidim-templates/app/controllers/decidim/templates/admin/application_controller.rb b/decidim-templates/app/controllers/decidim/templates/admin/application_controller.rb index e76405ec6282..417267ceda18 100644 --- a/decidim-templates/app/controllers/decidim/templates/admin/application_controller.rb +++ b/decidim-templates/app/controllers/decidim/templates/admin/application_controller.rb @@ -25,7 +25,8 @@ def permission_class_chain def template_types @template_types ||= { - I18n.t("template_types.questionnaires", scope: "decidim.templates") => decidim_admin_templates.questionnaire_templates_path + I18n.t("template_types.questionnaires", scope: "decidim.templates") => decidim_admin_templates.questionnaire_templates_path, + I18n.t("template_types.proposal_answer_templates", scope: "decidim.templates") => decidim_admin_templates.proposal_answer_templates_path } end end diff --git a/decidim-templates/app/controllers/decidim/templates/admin/concerns/templatable_poposal_answer.rb b/decidim-templates/app/controllers/decidim/templates/admin/concerns/templatable_poposal_answer.rb new file mode 100644 index 000000000000..02e799fa457d --- /dev/null +++ b/decidim-templates/app/controllers/decidim/templates/admin/concerns/templatable_poposal_answer.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module Decidim + module Templates + module Admin + module Concerns + module TemplatablePoposalAnswer + # Common logic to load template-related resources in controller + extend ActiveSupport::Concern + + included do + helper_method :proposal_answers_template_options + + def proposal_answers_template_options; end + end + end + end + end + end +end diff --git a/decidim-templates/app/controllers/decidim/templates/admin/proposal_answer_templates_controller.rb b/decidim-templates/app/controllers/decidim/templates/admin/proposal_answer_templates_controller.rb new file mode 100644 index 000000000000..6d42ef11a6ed --- /dev/null +++ b/decidim-templates/app/controllers/decidim/templates/admin/proposal_answer_templates_controller.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +module Decidim + module Templates + module Admin + class ProposalAnswerTemplatesController < Decidim::Templates::Admin::ApplicationController + include Decidim::TranslatableAttributes + include Decidim::Paginable + + helper_method :availability_option_as_text, :availability_options_for_select + + def new + enforce_permission_to :create, :template + @form = form(ProposalAnswerTemplateForm).instance + end + + def edit + enforce_permission_to :update, :template, template: template + @form = form(ProposalAnswerTemplateForm).from_model(template) + end + + def create + enforce_permission_to :create, :template + + @form = form(ProposalAnswerTemplateForm).from_params(params) + + CreateProposalAnswerTemplate.call(@form) do + on(:ok) do |_template| + flash[:notice] = I18n.t("templates.create.success", scope: "decidim.admin") + redirect_to proposal_answer_templates_path + end + + on(:invalid) do + flash.now[:alert] = I18n.t("templates.create.error", scope: "decidim.admin") + render :new + end + end + end + + def destroy + enforce_permission_to :destroy, :template, template: template + + DestroyTemplate.call(template, current_user) do + on(:ok) do + flash[:notice] = I18n.t("templates.destroy.success", scope: "decidim.admin") + redirect_to action: :index + end + end + end + + def fetch + enforce_permission_to :read, :template, template: template + + response_object = { + state: template.field_values["internal_state"], + template: populate_template_interpolations(proposal) + } + + respond_to do |format| + format.json do + render json: response_object.to_json + end + end + end + + def update + enforce_permission_to :update, :template, template: template + @form = form(ProposalAnswerTemplateForm).from_params(params) + UpdateProposalAnswerTemplate.call(template, @form, current_user) do + on(:ok) do |_questionnaire_template| + flash[:notice] = I18n.t("templates.update.success", scope: "decidim.admin") + redirect_to proposal_answer_templates_path + end + + on(:invalid) do |template| + @template = template + flash.now[:error] = I18n.t("templates.update.error", scope: "decidim.admin") + render action: :edit + end + end + end + + def copy + enforce_permission_to :copy, :template + + CopyProposalAnswerTemplate.call(template) do + on(:ok) do + flash[:notice] = I18n.t("templates.copy.success", scope: "decidim.admin") + redirect_to action: :index + end + + on(:invalid) do + flash[:alert] = I18n.t("templates.copy.error", scope: "decidim.admin") + redirect_to action: :index + end + end + end + + def index + enforce_permission_to :index, :templates + @templates = collection + + respond_to do |format| + format.html { render :index } + format.json do + term = params[:term] + + @templates = search(term) + + render json: @templates.map { |t| { value: t.id, label: translated_attribute(t.name) } } + end + end + end + + private + + def populate_template_interpolations(proposal) + template.description.map do |row| + language = row.first + value = row.last + value.gsub!("%{organization}", proposal.organization.name) + value.gsub!("%{name}", proposal.creator_author.name) + value.gsub!("%{admin}", current_user.name) + + [language, value] + end.to_h + end + + def proposal + @proposal ||= Decidim::Proposals::Proposal.find(params[:proposal_id]) + end + + def availability_option_as_text(template) + return unless template.templatable_type + + key = "#{template.templatable_type.demodulize.tableize}-#{template.templatable_id}" + avaliablity_options[key].presence || t("templates.missing_resource", scope: "decidim.admin") + end + + def availability_options_for_select + avaliablity_options.collect { |key, value| [value, key] }.to_a + end + + def avaliablity_options + @avaliablity_options = {} + Decidim::Component.includes(:participatory_space).where(manifest_name: accepted_components) + .select { |a| a.participatory_space.decidim_organization_id == current_organization.id }.each do |component| + @avaliablity_options["components-#{component.id}"] = formated_name(component) + end + global_scope = { "organizations-#{current_organization.id}" => t("global_scope", scope: "decidim.templates.admin.proposal_answer_templates.index") } + @avaliablity_options = global_scope.merge(Hash[@avaliablity_options.sort_by { |_, val| val }]) + end + + def formated_name(component) + space_type = t(component.participatory_space.class.name.underscore, scope: "activerecord.models", count: 1) + "#{space_type}: #{translated_attribute(component.participatory_space.title)} > #{translated_attribute(component.name)}" + end + + def accepted_components + [:proposals] + end + + def template + @template ||= Template.find_by(id: params[:id]) + end + + def collection + @collection ||= paginate(current_organization.templates.where(target: :proposal_answer).order(:id)) + end + end + end + end +end diff --git a/decidim-templates/app/controllers/decidim/templates/admin/questionnaire_templates_controller.rb b/decidim-templates/app/controllers/decidim/templates/admin/questionnaire_templates_controller.rb index 1fcfdcb4e386..5ee8e73c48d5 100644 --- a/decidim-templates/app/controllers/decidim/templates/admin/questionnaire_templates_controller.rb +++ b/decidim-templates/app/controllers/decidim/templates/admin/questionnaire_templates_controller.rb @@ -76,7 +76,7 @@ def edit def update enforce_permission_to :update, :template, template: template @form = form(TemplateForm).from_params(params) - UpdateTemplate.call(template, @form, current_user) do + UpdateQuestionnaireTemplate.call(template, @form, current_user) do on(:ok) do |questionnaire_template| flash[:notice] = I18n.t("templates.update.success", scope: "decidim.admin") redirect_to edit_questionnaire_template_path(questionnaire_template) @@ -93,7 +93,7 @@ def update def destroy enforce_permission_to :destroy, :template, template: template - DestroyTemplate.call(template, current_user) do + DestroyQuestionnaireTemplate.call(template, current_user) do on(:ok) do flash[:notice] = I18n.t("templates.destroy.success", scope: "decidim.admin") redirect_to action: :index diff --git a/decidim-templates/app/forms/decidim/templates/admin/proposal_answer_template_form.rb b/decidim-templates/app/forms/decidim/templates/admin/proposal_answer_template_form.rb new file mode 100644 index 000000000000..2278a96964d9 --- /dev/null +++ b/decidim-templates/app/forms/decidim/templates/admin/proposal_answer_template_form.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Decidim + module Templates + module Admin + class ProposalAnswerTemplateForm < TemplateForm + attribute :internal_state, String + attribute :scope_for_availability, String + + validates :internal_state, presence: true + + def map_model(model) + self.scope_for_availability = "#{model.templatable_type.try(:demodulize).try(:tableize)}-#{model.templatable_id.to_i}" + (model.field_values || []).to_h.map do |k, v| + self[k.to_sym] = v + end + end + end + end + end +end diff --git a/decidim-templates/app/models/decidim/templates/template.rb b/decidim-templates/app/models/decidim/templates/template.rb index 48cfbbce260a..5bb914c600f9 100644 --- a/decidim-templates/app/models/decidim/templates/template.rb +++ b/decidim-templates/app/models/decidim/templates/template.rb @@ -17,17 +17,11 @@ class Template < ApplicationRecord belongs_to :templatable, foreign_type: "templatable_type", polymorphic: true, optional: true - before_destroy :destroy_templatable - validates :name, presence: true def resource_name [templatable_type.demodulize.tableize.singularize, "templates"].join("_") end - - def destroy_templatable - templatable.destroy - end end end end diff --git a/decidim-templates/app/packs/entrypoints/decidim_templates_admin.js b/decidim-templates/app/packs/entrypoints/decidim_templates_admin.js new file mode 100644 index 000000000000..73f2f27d7b33 --- /dev/null +++ b/decidim-templates/app/packs/entrypoints/decidim_templates_admin.js @@ -0,0 +1 @@ +import "src/decidim/templates/admin/proposal_answer_template_chooser" diff --git a/decidim-templates/app/packs/src/decidim/templates/admin/proposal_answer_template_chooser.js b/decidim-templates/app/packs/src/decidim/templates/admin/proposal_answer_template_chooser.js new file mode 100644 index 000000000000..1f6e53355bbc --- /dev/null +++ b/decidim-templates/app/packs/src/decidim/templates/admin/proposal_answer_template_chooser.js @@ -0,0 +1,21 @@ +$(() => { + $("#proposal_answer_template_chooser").change(function() { + let dropDown = $("#proposal_answer_template_chooser"); + $.getJSON(dropDown.data("url"), { + id: dropDown.val(), + /* eslint camelcase: [0] */ + proposal_id: dropDown.data("proposal") + }).done(function(data) { + $(`#proposal_answer_internal_state_${data.state}`).trigger("click"); + + let $editors = dropDown.parent().parent().find(".tabs-panel").find(".editor-container"); + $editors.each(function(index, element) { + let localElement = $(element); + let $locale = localElement.siblings("input[type=hidden]").attr("id").replace("proposal_answer_answer_", ""); + let editor = Quill.find(element); + let delta = editor.clipboard.convert(data.template[$locale]); + editor.setContents(delta); + }); + }); + }); +}); diff --git a/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/_form.html.erb b/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/_form.html.erb new file mode 100644 index 000000000000..9a1f920f9b21 --- /dev/null +++ b/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/_form.html.erb @@ -0,0 +1,35 @@ +
+
+

<%= t(".template_title") %>

+ <%= form.submit t(".save"), class: "button tiny button--title" %> +
+ +
+
+ <%= form.translated :text_field, :name %> +
+ +
+ <%= form.translated :editor, :description, rows: 15, label: t(".answer_template") %> + + <%= t(".hint").html_safe %> +
    +
  • <%= t(".hint1").html_safe %>
  • +
  • <%= t(".hint2").html_safe %>
  • +
  • <%= t(".hint3").html_safe %>
  • +
+
+ +
+ <%= form.label :internal_state %> + <%= form.collection_radio_buttons :internal_state, + Decidim::Proposals::Proposal::POSSIBLE_STATES - %w(withdrawn), + :to_s, + ->(mode) { t(mode, scope: "decidim.proposals.admin.proposal_answers.form") } %> +
+ +
+ <%= form.select :scope_for_availability, availability_options_for_select, help_text: t(".scope_for_availability_help") %> +
+
+
diff --git a/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/_template_chooser.html.erb b/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/_template_chooser.html.erb new file mode 100644 index 000000000000..3852039ab595 --- /dev/null +++ b/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/_template_chooser.html.erb @@ -0,0 +1,17 @@ +<% templates = Decidim::Templates::Template .where( + target: :proposal_answer, + templatable: [current_organization, current_component] +).order(:templatable_id) %> + +<% if templates.any? %> +
+ <%= javascript_pack_tag "decidim_templates_admin" %> + + +
+<% end %> diff --git a/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/edit.html.erb b/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/edit.html.erb new file mode 100644 index 000000000000..27daf7ef3627 --- /dev/null +++ b/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/edit.html.erb @@ -0,0 +1,3 @@ +<%= decidim_form_for(@form, url: proposal_answer_template_path, html: { class: "form edit_proposal_answer_template" }) do |f| %> + <%= render partial: "form", object: f %> +<% end %> diff --git a/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/index.html.erb b/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/index.html.erb new file mode 100644 index 000000000000..4b34cc192edc --- /dev/null +++ b/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/index.html.erb @@ -0,0 +1,54 @@ +
+
+

+ <%= t ".title" %> + <% if allowed_to?(:create, :template) %> + <%= link_to t("actions.new", scope: "decidim.admin", name: t("template.name", scope: "decidim.models").downcase), [:new, :proposal_answer_template], class: "button tiny button--title new" %> + <% end %> +

+
+
+ <% if @templates.any? %> +
+ + + + + + + + + + + + <% @templates.each do |template| %> + + + + + + + + <% end %> + + +
<%= t("template.name", scope: "decidim.models") %><%= t(".internal_state") %><%= t(".scope_for_availability") %><%= t("template.fields.created_at", scope: "decidim.models") %>
<%= link_to_if allowed_to?(:update, :template, template: template) , translated_attribute(template.name), edit_proposal_answer_template_path(template) %> <%= t(template.field_values.dig("internal_state"), scope: "decidim.proposals.admin.proposal_answers.form") %><%= availability_option_as_text(template) %><%= l template.created_at, format: :long %> + <% if allowed_to?(:update, :template, template: template) %> + <%= icon_link_to "pencil", edit_proposal_answer_template_path(template), t("actions.edit", scope: "decidim.admin"), class: "edit" %> + <% end %> + + <% if allowed_to?(:copy, :template, template: template) %> + <%= icon_link_to "clipboard", copy_proposal_answer_template_path(template), t("actions.duplicate", scope: "decidim.admin"), method: :post %> + <% end %> + + <% if allowed_to?(:destroy, :template, template: template) %> + <%= icon_link_to "circle-x", proposal_answer_template_path(template), t("actions.destroy", scope: "decidim.admin"), method: :delete, data: { confirm: t(".confirm_delete") }, class: "action-icon--remove" %> + <% end %> +
+ <%= paginate @templates, theme: "decidim" %> +
+ <% else %> + <%= t("templates.empty", scope: "decidim.admin") %> + <% end %> +
+
diff --git a/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/new.html.erb b/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/new.html.erb new file mode 100644 index 000000000000..3be1fd1edb83 --- /dev/null +++ b/decidim-templates/app/views/decidim/templates/admin/proposal_answer_templates/new.html.erb @@ -0,0 +1,7 @@ +<% content_for :title do %> + <%= t("templates", scope: "decidim.admin.titles") %> +<% end %> + +<%= decidim_form_for(@form, url: proposal_answer_templates_path, html: { class: "form new_proposal_answer_template" }) do |f| %> + <%= render partial: "form", object: f %> +<% end %> diff --git a/decidim-templates/config/assets.rb b/decidim-templates/config/assets.rb index 7c60dfefc6a8..b755f0cdad02 100644 --- a/decidim-templates/config/assets.rb +++ b/decidim-templates/config/assets.rb @@ -4,5 +4,6 @@ Decidim::Webpacker.register_path("#{base_path}/app/packs") Decidim::Webpacker.register_entrypoints( - decidim_templates: "#{base_path}/app/packs/entrypoints/decidim_templates.js" + decidim_templates: "#{base_path}/app/packs/entrypoints/decidim_templates.js", + decidim_templates_admin: "#{base_path}/app/packs/entrypoints/decidim_templates_admin.js" ) diff --git a/decidim-templates/config/locales/en.yml b/decidim-templates/config/locales/en.yml index 5682ec36398b..a97337b0c834 100644 --- a/decidim-templates/config/locales/en.yml +++ b/decidim-templates/config/locales/en.yml @@ -7,6 +7,7 @@ en: template: description: Description name: Name + scope_for_availability: Restrict availability to the component decidim: admin: menu: @@ -24,6 +25,7 @@ en: destroy: success: Template deleted successfully empty: There are no templates yet. + missing_resource: "(missing resource)" update: error: There was a problem updating this template. success: Template updated successfully @@ -42,6 +44,24 @@ en: name: Template templates: admin: + proposal_answer_templates: + form: + answer_template: Answer template + hint: "Hint: You can use these variables anywhere on the answer template that will be replaced when using the template" + hint1: "%{organization} will be replaced by the organization's name" + hint2: "%{name} will be replaced by the author's name" + hint3: "%{admin} will be replaced by the admin's name (the one answering the proposal)" + save: Save + scope_for_availability_help: Note that only participatory spaces having components of the type "proposals" will be listed. + template_title: Template information + index: + confirm_delete: Are you sure you want to delete this template? + global_scope: Global (available everywhere) + internal_state: Internal State + scope_for_availability: Scope + title: Proposal answers + template_chooser: + select_template: Select a template answer questionnaire_templates: choose: create_from_template: Create from template @@ -65,4 +85,5 @@ en: of_total_steps: of %{total_steps} tos_agreement: By participating you accept its Terms of Service template_types: + proposal_answer_templates: Proposal Answers questionnaires: Questionnaires diff --git a/decidim-templates/db/migrate/20221006055954_add_field_values_to_decidim_templates.rb b/decidim-templates/db/migrate/20221006055954_add_field_values_to_decidim_templates.rb new file mode 100644 index 000000000000..0d129239f85a --- /dev/null +++ b/decidim-templates/db/migrate/20221006055954_add_field_values_to_decidim_templates.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddFieldValuesToDecidimTemplates < ActiveRecord::Migration[6.0] + def change + add_column :decidim_templates_templates, :field_values, :json, default: {} + end +end diff --git a/decidim-templates/db/migrate/20221006184809_add_target_to_decidim_templates_templates.rb b/decidim-templates/db/migrate/20221006184809_add_target_to_decidim_templates_templates.rb new file mode 100644 index 000000000000..e4e1ffba58b8 --- /dev/null +++ b/decidim-templates/db/migrate/20221006184809_add_target_to_decidim_templates_templates.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddTargetToDecidimTemplatesTemplates < ActiveRecord::Migration[6.0] + def change + add_column :decidim_templates_templates, :target, :string + end +end diff --git a/decidim-templates/db/migrate/20221006184905_migrate_templateable.rb b/decidim-templates/db/migrate/20221006184905_migrate_templateable.rb new file mode 100644 index 000000000000..0034e8821aa0 --- /dev/null +++ b/decidim-templates/db/migrate/20221006184905_migrate_templateable.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class MigrateTemplateable < ActiveRecord::Migration[6.0] + def self.up + Decidim::Templates::Template.find_each do |template| + template.update(target: template.templatable_type.demodulize.tableize.singularize) + end + end + + def self.down; end +end diff --git a/decidim-templates/lib/decidim/templates/admin_engine.rb b/decidim-templates/lib/decidim/templates/admin_engine.rb index 314483e1c1ed..f0d7fb74cdf7 100644 --- a/decidim-templates/lib/decidim/templates/admin_engine.rb +++ b/decidim-templates/lib/decidim/templates/admin_engine.rb @@ -11,6 +11,14 @@ class AdminEngine < ::Rails::Engine routes do ## Routes for Questionnaire Templates + resources :proposal_answer_templates do + member do + post :copy + end + collection do + get :fetch + end + end resources :questionnaire_templates do member do post :copy diff --git a/decidim-templates/lib/decidim/templates/test/factories.rb b/decidim-templates/lib/decidim/templates/test/factories.rb index ed94c23d09fe..a96618a0adbd 100644 --- a/decidim-templates/lib/decidim/templates/test/factories.rb +++ b/decidim-templates/lib/decidim/templates/test/factories.rb @@ -9,6 +9,12 @@ templatable { build(:dummy_resource) } name { Decidim::Faker::Localized.sentence } + trait :proposal_answer do + templatable { organization } + target { :proposal_answer } + field_values { { internal_state: :accepted } } + end + ## Questionnaire templates factory :questionnaire_template do trait :with_questions do diff --git a/decidim-templates/spec/commands/decidim/templates/admin/copy_proposal_answer_template_spec.rb b/decidim-templates/spec/commands/decidim/templates/admin/copy_proposal_answer_template_spec.rb new file mode 100644 index 000000000000..cc1156b89fa8 --- /dev/null +++ b/decidim-templates/spec/commands/decidim/templates/admin/copy_proposal_answer_template_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Templates + module Admin + describe CopyProposalAnswerTemplate do + let(:template) { create(:template, :proposal_answer) } + + describe "when the template is invalid" do + before do + template.update(name: nil) + end + + it "broadcasts invalid" do + expect { described_class.call(template) }.to broadcast(:invalid) + end + end + + describe "when the template is valid" do + let(:destination_template) do + events = described_class.call(template) + # events => { :ok => copied_template } + expect(events).to have_key(:ok) + events[:ok] + end + + it "applies template attributes to the questionnaire" do + expect(destination_template.name).to eq(template.name) + expect(destination_template.description).to eq(template.description) + expect(destination_template.field_values).to eq(template.field_values) + expect(destination_template.templatable).to eq(template.templatable) + expect(destination_template.target).to eq(template.target) + end + end + end + end + end +end diff --git a/decidim-templates/spec/commands/decidim/templates/admin/destroy_questionnaire_template_spec.rb b/decidim-templates/spec/commands/decidim/templates/admin/destroy_questionnaire_template_spec.rb new file mode 100644 index 000000000000..a3a0dfe00d5f --- /dev/null +++ b/decidim-templates/spec/commands/decidim/templates/admin/destroy_questionnaire_template_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Templates + module Admin + describe DestroyQuestionnaireTemplate do + let(:template) { create(:questionnaire_template) } + let(:admin) { create(:user, :admin) } + let!(:templatable) { template.templatable } + + it "destroy the templatable" do + described_class.call(template, admin) + expect { templatable.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "destroy the template" do + described_class.call(template, admin) + expect { template.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + end +end diff --git a/decidim-templates/spec/commands/decidim/templates/admin/destroy_template_spec.rb b/decidim-templates/spec/commands/decidim/templates/admin/destroy_template_spec.rb new file mode 100644 index 000000000000..59a67f238c28 --- /dev/null +++ b/decidim-templates/spec/commands/decidim/templates/admin/destroy_template_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Templates + module Admin + describe DestroyTemplate do + let(:template) { create(:questionnaire_template) } + let(:admin) { create(:user, :admin) } + let!(:templatable) { template.templatable } + + it "destroy the template" do + described_class.call(template, admin) + expect { template.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + end +end diff --git a/decidim-templates/spec/forms/decidim/templates/admin/proposal_answer_template_form_spec.rb b/decidim-templates/spec/forms/decidim/templates/admin/proposal_answer_template_form_spec.rb new file mode 100644 index 000000000000..622ad8d4a619 --- /dev/null +++ b/decidim-templates/spec/forms/decidim/templates/admin/proposal_answer_template_form_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Templates + module Admin + describe ProposalAnswerTemplateForm do + subject do + described_class.from_params(attributes).with_context( + current_organization: current_organization + ) + end + + let(:current_organization) { create(:organization) } + + let(:name) do + { + "en" => name_english, + "ca" => "Nom", + "es" => "Nombre" + } + end + + let(:description) do + { + "en" => "

Content

", + "ca" => "

Contingut

", + "es" => "

Contenido

" + } + end + + let(:internal_state) { :accepted } + + let(:name_english) { "Name" } + + let(:attributes) do + { + "name" => name, + "description" => description, + "internal_state" => internal_state + } + end + + context "when everything is OK" do + it { is_expected.to be_valid } + end + + context "when name is not valid" do + let(:name_english) { "" } + + it { is_expected.not_to be_valid } + end + + context "when internal_state is not valid" do + let(:internal_state) { "" } + + it { is_expected.not_to be_valid } + end + end + end + end +end diff --git a/decidim-templates/spec/forms/decidim/templates/admin/questionnaire_form_spec.rb b/decidim-templates/spec/forms/decidim/templates/admin/template_form_spec.rb similarity index 100% rename from decidim-templates/spec/forms/decidim/templates/admin/questionnaire_form_spec.rb rename to decidim-templates/spec/forms/decidim/templates/admin/template_form_spec.rb diff --git a/decidim-templates/spec/models/decidim/templates/template_spec.rb b/decidim-templates/spec/models/decidim/templates/template_spec.rb index edc3f35122ee..cbe58b813391 100644 --- a/decidim-templates/spec/models/decidim/templates/template_spec.rb +++ b/decidim-templates/spec/models/decidim/templates/template_spec.rb @@ -27,14 +27,14 @@ module Templates expect(subject.templatable).to be_a(Decidim::DummyResources::DummyResource) end - describe "on destroy" do - let(:templatable) { template.templatable } - - it "destroys the templatable" do - template.destroy! - expect { templatable.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end + # describe "on destroy" do + # let(:templatable) { template.templatable } + # + # it "destroys the templatable" do + # template.destroy! + # expect { templatable.reload }.to raise_error(ActiveRecord::RecordNotFound) + # end + # end describe "#resource_name" do it "returns the templatable model name without namespace, downcased and postfixed with _templates" do diff --git a/decidim-templates/spec/system/admin/admin_manages_proposal_answer_templates_spec.rb b/decidim-templates/spec/system/admin/admin_manages_proposal_answer_templates_spec.rb new file mode 100644 index 000000000000..498258b20b41 --- /dev/null +++ b/decidim-templates/spec/system/admin/admin_manages_proposal_answer_templates_spec.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/proposals/test/factories" + +describe "Admin manages proposal answer templates", type: :system do + let!(:organization) { create :organization } + let!(:user) { create :user, :admin, :confirmed, organization: organization } + + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin_templates.proposal_answer_templates_path + end + + describe "listing templates" do + let!(:template) { create(:template, :proposal_answer, organization: organization) } + + before do + visit decidim_admin_templates.proposal_answer_templates_path + end + + it "shows a table with the templates info" do + within ".questionnaire-templates" do + expect(page).to have_i18n_content(template.name) + expect(page).to have_i18n_content("Global (available everywhere)") + end + end + + context "when a template is scoped to an invalid resource" do + let!(:template) { create(:template, :proposal_answer, organization: organization, templatable: create(:dummy_resource)) } + + it "shows a table info about the invalid resource" do + within ".questionnaire-templates" do + expect(page).to have_i18n_content(template.name) + expect(page).to have_i18n_content("(missing resource)") + end + end + end + end + + describe "creating a proposal_answer_template" do + let(:participatory_process) { create :participatory_process, title: { en: "A participatory process" }, organization: organization } + let!(:proposals_component) { create :component, manifest_name: :proposals, name: { en: "A component" }, participatory_space: participatory_process } + + before do + within ".layout-content" do + click_link("New") + end + end + + shared_examples "creates a new template with scopes" do |scope_name| + it "creates a new template" do + within ".new_proposal_answer_template" do + fill_in_i18n( + :proposal_answer_template_name, + "#proposal_answer_template-name-tabs", + en: "My template", + es: "Mi plantilla", + ca: "La meva plantilla" + ) + fill_in_i18n_editor( + :proposal_answer_template_description, + "#proposal_answer_template-description-tabs", + en: "Description", + es: "DescripciĆ³n", + ca: "DescripciĆ³" + ) + + choose "Not answered" + select scope_name, from: :proposal_answer_template_scope_for_availability + + page.find("*[type=submit]").click + end + + expect(page).to have_admin_callout("successfully") + expect(page).to have_current_path decidim_admin_templates.proposal_answer_templates_path + within ".questionnaire-templates" do + expect(page).to have_i18n_content(scope_name) + expect(page).to have_content("My template") + end + end + end + + it_behaves_like "creates a new template with scopes", "Global (available everywhere)" + it_behaves_like "creates a new template with scopes", "Participatory process: A participatory process > A component" + end + + describe "updating a template" do + let!(:template) { create(:template, :proposal_answer, organization: organization) } + let(:participatory_process) { create :participatory_process, title: { en: "A participatory process" }, organization: organization } + let!(:proposals_component) { create :component, manifest_name: :proposals, name: { en: "A component" }, participatory_space: participatory_process } + + before do + visit decidim_admin_templates.proposal_answer_templates_path + click_link translated(template.name) + end + + shared_examples "updates a template with scopes" do |scope_name| + it "updates a template" do + fill_in_i18n( + :proposal_answer_template_name, + "#proposal_answer_template-name-tabs", + en: "My new name", + es: "Mi nuevo nombre", + ca: "El meu nou nom" + ) + + select scope_name, from: :proposal_answer_template_scope_for_availability + + within ".edit_proposal_answer_template" do + page.find("*[type=submit]").click + end + + expect(page).to have_admin_callout("successfully") + expect(page).to have_current_path decidim_admin_templates.proposal_answer_templates_path + within ".questionnaire-templates" do + expect(page).to have_i18n_content(scope_name) + expect(page).to have_content("My new name") + end + end + end + + it_behaves_like "updates a template with scopes", "Global (available everywhere)" + it_behaves_like "updates a template with scopes", "Participatory process: A participatory process > A component" + end + + describe "updating a template with invalid values" do + let!(:template) { create(:template, :proposal_answer, organization: organization) } + + before do + visit decidim_admin_templates.proposal_answer_templates_path + click_link translated(template.name) + end + + it "does not update the template" do + fill_in_i18n( + :proposal_answer_template_name, + "#proposal_answer_template-name-tabs", + en: "", + es: "", + ca: "" + ) + + within ".edit_proposal_answer_template" do + find("*[type=submit]").click + end + + expect(page).to have_admin_callout("problem") + end + end + + describe "copying a template" do + let!(:template) { create(:template, :proposal_answer, organization: organization) } + + before do + visit decidim_admin_templates.proposal_answer_templates_path + end + + it "copies the template" do + within find("tr", text: translated(template.name)) do + click_link "Duplicate" + end + + expect(page).to have_admin_callout("successfully") + expect(page).to have_content(template.name["en"], count: 2) + end + end + + describe "destroying a template" do + let!(:template) { create(:template, :proposal_answer, organization: organization) } + + before do + visit decidim_admin_templates.proposal_answer_templates_path + end + + it "destroys the template" do + within find("tr", text: translated(template.name)) do + accept_confirm { click_link "Delete" } + end + + expect(page).to have_admin_callout("successfully") + expect(page).to have_no_i18n_content(template.name) + end + end + + describe "using a proposal_answer_template" do + let(:participatory_process) { create :participatory_process, title: { en: "A participatory process" }, organization: organization } + let!(:component) { create :component, manifest_name: :proposals, name: { en: "A component" }, participatory_space: participatory_process } + + let(:description) { "Some meaningful answer" } + let(:values) do + { internal_state: "rejected" } + end + let!(:template) { create(:template, :proposal_answer, description: { en: description }, field_values: values, organization: organization, templatable: component) } + let!(:proposal) { create(:proposal, component: component) } + + before do + visit Decidim::EngineRouter.admin_proxy(component).root_path + find("a", class: "action-icon--show-proposal").click + end + + it "uses the template" do + within ".edit_proposal_answer" do + select template.name["en"], from: :proposal_answer_template_chooser + expect(page).to have_content(description) + click_button "Answer" + end + + expect(page).to have_admin_callout("Proposal successfully answered") + + within find("tr", text: proposal.title["en"]) do + expect(page).to have_content("Rejected") + end + end + end +end