diff --git a/Dockerfile b/Dockerfile index 5a070110..d9ea5e45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -68,6 +68,7 @@ COPY ./lib ./lib COPY ./public ./public COPY ./tmp ./tmp COPY LICENSE README.md Rakefile config.ru .version ./ +COPY .version ./public/version COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index e0de0b4c..08b2ed02 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -93,7 +93,7 @@ module Api end def destroy - if params[:permanently] == 'true' + if params[:permanently].in?(['true', true]) @submission.destroy! else @submission.update!(archived_at: Time.current) diff --git a/app/controllers/api/templates_clone_controller.rb b/app/controllers/api/templates_clone_controller.rb index f869fa28..e7e1db7e 100644 --- a/app/controllers/api/templates_clone_controller.rb +++ b/app/controllers/api/templates_clone_controller.rb @@ -23,7 +23,9 @@ module Api cloned_template.source = :api cloned_template.save! - schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template) + schema_documents = Templates::CloneAttachments.call(template: cloned_template, + original_template: @template, + documents: params[:documents]) WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url| SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id, diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index e922102a..94d2c8f3 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -74,7 +74,7 @@ module Api end def destroy - if params[:permanently] == 'true' + if params[:permanently].in?(['true', true]) @template.destroy! else @template.update!(archived_at: Time.current) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index d2c20653..1dac63bd 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -61,7 +61,7 @@ class SubmissionsController < ApplicationController def destroy notice = - if params[:permanently].present? + if params[:permanently].in?(['true', true]) @submission.destroy! I18n.t('submission_has_been_removed') diff --git a/app/controllers/submissions_dashboard_controller.rb b/app/controllers/submissions_dashboard_controller.rb index f71e10b5..3403d22c 100644 --- a/app/controllers/submissions_dashboard_controller.rb +++ b/app/controllers/submissions_dashboard_controller.rb @@ -15,9 +15,6 @@ class SubmissionsDashboardController < ApplicationController @submissions = Submissions.search(@submissions, params[:q], search_template: true) @submissions = Submissions::Filter.call(@submissions, current_user, params) - @submissions = @submissions.pending if params[:status] == 'pending' - @submissions = @submissions.completed if params[:status] == 'completed' - @submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? @submissions.order(Submitter.arel_table[:completed_at].maximum.desc) else diff --git a/app/controllers/submissions_filters_controller.rb b/app/controllers/submissions_filters_controller.rb index bd5eae1e..b0298d4c 100644 --- a/app/controllers/submissions_filters_controller.rb +++ b/app/controllers/submissions_filters_controller.rb @@ -4,6 +4,7 @@ class SubmissionsFiltersController < ApplicationController ALLOWED_NAMES = %w[ author completed_at + status created_at ].freeze diff --git a/app/controllers/template_folders_controller.rb b/app/controllers/template_folders_controller.rb index dd7010bb..a3418015 100644 --- a/app/controllers/template_folders_controller.rb +++ b/app/controllers/template_folders_controller.rb @@ -4,7 +4,8 @@ class TemplateFoldersController < ApplicationController load_and_authorize_resource :template_folder def show - @templates = @template_folder.templates.active.preload(:author, :template_accesses).order(id: :desc) + @templates = @template_folder.templates.active.accessible_by(current_ability) + .preload(:author, :template_accesses).order(id: :desc) @templates = Templates.search(@templates, params[:q]) @pagy, @templates = pagy(@templates, limit: 12) diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 1b52c23b..c023a5e7 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -9,12 +9,11 @@ class TemplatesController < ApplicationController submissions = @template.submissions.accessible_by(current_ability) submissions = submissions.active if @template.archived_at.blank? submissions = Submissions.search(submissions, params[:q], search_values: true) - submissions = Submissions::Filter.call(submissions, current_user, params) + submissions = Submissions::Filter.call(submissions, current_user, params.except(:status)) @base_submissions = submissions - submissions = submissions.pending if params[:status] == 'pending' - submissions = submissions.completed if params[:status] == 'completed' + submissions = Submissions::Filter.filter_by_status(submissions, params) submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? submissions.order(Submitter.arel_table[:completed_at].maximum.desc) @@ -93,7 +92,7 @@ class TemplatesController < ApplicationController def destroy notice = - if params[:permanently].present? + if params[:permanently].in?(['true', true]) @template.destroy! I18n.t('template_has_been_removed') diff --git a/app/controllers/templates_dashboard_controller.rb b/app/controllers/templates_dashboard_controller.rb index 3a6a66df..7c806fa9 100644 --- a/app/controllers/templates_dashboard_controller.rb +++ b/app/controllers/templates_dashboard_controller.rb @@ -9,7 +9,9 @@ class TemplatesDashboardController < ApplicationController FOLDERS_PER_PAGE = 18 def index - @template_folders = filter_template_folders(@template_folders) + @template_folders = @template_folders.where(id: @templates.active.select(:folder_id)).order(id: :desc) + + @template_folders = TemplateFolders.search(@template_folders, params[:q]) @pagy, @template_folders = pagy( @template_folders, @@ -36,14 +38,6 @@ class TemplatesDashboardController < ApplicationController private - def filter_template_folders(template_folders) - rel = template_folders.joins(:active_templates) - .order(id: :desc) - .distinct - - TemplateFolders.search(rel, params[:q]) - end - def filter_templates(templates) rel = templates.active.preload(:author, :template_accesses).order(id: :desc) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index d11e9ba8..4a195104 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -24,14 +24,10 @@ class UsersController < ApplicationController def edit; end def create - existing_user = User.accessible_by(current_ability).find_by(email: @user.email) + if User.accessible_by(current_ability).exists?(email: @user.email) + @user.errors.add(:email, I18n.t('already_exists')) - if existing_user - existing_user.archived_at = nil - existing_user.assign_attributes(user_params) - existing_user.account = current_account - - @user = existing_user + return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity end @user.role = User::ADMIN_ROLE unless role_valid?(@user.role) @@ -83,14 +79,7 @@ class UsersController < ApplicationController end def build_user - @user = current_account.users.find_by(email: user_params[:email])&.tap do |user| - user.assign_attributes(user_params) - user.archived_at = nil - end - - @user ||= current_account.users.new(user_params) - - @user + @user = current_account.users.new(user_params) end def user_params diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index 2c3fc5d4..d3f7015e 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -74,7 +74,7 @@ ID: {{ signature.uuid }}
- {{ t('reason') }}: {{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }} + {{ t('reason') }}: {{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }} @@ -168,6 +168,7 @@
@@ -36,7 +36,7 @@ @change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]" >
- <%= t('reason') %>: <%= submitter.values[field.dig('preferences', 'reason_field_uuid')].presence || t('digitally_signed_by') %> <%= submitter.name %> + <% reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence %> + <% if reason_value %><%= t('reason') %>: <% end %><%= reason_value || t('digitally_signed_by') %> <%= submitter.name %> <% if submitter.email %> <<%= submitter.email %>> <% end %> @@ -49,7 +50,7 @@ <% end %> <% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %> <% cell_width = area['cell_w'] / area['w'] * 100 %> -
+
<% (0..(area['w'] / area['cell_w']).ceil).each do |index| %> <% if value[index] %>
<%= value[index] %>
diff --git a/app/views/submissions_filters/_applied_filters.html.erb b/app/views/submissions_filters/_applied_filters.html.erb index ad988d71..f1cdb070 100644 --- a/app/views/submissions_filters/_applied_filters.html.erb +++ b/app/views/submissions_filters/_applied_filters.html.erb @@ -1,4 +1,26 @@ -<% query_params = params.permit(:q, :status).merge(filter_params) %> +<% query_params = params.permit(:q).merge(filter_params) %> +<% if icon = { 'declined' => 'x_circle', 'expired' => 'clock_cancel', 'partially_completed' => 'clock_edit' }[params[:status]] %> +
+ <%= link_to submissions_filter_path('status', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %> + <%= svg_icon(icon, class: 'w-5 h-5 shrink-0') %> + <%= t(params[:status]) %> + <% end %> + <%= link_to url_for(params.to_unsafe_h.except(:status)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> + <%= svg_icon('x', class: 'w-5 h-5') %> + <% end %> +
+<% end %> +<% if params[:author].present? %> +
+ <%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %> + <%= svg_icon('user', class: 'w-5 h-5 shrink-0') %> + <%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %> + <% end %> + <%= link_to url_for(params.to_unsafe_h.except(:author)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> + <%= svg_icon('x', class: 'w-5 h-5') %> + <% end %> +
+<% end %> <% if query_params[:completed_at_from].present? || query_params[:completed_at_to].present? %>
<%= link_to submissions_filter_path('completed_at', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %> @@ -37,14 +59,3 @@ <% end %>
<% end %> -<% if params[:author].present? %> -
- <%= link_to submissions_filter_path('author', query_params.merge(path: url_for, with_remove: true)), data: { turbo_frame: 'modal' }, class: 'flex items-center space-x-1 w-full pr-1 md:max-w-[140px]' do %> - <%= svg_icon('user', class: 'w-5 h-5 shrink-0') %> - <%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %> - <% end %> - <%= link_to url_for(params.to_unsafe_h.except(:author)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> - <%= svg_icon('x', class: 'w-5 h-5') %> - <% end %> -
-<% end %> diff --git a/app/views/submissions_filters/_filter_button.html.erb b/app/views/submissions_filters/_filter_button.html.erb index b840729d..3f24aaf6 100644 --- a/app/views/submissions_filters/_filter_button.html.erb +++ b/app/views/submissions_filters/_filter_button.html.erb @@ -1,8 +1,8 @@ -<% query_params = params.permit(:q, :status).merge(filter_params) %> +<% query_params = params.permit(:q).merge(filter_params) %> <% if params[:with_remove] %>
- <%= link_to t('remove_filter'), "#{params[:path]}?#{params.to_unsafe_h.slice(:q, :status).merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %> + <%= link_to t('remove_filter'), "#{params[:path]}?#{params.to_unsafe_h.slice(:q).merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
<% end %> <% end %> diff --git a/app/views/submissions_filters/status.html.erb b/app/views/submissions_filters/status.html.erb new file mode 100644 index 00000000..d614b540 --- /dev/null +++ b/app/views/submissions_filters/status.html.erb @@ -0,0 +1,14 @@ +<%= render 'filter_modal', title: t('status'), default_params: params.permit(*(Submissions::Filter::ALLOWED_PARAMS - %w[status])) do %> +
+
+
+ <% ['', 'pending', 'completed', 'partially_completed', 'declined', 'expired'].each do |status| %> + + <% end %> +
+
+
+<% end %> diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 87c69fcd..92ee1d8e 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -20,6 +20,8 @@ en: &en language_ko: 한국어 hi_there: Hi there thanks: Thanks + pending_by_me: Pending by me + partially_completed: Partially completed unarchive: Unarchive first_party: 'First Party' remove_filter: Remove filter @@ -660,6 +662,7 @@ en: &en policy_links: Policy Links markdown_content_e_g: Markdown content, e.g. privacy_policy: Privacy Policy + use_the_edit_form_to_move_it_to_another_team: Use the edit form to move it to another team. submission_event_names: send_email_to_html: 'Email sent to %{submitter_name}' send_reminder_email_to_html: 'Reminder email sent to %{submitter_name}' @@ -698,6 +701,8 @@ en: &en read: Read your data es: &es + partially_completed: Parcialmente completado + pending_by_me: Pendiente por mi add: Agregar adding: Agregando owner: Propietario @@ -1340,6 +1345,7 @@ es: &es policy_links: Enlaces de Políticas markdown_content_e_g: Contenido Markdown, por ej. privacy_policy: Política de Privacidad + use_the_edit_form_to_move_it_to_another_team: Usa el formulario de edición para moverlo a otro equipo. submission_event_names: send_email_to_html: 'Correo electrónico enviado a %{submitter_name}' send_reminder_email_to_html: 'Correo de recordatorio enviado a %{submitter_name}' @@ -1378,6 +1384,7 @@ es: &es read: Leer tus datos it: &it + pending_by_me: In sospeso da me add: Aggiungi adding: Aggiungendo owner: Proprietario @@ -2020,6 +2027,7 @@ it: &it policy_links: Collegamenti alle Politiche markdown_content_e_g: Contenuto Markdown, ad es. privacy_policy: Politica sulla Privacy + use_the_edit_form_to_move_it_to_another_team: Usa il modulo di modifica per spostarlo in un altro team. submission_event_names: send_email_to_html: 'E-mail inviato a %{submitter_name}' send_reminder_email_to_html: 'E-mail di promemoria inviato a %{submitter_name}' @@ -2058,6 +2066,8 @@ it: &it read: Leggi i tuoi dati fr: &fr + partially_completed: Partiellement complété + pending_by_me: En attente par moi add: Ajouter adding: Ajout owner: Propriétaire @@ -2701,6 +2711,7 @@ fr: &fr policy_links: Liens des Politiques markdown_content_e_g: Contenu Markdown, par ex. privacy_policy: Politique de Confidentialité + use_the_edit_form_to_move_it_to_another_team: Utilisez le formulaire de modification pour le déplacer vers une autre équipe. submission_event_names: send_email_to_html: 'E-mail envoyé à %{submitter_name}' send_reminder_email_to_html: 'E-mail de rappel envoyé à %{submitter_name}' @@ -2739,6 +2750,8 @@ fr: &fr read: Lire vos données pt: &pt + partially_completed: Parcialmente concluído + pending_by_me: Pendente por mim add: Adicionar adding: Adicionando owner: Proprietário @@ -3381,6 +3394,7 @@ pt: &pt policy_links: Links de Políticas markdown_content_e_g: Conteúdo Markdown, ex. privacy_policy: Política de Privacidade + use_the_edit_form_to_move_it_to_another_team: Use o formulário de edição para movê-lo para outra equipe. submission_event_names: send_email_to_html: 'E-mail enviado para %{submitter_name}' send_reminder_email_to_html: 'E-mail de lembrete enviado para %{submitter_name}' @@ -3419,6 +3433,8 @@ pt: &pt read: Ler seus dados de: &de + partially_completed: Teilweise abgeschlossen + pending_by_me: Ausstehend von mir add: Hinzufügen adding: Hinzufügen owner: Eigentümer @@ -4061,6 +4077,7 @@ de: &de policy_links: Richtlinien-Links markdown_content_e_g: Markdown-Inhalt, z. B. privacy_policy: Datenschutzrichtlinie + use_the_edit_form_to_move_it_to_another_team: Verwenden Sie das Bearbeitungsformular, um ihn in ein anderes Team zu verschieben. submission_event_names: send_email_to_html: 'E-Mail gesendet an %{submitter_name}' send_reminder_email_to_html: 'Erinnerungs-E-Mail gesendet an %{submitter_name}' diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 2df9d73e..01ba85a5 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -1,6 +1,7 @@ queues: - [default, 1] - [webhooks, 1] + - [sms, 2] - [images, 1] - [mailers, 1] - [recurrent, 1] diff --git a/lib/params/base_validator.rb b/lib/params/base_validator.rb index 29c1985f..8a5abcf1 100644 --- a/lib/params/base_validator.rb +++ b/lib/params/base_validator.rb @@ -89,12 +89,13 @@ module Params raise_error(message || "#{key} must be unique") end - def in_path(params, path = []) + def in_path(params, path = [], skip_blank: false) old_path = @current_path @current_path = [old_path, *path].compact_blank.map(&:to_s).join('.') param = params.dig(*path) + param = nil if skip_blank && param.blank? yield params.dig(*path) if param diff --git a/lib/params/submission_create_validator.rb b/lib/params/submission_create_validator.rb index eef14391..5737e1dd 100644 --- a/lib/params/submission_create_validator.rb +++ b/lib/params/submission_create_validator.rb @@ -49,7 +49,7 @@ module Params type(params, :message, Hash) type(params, :submitters, Array) - in_path(params, :message) do |message_params| + in_path(params, :message, skip_blank: true) do |message_params| type(message_params, :subject, String) type(message_params, :body, String) diff --git a/lib/submissions/filter.rb b/lib/submissions/filter.rb index f4a2271a..11d40a17 100644 --- a/lib/submissions/filter.rb +++ b/lib/submissions/filter.rb @@ -4,6 +4,7 @@ module Submissions module Filter ALLOWED_PARAMS = %w[ author + status completed_at_from completed_at_to created_at_from @@ -22,31 +23,68 @@ module Submissions def call(submissions, current_user, params) filters = normalize_filter_params(params, current_user) - if filters[:author].present? - user = current_user.account.users.find_by(email: filters[:author]) - submissions = submissions.where(created_by_user_id: user&.id || -1) + submissions = filter_by_author(submissions, filters, current_user) + submissions = filter_by_status(submissions, filters) + submissions = filter_by_created_at(submissions, filters) + + filter_by_completed_at(submissions, filters) + end + + def filter_by_author(submissions, filters, current_user) + return submissions if filters[:author].blank? + + user = current_user.account.users.find_by(email: filters[:author]) + submissions.where(created_by_user_id: user&.id || -1) + end + + def filter_by_status(submissions, filters) + submissions = submissions.pending if filters[:status] == 'pending' + submissions = submissions.completed if filters[:status] == 'completed' + submissions = submissions.declined if filters[:status] == 'declined' + submissions = submissions.expired if filters[:status] == 'expired' + + if filters[:status] == 'partially_completed' + submissions = + submissions.joins(:submitters) + .group(:id) + .having(Arel::Nodes::NamedFunction.new( + 'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF', + [Submitter.arel_table[:completed_at].eq(nil), + Arel::Nodes.build_quoted(false)])] + ).gt(0)) + .having(Arel::Nodes::NamedFunction.new( + 'COUNT', [Arel::Nodes::NamedFunction.new('NULLIF', + [Submitter.arel_table[:completed_at].not_eq(nil), + Arel::Nodes.build_quoted(false)])] + ).gt(0)) end + submissions + end + + def filter_by_created_at(submissions, filters) submissions = submissions.where(created_at: filters[:created_at_from]..) if filters[:created_at_from].present? if filters[:created_at_to].present? submissions = submissions.where(created_at: ..filters[:created_at_to].end_of_day) end - if filters[:completed_at_from].present? || filters[:completed_at_to].present? - completed_arel = Submitter.arel_table[:completed_at].maximum - submissions = submissions.completed.joins(:submitters).group(:id) + submissions + end - if filters[:completed_at_from].present? - submissions = submissions.having(completed_arel.gteq(filters[:completed_at_from])) - end + def filter_by_completed_at(submissions, filters) + return submissions unless filters[:completed_at_from].present? || filters[:completed_at_to].present? - if filters[:completed_at_to].present? - submissions = submissions.having(completed_arel.lteq(filters[:completed_at_to].end_of_day)) - end + completed_arel = Submitter.arel_table[:completed_at].maximum + submissions = submissions.completed.joins(:submitters).group(:id) + + if filters[:completed_at_from].present? + submissions = submissions.having(completed_arel.gteq(filters[:completed_at_from])) end - submissions + return submissions if filters[:completed_at_to].blank? + + submissions.having(completed_arel.lteq(filters[:completed_at_to].end_of_day)) end def normalize_filter_params(params, current_user) diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 317fdc37..f9c66a89 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -236,7 +236,7 @@ module Submissions reason_string = I18n.with_locale(submitter.account.locale) do - "#{I18n.t('reason')}: #{reason_value || I18n.t('digitally_signed_by')} " \ + "#{reason_value ? "#{I18n.t('reason')}: " : ''}#{reason_value || I18n.t('digitally_signed_by')} " \ "#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \ "#{I18n.l(attachment.created_at.in_time_zone(submitter.account.timezone), format: :long)} " \ "#{TimeUtils.timezone_abbr(submitter.account.timezone, attachment.created_at)}" @@ -382,7 +382,10 @@ module Submissions when ->(type) { type == 'cells' && !area['cell_w'].to_f.zero? } cell_width = area['cell_w'] * width - TextUtils.maybe_rtl_reverse(value).chars.each_with_index do |char, index| + chars = TextUtils.maybe_rtl_reverse(value).chars + chars = chars.reverse if field.dig('preferences', 'align') == 'right' + + chars.each_with_index do |char, index| next if char.blank? text = HexaPDF::Layout::TextFragment.create(char, font:, @@ -409,9 +412,15 @@ module Submissions line_height = layouter.fit([text], cell_width, height).lines.first.height end + x = + if field.dig('preferences', 'align') == 'right' + ((area['x'] + area['w']) * width) - (cell_width * (index + 1)) + else + (area['x'] * width) + (cell_width * index) + end + cell_layouter.fit([text], cell_width, [line_height, area['h'] * height].max) - .draw(canvas, ((area['x'] * width) + (cell_width * index)), - height - (area['y'] * height)) + .draw(canvas, x, height - (area['y'] * height)) end else if field['type'] == 'date' diff --git a/lib/templates/clone.rb b/lib/templates/clone.rb index 45bd5350..cd1118a3 100644 --- a/lib/templates/clone.rb +++ b/lib/templates/clone.rb @@ -10,24 +10,32 @@ module Templates template.external_id = external_id template.author = author template.preferences = original_template.preferences.deep_dup - template.name = name || "#{original_template.name} (#{I18n.t('clone')})" + template.name = name.presence || "#{original_template.name} (#{I18n.t('clone')})" - template.assign_attributes(original_template.slice(:folder_id, :schema)) + if folder_name.present? + template.folder = TemplateFolders.find_or_create_by_name(author, folder_name) + else + template.folder_id = original_template.folder_id + end - template.folder = TemplateFolders.find_or_create_by_name(author, folder_name) if folder_name.present? + template.submitters, template.fields, template.schema = + update_submitters_and_fields_and_schema(original_template.submitters.deep_dup, + original_template.fields.deep_dup, + original_template.schema.deep_dup) - template.submitters, template.fields = clone_submitters_and_fields(original_template) + if name.present? && template.schema.size == 1 && + original_template.schema.first['name'] == original_template.name && + template.name != "#{original_template.name} (#{I18n.t('clone')})" + template.schema.first['name'] = template.name + end template end - def clone_submitters_and_fields(original_template) + def update_submitters_and_fields_and_schema(cloned_submitters, cloned_fields, cloned_schema) submitter_uuids_replacements = {} field_uuids_replacements = {} - cloned_submitters = original_template['submitters'].deep_dup - cloned_fields = original_template['fields'].deep_dup - cloned_submitters.each do |submitter| new_submitter_uuid = SecureRandom.uuid @@ -44,20 +52,28 @@ module Templates field['submitter_uuid'] = submitter_uuids_replacements[field['submitter_uuid']] end - replace_fields_regexp = Regexp.union(field_uuids_replacements.keys) + replace_fields_regexp = nil cloned_fields.each do |field| Array.wrap(field['conditions']).each do |condition| condition['field_uuid'] = field_uuids_replacements[condition['field_uuid']] end - if field.dig('preferences', 'formula').present? - field['preferences']['formula'] = - field['preferences']['formula'].gsub(replace_fields_regexp, field_uuids_replacements) + next if field.dig('preferences', 'formula').blank? + + replace_fields_regexp ||= Regexp.union(field_uuids_replacements.keys) + + field['preferences']['formula'] = + field['preferences']['formula'].gsub(replace_fields_regexp, field_uuids_replacements) + end + + cloned_schema.each do |field| + Array.wrap(field['conditions']).each do |condition| + condition['field_uuid'] = field_uuids_replacements[condition['field_uuid']] end end - [cloned_submitters, cloned_fields] + [cloned_submitters, cloned_fields, cloned_schema] end end end diff --git a/lib/templates/clone_attachments.rb b/lib/templates/clone_attachments.rb index 970214b3..8b08735d 100644 --- a/lib/templates/clone_attachments.rb +++ b/lib/templates/clone_attachments.rb @@ -4,20 +4,21 @@ module Templates module CloneAttachments module_function - def call(template:, original_template:) + def call(template:, original_template:, documents: []) schema_uuids_replacements = {} - cloned_schema = original_template.schema.deep_dup - cloned_fields = template.fields.deep_dup - - cloned_schema.each do |schema_item| + template.schema.each_with_index do |schema_item, index| new_schema_item_uuid = SecureRandom.uuid schema_uuids_replacements[schema_item['attachment_uuid']] = new_schema_item_uuid schema_item['attachment_uuid'] = new_schema_item_uuid + + new_name = documents&.dig(index, 'name') + + schema_item['name'] = new_name if new_name.present? end - cloned_fields.each do |field| + template.fields.each do |field| next if field['areas'].blank? field['areas'].each do |area| @@ -25,7 +26,7 @@ module Templates end end - template.update!(schema: cloned_schema, fields: cloned_fields) + template.save! original_template.schema_documents.map do |document| new_document = diff --git a/spec/factories/accounts.rb b/spec/factories/accounts.rb index b0551e06..6a5229bf 100644 --- a/spec/factories/accounts.rb +++ b/spec/factories/accounts.rb @@ -6,6 +6,10 @@ FactoryBot.define do locale { 'en-US' } timezone { 'UTC' } + transient do + teams_count { 2 } + end + trait :with_testing_account do after(:create) do |account| testing_account = account.dup.tap { |a| a.name = "Testing - #{account.name}" } @@ -14,5 +18,16 @@ FactoryBot.define do account.save! end end + + trait :with_teams do + after(:create) do |account, evaluator| + Array.new(evaluator.teams_count) do |i| + Account.create!( + name: "Team #{i}", + linked_account_account: AccountLinkedAccount.new(account_type: :linked, account:) + ) + end + end + end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 1157da61..e2195ceb 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -68,6 +68,10 @@ RSpec.configure do |config| config.before do |example| Sidekiq::Testing.inline! if example.metadata[:sidekiq] == :inline end + + config.before(multitenant: true) do + allow(Docuseal).to receive(:multitenant?).and_return(true) + end end ActiveSupport.run_load_hooks(:rails_specs, self) diff --git a/spec/requests/submissions_spec.rb b/spec/requests/submissions_spec.rb index c5d02f4b..502f6520 100644 --- a/spec/requests/submissions_spec.rb +++ b/spec/requests/submissions_spec.rb @@ -89,6 +89,21 @@ describe 'Submission API', type: :request do expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json)) end + it 'creates a submission when the message is empty' do + post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: { + template_id: templates[0].id, + send_email: true, + submitters: [{ role: 'First Party', email: 'john.doe@example.com' }], + message: {} + }.to_json + + expect(response).to have_http_status(:ok) + + submission = Submission.last + + expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json)) + end + it 'creates a submission when some submitter roles are not provided' do post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: { template_id: multiple_submitters_template.id, @@ -168,6 +183,22 @@ describe 'Submission API', type: :request do expect(response).to have_http_status(:unprocessable_entity) expect(response.parsed_body).to eq({ 'error' => 'Defined more signing parties than in template' }) end + + it 'returns an error if the message has no body value' do + post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: { + template_id: templates[0].id, + send_email: true, + submitters: [ + { role: 'First Party', email: 'john.doe@example.com' } + ], + message: { + subject: 'Custom Email Subject' + } + }.to_json + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body).to eq({ 'error' => 'body is required in `message`.' }) + end end describe 'POST /api/submissions/emails' do diff --git a/spec/system/team_settings_spec.rb b/spec/system/team_settings_spec.rb index 3472917a..077c5104 100644 --- a/spec/system/team_settings_spec.rb +++ b/spec/system/team_settings_spec.rb @@ -4,6 +4,7 @@ require 'rails_helper' RSpec.describe 'Team Settings' do let(:account) { create(:account) } + let(:second_account) { create(:account) } let(:current_user) { create(:user, account:) } before do @@ -56,6 +57,43 @@ RSpec.describe 'Team Settings' do end end + it "doesn't create a new user if a user already exists" do + click_link 'New User' + + within '#modal' do + fill_in 'First name', with: 'Michael' + fill_in 'Last name', with: 'Jordan' + fill_in 'Email', with: users.first.email + fill_in 'Password', with: 'password' + + expect do + click_button 'Submit' + end.not_to change(User, :count) + end + + expect(page).to have_content('Email already exists') + end + + it "doesn't create a new user if a user belongs to another account" do + user = create(:user, account: second_account) + visit settings_users_path + + click_link 'New User' + + within '#modal' do + fill_in 'First name', with: 'Michael' + fill_in 'Last name', with: 'Jordan' + fill_in 'Email', with: user.email + fill_in 'Password', with: 'password' + + expect do + click_button 'Submit' + end.not_to change(User, :count) + + expect(page).to have_content('Email has already been taken') + end + end + it 'updates a user' do first(:link, 'Edit').click