From 52e2c50b37ba0449549251052b85199d27152de5 Mon Sep 17 00:00:00 2001 From: Martin Artnik Date: Wed, 18 Dec 2024 13:24:35 +0100 Subject: [PATCH] Add form response models and controllers [SCI-11356] --- .../form_field_values_controller.rb | 39 ++++++ app/controllers/form_responses_controller.rb | 68 ++++++++++ app/models/form.rb | 3 +- app/models/form_datetime_field_value.rb | 20 +++ app/models/form_field.rb | 3 +- app/models/form_field_value.rb | 18 +++ .../form_multiple_choice_field_value.rb | 11 ++ app/models/form_number_field_value.rb | 22 +++ app/models/form_response.rb | 72 ++++++++++ app/models/form_single_choice_field_value.rb | 11 ++ app/models/form_text_field_value.rb | 11 ++ app/models/user_role.rb | 4 + app/permissions/form_response.rb | 38 ++++++ .../form_field_value_serializer.rb | 11 ++ app/serializers/form_response_serializer.rb | 15 ++ config/initializers/extends.rb | 10 +- .../extends/permission_extends.rb | 20 ++- config/routes.rb | 9 ++ .../20241213095430_create_form_responses.rb | 16 +++ ...20241213100803_create_form_field_values.rb | 33 +++++ ...218085759_add_form_response_permissions.rb | 40 ++++++ .../form_field_values_controller_spec.rb | 94 +++++++++++++ .../form_responses_controller_spec.rb | 128 ++++++++++++++++++ spec/factories/form_field_values.rb | 12 ++ spec/factories/form_responses.rb | 15 ++ spec/models/form_field_value_spec.rb | 39 ++++++ spec/models/form_response_spec.rb | 32 +++++ 27 files changed, 787 insertions(+), 7 deletions(-) create mode 100644 app/controllers/form_field_values_controller.rb create mode 100644 app/controllers/form_responses_controller.rb create mode 100644 app/models/form_datetime_field_value.rb create mode 100644 app/models/form_field_value.rb create mode 100644 app/models/form_multiple_choice_field_value.rb create mode 100644 app/models/form_number_field_value.rb create mode 100644 app/models/form_response.rb create mode 100644 app/models/form_single_choice_field_value.rb create mode 100644 app/models/form_text_field_value.rb create mode 100644 app/permissions/form_response.rb create mode 100644 app/serializers/form_field_value_serializer.rb create mode 100644 app/serializers/form_response_serializer.rb create mode 100644 db/migrate/20241213095430_create_form_responses.rb create mode 100644 db/migrate/20241213100803_create_form_field_values.rb create mode 100644 db/migrate/20241218085759_add_form_response_permissions.rb create mode 100644 spec/controllers/form_field_values_controller_spec.rb create mode 100644 spec/controllers/form_responses_controller_spec.rb create mode 100644 spec/factories/form_field_values.rb create mode 100644 spec/factories/form_responses.rb create mode 100644 spec/models/form_field_value_spec.rb create mode 100644 spec/models/form_response_spec.rb diff --git a/app/controllers/form_field_values_controller.rb b/app/controllers/form_field_values_controller.rb new file mode 100644 index 000000000..1088389cf --- /dev/null +++ b/app/controllers/form_field_values_controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class FormFieldValuesController < ApplicationController + before_action :load_form_response + before_action :load_form_field + before_action :check_create_permissions + + def create + @form_field_value = @form_response.create_value!( + current_user, + @form_field, + form_field_value_params[:value] + ) + + render json: @form_field_value, serializer: FormFieldValueSerializer, user: current_user + end + + private + + def form_field_value_params + params.require(:form_field_value).permit(:form_field_id, :value) + end + + def load_form_response + @form_response = FormResponse.find_by(id: params[:form_response_id]) + + render_404 unless @form_response + end + + def load_form_field + @form_field = @form_response.form.form_fields.find_by(id: form_field_value_params[:form_field_id]) + + render_404 unless @form_field + end + + def check_create_permissions + render_403 unless can_submit_form_response?(@form_response) + end +end diff --git a/app/controllers/form_responses_controller.rb b/app/controllers/form_responses_controller.rb new file mode 100644 index 000000000..707d08183 --- /dev/null +++ b/app/controllers/form_responses_controller.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +class FormResponsesController < ApplicationController + before_action :load_form, only: :create + before_action :load_parent, only: :create + before_action :load_form_response, except: :create + + def create + case @parent + when Step + render_403 and return unless can_create_protocol_form_responses?(@parent.protocol) + + ActiveRecord::Base.transaction do + @form_response = FormResponse.create!(form: @form, created_by: current_user) + @parent.step_orderable_elements.create!(orderable: @form_response) + end + else + render_422 + end + + render json: @form_response, serializer: FormResponseSerializer, user: current_user + end + + def submit + render_403 and return unless can_submit_form_response?(@form_response) + + @form_response.submit!(current_user) + + render json: @form_response, serializer: FormResponseSerializer, user: current_user + end + + def reset + render_403 and return unless can_reset_form_response?(@form_response) + + @form_response.reset!(current_user) + + render json: @form_response, serializer: FormResponseSerializer, user: current_user + end + + private + + def form_response_params + params.require(:form_response).permit(:form_id, :parent_id, :parent_type) + end + + def load_form + @form = Form.find_by(id: form_response_params[:form_id]) + + render_404 unless @form && can_read_form?(@form) + end + + def load_parent + case form_response_params[:parent_type] + when 'Step' + @parent = Step.find_by(id: form_response_params[:parent_id]) + else + return render_422 + end + + render_404 unless @parent + end + + def load_form_response + @form_response = FormResponse.find_by(id: params[:id]) + + render_404 unless @form_response + end +end diff --git a/app/models/form.rb b/app/models/form.rb index 13a311c09..91a809c93 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -17,6 +17,7 @@ class Form < ApplicationRecord belongs_to :default_public_user_role, class_name: 'UserRole', optional: true has_many :form_fields, inverse_of: :form, dependent: :destroy + has_many :form_responses, dependent: :destroy has_many :users, through: :user_assignments validates :name, length: { minimum: Constants::NAME_MIN_LENGTH, maximum: Constants::NAME_MAX_LENGTH } @@ -27,7 +28,7 @@ class Form < ApplicationRecord after_update :update_automatic_user_assignments, if: -> { saved_change_to_default_public_user_role_id? } - enum visibility: { hidden: 0, visible: 1 } + enum :visibility, { hidden: 0, visible: 1 } def permission_parent nil diff --git a/app/models/form_datetime_field_value.rb b/app/models/form_datetime_field_value.rb new file mode 100644 index 000000000..1a4645aaf --- /dev/null +++ b/app/models/form_datetime_field_value.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class FormDatetimeFieldValue < FormFieldValue + def value=(val) + if val.is_a?(Array) + self.datetime = val[0] + self.datetime_to = val[1] + else + self.datetime = val + end + end + + def value + range? ? [datetime, datetime_to] : datetime + end + + def range? + datetime_to.present? + end +end diff --git a/app/models/form_field.rb b/app/models/form_field.rb index 03b4f6978..e4b1e9678 100644 --- a/app/models/form_field.rb +++ b/app/models/form_field.rb @@ -1,15 +1,14 @@ # frozen_string_literal: true class FormField < ApplicationRecord - belongs_to :form belongs_to :created_by, class_name: 'User' belongs_to :last_modified_by, class_name: 'User' + has_many :form_field_values, dependent: :destroy validates :name, length: { minimum: Constants::NAME_MIN_LENGTH, maximum: Constants::NAME_MAX_LENGTH } validates :description, length: { maximum: Constants::NAME_MAX_LENGTH } validates :position, presence: true, uniqueness: { scope: :form } acts_as_list scope: :form, top_of_list: 0, sequential_updates: true - end diff --git a/app/models/form_field_value.rb b/app/models/form_field_value.rb new file mode 100644 index 000000000..bc3b280f5 --- /dev/null +++ b/app/models/form_field_value.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class FormFieldValue < ApplicationRecord + belongs_to :form_response + belongs_to :form_field + belongs_to :created_by, class_name: 'User' + belongs_to :submitted_by, class_name: 'User' + + scope :latest, -> { where(latest: true) } + + def value=(_) + raise NotImplementedError + end + + def value + raise NotImplementedError + end +end diff --git a/app/models/form_multiple_choice_field_value.rb b/app/models/form_multiple_choice_field_value.rb new file mode 100644 index 000000000..6d020a0a8 --- /dev/null +++ b/app/models/form_multiple_choice_field_value.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class FormMultipleChoiceFieldValue < FormFieldValue + def value=(val) + self.selection = val + end + + def value + selection + end +end diff --git a/app/models/form_number_field_value.rb b/app/models/form_number_field_value.rb new file mode 100644 index 000000000..546e2cc94 --- /dev/null +++ b/app/models/form_number_field_value.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class FormNumberFieldValue < FormFieldValue + def value=(val) + self.unit = form_field.data['unit'] + + if val.is_a?(Array) + self.number = val[0] + self.number_to = val[1] + else + self.number = val + end + end + + def value + range? ? [number, number_to] : number + end + + def range? + number_to.present? + end +end diff --git a/app/models/form_response.rb b/app/models/form_response.rb new file mode 100644 index 000000000..59e2edd1c --- /dev/null +++ b/app/models/form_response.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +class InvalidStatusError < StandardError; end + +class FormResponse < ApplicationRecord + include Discard::Model + + default_scope -> { kept } + + belongs_to :form + belongs_to :created_by, class_name: 'User' + belongs_to :submitted_by, class_name: 'User', optional: true + + has_one :step_orderable_element, as: :orderable, dependent: :destroy + + enum :status, { pending: 0, submitted: 1, locked: 2 } + + has_many :form_field_values, dependent: :destroy + + def step + step_orderable_element&.step + end + + def parent + step_orderable_element&.step + end + + def create_value!(created_by, form_field, value) + ActiveRecord::Base.transaction(requires_new: true) do + form_field_values.where(form_field: form_field).find_each do |form_field_value| + form_field_value.update!(latest: false) + end + + "Form#{form_field.data['type']}Value".constantize.create!( + form_field: form_field, + form_response: self, + # these can change if the form_response is reset, as submitted_by will be kept the same, but created_by will change + created_by: created_by, + submitted_by: created_by, + value: value + ) + end + end + + def submit!(user) + update!( + status: :submitted, + submitted_by: user, + submitted_at: DateTime.current + ) + end + + def reset!(user) + raise InvalidStatusError, 'Cannot reset form that has not been submitted yet!' if status != 'submitted' + + ActiveRecord::Base.transaction(requires_new: true) do + new_form_response = dup + new_form_response.update!(status: 'pending', created_by: user) + + form_field_values.latest.find_each do |form_field_value| + form_field_value.dup.update!(form_response: new_form_response, created_by: user) + end + + # if attached to step, reattach new form response + self&.step_orderable_element&.update!(orderable: new_form_response) + + discard + + new_form_response + end + end +end diff --git a/app/models/form_single_choice_field_value.rb b/app/models/form_single_choice_field_value.rb new file mode 100644 index 000000000..04e05dc3d --- /dev/null +++ b/app/models/form_single_choice_field_value.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class FormSingleChoiceFieldValue < FormFieldValue + def value=(val) + self.text = val + end + + def value + text + end +end diff --git a/app/models/form_text_field_value.rb b/app/models/form_text_field_value.rb new file mode 100644 index 000000000..00d42b055 --- /dev/null +++ b/app/models/form_text_field_value.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class FormTextFieldValue < FormFieldValue + def value=(val) + self.text = val + end + + def value + text + end +end diff --git a/app/models/user_role.rb b/app/models/user_role.rb index 836f20592..194e2ff34 100644 --- a/app/models/user_role.rb +++ b/app/models/user_role.rb @@ -61,6 +61,10 @@ class UserRole < ApplicationRecord predefined.find_by(name: UserRole.public_send('viewer_role').name) end + def self.find_predefined_technician_role + predefined.find_by(name: UserRole.public_send('technician_role').name) + end + def has_permission?(permission) permissions.include?(permission) end diff --git a/app/permissions/form_response.rb b/app/permissions/form_response.rb new file mode 100644 index 000000000..401f03034 --- /dev/null +++ b/app/permissions/form_response.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +Canaid::Permissions.register_for(FormResponse) do + %i( + submit_form_response + reset_form_response + ).each do |perm| + can perm do |_, form_response| + !form_response.locked? + end + end + + can :submit_form_response do |user, form_response| + parent = form_response.parent + case parent + when Step + next false unless parent.protocol.my_module # protocol template forms can't be submitted + + parent.protocol.my_module.permission_granted?(user, FormResponsePermissions::SUBMIT) + end + end + + can :reset_form_response do |user, form_response| + parent = form_response.parent + case parent + when Step + next false unless parent.protocol.my_module # protocol template forms can't be reset + + parent.protocol.my_module.permission_granted?(user, FormResponsePermissions::SUBMIT) + end + end +end + +Canaid::Permissions.register_for(Protocol) do + can :create_protocol_form_responses do |user, protocol| + (protocol.my_module || protocol).permission_granted?(user, FormResponsePermissions::CREATE) + end +end diff --git a/app/serializers/form_field_value_serializer.rb b/app/serializers/form_field_value_serializer.rb new file mode 100644 index 000000000..a3585b403 --- /dev/null +++ b/app/serializers/form_field_value_serializer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class FormFieldValueSerializer < ActiveModel::Serializer + include Canaid::Helpers::PermissionsHelper + + attributes :form_field_id, :type, :value, :submitted_at, :submitted_by_full_name, :unit + + def submitted_by_full_name + object.submitted_by.full_name + end +end diff --git a/app/serializers/form_response_serializer.rb b/app/serializers/form_response_serializer.rb new file mode 100644 index 000000000..b38c0d965 --- /dev/null +++ b/app/serializers/form_response_serializer.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class FormResponseSerializer < ActiveModel::Serializer + include Canaid::Helpers::PermissionsHelper + + attributes :id, :created_at, :form_id + + has_many :form_field_values do + object.form_field_values.latest + end + + def submitted_by_full_name + object.submitted_by.full_name + end +end diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb index d701aef18..4055f217b 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -139,7 +139,15 @@ class Extends RepositoryStockValue ) - STI_PRELOAD_CLASSES = %w(LinkedRepository SoftLockedRepository) + STI_PRELOAD_CLASSES = %w( + LinkedRepository + SoftLockedRepository + FormTextFieldValue + FormNumberFieldValue + FormDatetimeFieldValue + FormMultipleChoiceFieldValue + FormSingleChoiceFieldValue + ) # Array of preload relations used in search query for repository rows REPOSITORY_ROWS_PRELOAD_RELATIONS = [] diff --git a/config/initializers/extends/permission_extends.rb b/config/initializers/extends/permission_extends.rb index 265ef4137..42c554546 100644 --- a/config/initializers/extends/permission_extends.rb +++ b/config/initializers/extends/permission_extends.rb @@ -44,6 +44,15 @@ module PermissionExtends ).each { |permission| const_set(permission, "form_#{permission.parameterize}") } end + module FormResponsePermissions + %w( + NONE + CREATE + SUBMIT + RESET + ).each { |permission| const_set(permission, "form_response_#{permission.parameterize}") } + end + module ReportPermissions %w( NONE @@ -156,8 +165,9 @@ module PermissionExtends ProjectPermissions.constants.map { |const| ProjectPermissions.const_get(const) } + ExperimentPermissions.constants.map { |const| ExperimentPermissions.const_get(const) } + MyModulePermissions.constants.map { |const| MyModulePermissions.const_get(const) } + - RepositoryPermissions.constants.map { |const| RepositoryPermissions.const_get(const) } - ).reject { |p| p.end_with?("_none") } + RepositoryPermissions.constants.map { |const| RepositoryPermissions.const_get(const) } + + FormResponsePermissions.constants.map { |const| FormResponsePermissions.const_get(const) } + ).reject { |p| p.end_with?('_none') } NORMAL_USER_PERMISSIONS = [ TeamPermissions::PROJECTS_CREATE, @@ -177,6 +187,9 @@ module PermissionExtends ProtocolPermissions::MANAGE_DRAFT, FormPermissions::READ, FormPermissions::READ_ARCHIVED, + FormResponsePermissions::CREATE, + FormResponsePermissions::SUBMIT, + FormResponsePermissions::RESET, ReportPermissions::READ, ReportPermissions::MANAGE, ProjectPermissions::READ, @@ -265,7 +278,8 @@ module PermissionExtends MyModulePermissions::REPOSITORY_ROWS_ASSIGN, MyModulePermissions::REPOSITORY_ROWS_MANAGE, MyModulePermissions::USERS_READ, - MyModulePermissions::STOCK_CONSUMPTION_UPDATE + MyModulePermissions::STOCK_CONSUMPTION_UPDATE, + FormResponsePermissions::SUBMIT ] VIEWER_PERMISSIONS = [ diff --git a/config/routes.rb b/config/routes.rb index 5ff087b21..cf487dd13 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -874,6 +874,15 @@ Rails.application.routes.draw do end end + resources :form_responses, only: %i(create) do + member do + post :submit + post :reset + end + + resources :form_field_values, only: %i(create) + end + get 'search' => 'search#index' get 'search/new' => 'search#new', as: :new_search resource :search, only: [], controller: :search do diff --git a/db/migrate/20241213095430_create_form_responses.rb b/db/migrate/20241213095430_create_form_responses.rb new file mode 100644 index 000000000..cab7ea11d --- /dev/null +++ b/db/migrate/20241213095430_create_form_responses.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CreateFormResponses < ActiveRecord::Migration[7.0] + def change + create_table :form_responses do |t| + t.references :form, null: false, foreign_key: true + t.references :created_by, null: false, foreign_key: { to_table: :users } + t.references :submitted_by, null: true, foreign_key: { to_table: :users } + t.integer :status, default: 0 + t.datetime :submitted_at + t.datetime :discarded_at + + t.timestamps + end + end +end diff --git a/db/migrate/20241213100803_create_form_field_values.rb b/db/migrate/20241213100803_create_form_field_values.rb new file mode 100644 index 000000000..9d8776237 --- /dev/null +++ b/db/migrate/20241213100803_create_form_field_values.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class CreateFormFieldValues < ActiveRecord::Migration[7.0] + def change + create_table :form_field_values do |t| + t.string :type, index: true + t.references :form_response, null: false, foreign_key: true + t.references :form_field, null: false, foreign_key: true + t.references :created_by, null: false, foreign_key: { to_table: :users } + t.references :submitted_by, null: true, foreign_key: { to_table: :users } + t.timestamp :submitted_at + t.boolean :latest, null: false, default: true + t.boolean :not_applicable, null: false, default: false + + # FormFieldDateTimeValue + t.datetime :datetime + t.datetime :datetime_to + + # FormFieldNumberValue + t.decimal :number + t.decimal :number_to + t.text :unit + + # FormFieldTextValue, FormFieldSingleChoiceValue + t.text :text + + # FormFieldMultipleCohiceValue + t.text :selection, array: true + + t.timestamps + end + end +end diff --git a/db/migrate/20241218085759_add_form_response_permissions.rb b/db/migrate/20241218085759_add_form_response_permissions.rb new file mode 100644 index 000000000..d922cf5c7 --- /dev/null +++ b/db/migrate/20241218085759_add_form_response_permissions.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class AddFormResponsePermissions < ActiveRecord::Migration[7.0] + FORM_RESPONSE_MANAGE_PERMISSION = [ + FormResponsePermissions::RESET, + FormResponsePermissions::CREATE + ].freeze + + FORM_RESPONSE_SUBMIT_PERMISSION = [ + FormResponsePermissions::SUBMIT + ].freeze + + def up + @owner_role = UserRole.find_predefined_owner_role + @normal_user_role = UserRole.find_predefined_normal_user_role + @technician_user_role = UserRole.find_predefined_technician_role + + @owner_role.permissions = @owner_role.permissions | (FORM_RESPONSE_SUBMIT_PERMISSION + FORM_RESPONSE_MANAGE_PERMISSION) + @normal_user_role.permissions = @normal_user_role.permissions | (FORM_RESPONSE_SUBMIT_PERMISSION + FORM_RESPONSE_MANAGE_PERMISSION) + @technician_user_role.permissions = @technician_user_role.permissions | FORM_RESPONSE_SUBMIT_PERMISSION + + @owner_role.save(validate: false) + @normal_user_role.save(validate: false) + @technician_user_role.save(validate: false) + end + + def down + @owner_role = UserRole.find_predefined_owner_role + @normal_user_role = UserRole.find_predefined_normal_user_role + @technician_user_role = UserRole.find_predefined_technician_role + + @owner_role.permissions = @owner_role.permissions - (FORM_RESPONSE_SUBMIT_PERMISSION + FORM_RESPONSE_MANAGE_PERMISSION) + @normal_user_role.permissions = @normal_user_role.permissions - (FORM_RESPONSE_SUBMIT_PERMISSION + FORM_RESPONSE_MANAGE_PERMISSION) + @technician_user_role.permissions = @technician_user_role.permissions - FORM_RESPONSE_SUBMIT_PERMISSION + + @owner_role.save(validate: false) + @normal_user_role.save(validate: false) + @technician_user_role.save(validate: false) + end +end diff --git a/spec/controllers/form_field_values_controller_spec.rb b/spec/controllers/form_field_values_controller_spec.rb new file mode 100644 index 000000000..cc3b33907 --- /dev/null +++ b/spec/controllers/form_field_values_controller_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe FormFieldValuesController, type: :controller do + login_user + + include_context 'reference_project_structure' + + let!(:form) { create(:form, team: team, created_by: user) } + let!(:form_response) { create(:form_response, form: form, created_by: user) } + let!(:form_field) { create(:form_field, form: form, created_by: user, data: { type: 'TextField' }) } + + describe 'POST create' do + let(:action) { post :create, params: params, format: :json } + let(:params) do + { + form_response_id: form_response.id, + form_field_value: { + form_field_id: form_field.id, + value: 'Test value' + } + } + end + + context 'when user has permissions' do + before { allow(controller).to receive(:can_submit_form_response?).and_return(true) } + + it 'creates a form field value successfully' do + expect { action }.to change(FormFieldValue, :count).by(1) + expect(response).to have_http_status(:success) + response_body = JSON.parse(response.body) + expect(response_body['data']['attributes']['value']).to eq 'Test value' + expect(response_body['data']['attributes']['form_field_id']).to eq form_field.id + end + end + + context 'when user lacks permissions' do + before { allow(controller).to receive(:can_submit_form_response?).and_return(false) } + + it 'returns a forbidden response' do + action + expect(response).to have_http_status(:forbidden) + end + end + + context 'when invalid form_field_id is provided' do + let(:params) do + { + form_response_id: form_response.id, + form_field_value: { + form_field_id: -1, + value: 'Test value' + } + } + end + + it 'returns a not found response' do + action + expect(response).to have_http_status(:not_found) + end + end + + context 'when invalid form_response_id is provided' do + let(:params) do + { + form_response_id: 0, + form_field_value: { + form_field_id: form_field.id, + value: 'Test value' + } + } + end + + it 'returns a not found response' do + action + expect(response).to have_http_status(:not_found) + end + end + + context 'when missing value parameter' do + let(:params) do + { + form_response_id: form_response.id, + nothing: {} + } + end + + it 'raises a parameter missing error' do + expect { action }.to raise_error(ActionController::ParameterMissing) + end + end + end +end diff --git a/spec/controllers/form_responses_controller_spec.rb b/spec/controllers/form_responses_controller_spec.rb new file mode 100644 index 000000000..743c185fc --- /dev/null +++ b/spec/controllers/form_responses_controller_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe FormResponsesController, type: :controller do + login_user + + include_context 'reference_project_structure' + + let!(:form) { create(:form, team: team, created_by: user) } + let!(:step) { create(:step, protocol: protocol) } + let!(:protocol) { create(:protocol, added_by: user) } + let!(:form_response) { create(:form_response, form: form, created_by: user) } + + describe 'POST create' do + let(:action) { post :create, params: params, format: :json } + let(:params) do + { + form_response: { + form_id: form.id, + parent_id: step.id, + parent_type: 'Step' + } + } + end + + context 'when user has permissions' do + before { allow(controller).to receive(:can_create_protocol_form_responses?).and_return(true) } + + it 'creates a form response successfully' do + expect { action }.to change(FormResponse, :count).by(1) + expect(response).to have_http_status(:success) + response_body = JSON.parse(response.body) + expect(response_body['data']['attributes']['form_id']).to eq form.id + end + end + + context 'when user lacks permissions' do + before { allow(controller).to receive(:can_create_protocol_form_responses?).and_return(false) } + + it 'returns a forbidden response' do + action + expect(response).to have_http_status(:forbidden) + end + end + + context 'when invalid parent type' do + let(:params) do + { + form_response: { + form_id: form.id, + parent_id: step.id, + parent_type: 'InvalidType' + } + } + end + + it 'returns an unprocessable entity response' do + action + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + describe 'PUT submit' do + let(:action) { put :submit, params: { id: form_response.id }, format: :json } + + context 'when user has permissions' do + before { allow(controller).to receive(:can_submit_form_response?).and_return(true) } + + it 'submits the form response successfully' do + expect(form_response.status).to eq 'pending' + action + expect(response).to have_http_status(:success) + form_response.reload + expect(form_response.status).to eq 'submitted' + expect(form_response.submitted_by).to eq user + end + end + + context 'when user lacks permissions' do + before { allow(controller).to receive(:can_submit_form_response?).and_return(false) } + + it 'returns a forbidden response' do + action + expect(response).to have_http_status(:forbidden) + end + end + end + + describe 'PUT reset' do + let(:action) { put :reset, params: { id: form_response.id }, format: :json } + + context 'when user has permissions and form is submitted' do + before do + allow(controller).to receive(:can_reset_form_response?).and_return(true) + form_response.update!(status: 'submitted') + end + + it 'resets the form response successfully' do + expect { action }.to change(FormResponse.unscoped, :count).by(1) + expect(response).to have_http_status(:success) + form_response.reload + expect(form_response.discarded?).to be true + end + end + + context 'when form is not submitted' do + before do + allow(controller).to receive(:can_reset_form_response?).and_return(true) + form_response.update!(status: 'pending') + end + + it 'raises an error' do + expect { action }.to raise_error(InvalidStatusError) + end + end + + context 'when user lacks permissions' do + before { allow(controller).to receive(:can_reset_form_response?).and_return(false) } + + it 'returns a forbidden response' do + action + expect(response).to have_http_status(:forbidden) + end + end + end +end diff --git a/spec/factories/form_field_values.rb b/spec/factories/form_field_values.rb new file mode 100644 index 000000000..f8b6bd72e --- /dev/null +++ b/spec/factories/form_field_values.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :form_field_value do + type { 'FormFieldTextValue' } + text { 'hello' } + association :form_response + association :form_field + association :created_by, factory: :user + association :submitted_by, factory: :user + end +end diff --git a/spec/factories/form_responses.rb b/spec/factories/form_responses.rb new file mode 100644 index 000000000..06fca1575 --- /dev/null +++ b/spec/factories/form_responses.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :form_response do + association :form + association :created_by, factory: :user + status { :pending } + + trait :submitted do + status { :submitted } + association :submitted_by, factory: :user + submitted_at { DateTime.current } + end + end +end diff --git a/spec/models/form_field_value_spec.rb b/spec/models/form_field_value_spec.rb new file mode 100644 index 000000000..ba01c1ab3 --- /dev/null +++ b/spec/models/form_field_value_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe FormFieldValue, type: :model do + let(:form_response) { build :form_field_value } + + it 'is valid' do + expect(form_response).to be_valid + end + + it 'should be of class form field value' do + expect(subject.class).to eq FormFieldValue + end + + describe 'Database table' do + it { should have_db_column :form_response_id } + it { should have_db_column :created_by_id } + it { should have_db_column :submitted_by_id } + it { should have_db_column :submitted_at } + it { should have_db_column :created_at } + it { should have_db_column :updated_at } + it { should have_db_column :latest } + it { should have_db_column :not_applicable } + it { should have_db_column :datetime } + it { should have_db_column :datetime_to } + it { should have_db_column :number } + it { should have_db_column :number_to } + it { should have_db_column :text } + it { should have_db_column :selection } + end + + describe 'Relations' do + it { should belong_to(:form_response) } + it { should belong_to(:form_field) } + it { should belong_to(:created_by).class_name('User') } + it { should belong_to(:submitted_by).class_name('User') } + end +end diff --git a/spec/models/form_response_spec.rb b/spec/models/form_response_spec.rb new file mode 100644 index 000000000..d8c3ccdd3 --- /dev/null +++ b/spec/models/form_response_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe FormResponse, type: :model do + let(:form_response) { build :form_response } + + it 'is valid' do + expect(form_response).to be_valid + end + + it 'should be of class form field' do + expect(subject.class).to eq FormResponse + end + + describe 'Database table' do + it { should have_db_column :form_id } + it { should have_db_column :created_by_id } + it { should have_db_column :submitted_by_id } + it { should have_db_column :status } + it { should have_db_column :submitted_at } + it { should have_db_column :discarded_at } + it { should have_db_column :created_at } + it { should have_db_column :updated_at } + end + + describe 'Relations' do + it { should belong_to(:form) } + it { should belong_to(:created_by).class_name('User') } + it { should belong_to(:submitted_by).class_name('User').optional } + end +end