From dc481cf23665addc94c616b46c627f7bea52c453 Mon Sep 17 00:00:00 2001 From: Oleksii Kriuchykhin Date: Thu, 2 Jul 2020 17:03:29 +0200 Subject: [PATCH] Add API endpoints for checklists, checklist items, tables, API code improvements [SCI-4776] --- .rubocop.yml | 3 - app/controllers/api/v1/base_controller.rb | 70 +++++ .../api/v1/checklist_items_controller.rb | 59 ++++ .../api/v1/checklists_controller.rb | 63 +++++ .../api/v1/connections_controller.rb | 7 +- .../api/v1/inventory_items_controller.rb | 22 +- app/controllers/api/v1/projects_controller.rb | 11 +- app/controllers/api/v1/steps_controller.rb | 49 +++- app/controllers/api/v1/tables_controller.rb | 59 ++++ .../api/v1/task_groups_controller.rb | 9 +- .../api/v1/task_inventory_items_controller.rb | 9 +- app/controllers/api/v1/tasks_controller.rb | 45 ++-- .../concerns/api/v1/extra_params.rb | 13 + app/controllers/my_modules_controller.rb | 80 +++--- app/controllers/steps_controller.rb | 149 ++++------ app/models/checklist_item.rb | 1 + app/models/my_module.rb | 15 +- app/models/protocol.rb | 12 +- app/models/step.rb | 34 +-- app/models/table.rb | 2 +- .../api/v1/checklist_item_serializer.rb | 10 + .../api/v1/checklist_serializer.rb | 11 + app/serializers/api/v1/step_serializer.rb | 17 +- app/serializers/api/v1/table_serializer.rb | 14 + app/serializers/api/v1/task_serializer.rb | 8 + app/utilities/protocols_importer.rb | 2 +- config/locales/en.yml | 6 + config/routes.rb | 8 +- spec/factories/assets.rb | 4 +- .../api/v1/checklist_items_controller_spec.rb | 255 ++++++++++++++++++ .../api/v1/checklists_controller_spec.rb | 247 +++++++++++++++++ .../api/v1/results_controller_spec.rb | 4 +- spec/requests/api/v1/steps_controller_spec.rb | 113 ++++++++ .../requests/api/v1/tables_controller_spec.rb | 250 +++++++++++++++++ spec/requests/api/v1/tasks_controller_spec.rb | 104 +++++++ 35 files changed, 1505 insertions(+), 260 deletions(-) create mode 100644 app/controllers/api/v1/checklist_items_controller.rb create mode 100644 app/controllers/api/v1/checklists_controller.rb create mode 100644 app/controllers/api/v1/tables_controller.rb create mode 100644 app/controllers/concerns/api/v1/extra_params.rb create mode 100644 app/serializers/api/v1/checklist_item_serializer.rb create mode 100644 app/serializers/api/v1/checklist_serializer.rb create mode 100644 app/serializers/api/v1/table_serializer.rb create mode 100644 spec/requests/api/v1/checklist_items_controller_spec.rb create mode 100644 spec/requests/api/v1/checklists_controller_spec.rb create mode 100644 spec/requests/api/v1/tables_controller_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 6da553e4e..629a3d4aa 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -31,9 +31,6 @@ Style/BarePercentLiterals: Style/BlockDelimiters: EnforcedStyle: line_count_based -Style/BracesAroundHashParameters: - EnforcedStyle: no_braces - Layout/CaseIndentation: EnforcedStyle: case diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 19dc48c47..79d5a0ea8 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -5,6 +5,7 @@ module Api class BaseController < ApiController class TypeError < StandardError; end class IDMismatchError < StandardError; end + class IncludeNotSupportedError < StandardError; end class PermissionError < StandardError attr_reader :klass attr_reader :mode @@ -43,6 +44,18 @@ module Api :bad_request) end + rescue_from NotImplementedError do + render_error(I18n.t('api.core.errors.not_implemented.title'), + I18n.t('api.core.errors.not_implemented.detail'), + :bad_request) + end + + rescue_from IncludeNotSupportedError do + render_error(I18n.t('api.core.errors.include_not_supported.title'), + I18n.t('api.core.errors.include_not_supported.detail'), + :bad_request) + end + rescue_from ActionController::ParameterMissing do |e| render_error( I18n.t('api.core.errors.parameter.title'), e.message, :bad_request @@ -74,6 +87,28 @@ module Api ) end + before_action :check_include_param, only: %i(index show) + + def index + raise NotImplementedError + end + + def show + raise NotImplementedError + end + + def create + raise NotImplementedError + end + + def update + raise NotImplementedError + end + + def destroy + raise NotImplementedError + end + private def render_error(title, message, status) @@ -90,6 +125,26 @@ module Api }, status: status end + def check_include_param + return if params[:include].blank? + + include_params + end + + # redefine it in the specific controller if includes are used there + def permitted_includes + [] + end + + def include_params + return nil if params[:include].blank? + + provided_includes = params[:include].split(',') + raise IncludeNotSupportedError if (provided_includes - permitted_includes).any? + + provided_includes + end + def load_team(key = :team_id) @team = Team.find(params.require(key)) raise PermissionError.new(Team, :read) unless can_read_team?(@team) @@ -131,6 +186,21 @@ module Api @step = @protocol.steps.find(params.require(key)) raise PermissionError.new(Protocol, :read) unless can_read_protocol_in_module?(@step.protocol) end + + def load_table(key = :table_id) + @table = @step.tables.find(params.require(key)) + raise PermissionError.new(Protocol, :read) unless can_read_protocol_in_module?(@step.protocol) + end + + def load_checklist(key = :checklist_id) + @checklist = @step.checklists.find(params.require(key)) + raise PermissionError.new(Protocol, :read) unless can_read_protocol_in_module?(@step.protocol) + end + + def load_checklist_item(key = :checklist_item_id) + @checklist_item = @checklist.checklist_items.find(params.require(key)) + raise PermissionError.new(Protocol, :read) unless can_read_protocol_in_module?(@step.protocol) + end end end end diff --git a/app/controllers/api/v1/checklist_items_controller.rb b/app/controllers/api/v1/checklist_items_controller.rb new file mode 100644 index 000000000..d8f1c6c5f --- /dev/null +++ b/app/controllers/api/v1/checklist_items_controller.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Api + module V1 + class ChecklistItemsController < BaseController + before_action :load_team, :load_project, :load_experiment, :load_task, :load_protocol, :load_step, :load_checklist + before_action only: :show do + load_checklist_item(:id) + end + before_action :load_checklist_item_for_managing, only: %i(update destroy) + + def index + checklist_items = @checklist.checklist_items.page(params.dig(:page, :number)).per(params.dig(:page, :size)) + + render jsonapi: checklist_items, each_serializer: ChecklistItemSerializer + end + + def show + render jsonapi: @checklist_item, serializer: ChecklistItemSerializer + end + + def create + raise PermissionError.new(Protocol, :create) unless can_manage_protocol_in_module?(@protocol) + + checklist_item = @checklist.checklist_items.create!(checklist_item_params.merge!(created_by: current_user)) + + render jsonapi: checklist_item, serializer: ChecklistItemSerializer, status: :created + end + + def update + @checklist_item.assign_attributes(checklist_item_params) + + if @checklist_item.changed? && @checklist_item.save! + render jsonapi: @checklist_item, serializer: ChecklistItemSerializer, status: :ok + else + render body: nil, status: :no_content + end + end + + def destroy + @checklist_item.destroy! + render body: nil + end + + private + + def checklist_item_params + raise TypeError unless params.require(:data).require(:type) == 'checklist_items' + + params.require(:data).require(:attributes).permit(:text, :checked, :position) + end + + def load_checklist_item_for_managing + @checklist_item = @checklist.checklist_items.find(params.require(:id)) + raise PermissionError.new(Protocol, :manage) unless can_manage_protocol_in_module?(@protocol) + end + end + end +end diff --git a/app/controllers/api/v1/checklists_controller.rb b/app/controllers/api/v1/checklists_controller.rb new file mode 100644 index 000000000..b91f005f0 --- /dev/null +++ b/app/controllers/api/v1/checklists_controller.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Api + module V1 + class ChecklistsController < BaseController + before_action :load_team, :load_project, :load_experiment, :load_task, :load_protocol, :load_step + before_action only: :show do + load_checklist(:id) + end + before_action :load_checklist_for_managing, only: %i(update destroy) + + def index + checklists = @step.checklists.page(params.dig(:page, :number)).per(params.dig(:page, :size)) + + render jsonapi: checklists, each_serializer: ChecklistSerializer, include: include_params + end + + def show + render jsonapi: @checklist, serializer: ChecklistSerializer, include: include_params + end + + def create + raise PermissionError.new(Protocol, :create) unless can_manage_protocol_in_module?(@protocol) + + checklist = @step.checklists.create!(checklist_params.merge!(created_by: current_user)) + + render jsonapi: checklist, serializer: ChecklistSerializer, status: :created + end + + def update + @checklist.assign_attributes(checklist_params) + + if @checklist.changed? && @checklist.save! + render jsonapi: @checklist, serializer: ChecklistSerializer, status: :ok + else + render body: nil, status: :no_content + end + end + + def destroy + @checklist.destroy! + render body: nil + end + + private + + def checklist_params + raise TypeError unless params.require(:data).require(:type) == 'checklists' + + params.require(:data).require(:attributes).permit(:name) + end + + def permitted_includes + %w(checklist_items) + end + + def load_checklist_for_managing + @checklist = @step.checklists.find(params.require(:id)) + raise PermissionError.new(Protocol, :manage) unless can_manage_protocol_in_module?(@protocol) + end + end + end +end diff --git a/app/controllers/api/v1/connections_controller.rb b/app/controllers/api/v1/connections_controller.rb index 5e030e0b5..f084c84de 100644 --- a/app/controllers/api/v1/connections_controller.rb +++ b/app/controllers/api/v1/connections_controller.rb @@ -12,10 +12,9 @@ module Api def index @connections = @connections.page(params.dig(:page, :number)) .per(params.dig(:page, :size)) - incl = params[:include] == 'tasks' ? %i(input_task output_task) : nil render jsonapi: @connections, each_serializer: ConnectionSerializer, - include: incl + include: include_params end def show @@ -40,6 +39,10 @@ module Api def load_connection @connection = @connections.find(params.require(:id)) end + + def permitted_includes + %w(tasks) + end end end end diff --git a/app/controllers/api/v1/inventory_items_controller.rb b/app/controllers/api/v1/inventory_items_controller.rb index be8850a83..aecdff04e 100644 --- a/app/controllers/api/v1/inventory_items_controller.rb +++ b/app/controllers/api/v1/inventory_items_controller.rb @@ -11,17 +11,13 @@ module Api before_action :check_manage_permissions, only: %i(create update destroy) def index - items = - @inventory.repository_rows - .active - .preload(repository_cells: :repository_column) - .preload(repository_cells: @inventory.cell_preload_includes) - .page(params.dig(:page, :number)) - .per(params.dig(:page, :size)) - incl = params[:include] == 'inventory_cells' ? :inventory_cells : nil - render jsonapi: items, - each_serializer: InventoryItemSerializer, - include: incl + items = @inventory.repository_rows + .active + .preload(repository_cells: :repository_column) + .preload(repository_cells: @inventory.cell_preload_includes) + .page(params.dig(:page, :number)) + .per(params.dig(:page, :size)) + render jsonapi: items, each_serializer: InventoryItemSerializer, include: include_params end def create @@ -120,6 +116,10 @@ module Api def inventory_cells_params params[:included]&.select { |el| el[:type] == 'inventory_cells' } end + + def permitted_includes + %w(inventory_cells) + end end end end diff --git a/app/controllers/api/v1/projects_controller.rb b/app/controllers/api/v1/projects_controller.rb index f6d425345..c527e7761 100644 --- a/app/controllers/api/v1/projects_controller.rb +++ b/app/controllers/api/v1/projects_controller.rb @@ -7,7 +7,7 @@ module Api before_action only: :show do load_project(:id) end - before_action :load_project_relative, only: :activities + before_action :load_project, only: :activities def index projects = @team.projects @@ -29,15 +29,6 @@ module Api render jsonapi: activities, each_serializer: ActivitySerializer end - - private - - def load_project_relative - @project = @team.projects.find(params.require(:project_id)) - unless can_read_project?(@project) - raise PermissionError.new(Project, :read) - end - end end end end diff --git a/app/controllers/api/v1/steps_controller.rb b/app/controllers/api/v1/steps_controller.rb index 50f764a52..367ad09e4 100644 --- a/app/controllers/api/v1/steps_controller.rb +++ b/app/controllers/api/v1/steps_controller.rb @@ -3,33 +3,51 @@ module Api module V1 class StepsController < BaseController + include Api::V1::ExtraParams + before_action :load_team, :load_project, :load_experiment, :load_task, :load_protocol before_action only: :show do load_step(:id) end + before_action :load_step_for_managing, only: %i(update destroy) def index - steps = @protocol.steps - .page(params.dig(:page, :number)) - .per(params.dig(:page, :size)) + steps = @protocol.steps.page(params.dig(:page, :number)).per(params.dig(:page, :size)) - render jsonapi: steps, each_serializer: StepSerializer + render jsonapi: steps, each_serializer: StepSerializer, + include: include_params, + rte_rendering: render_rte? end def show - render jsonapi: @step, serializer: StepSerializer + render jsonapi: @step, serializer: StepSerializer, + include: include_params, + rte_rendering: render_rte? end def create raise PermissionError.new(Protocol, :create) unless can_manage_protocol_in_module?(@protocol) step = @protocol.steps.create!(step_params.merge!(completed: false, - user_id: current_user.id, + user: current_user, position: @protocol.number_of_steps)) - render jsonapi: step, - serializer: StepSerializer, - status: :created + render jsonapi: step, serializer: StepSerializer, status: :created + end + + def update + @step.assign_attributes(step_params) + + if @step.changed? && @step.save! + render jsonapi: @step, serializer: StepSerializer, status: :ok + else + render body: nil, status: :no_content + end + end + + def destroy + @step.destroy! + render body: nil end private @@ -37,9 +55,16 @@ module Api def step_params raise TypeError unless params.require(:data).require(:type) == 'steps' - attr_list = %i(name) - params.require(:data).require(:attributes).require(attr_list) - params.require(:data).require(:attributes).permit(attr_list + [:description]) + params.require(:data).require(:attributes).permit(:name, :description, :completed) + end + + def permitted_includes + %w(tables assets checklists checklists.checklist_items comments) + end + + def load_step_for_managing + @step = @protocol.steps.find(params.require(:id)) + raise PermissionError.new(Protocol, :manage) unless can_manage_protocol_in_module?(@step.protocol) end end end diff --git a/app/controllers/api/v1/tables_controller.rb b/app/controllers/api/v1/tables_controller.rb new file mode 100644 index 000000000..99626f97c --- /dev/null +++ b/app/controllers/api/v1/tables_controller.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Api + module V1 + class TablesController < BaseController + before_action :load_team, :load_project, :load_experiment, :load_task, :load_protocol, :load_step + before_action only: :show do + load_table(:id) + end + before_action :load_table_for_managing, only: %i(update destroy) + + def index + tables = @step.tables.page(params.dig(:page, :number)).per(params.dig(:page, :size)) + + render jsonapi: tables, each_serializer: TableSerializer + end + + def show + render jsonapi: @table, serializer: TableSerializer + end + + def create + raise PermissionError.new(Protocol, :create) unless can_manage_protocol_in_module?(@protocol) + + table = @step.tables.create!(table_params.merge!(team: @team, created_by: current_user)) + + render jsonapi: table, serializer: TableSerializer, status: :created + end + + def update + @table.assign_attributes(table_params) + + if @table.changed? && @table.save! + render jsonapi: @table, serializer: TableSerializer, status: :ok + else + render body: nil, status: :no_content + end + end + + def destroy + @table.destroy! + render body: nil + end + + private + + def table_params + raise TypeError unless params.require(:data).require(:type) == 'tables' + + params.require(:data).require(:attributes).permit(:name, :contents) + end + + def load_table_for_managing + @table = @step.tables.find(params.require(:id)) + raise PermissionError.new(Protocol, :manage) unless can_manage_protocol_in_module?(@protocol) + end + end + end +end diff --git a/app/controllers/api/v1/task_groups_controller.rb b/app/controllers/api/v1/task_groups_controller.rb index e095657ba..09c9c1c54 100644 --- a/app/controllers/api/v1/task_groups_controller.rb +++ b/app/controllers/api/v1/task_groups_controller.rb @@ -12,10 +12,7 @@ module Api task_groups = @experiment.my_module_groups .page(params.dig(:page, :number)) .per(params.dig(:page, :size)) - incl = params[:include] == 'tasks' ? :tasks : nil - render jsonapi: task_groups, - each_serializer: TaskGroupSerializer, - include: incl + render jsonapi: task_groups, each_serializer: TaskGroupSerializer, include: include_params end def show @@ -29,6 +26,10 @@ module Api def load_task_group @task_group = @experiment.my_module_groups.find(params.require(:id)) end + + def permitted_includes + %w(tasks) + end end end end diff --git a/app/controllers/api/v1/task_inventory_items_controller.rb b/app/controllers/api/v1/task_inventory_items_controller.rb index 30d4fbca6..fceac56b8 100644 --- a/app/controllers/api/v1/task_inventory_items_controller.rb +++ b/app/controllers/api/v1/task_inventory_items_controller.rb @@ -15,11 +15,10 @@ module Api .includes(repository_cells: Extends::REPOSITORY_SEARCH_INCLUDES) .page(params.dig(:page, :number)) .per(params.dig(:page, :size)) - incl = params[:include] == 'inventory_cells' ? :inventory_cells : nil render jsonapi: items, each_serializer: InventoryItemSerializer, show_repository: true, - include: incl + include: include_params end def show @@ -28,6 +27,12 @@ module Api show_repository: true, include: %i(inventory_cells inventory) end + + private + + def permitted_includes + %w(inventory_cells) + end end end end diff --git a/app/controllers/api/v1/tasks_controller.rb b/app/controllers/api/v1/tasks_controller.rb index db6e02348..0e304f7b3 100644 --- a/app/controllers/api/v1/tasks_controller.rb +++ b/app/controllers/api/v1/tasks_controller.rb @@ -3,34 +3,47 @@ module Api module V1 class TasksController < BaseController + include Api::V1::ExtraParams + before_action :load_team before_action :load_project before_action :load_experiment before_action only: :show do load_task(:id) end - before_action :load_task_relative, only: :activities + before_action :load_task_for_managing, only: %i(update) + before_action :load_task, only: :activities def index tasks = @experiment.my_modules .page(params.dig(:page, :number)) .per(params.dig(:page, :size)) - render jsonapi: tasks, each_serializer: TaskSerializer + render jsonapi: tasks, each_serializer: TaskSerializer, rte_rendering: render_rte? end def show - render jsonapi: @task, serializer: TaskSerializer + render jsonapi: @task, serializer: TaskSerializer, rte_rendering: render_rte? end def create raise PermissionError.new(MyModule, :create) unless can_manage_experiment?(@experiment) - my_module = @experiment.my_modules.create!(my_module_params) + my_module = @experiment.my_modules.create!(task_params) - render jsonapi: my_module, - serializer: TaskSerializer, - status: :created + render jsonapi: my_module, serializer: TaskSerializer, + rte_rendering: render_rte?, + status: :created + end + + def update + @task.assign_attributes(task_params) + + if @task.changed? && @task.save! + render jsonapi: @task, serializer: TaskSerializer, status: :ok + else + render body: nil, status: :no_content + end end def activities @@ -38,26 +51,20 @@ module Api .page(params.dig(:page, :number)) .per(params.dig(:page, :size)) - render jsonapi: activities, - each_serializer: ActivitySerializer + render jsonapi: activities, each_serializer: ActivitySerializer end private - def my_module_params + def task_params raise TypeError unless params.require(:data).require(:type) == 'tasks' - attr_list = %i(name x y) - params.require(:data).require(:attributes).require(attr_list) - params.require(:data).require(:attributes).permit(attr_list + [:description]) + params.require(:data).require(:attributes).permit(%i(name x y description state)) end - # Made the method below because its more elegant than changing parameters - # in routes file, and here. It exists because when we call input or output - # for a task, the "id" that used to be task id is now an id for the output - # or input. - def load_task_relative - @task = @experiment.my_modules.find(params.require(:task_id)) + def load_task_for_managing + @task = @experiment.my_modules.find(params.require(:id)) + raise PermissionError.new(MyModule, :manage) unless can_manage_module?(@task) end end end diff --git a/app/controllers/concerns/api/v1/extra_params.rb b/app/controllers/concerns/api/v1/extra_params.rb new file mode 100644 index 000000000..ccdb58fc3 --- /dev/null +++ b/app/controllers/concerns/api/v1/extra_params.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Api + module V1 + module ExtraParams + extend ActiveSupport::Concern + + def render_rte? + params[:render_rte] == 'true' + end + end + end +end diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index 1a7951768..dffc997ff 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -13,7 +13,7 @@ class MyModulesController < ApplicationController before_action :check_manage_permissions, only: %i(description due_date update_description update_protocol_description) before_action :check_view_permissions, except: %i(update update_description update_protocol_description toggle_task_state) - before_action :check_complete_module_permission, only: :complete_my_module + before_action :check_complete_module_permission, only: %i(complete_my_module toggle_task_state) before_action :set_inline_name_editing, only: %i(protocols results activities archive) layout 'fluid'.freeze @@ -258,38 +258,29 @@ class MyModulesController < ApplicationController # Complete/uncomplete task def toggle_task_state respond_to do |format| - if can_complete_module?(@my_module) - @my_module.completed? ? @my_module.uncomplete : @my_module.complete - completed = @my_module.completed? - if @my_module.save - task_completion_activity + @my_module.completed? ? @my_module.uncompleted! : @my_module.completed! + task_completion_activity - # Render new button HTML - if completed - new_btn_partial = 'my_modules/state_button_uncomplete.html.erb' - else - new_btn_partial = 'my_modules/state_button_complete.html.erb' - end + # Render new button HTML + new_btn_partial = if @my_module.completed? + 'my_modules/state_button_uncomplete.html.erb' + else + 'my_modules/state_button_complete.html.erb' + end - format.json do - render json: { - new_btn: render_to_string(partial: new_btn_partial), - completed: completed, - module_header_due_date: render_to_string( - partial: 'my_modules/module_header_due_date.html.erb', - locals: { my_module: @my_module } - ), - module_state_label: render_to_string( - partial: 'my_modules/module_state_label.html.erb', - locals: { my_module: @my_module } - ) - } - end - else - format.json { render json: {}, status: :unprocessable_entity } - end - else - format.json { render json: {}, status: :unauthorized } + format.json do + render json: { + new_btn: render_to_string(partial: new_btn_partial), + completed: @my_module.completed?, + module_header_due_date: render_to_string( + partial: 'my_modules/module_header_due_date.html.erb', + locals: { my_module: @my_module } + ), + module_state_label: render_to_string( + partial: 'my_modules/module_state_label.html.erb', + locals: { my_module: @my_module } + ) + } end end end @@ -297,22 +288,21 @@ class MyModulesController < ApplicationController def complete_my_module respond_to do |format| if @my_module.uncompleted? && @my_module.check_completness_status - @my_module.complete - @my_module.save + @my_module.completed! task_completion_activity format.json do - render json: { - task_button_title: t('my_modules.buttons.uncomplete'), - module_header_due_date: render_to_string( - partial: 'my_modules/module_header_due_date.html.erb', - locals: { my_module: @my_module } - ), - module_state_label: render_to_string( - partial: 'my_modules/module_state_label.html.erb', - locals: { my_module: @my_module } - ) - }, status: :ok - end + render json: { + task_button_title: t('my_modules.buttons.uncomplete'), + module_header_due_date: render_to_string( + partial: 'my_modules/module_header_due_date.html.erb', + locals: { my_module: @my_module } + ), + module_state_label: render_to_string( + partial: 'my_modules/module_state_label.html.erb', + locals: { my_module: @my_module } + ) + }, status: :ok + end else format.json { render json: {}, status: :unprocessable_entity } end diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb index 339ce5dfd..e61082709 100644 --- a/app/controllers/steps_controller.rb +++ b/app/controllers/steps_controller.rb @@ -4,17 +4,16 @@ class StepsController < ApplicationController include StepsActions include MarvinJsActions - before_action :load_vars, only: %i(edit update destroy show toggle_step_state checklistitem_state update_view_state) - before_action :load_vars_nested, only: [:new, :create] - before_action :convert_table_contents_to_utf8, only: [:create, :update] + before_action :load_vars, only: %i(edit update destroy show toggle_step_state checklistitem_state update_view_state + move_up move_down) + before_action :load_vars_nested, only: %i(new create) + before_action :convert_table_contents_to_utf8, only: %i(create update) before_action :check_view_permissions, only: %i(show update_view_state) - before_action :check_manage_permissions, only: %i(new create edit update - destroy) - before_action :check_complete_and_checkbox_permissions, only: - %i(toggle_step_state checklistitem_state) + before_action :check_manage_permissions, only: %i(new create edit update destroy move_up move_down) + before_action :check_complete_and_checkbox_permissions, only: %i(toggle_step_state checklistitem_state) - before_action :update_checklist_item_positions, only: [:create, :update] + before_action :update_checklist_item_positions, only: %i(create update) def new @step = Step.new @@ -244,7 +243,7 @@ class StepsController < ApplicationController end # Destroy the step - @step.destroy(current_user) + @step.destroy # Release space taken by the step team.release_space(previous_size) @@ -371,95 +370,58 @@ class StepsController < ApplicationController end def move_up - step = Step.find_by_id(params[:id]) - respond_to do |format| - if step - protocol = step.protocol - if can_manage_protocol_in_module?(protocol) || - can_manage_protocol_in_repository?(protocol) - if step.position > 0 - step_down = step.protocol.steps.where(position: step.position - 1).first - step.position -= 1 - step.save + if @step.position.positive? + step_down = @step.protocol.steps.find_by(position: @step.position - 1) + @step.position -= 1 + @step.save - if step_down - step_down.position += 1 - step_down.save + if step_down + step_down.position += 1 + step_down.save - # Update protocol timestamp - update_protocol_ts(step) + # Update protocol timestamp + update_protocol_ts(@step) - format.json { - render json: { move_direction: "up", step_up_position: step.position, step_down_position: step_down.position }, - status: :ok - } - else - format.json { - render json: {}, status: :forbidden - } - end - else - format.json { - render json: {}, status: :forbidden - } + format.json do + render json: { move_direction: 'up', + step_up_position: @step.position, + step_down_position: step_down.position }, + status: :ok end else - format.json { - render json: {}, status: :forbidden - } + format.json { render json: {}, status: :forbidden } end else - format.json { - render json: {}, status: :not_found - } + format.json { render json: {}, status: :forbidden } end end end def move_down - step = Step.find_by_id(params[:id]) - respond_to do |format| - if step - protocol = step.protocol - if can_manage_protocol_in_module?(protocol) || - can_manage_protocol_in_repository?(protocol) - if step.position < step.protocol.steps.count - 1 - step_up = step.protocol.steps.where(position: step.position + 1).first - step.position += 1 - step.save + if @step.position < @step.protocol.steps.count - 1 + step_up = @step.protocol.steps.find_by(position: @step.position + 1) + @step.position += 1 + @step.save - if step_up - step_up.position -= 1 - step_up.save + if step_up + step_up.position -= 1 + step_up.save - # Update protocol timestamp - update_protocol_ts(step) + # Update protocol timestamp + update_protocol_ts(@step) - format.json { - render json: { move_direction: "down", step_up_position: step_up.position, step_down_position: step.position }, - status: :ok - } - else - format.json { - render json: {}, status: :forbidden - } - end - else - format.json { - render json: {}, status: :forbidden - } + format.json do + render json: { move_direction: 'down', + step_up_position: step_up.position, + step_down_position: @step.position } end else - format.json { - render json: {}, status: :forbidden - } + format.json { render json: {}, status: :forbidden } end else - format.json { - render json: {}, status: :not_found - } + format.json { render json: {}, status: :forbidden } end end end @@ -545,31 +507,20 @@ class StepsController < ApplicationController end def load_vars - @step = Step.find_by_id(params[:id]) - @protocol = @step&.protocol - if params[:checklistitem_id] - @chk_item = ChecklistItem.find_by_id(params[:checklistitem_id]) - end + @step = Step.find_by(id: params[:id]) + return render_404 unless @step - unless @protocol - render_404 - end - - if @protocol.in_module? - @my_module = @protocol.my_module - end + @protocol = @step.protocol + @chk_item = ChecklistItem.find_by(id: params[:checklistitem_id]) if params[:checklistitem_id] + @my_module = @protocol.my_module if @protocol.in_module? end def load_vars_nested - @protocol = Protocol.find_by_id(params[:protocol_id]) + @protocol = Protocol.find_by(id: params[:protocol_id]) - unless @protocol - render_404 - end + return render_404 unless @protocol - if @protocol.in_module? - @my_module = @protocol.my_module - end + @my_module = @protocol.my_module if @protocol.in_module? end def convert_table_contents_to_utf8 @@ -589,13 +540,11 @@ class StepsController < ApplicationController end def check_view_permissions - render_403 unless can_read_protocol_in_module?(@protocol) || - can_read_protocol_in_repository?(@protocol) + render_403 unless can_read_protocol_in_module?(@protocol) || can_read_protocol_in_repository?(@protocol) end def check_manage_permissions - render_403 unless can_manage_protocol_in_module?(@protocol) || - can_manage_protocol_in_repository?(@protocol) + render_403 unless can_manage_protocol_in_module?(@protocol) || can_manage_protocol_in_repository?(@protocol) end def check_complete_and_checkbox_permissions diff --git a/app/models/checklist_item.rb b/app/models/checklist_item.rb index d66c1d14c..c61c52eb3 100644 --- a/app/models/checklist_item.rb +++ b/app/models/checklist_item.rb @@ -5,6 +5,7 @@ class ChecklistItem < ApplicationRecord length: { maximum: Constants::TEXT_MAX_LENGTH } validates :checklist, presence: true validates :checked, inclusion: { in: [true, false] } + validates :position, uniqueness: { scope: :checklist } belongs_to :checklist, inverse_of: :checklist_items diff --git a/app/models/my_module.rb b/app/models/my_module.rb index a96713711..bf466a4d6 100644 --- a/app/models/my_module.rb +++ b/app/models/my_module.rb @@ -7,6 +7,7 @@ class MyModule < ApplicationRecord enum state: Extends::TASKS_STATES before_create :create_blank_protocol + before_save -> { self.completed_on = completed? ? DateTime.now : nil }, if: :state_changed? auto_strip_attributes :name, :description, nullify: false validates :name, @@ -506,10 +507,6 @@ class MyModule < ApplicationRecord { x: 0, y: positions.last[1] + HEIGHT } end - def completed? - state == 'completed' - end - # Check if my_module is ready to become completed def check_completness_status if protocol && protocol.steps.count > 0 @@ -522,16 +519,6 @@ class MyModule < ApplicationRecord false end - def complete - self.state = 'completed' - self.completed_on = DateTime.now - end - - def uncomplete - self.state = 'uncompleted' - self.completed_on = nil - end - def assign_user(user, assigned_by = nil) user_my_modules.create( assigned_by: assigned_by || user, diff --git a/app/models/protocol.rb b/app/models/protocol.rb index e7db69345..88f8b6e99 100644 --- a/app/models/protocol.rb +++ b/app/models/protocol.rb @@ -525,7 +525,7 @@ class Protocol < ApplicationRecord def update_parent(current_user) # First, destroy parent's step contents - parent.destroy_contents(current_user) + parent.destroy_contents parent.reload # Now, clone step contents @@ -543,7 +543,7 @@ class Protocol < ApplicationRecord def update_from_parent(current_user) # First, destroy step contents - destroy_contents(current_user) + destroy_contents # Now, clone parent's step contents Protocol.clone_contents(parent, self, current_user, false) @@ -559,7 +559,7 @@ class Protocol < ApplicationRecord def load_from_repository(source, current_user) # First, destroy step contents - destroy_contents(current_user) + destroy_contents # Now, clone source's step contents Protocol.clone_contents(source, self, current_user, false) @@ -656,12 +656,10 @@ class Protocol < ApplicationRecord cloned end - def destroy_contents(current_user) + def destroy_contents # Calculate total space taken by the protocol st = space_taken - steps.pluck(:id).each do |id| - raise ActiveRecord::RecordNotDestroyed unless Step.find(id).destroy(current_user) - end + steps.destroy_all # Release space taken by the step team.release_space(st) diff --git a/app/models/step.rb b/app/models/step.rb index 600921c09..8e1fccedc 100644 --- a/app/models/step.rb +++ b/app/models/step.rb @@ -14,6 +14,10 @@ class Step < ApplicationRecord validates :user, :protocol, presence: true validates :completed_on, presence: true, if: proc { |s| s.completed? } + before_destroy :cascade_before_destroy + before_destroy :adjust_positions_on_destroy + before_save :set_last_modified_by + belongs_to :user, inverse_of: :steps belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User', optional: true belongs_to :protocol, inverse_of: :steps @@ -37,9 +41,6 @@ class Step < ApplicationRecord }, allow_destroy: true - after_destroy :cascade_after_destroy - before_save :set_last_modified_by - def self.search(user, include_archived, query = nil, @@ -76,17 +77,6 @@ class Step < ApplicationRecord end end - def destroy(current_user) - @current_user = current_user - - # Store IDs of assets & tables so they - # can be destroyed in after_destroy - @a_ids = self.assets.collect { |a| a.id } - @t_ids = self.tables.collect { |t| t.id } - - super() - end - def self.viewable_by_user(user, teams) where(protocol: Protocol.viewable_by_user(user, teams)) end @@ -132,13 +122,17 @@ class Step < ApplicationRecord end end - protected + private - def cascade_after_destroy - # Assets already deleted by here - @a_ids = nil - Table.destroy(@t_ids) - @t_ids = nil + def adjust_positions_on_destroy + protocol.steps.where('position > ?', position).find_each do |step| + step.update(position: step.position - 1) + end + end + + def cascade_before_destroy + assets.each(&:destroy) + tables.each(&:destroy) end def set_last_modified_by diff --git a/app/models/table.rb b/app/models/table.rb index 3a5a52c3a..a4b37df71 100644 --- a/app/models/table.rb +++ b/app/models/table.rb @@ -19,7 +19,7 @@ class Table < ApplicationRecord class_name: 'User', optional: true belongs_to :team, optional: true - has_one :step_table, inverse_of: :table + has_one :step_table, inverse_of: :table, dependent: :destroy has_one :step, through: :step_table has_one :result_table, inverse_of: :table diff --git a/app/serializers/api/v1/checklist_item_serializer.rb b/app/serializers/api/v1/checklist_item_serializer.rb new file mode 100644 index 000000000..d159c01fe --- /dev/null +++ b/app/serializers/api/v1/checklist_item_serializer.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Api + module V1 + class ChecklistItemSerializer < ActiveModel::Serializer + type :checklist_items + attributes :id, :text, :checked, :position + end + end +end diff --git a/app/serializers/api/v1/checklist_serializer.rb b/app/serializers/api/v1/checklist_serializer.rb new file mode 100644 index 000000000..dd2cc98f2 --- /dev/null +++ b/app/serializers/api/v1/checklist_serializer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Api + module V1 + class ChecklistSerializer < ActiveModel::Serializer + type :checklists + attributes :id, :name + has_many :checklist_items, serializer: ChecklistItemSerializer + end + end +end diff --git a/app/serializers/api/v1/step_serializer.rb b/app/serializers/api/v1/step_serializer.rb index 9a6b3c285..9ed994531 100644 --- a/app/serializers/api/v1/step_serializer.rb +++ b/app/serializers/api/v1/step_serializer.rb @@ -3,14 +3,25 @@ module Api module V1 class StepSerializer < ActiveModel::Serializer + include ApplicationHelper + include ActionView::Helpers::TextHelper + include InputSanitizeHelper + type :steps attributes :id, :name, :description, :position, :completed - attribute :completed_on, if: :completed? + attribute :completed_on, if: -> { object.completed? } belongs_to :protocol, serializer: ProtocolSerializer has_many :assets, serializer: AssetSerializer + has_many :checklists, serializer: ChecklistSerializer + has_many :tables, serializer: TableSerializer + has_many :step_comments, key: :comments, serializer: CommentSerializer - def completed? - object.completed + def description + if instance_options[:rte_rendering] + custom_auto_link(object.tinymce_render(:description), simple_format: false, tags: %w(img)) + else + object.description + end end end end diff --git a/app/serializers/api/v1/table_serializer.rb b/app/serializers/api/v1/table_serializer.rb new file mode 100644 index 000000000..f969bf032 --- /dev/null +++ b/app/serializers/api/v1/table_serializer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Api + module V1 + class TableSerializer < ActiveModel::Serializer + type :tables + attributes :id, :name, :contents + + def contents + object.contents&.force_encoding(Encoding::UTF_8) + end + end + end +end diff --git a/app/serializers/api/v1/task_serializer.rb b/app/serializers/api/v1/task_serializer.rb index eaef1521b..ebf635928 100644 --- a/app/serializers/api/v1/task_serializer.rb +++ b/app/serializers/api/v1/task_serializer.rb @@ -19,6 +19,14 @@ module Api def input_tasks object.my_module_antecessors end + + def description + if instance_options[:rte_rendering] + custom_auto_link(object.tinymce_render(:description), simple_format: false, tags: %w(img)) + else + object.description + end + end end end end diff --git a/app/utilities/protocols_importer.rb b/app/utilities/protocols_importer.rb index cb642648a..d19887583 100644 --- a/app/utilities/protocols_importer.rb +++ b/app/utilities/protocols_importer.rb @@ -29,7 +29,7 @@ module ProtocolsImporter def import_into_existing(protocol, protocol_json, user, team) # Firstly, destroy existing protocol's contents protocol.tiny_mce_assets.destroy_all - protocol.destroy_contents(user) + protocol.destroy_contents protocol.reload # Alright, now populate the protocol diff --git a/config/locales/en.yml b/config/locales/en.yml index c9dbc69c8..138ec4068 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2339,6 +2339,12 @@ en: record_not_found: title: "Not found" detail: "%{model} record with id %{id} not found in the specified scope" + not_implemented: + title: "Not implemented" + detail: "This endpoint is not implemented yet" + include_not_supported: + title: "Not supported" + detail: "Include param is not supported by this endpoint" id_mismatch: title: "Object ID mismatch" detail: "Object ID mismatch in URL and request body" diff --git a/config/routes.rb b/config/routes.rb index 685c6504f..99fe335e6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -706,7 +706,7 @@ Rails.application.routes.draw do resources :experiments, only: %i(index show) do resources :task_groups, only: %i(index show) resources :connections, only: %i(index show) - resources :tasks, only: %i(index show create) do + resources :tasks, only: %i(index show create update) do resources :task_inventory_items, only: %i(index show), path: 'items', as: :items @@ -717,8 +717,12 @@ Rails.application.routes.draw do path: 'tags', as: :tags resources :protocols, only: %i(index) do - resources :steps, only: %i(index show create) do + resources :steps do resources :assets, only: %i(index show create), path: 'attachments' + resources :checklists, path: 'checklists' do + resources :checklist_items, as: :items, path: 'items' + end + resources :tables, path: 'tables' end end resources :results, only: %i(index create show update) diff --git a/spec/factories/assets.rb b/spec/factories/assets.rb index a701f048a..26faa3ef6 100644 --- a/spec/factories/assets.rb +++ b/spec/factories/assets.rb @@ -2,8 +2,8 @@ FactoryBot.define do factory :asset do - file do - fixture_file_upload(Rails.root.join('spec', 'fixtures', 'files', 'test.jpg'), 'image/jpg') + after(:create) do |asset| + asset.file.attach(io: File.open(Rails.root.join('spec/fixtures/files/test.jpg')), filename: 'test.jpg') end end end diff --git a/spec/requests/api/v1/checklist_items_controller_spec.rb b/spec/requests/api/v1/checklist_items_controller_spec.rb new file mode 100644 index 000000000..4911ea8fe --- /dev/null +++ b/spec/requests/api/v1/checklist_items_controller_spec.rb @@ -0,0 +1,255 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::V1::ChecklistsController', type: :request do + before :all do + @user = create(:user) + @team = create(:team, created_by: @user) + @project = create(:project, team: @team) + @experiment = create(:experiment, :with_tasks, project: @project) + @task = @experiment.my_modules.first + @protocol = create(:protocol, my_module: @task) + @step = create(:step, protocol: @protocol) + @checklist = create(:checklist, step: @step) + create(:user_team, user: @user, team: @team) + create(:user_project, :normal_user, user: @user, project: @project) + + @valid_headers = { + 'Authorization': 'Bearer ' + generate_token(@user.id), + 'Content-Type': 'application/json' + } + end + + describe 'GET checklist_items, #index' do + context 'when has valid params' do + it 'renders 200' do + get api_v1_team_project_experiment_task_protocol_step_checklist_items_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + checklist_id: @checklist.id + ), headers: @valid_headers + + expect(response).to have_http_status(200) + end + end + + context 'when checklist is not found' do + it 'renders 404' do + get api_v1_team_project_experiment_task_protocol_step_checklist_items_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + checklist_id: -1 + ), headers: @valid_headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET checklist_item, #show' do + let(:checklist_item) { create(:checklist_item, checklist: @checklist) } + + context 'when has valid params' do + it 'renders 200' do + get api_v1_team_project_experiment_task_protocol_step_checklist_item_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + checklist_id: @checklist.id, + id: checklist_item.id + ), headers: @valid_headers + + expect(response).to have_http_status(200) + end + end + + context 'when experiment is archived and permission checks fails' do + it 'renders 403' do + @experiment.update_attribute(:archived, true) + + get api_v1_team_project_experiment_task_protocol_step_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + checklist_id: @checklist.id, + id: checklist_item.id + ), headers: @valid_headers + + expect(response).to have_http_status(403) + end + end + end + + describe 'POST checklist_item, #create' do + let(:action) do + post(api_v1_team_project_experiment_task_protocol_step_checklist_items_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + checklist_id: @checklist.id + ), params: request_body.to_json, headers: @valid_headers) + end + + context 'when has valid params' do + let(:request_body) do + { + data: { + type: 'checklist_items', + attributes: { + text: 'New checklist_item' + } + } + } + end + + it 'creates new checklist_item' do + expect { action }.to change { ChecklistItem.count }.by(1) + end + + it 'returns status 201' do + action + + expect(response).to have_http_status 201 + end + + it 'returns well formated response' do + action + + expect(json).to match( + hash_including( + data: hash_including( + type: 'checklist_items', + attributes: hash_including(text: 'New checklist_item') + ) + ) + ) + end + end + + context 'when has missing param' do + let(:request_body) do + { + data: { + type: 'checklist_items', + attributes: { + } + } + } + end + + it 'renders 400' do + action + + expect(response).to have_http_status(400) + end + end + end + + describe 'PATCH checklist_item, #update' do + let(:checklist_item) { create(:checklist_item, checklist: @checklist) } + let(:action) do + patch(api_v1_team_project_experiment_task_protocol_step_checklist_item_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + checklist_id: @checklist.id, + id: checklist_item.id + ), params: request_body.to_json, headers: @valid_headers) + end + + context 'when has valid params' do + let(:request_body) do + { + data: { + type: 'checklist_items', + attributes: { + text: 'New checklist_item name' + } + } + } + end + + it 'returns status 200' do + action + + expect(response).to have_http_status 200 + end + + it 'returns well formated response' do + action + + expect(json).to match( + hash_including( + data: hash_including( + type: 'checklist_items', + attributes: hash_including( + text: 'New checklist_item name' + ) + ) + ) + ) + end + end + + context 'when has missing param' do + let(:request_body) do + { + data: { + type: 'checklist_items', + attributes: { + } + } + } + end + + it 'renders 400' do + action + + expect(response).to have_http_status(400) + end + end + end + + describe 'DELETE checklist_item, #destroy' do + let(:checklist_item) { create(:checklist_item, checklist: @checklist) } + let(:action) do + delete(api_v1_team_project_experiment_task_protocol_step_checklist_item_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + checklist_id: @checklist.id, + id: checklist_item.id + ), headers: @valid_headers) + end + + it 'deletes checklist_item' do + action + expect(response).to have_http_status(200) + expect(ChecklistItem.where(id: checklist_item.id)).to_not exist + end + end +end diff --git a/spec/requests/api/v1/checklists_controller_spec.rb b/spec/requests/api/v1/checklists_controller_spec.rb new file mode 100644 index 000000000..f025b3633 --- /dev/null +++ b/spec/requests/api/v1/checklists_controller_spec.rb @@ -0,0 +1,247 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::V1::ChecklistsController', type: :request do + before :all do + @user = create(:user) + @team = create(:team, created_by: @user) + @project = create(:project, team: @team) + @experiment = create(:experiment, :with_tasks, project: @project) + @task = @experiment.my_modules.first + @protocol = create(:protocol, my_module: @task) + @step = create(:step, protocol: @protocol) + create(:user_team, user: @user, team: @team) + create(:user_project, :normal_user, user: @user, project: @project) + + @valid_headers = { + 'Authorization': 'Bearer ' + generate_token(@user.id), + 'Content-Type': 'application/json' + } + end + + describe 'GET checklists, #index' do + context 'when has valid params' do + it 'renders 200' do + get api_v1_team_project_experiment_task_protocol_step_checklists_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id + ), headers: @valid_headers + + expect(response).to have_http_status(200) + end + end + + context 'when step is not found' do + it 'renders 404' do + get api_v1_team_project_experiment_task_protocol_step_checklists_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: -1 + ), headers: @valid_headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET checklist, #show' do + let(:checklist) { create(:checklist, step: @step) } + + context 'when has valid params' do + it 'renders 200' do + get api_v1_team_project_experiment_task_protocol_step_checklist_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + id: checklist.id + ), headers: @valid_headers + + expect(response).to have_http_status(200) + end + end + + context 'when experiment is archived and permission checks fails' do + it 'renders 403' do + @experiment.update_attribute(:archived, true) + + get api_v1_team_project_experiment_task_protocol_step_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + id: checklist.id + ), headers: @valid_headers + + expect(response).to have_http_status(403) + end + end + end + + describe 'POST checklist, #create' do + let(:action) do + post(api_v1_team_project_experiment_task_protocol_step_checklists_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id + ), params: request_body.to_json, headers: @valid_headers) + end + + context 'when has valid params' do + let(:request_body) do + { + data: { + type: 'checklists', + attributes: { + name: 'New checklist' + } + } + } + end + + it 'creates new checklist' do + expect { action }.to change { Checklist.count }.by(1) + end + + it 'returns status 201' do + action + + expect(response).to have_http_status 201 + end + + it 'returns well formated response' do + action + + expect(json).to match( + hash_including( + data: hash_including( + type: 'checklists', + attributes: hash_including(name: 'New checklist') + ) + ) + ) + end + end + + context 'when has missing param' do + let(:request_body) do + { + data: { + type: 'checklists', + attributes: { + } + } + } + end + + it 'renders 400' do + action + + expect(response).to have_http_status(400) + end + end + end + + describe 'PATCH checklist, #update' do + let(:checklist) { create(:checklist, step: @step) } + let(:action) do + patch(api_v1_team_project_experiment_task_protocol_step_checklist_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + id: checklist.id + ), params: request_body.to_json, headers: @valid_headers) + end + + context 'when has valid params' do + let(:request_body) do + { + data: { + type: 'checklists', + attributes: { + name: 'New checklist name' + } + } + } + end + + it 'returns status 200' do + action + + expect(response).to have_http_status 200 + end + + it 'returns well formated response' do + action + + expect(json).to match( + hash_including( + data: hash_including( + type: 'checklists', + attributes: hash_including( + name: 'New checklist name' + ) + ) + ) + ) + end + end + + context 'when has missing param' do + let(:request_body) do + { + data: { + type: 'checklists', + attributes: { + } + } + } + end + + it 'renders 400' do + action + + expect(response).to have_http_status(400) + end + end + end + + describe 'DELETE checklist, #destroy' do + let(:checklist) { create(:checklist, step: @step) } + let(:action) do + delete(api_v1_team_project_experiment_task_protocol_step_checklist_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + id: checklist.id + ), headers: @valid_headers) + end + + it 'deletes checklist' do + action + expect(response).to have_http_status(200) + expect(Checklist.where(id: checklist.id)).to_not exist + end + end +end diff --git a/spec/requests/api/v1/results_controller_spec.rb b/spec/requests/api/v1/results_controller_spec.rb index d346ae686..52d16aeb9 100644 --- a/spec/requests/api/v1/results_controller_spec.rb +++ b/spec/requests/api/v1/results_controller_spec.rb @@ -481,10 +481,10 @@ RSpec.describe 'Api::V1::ResultsController', type: :request do ), params: @valid_text_hash_body.to_json, headers: @valid_headers) end - it 'returns status 500' do + it 'returns status 400' do action - expect(response).to have_http_status 500 + expect(response).to have_http_status 400 end end end diff --git a/spec/requests/api/v1/steps_controller_spec.rb b/spec/requests/api/v1/steps_controller_spec.rb index 428e31660..e2fb83ba4 100644 --- a/spec/requests/api/v1/steps_controller_spec.rb +++ b/spec/requests/api/v1/steps_controller_spec.rb @@ -18,6 +18,7 @@ RSpec.describe 'Api::V1::StepsController', type: :request do let(:protocol) { create :protocol, my_module: @task } let(:steps) { create_list(:step, 3, protocol: protocol) } + let(:step) { steps.first } describe 'GET steps, #index' do context 'when has valid params' do @@ -34,6 +35,20 @@ RSpec.describe 'Api::V1::StepsController', type: :request do end end + context 'when has valid params, with rendered RTE field' do + it 'renders 200' do + get api_v1_team_project_experiment_task_protocol_steps_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: protocol.id + ), params: { render_rte: true }, headers: @valid_headers + + expect(response).to have_http_status(200) + end + end + context 'when protocol is not found' do it 'renders 404' do get api_v1_team_project_experiment_task_protocol_steps_path( @@ -155,4 +170,102 @@ RSpec.describe 'Api::V1::StepsController', type: :request do end end end + + describe 'PATCH step, #update' do + before :all do + @valid_headers['Content-Type'] = 'application/json' + end + + let(:action) do + patch( + api_v1_team_project_experiment_task_protocol_step_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: protocol.id, + id: step.id + ), + params: request_body.to_json, + headers: @valid_headers + ) + end + + context 'when has valid params' do + let(:request_body) do + { + data: { + type: 'steps', + attributes: { + name: 'New step name', + description: 'New description about step' + } + } + } + end + + it 'returns status 200' do + action + + expect(response).to have_http_status 200 + end + + it 'returns well formated response' do + action + + expect(json).to match( + hash_including( + data: hash_including( + type: 'steps', + attributes: hash_including(name: 'New step name', description: 'New description about step') + ) + ) + ) + end + end + + context 'when has missing param' do + let(:request_body) do + { + data: { + type: 'steps', + attributes: { + } + } + } + end + + it 'renders 400' do + action + + expect(response).to have_http_status(400) + end + end + end + + describe 'DELETE step, #destroy' do + before :all do + @valid_headers['Content-Type'] = 'application/json' + end + + let(:action) do + delete( + api_v1_team_project_experiment_task_protocol_step_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: protocol.id, + id: step.id + ), + headers: @valid_headers + ) + end + + it 'deletes step' do + action + expect(response).to have_http_status(200) + expect(Step.where(id: step.id)).to_not exist + end + end end diff --git a/spec/requests/api/v1/tables_controller_spec.rb b/spec/requests/api/v1/tables_controller_spec.rb new file mode 100644 index 000000000..b6d543bee --- /dev/null +++ b/spec/requests/api/v1/tables_controller_spec.rb @@ -0,0 +1,250 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::V1::TablesController', type: :request do + before :all do + @user = create(:user) + @team = create(:team, created_by: @user) + @project = create(:project, team: @team) + @experiment = create(:experiment, :with_tasks, project: @project) + @task = @experiment.my_modules.first + @protocol = create(:protocol, my_module: @task) + @step = create(:step, protocol: @protocol) + create(:user_team, user: @user, team: @team) + create(:user_project, :normal_user, user: @user, project: @project) + + @valid_headers = { + 'Authorization': 'Bearer ' + generate_token(@user.id), + 'Content-Type': 'application/json' + } + end + + describe 'GET tables, #index' do + context 'when has valid params' do + it 'renders 200' do + get api_v1_team_project_experiment_task_protocol_step_tables_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id + ), headers: @valid_headers + + expect(response).to have_http_status(200) + end + end + + context 'when step is not found' do + it 'renders 404' do + get api_v1_team_project_experiment_task_protocol_step_tables_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: -1 + ), headers: @valid_headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET table, #show' do + let(:table) { create(:table, step: @step) } + + context 'when has valid params' do + it 'renders 200' do + get api_v1_team_project_experiment_task_protocol_step_table_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + id: table.id + ), headers: @valid_headers + + expect(response).to have_http_status(200) + end + end + + context 'when experiment is archived and permission checks fails' do + it 'renders 403' do + @experiment.update_attribute(:archived, true) + + get api_v1_team_project_experiment_task_protocol_step_table_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + id: table.id + ), headers: @valid_headers + + expect(response).to have_http_status(403) + end + end + end + + describe 'POST table, #create' do + let(:action) do + post(api_v1_team_project_experiment_task_protocol_step_tables_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id + ), params: request_body.to_json, headers: @valid_headers) + end + + context 'when has valid params' do + let(:request_body) do + { + data: { + type: 'tables', + attributes: { + name: 'New table', + contents: '{"data": [["group/time", "1 dpi", "6 dpi", "", ""], ["PVYNTN", "1", "1", "", ""]]}' + } + } + } + end + + it 'creates new table' do + expect { action }.to change { Table.count }.by(1) + end + + it 'returns status 201' do + action + + expect(response).to have_http_status 201 + end + + it 'returns well formated response' do + action + + expect(json).to match( + hash_including( + data: hash_including( + type: 'tables', + attributes: hash_including(name: 'New table') + ) + ) + ) + end + end + + context 'when has missing param' do + let(:request_body) do + { + data: { + type: 'tables', + attributes: { + } + } + } + end + + it 'renders 400' do + action + + expect(response).to have_http_status(400) + end + end + end + + describe 'PATCH table, #update' do + let(:table) { create(:table, step: @step) } + let(:action) do + patch(api_v1_team_project_experiment_task_protocol_step_table_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + id: table.id + ), params: request_body.to_json, headers: @valid_headers) + end + + context 'when has valid params' do + let(:request_body) do + { + data: { + type: 'tables', + attributes: { + name: 'New table name', + contents: '{"data": [["group/time", "2 dpi", "7 dpi", "", ""], ["PVYNTN", "2", "2", "", ""]]}' + } + } + } + end + + it 'returns status 200' do + action + + expect(response).to have_http_status 200 + end + + it 'returns well formated response' do + action + + expect(json).to match( + hash_including( + data: hash_including( + type: 'tables', + attributes: hash_including( + name: 'New table name', + contents: '{"data": [["group/time", "2 dpi", "7 dpi", "", ""], ["PVYNTN", "2", "2", "", ""]]}' + ) + ) + ) + ) + end + end + + context 'when has missing param' do + let(:request_body) do + { + data: { + type: 'tables', + attributes: { + } + } + } + end + + it 'renders 400' do + action + + expect(response).to have_http_status(400) + end + end + end + + describe 'DELETE table, #destroy' do + let(:table) { create(:table, step: @step) } + let(:action) do + delete(api_v1_team_project_experiment_task_protocol_step_table_path( + team_id: @team.id, + project_id: @project.id, + experiment_id: @experiment.id, + task_id: @task.id, + protocol_id: @protocol.id, + step_id: @step.id, + id: table.id + ), headers: @valid_headers) + end + + it 'deletes table' do + action + expect(response).to have_http_status(200) + expect(Table.where(id: table.id)).to_not exist + end + end +end diff --git a/spec/requests/api/v1/tasks_controller_spec.rb b/spec/requests/api/v1/tasks_controller_spec.rb index 3059aab09..abeb9619e 100644 --- a/spec/requests/api/v1/tasks_controller_spec.rb +++ b/spec/requests/api/v1/tasks_controller_spec.rb @@ -236,4 +236,108 @@ RSpec.describe 'Api::V1::TasksController', type: :request do end end end + + describe 'PATCH task, #update' do + before :all do + @valid_headers['Content-Type'] = 'application/json' + end + + let(:task) { @valid_experiment.my_modules.take } + + let(:action) do + patch( + api_v1_team_project_experiment_task_path( + team_id: @valid_project.team.id, + project_id: @valid_project.id, + experiment_id: @valid_experiment.id, + id: task.id + ), + params: request_body.to_json, + headers: @valid_headers + ) + end + + context 'when has valid params' do + let(:request_body) do + { + data: { + type: 'tasks', + attributes: { + name: 'New task name', + description: 'New description about task' + } + } + } + end + + it 'returns status 200' do + action + + expect(response).to have_http_status 200 + end + + it 'returns well formated response' do + action + + expect(json).to match( + hash_including( + data: hash_including( + type: 'tasks', + attributes: hash_including(name: 'New task name', description: 'New description about task') + ) + ) + ) + end + end + + context 'task completion, when has valid params' do + let(:request_body) do + { + data: { + type: 'tasks', + attributes: { + state: 'completed' + } + } + } + end + + it 'returns status 200' do + action + + expect(response).to have_http_status 200 + end + + it 'returns well formated response' do + action + + expect(json).to match( + hash_including( + data: hash_including( + type: 'tasks', + attributes: hash_including(state: 'completed') + ) + ) + ) + end + end + + context 'when has missing param' do + let(:request_body) do + { + data: { + type: 'tasks', + attributes: { + } + } + } + end + + it 'renders 400' do + action + + expect(response).to have_http_status(400) + end + end + end end