mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-10 07:05:57 +08:00
Merge pull request #2214 from biosistemika/features/task-api-endpoints
Develop << Features/task api endpoints
This commit is contained in:
commit
759b3acfe9
24 changed files with 996 additions and 58 deletions
|
@ -478,6 +478,11 @@ Rails/ScopeArgs:
|
|||
Include:
|
||||
- app/models/**/*.rb
|
||||
|
||||
Rails/SkipsModelValidations:
|
||||
Exclude:
|
||||
- spec/**/*_spec.rb
|
||||
- spec/factories/*.rb
|
||||
|
||||
Rails/TimeZone:
|
||||
EnforcedStyle: flexible
|
||||
|
||||
|
|
50
app/controllers/api/v1/assets_controller.rb
Normal file
50
app/controllers/api/v1/assets_controller.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class AssetsController < BaseController
|
||||
before_action :load_team, :load_project, :load_experiment, :load_task, :load_protocol, :load_step
|
||||
before_action :load_asset, only: :show
|
||||
|
||||
def index
|
||||
attachments = @step.assets
|
||||
.page(params.dig(:page, :number))
|
||||
.per(params.dig(:page, :size))
|
||||
|
||||
render jsonapi: attachments, each_serializer: AssetSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
render jsonapi: @asset, serializer: AssetSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
raise PermissionError.new(Asset, :create) unless can_manage_protocol_in_module?(@protocol)
|
||||
|
||||
asset = @step.assets.new(asset_params)
|
||||
asset.save!(context: :on_api_upload)
|
||||
|
||||
asset.post_process_file
|
||||
|
||||
render jsonapi: asset,
|
||||
serializer: AssetSerializer,
|
||||
status: :created
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def asset_params
|
||||
raise TypeError unless params.require(:data).require(:type) == 'attachments'
|
||||
|
||||
attr_list = %i(file)
|
||||
params.require(:data).require(:attributes).require(attr_list)
|
||||
params.require(:data).require(:attributes).permit(attr_list)
|
||||
end
|
||||
|
||||
def load_asset
|
||||
@asset = @step.assets.find(params.require(:id))
|
||||
raise PermissionError.new(Asset, :read) unless can_read_protocol_in_module?(@asset.step.protocol)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -110,21 +110,27 @@ module Api
|
|||
|
||||
def load_project(key = :project_id)
|
||||
@project = @team.projects.find(params.require(key))
|
||||
unless can_read_project?(@project)
|
||||
raise PermissionError.new(Project, :read)
|
||||
end
|
||||
raise PermissionError.new(Project, :read) unless can_read_project?(@project)
|
||||
end
|
||||
|
||||
def load_experiment(key = :experiment_id)
|
||||
@experiment = @project.experiments.find(params.require(key))
|
||||
unless can_read_experiment?(@experiment)
|
||||
raise PermissionError.new(Experiment, :read)
|
||||
end
|
||||
raise PermissionError.new(Experiment, :read) unless can_read_experiment?(@experiment)
|
||||
end
|
||||
|
||||
def load_task(key = :task_id)
|
||||
@task = @experiment.my_modules.find(params.require(key))
|
||||
end
|
||||
|
||||
def load_protocol(key = :protocol_id)
|
||||
@protocol = @task.protocols.find(params.require(key))
|
||||
raise PermissionError.new(Protocol, :read) unless can_read_protocol_in_module?(@protocol)
|
||||
end
|
||||
|
||||
def load_step(key = :step_id)
|
||||
@step = @protocol.steps.find(params.require(key))
|
||||
raise PermissionError.new(Protocol, :read) unless can_read_protocol_in_module?(@step.protocol)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# rubocop:disable Metrics/LineLength
|
||||
module Api
|
||||
module V1
|
||||
class ResultsController < BaseController
|
||||
before_action :load_team
|
||||
before_action :load_project
|
||||
before_action :load_experiment
|
||||
before_action :load_task
|
||||
before_action :load_result, only: %i(show)
|
||||
before_action :check_manage_permissions, only: %i(create)
|
||||
before_action :load_team, :load_project, :load_experiment, :load_task
|
||||
before_action :load_result, only: %i(show update)
|
||||
before_action :check_manage_permissions, only: %i(create update)
|
||||
|
||||
def index
|
||||
results = @task.results
|
||||
|
@ -21,12 +17,31 @@ module Api
|
|||
|
||||
def create
|
||||
create_text_result if result_text_params.present?
|
||||
|
||||
create_file_result if !@result && result_file_params.present?
|
||||
|
||||
render jsonapi: @result,
|
||||
serializer: ResultSerializer,
|
||||
include: %i(text table file),
|
||||
status: :created
|
||||
end
|
||||
|
||||
def update
|
||||
@result.attributes = result_params
|
||||
|
||||
update_file_result if result_file_params.present? && @result.is_asset
|
||||
update_text_result if result_text_params.present? && @result.is_text
|
||||
|
||||
if (@result.changed? && @result.save!) || @asset_result_updated
|
||||
render jsonapi: @result,
|
||||
serializer: ResultSerializer,
|
||||
include: %i(text table file),
|
||||
status: :ok
|
||||
else
|
||||
render body: nil, status: :no_content
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
render jsonapi: @result, serializer: ResultSerializer,
|
||||
include: %i(text table file)
|
||||
|
@ -39,12 +54,11 @@ module Api
|
|||
end
|
||||
|
||||
def check_manage_permissions
|
||||
unless can_manage_module?(@task)
|
||||
raise PermissionError.new(MyModule, :manage)
|
||||
end
|
||||
raise PermissionError.new(MyModule, :manage) unless can_manage_module?(@task)
|
||||
end
|
||||
|
||||
def create_text_result
|
||||
# rubocop:disable Metrics/BlockLength:
|
||||
Result.transaction do
|
||||
@result = Result.create!(
|
||||
user: current_user,
|
||||
|
@ -80,6 +94,29 @@ module Api
|
|||
result_text.save!
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/BlockLength:
|
||||
end
|
||||
|
||||
def update_text_result
|
||||
raise NotImplementedError, 'update_text_result should be implemented!'
|
||||
end
|
||||
|
||||
def create_file_result
|
||||
Result.transaction do
|
||||
@result = @task.results.create!(result_params.merge(user_id: current_user.id))
|
||||
asset = Asset.create!(result_file_params)
|
||||
ResultAsset.create!(asset: asset, result: @result)
|
||||
end
|
||||
end
|
||||
|
||||
def update_file_result
|
||||
old_checksum, new_checksum = nil
|
||||
Result.transaction do
|
||||
old_checksum = @result.asset.file.blob.checksum
|
||||
@result.asset.file.attach(result_file_params[:file])
|
||||
new_checksum = @result.asset.file.blob.checksum
|
||||
end
|
||||
@asset_result_updated = old_checksum != new_checksum
|
||||
end
|
||||
|
||||
def result_params
|
||||
|
@ -92,10 +129,19 @@ module Api
|
|||
# Partially implement sideposting draft
|
||||
# https://github.com/json-api/json-api/pull/1197
|
||||
def result_text_params
|
||||
prms =
|
||||
params[:included]&.select { |el| el[:type] == 'result_texts' }&.first
|
||||
prms = params[:included]&.select { |el| el[:type] == 'result_texts' }&.first
|
||||
return nil unless prms
|
||||
|
||||
prms.require(:attributes).require(:text)
|
||||
prms[:attributes]
|
||||
prms.dig(:attributes).permit(:text)
|
||||
end
|
||||
|
||||
def result_file_params
|
||||
prms = params[:included]&.select { |el| el[:type] == 'result_files' }&.first
|
||||
return nil unless prms
|
||||
|
||||
prms.require(:attributes).require(:file)
|
||||
prms.dig(:attributes).permit(:file)
|
||||
end
|
||||
|
||||
def tiny_mce_asset_params
|
||||
|
@ -126,4 +172,3 @@ module Api
|
|||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/LineLength
|
||||
|
|
46
app/controllers/api/v1/steps_controller.rb
Normal file
46
app/controllers/api/v1/steps_controller.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class StepsController < BaseController
|
||||
before_action :load_team, :load_project, :load_experiment, :load_task, :load_protocol
|
||||
before_action only: :show do
|
||||
load_step(:id)
|
||||
end
|
||||
|
||||
def index
|
||||
steps = @protocol.steps
|
||||
.page(params.dig(:page, :number))
|
||||
.per(params.dig(:page, :size))
|
||||
|
||||
render jsonapi: steps, each_serializer: StepSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
render jsonapi: @step, serializer: StepSerializer
|
||||
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,
|
||||
position: @protocol.number_of_steps))
|
||||
|
||||
render jsonapi: step,
|
||||
serializer: StepSerializer,
|
||||
status: :created
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -23,6 +23,16 @@ module Api
|
|||
render jsonapi: @task, serializer: TaskSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
raise PermissionError.new(MyModule, :create) unless can_manage_experiment?(@experiment)
|
||||
|
||||
my_module = @experiment.my_modules.create!(my_module_params)
|
||||
|
||||
render jsonapi: my_module,
|
||||
serializer: TaskSerializer,
|
||||
status: :created
|
||||
end
|
||||
|
||||
def activities
|
||||
activities = ActivitiesService.my_module_activities(@task)
|
||||
.page(params.dig(:page, :number))
|
||||
|
@ -34,6 +44,14 @@ module Api
|
|||
|
||||
private
|
||||
|
||||
def my_module_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])
|
||||
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
|
||||
|
|
|
@ -19,8 +19,8 @@ class Asset < ApplicationRecord
|
|||
# This could cause some problems if you create empty asset and want to
|
||||
# assign it to result
|
||||
validate :step_or_result_or_repository_asset_value
|
||||
validate :wopi_filename_valid,
|
||||
on: :wopi_file_creation
|
||||
validate :wopi_filename_valid, on: :wopi_file_creation
|
||||
validate :check_file_size, on: :on_api_upload
|
||||
|
||||
belongs_to :created_by,
|
||||
foreign_key: 'created_by_id',
|
||||
|
@ -448,4 +448,12 @@ class Asset < ApplicationRecord
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
def check_file_size
|
||||
if file.attached?
|
||||
if file.blob.byte_size > Rails.application.config.x.file_max_size_mb.megabytes
|
||||
errors.add(:file, I18n.t('activerecord.errors.models.asset.attributes.file.too_big'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,8 +15,8 @@ class MyModule < ApplicationRecord
|
|||
validates :description, length: { maximum: Constants::RICH_TEXT_MAX_LENGTH }
|
||||
validates :x, :y, :workflow_order, presence: true
|
||||
validates :experiment, presence: true
|
||||
validates :my_module_group, presence: true,
|
||||
if: proc { |mm| !mm.my_module_group_id.nil? }
|
||||
validates :my_module_group, presence: true, if: proc { |mm| !mm.my_module_group_id.nil? }
|
||||
validate :coordinates_uniqueness_check, if: :active?
|
||||
|
||||
belongs_to :created_by,
|
||||
foreign_key: 'created_by_id',
|
||||
|
@ -419,13 +419,12 @@ class MyModule < ApplicationRecord
|
|||
|
||||
def deep_clone_to_experiment(current_user, experiment)
|
||||
# Copy the module
|
||||
clone = MyModule.new(
|
||||
name: self.name,
|
||||
experiment: experiment,
|
||||
description: self.description,
|
||||
x: self.x,
|
||||
y: self.y)
|
||||
clone.save
|
||||
clone = MyModule.new(name: name, experiment: experiment, description: description, x: x, y: y)
|
||||
|
||||
# set new position if cloning in the same experiment
|
||||
clone.attributes = get_new_position if clone.experiment == experiment
|
||||
|
||||
clone.save!
|
||||
|
||||
# Remove the automatically generated protocol,
|
||||
# & clone the protocol instead
|
||||
|
@ -434,17 +433,18 @@ class MyModule < ApplicationRecord
|
|||
|
||||
# Update the cloned protocol if neccesary
|
||||
clone_tinymce_assets(clone, clone.experiment.project.team)
|
||||
clone.protocols << self.protocol.deep_clone_my_module(self, current_user)
|
||||
clone.protocols << protocol.deep_clone_my_module(self, current_user)
|
||||
clone.reload
|
||||
|
||||
# fixes linked protocols
|
||||
clone.protocols.each do |protocol|
|
||||
next unless protocol.linked?
|
||||
|
||||
protocol.updated_at = protocol.parent_updated_at
|
||||
protocol.save
|
||||
end
|
||||
|
||||
return clone
|
||||
clone
|
||||
end
|
||||
|
||||
# Find an empty position for the restored module. It's
|
||||
|
@ -500,4 +500,10 @@ class MyModule < ApplicationRecord
|
|||
def create_blank_protocol
|
||||
protocols << Protocol.new_blank_for_module(self)
|
||||
end
|
||||
|
||||
def coordinates_uniqueness_check
|
||||
if experiment && experiment.my_modules.active.where(x: x, y: y).where.not(id: id).any?
|
||||
errors.add(:position, I18n.t('activerecord.errors.models.my_module.attributes.position.not_unique'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
21
app/serializers/api/v1/asset_serializer.rb
Normal file
21
app/serializers/api/v1/asset_serializer.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class AssetSerializer < ActiveModel::Serializer
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
type :attachments
|
||||
attributes :id, :file_name, :file_size, :file_type, :file_url
|
||||
belongs_to :step, serializer: StepSerializer
|
||||
|
||||
def file_type
|
||||
object.content_type
|
||||
end
|
||||
|
||||
def file_url
|
||||
rails_blob_url(object.file, disposition: 'attachment') if object.file.attached?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,8 +10,8 @@ module Api
|
|||
serializer: ProtocolKeywordSerializer,
|
||||
class_name: 'ProtocolKeyword',
|
||||
unless: -> { object.protocol_keywords.empty? }
|
||||
belongs_to :parent, serializer: ProtocolSerializer,
|
||||
if: -> { object.parent.present? }
|
||||
has_many :steps, serializer: StepSerializer, if: -> { object.steps.any? }
|
||||
belongs_to :parent, serializer: ProtocolSerializer, if: -> { object.parent.present? }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
17
app/serializers/api/v1/step_serializer.rb
Normal file
17
app/serializers/api/v1/step_serializer.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class StepSerializer < ActiveModel::Serializer
|
||||
type :steps
|
||||
attributes :id, :name, :description, :position, :completed
|
||||
attribute :completed_on, if: :completed?
|
||||
belongs_to :protocol, serializer: ProtocolSerializer
|
||||
has_many :assets, serializer: AssetSerializer
|
||||
|
||||
def completed?
|
||||
object.completed
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -81,6 +81,15 @@ en:
|
|||
attributes:
|
||||
team_id:
|
||||
same_team: "Inventory can't be shared to the same team as it belongs to"
|
||||
my_module:
|
||||
attributes:
|
||||
position:
|
||||
not_unique: "X and Y position has already been taken by another task in the experiment."
|
||||
asset:
|
||||
attributes:
|
||||
file:
|
||||
too_big: "is too big"
|
||||
|
||||
|
||||
helpers:
|
||||
label:
|
||||
|
|
|
@ -620,7 +620,7 @@ Rails.application.routes.draw do
|
|||
namespace :api, defaults: { format: 'json' } do
|
||||
get 'health', to: 'api#health'
|
||||
get 'status', to: 'api#status'
|
||||
if Api.configuration.core_api_v1_enabled
|
||||
if Api.configuration.core_api_v1_enabled || Rails.env.development?
|
||||
namespace :v1 do
|
||||
resources :teams, only: %i(index show) do
|
||||
resources :inventories,
|
||||
|
@ -655,7 +655,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) do
|
||||
resources :tasks, only: %i(index show create) do
|
||||
resources :task_inventory_items, only: %i(index show),
|
||||
path: 'items',
|
||||
as: :items
|
||||
|
@ -665,8 +665,12 @@ Rails.application.routes.draw do
|
|||
resources :task_tags, only: %i(index show),
|
||||
path: 'tags',
|
||||
as: :tags
|
||||
resources :protocols, only: %i(index)
|
||||
resources :results, only: %i(index create show)
|
||||
resources :protocols, only: %i(index) do
|
||||
resources :steps, only: %i(index show create) do
|
||||
resources :assets, only: %i(index show create), path: 'attachments'
|
||||
end
|
||||
end
|
||||
resources :results, only: %i(index create show update)
|
||||
get 'activities', to: 'tasks#activities'
|
||||
end
|
||||
end
|
||||
|
|
40
lib/tasks/my_modules.rake
Normal file
40
lib/tasks/my_modules.rake
Normal file
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# On production around 2k records for repair, for production DB it tooked cca 1 min
|
||||
|
||||
# rubocop:disable Metrics/BlockLength
|
||||
namespace :my_modules do
|
||||
desc 'Find new positions on canvas for already taken coordiantes'
|
||||
task fix_positions: :environment do
|
||||
query = MyModule.select('COUNT(*) as duplicates', :x, :y, :experiment_id)
|
||||
.where(archived: false)
|
||||
.group(:x, :y, :experiment_id)
|
||||
.having('COUNT(my_modules.id)>1')
|
||||
|
||||
Rails.logger.info '*********************************************************************************'
|
||||
Rails.logger.info "You have to relocate #{query.sum { |a| a.duplicates - 1 }} tasks"
|
||||
Rails.logger.info '*********************************************************************************'
|
||||
|
||||
query.each do |row|
|
||||
tasks_to_update = MyModule.where(experiment_id: row.experiment, x: row.x, y: row.y, archived: false)
|
||||
.order(created_at: :asc)
|
||||
.offset(1)
|
||||
|
||||
tasks_to_update.find_each do |task|
|
||||
coordinates = task.get_new_position
|
||||
task.attributes = coordinates
|
||||
begin
|
||||
task.save!
|
||||
Rails.logger.info "Relocated task with ID #{task.id}"
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.error "Unable to save task with ID #{task.id}: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Rails.logger.info '*********************************************************************************'
|
||||
Rails.logger.info "You have #{query.reload.sum { |a| a.duplicates - 1 }} tasks on invalid positions"
|
||||
Rails.logger.info '*********************************************************************************'
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/BlockLength
|
|
@ -4,7 +4,7 @@ FactoryBot.define do
|
|||
factory :my_module do
|
||||
sequence(:name) { |n| "Task-#{n}" }
|
||||
x { Faker::Number.between(from: 1, to: 100) }
|
||||
y { Faker::Number.between(from: 1, to: 100) }
|
||||
sequence(:y) { |n| n }
|
||||
workflow_order { MyModule.where(experiment_id: experiment.id).count + 1 }
|
||||
experiment
|
||||
my_module_group { create :my_module_group, experiment: experiment }
|
||||
|
|
BIN
spec/fixtures/files/apple.jpg
vendored
Normal file
BIN
spec/fixtures/files/apple.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
41
spec/lib/tasks/my_modules_spec.rb
Normal file
41
spec/lib/tasks/my_modules_spec.rb
Normal file
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe 'my_modules:fix_positions' do
|
||||
include_context 'rake'
|
||||
|
||||
before(:all) do
|
||||
experiment = create :experiment
|
||||
|
||||
100.times do
|
||||
create :my_module, experiment: experiment
|
||||
end
|
||||
|
||||
# set 10 tasks same position
|
||||
my_modules_with_same_position = MyModule.limit(10)
|
||||
my_modules_with_same_position.update_all(x: 0, y: 0)
|
||||
|
||||
# 1 module should be invalid
|
||||
my_modules_with_same_position.second.update_column(:name, 'a')
|
||||
|
||||
my_modules_with_same_position.third.update_column(:archived, true)
|
||||
@my_module_id = my_modules_with_same_position.fourth.id
|
||||
end
|
||||
context 'when record is valid except position' do
|
||||
it 'changes position for my_module' do
|
||||
expect { subject.invoke }.to(change { MyModule.find(@my_module_id).y })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when record is invalid' do
|
||||
it 'remains error on position' do
|
||||
subject.invoke
|
||||
my_module = MyModule.find_by(name: 'a')
|
||||
my_module.valid?
|
||||
|
||||
expect(my_module.errors.messages[:position])
|
||||
.to(eq ['X and Y position has already been taken by another task in the experiment.'])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -120,7 +120,7 @@ describe Experiment, type: :model do
|
|||
{ id: 'n' + i.to_s,
|
||||
name: t.name + '_new',
|
||||
x: 50,
|
||||
y: 50 }
|
||||
y: 50 + i }
|
||||
end
|
||||
end
|
||||
let(:to_clone) do
|
||||
|
|
|
@ -63,19 +63,48 @@ describe MyModule, type: :model do
|
|||
it { should have_many(:my_module_antecessors).class_name('MyModule') }
|
||||
end
|
||||
|
||||
describe 'Should be a valid object' do
|
||||
it { should validate_presence_of :x }
|
||||
it { should validate_presence_of :y }
|
||||
it { should validate_presence_of :workflow_order }
|
||||
it { should validate_presence_of :experiment }
|
||||
it do
|
||||
should validate_length_of(:name)
|
||||
.is_at_least(Constants::NAME_MIN_LENGTH)
|
||||
.is_at_most(Constants::NAME_MAX_LENGTH)
|
||||
describe 'Validations' do
|
||||
describe '#name' do
|
||||
it do
|
||||
is_expected.to(validate_length_of(:name)
|
||||
.is_at_least(Constants::NAME_MIN_LENGTH)
|
||||
.is_at_most(Constants::NAME_MAX_LENGTH))
|
||||
end
|
||||
end
|
||||
it do
|
||||
should validate_length_of(:description)
|
||||
.is_at_most(Constants::RICH_TEXT_MAX_LENGTH)
|
||||
|
||||
describe '#description' do
|
||||
it do
|
||||
is_expected.to(validate_length_of(:description)
|
||||
.is_at_most(Constants::RICH_TEXT_MAX_LENGTH))
|
||||
end
|
||||
end
|
||||
|
||||
describe '#x, #y scoped to experiment for active modules' do
|
||||
it { is_expected.to validate_presence_of :x }
|
||||
it { is_expected.to validate_presence_of :y }
|
||||
|
||||
it 'should be invalid for same x, y, and experiment' do
|
||||
my_module.save
|
||||
new_my_module = my_module.dup
|
||||
|
||||
expect(new_my_module).not_to be_valid
|
||||
end
|
||||
|
||||
it 'should be valid when module with same x, y and expriment is archived' do
|
||||
my_module.save
|
||||
new_my_module = my_module.dup
|
||||
my_module.update_column(:archived, true)
|
||||
|
||||
expect(new_my_module).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe '#workflow_order' do
|
||||
it { is_expected.to validate_presence_of :workflow_order }
|
||||
end
|
||||
|
||||
describe '#experiment' do
|
||||
it { is_expected.to validate_presence_of :experiment }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
180
spec/requests/api/v1/assets_controller_spec.rb
Normal file
180
spec/requests/api/v1/assets_controller_spec.rb
Normal file
|
@ -0,0 +1,180 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::AssetsController', 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) }
|
||||
end
|
||||
|
||||
let(:asset) { create :asset, step: @step }
|
||||
|
||||
describe 'GET steps, #index' do
|
||||
context 'when has valid params' do
|
||||
it 'renders 200' do
|
||||
create(:step_asset, step: @step, asset: asset)
|
||||
|
||||
get(api_v1_team_project_experiment_task_protocol_step_assets_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 protocol is not found' do
|
||||
it 'renders 404' do
|
||||
get(api_v1_team_project_experiment_task_protocol_step_assets_path(
|
||||
team_id: @team.id,
|
||||
project_id: @project.id,
|
||||
experiment_id: @experiment.id,
|
||||
task_id: @task.id,
|
||||
protocol_id: -1,
|
||||
step_id: @step.id
|
||||
), headers: @valid_headers)
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET step, #show' do
|
||||
context 'when has valid params' do
|
||||
it 'renders 200' do
|
||||
create(:step_asset, step: @step, asset: asset)
|
||||
|
||||
get(api_v1_team_project_experiment_task_protocol_step_asset_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: asset.id
|
||||
), headers: @valid_headers)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when experiment is not found' do
|
||||
it 'renders 404' do
|
||||
get(api_v1_team_project_experiment_task_protocol_step_asset_path(
|
||||
team_id: @team.id,
|
||||
project_id: @project.id,
|
||||
experiment_id: -1,
|
||||
task_id: @task.id,
|
||||
protocol_id: @protocol.id,
|
||||
step_id: @step.id,
|
||||
id: asset.id
|
||||
), headers: @valid_headers)
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when asset is not found' do
|
||||
it 'renders 404' do
|
||||
get(api_v1_team_project_experiment_task_protocol_step_asset_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: -1
|
||||
), headers: @valid_headers)
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST step, #create' do
|
||||
before :all do
|
||||
@valid_headers['Content-Type'] = 'application/json'
|
||||
end
|
||||
|
||||
before :each do
|
||||
@file = fixture_file_upload('files/test.jpg', 'image/jpg')
|
||||
allow_any_instance_of(Asset).to receive(:post_process_file)
|
||||
end
|
||||
|
||||
let(:action) do
|
||||
post(api_v1_team_project_experiment_task_protocol_step_assets_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,
|
||||
headers: @valid_headers)
|
||||
end
|
||||
|
||||
context 'when has valid params' do
|
||||
let(:request_body) do
|
||||
{
|
||||
data: {
|
||||
type: 'attachments',
|
||||
attributes: {
|
||||
file: @file
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates new asset' do
|
||||
expect { action }.to change { Asset.count }.by(1)
|
||||
end
|
||||
|
||||
it 'returns status 201' do
|
||||
action
|
||||
|
||||
expect(response).to have_http_status 201
|
||||
end
|
||||
|
||||
it 'calls post_process_file function for text extraction' do
|
||||
expect_any_instance_of(Asset).to receive(:post_process_file)
|
||||
|
||||
action
|
||||
end
|
||||
end
|
||||
|
||||
context 'when has missing param' do
|
||||
let(:request_body) do
|
||||
{
|
||||
data: {
|
||||
type: 'attachments',
|
||||
attributes: {
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'renders 400' do
|
||||
action
|
||||
|
||||
expect(response).to have_http_status(400)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# rubocop:disable Metrics/LineLength
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::ResultsController', type: :request do
|
||||
|
@ -47,7 +46,7 @@ RSpec.describe 'Api::V1::ResultsController', type: :request do
|
|||
attributes: {
|
||||
name: Faker::Name.unique.name
|
||||
} },
|
||||
included: [
|
||||
included: [
|
||||
{ type: 'result_texts',
|
||||
attributes: {
|
||||
text: Faker::Lorem.sentence(word_count: 25)
|
||||
|
@ -267,6 +266,44 @@ RSpec.describe 'Api::V1::ResultsController', type: :request do
|
|||
expect { hash_body = json }.not_to raise_exception
|
||||
expect(hash_body['errors'][0]).to include('status': 404)
|
||||
end
|
||||
|
||||
context 'when resultType is File' do
|
||||
let(:file) { fixture_file_upload('files/test.jpg', 'image/jpg') }
|
||||
let(:request_body) do
|
||||
{
|
||||
data: {
|
||||
type: 'results',
|
||||
attributes: {
|
||||
name: 'my result'
|
||||
}
|
||||
},
|
||||
included: [
|
||||
{ type: 'result_files',
|
||||
attributes: {
|
||||
file: file
|
||||
} }
|
||||
]
|
||||
}
|
||||
end
|
||||
let(:action) do
|
||||
post(api_v1_team_project_experiment_task_results_path(
|
||||
team_id: @teams.first.id,
|
||||
project_id: @valid_project,
|
||||
experiment_id: @valid_experiment,
|
||||
task_id: @valid_task
|
||||
), params: request_body, headers: @valid_headers)
|
||||
end
|
||||
|
||||
it 'creates new asset' do
|
||||
expect { action }.to change { ResultAsset.count }.by(1)
|
||||
end
|
||||
|
||||
it 'returns status 201' do
|
||||
action
|
||||
|
||||
expect(response).to have_http_status 201
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET result, #show' do
|
||||
|
@ -329,5 +366,126 @@ RSpec.describe 'Api::V1::ResultsController', type: :request do
|
|||
expect(hash_body['errors'][0]).to include('status': 404)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT result, #update' do
|
||||
context 'when resultType is file' do
|
||||
let(:result_file) { @valid_task.results.last }
|
||||
let(:file) { fixture_file_upload('files/test.jpg', 'image/jpg') }
|
||||
let(:second_file) { fixture_file_upload('files/apple.jpg', 'image/jpg') }
|
||||
let(:request_body) do
|
||||
{
|
||||
data: {
|
||||
type: 'results',
|
||||
attributes: {
|
||||
name: 'my result'
|
||||
}
|
||||
},
|
||||
included: [
|
||||
{ type: 'result_files',
|
||||
attributes: {
|
||||
file: file
|
||||
} }
|
||||
]
|
||||
}
|
||||
end
|
||||
let(:action) do
|
||||
put(api_v1_team_project_experiment_task_result_path(
|
||||
team_id: @teams.first.id,
|
||||
project_id: @valid_project,
|
||||
experiment_id: @valid_experiment,
|
||||
task_id: @valid_task,
|
||||
id: result_file.id
|
||||
), params: request_body, headers: @valid_headers)
|
||||
end
|
||||
|
||||
context 'when has attributes for update' do
|
||||
it 'updates tasks name' do
|
||||
action
|
||||
|
||||
expect(result_file.reload.name).to eq('my result')
|
||||
end
|
||||
|
||||
it 'returns status 200' do
|
||||
action
|
||||
|
||||
expect(response).to have_http_status 200
|
||||
end
|
||||
end
|
||||
|
||||
context 'when has new image for update' do
|
||||
let(:request_body_with_same_name_new_file) do
|
||||
{
|
||||
data: {
|
||||
type: 'results',
|
||||
attributes: {
|
||||
name: result_file.reload.name
|
||||
}
|
||||
},
|
||||
included: [
|
||||
{ type: 'result_files',
|
||||
attributes: {
|
||||
file: second_file
|
||||
} }
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns status 200' do
|
||||
put(api_v1_team_project_experiment_task_result_path(
|
||||
team_id: @teams.first.id,
|
||||
project_id: @valid_project,
|
||||
experiment_id: @valid_experiment,
|
||||
task_id: @valid_task,
|
||||
id: result_file.id
|
||||
), params: request_body_with_same_name_new_file, headers: @valid_headers)
|
||||
|
||||
expect(response).to have_http_status 200
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is nothing to update' do
|
||||
let(:request_body_with_same_name) do
|
||||
{
|
||||
data: {
|
||||
type: 'results',
|
||||
attributes: {
|
||||
name: result_file.reload.name
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns 204' do
|
||||
put(api_v1_team_project_experiment_task_result_path(
|
||||
team_id: @teams.first.id,
|
||||
project_id: @valid_project,
|
||||
experiment_id: @valid_experiment,
|
||||
task_id: @valid_task,
|
||||
id: result_file.id
|
||||
), params: request_body_with_same_name.to_json, headers: @valid_headers)
|
||||
|
||||
expect(response).to have_http_status 204
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when resultType is text' do
|
||||
let(:result_text) { @valid_task.results.first }
|
||||
let(:action) do
|
||||
put(api_v1_team_project_experiment_task_result_path(
|
||||
team_id: @teams.first.id,
|
||||
project_id: @valid_project,
|
||||
experiment_id: @valid_experiment,
|
||||
task_id: @valid_task,
|
||||
id: result_text.id
|
||||
), params: @valid_text_hash_body.to_json, headers: @valid_headers)
|
||||
end
|
||||
|
||||
it 'returns status 500' do
|
||||
action
|
||||
|
||||
expect(response).to have_http_status 500
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/LineLength
|
||||
|
|
158
spec/requests/api/v1/steps_controller_spec.rb
Normal file
158
spec/requests/api/v1/steps_controller_spec.rb
Normal file
|
@ -0,0 +1,158 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::StepsController', 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
|
||||
create(:user_team, user: @user, team: @team)
|
||||
create(:user_project, :normal_user, user: @user, project: @project)
|
||||
|
||||
@valid_headers =
|
||||
{ 'Authorization': 'Bearer ' + generate_token(@user.id) }
|
||||
end
|
||||
|
||||
let(:protocol) { create :protocol, my_module: @task }
|
||||
let(:steps) { create_list(:step, 3, protocol: protocol) }
|
||||
|
||||
describe 'GET steps, #index' do
|
||||
context 'when has valid params' 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
|
||||
), 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(
|
||||
team_id: @team.id,
|
||||
project_id: @project.id,
|
||||
experiment_id: @experiment.id,
|
||||
task_id: @task.id,
|
||||
protocol_id: -1
|
||||
), headers: @valid_headers
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET step, #show' do
|
||||
context 'when has valid params' do
|
||||
it 'renders 200' do
|
||||
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,
|
||||
id: steps.first.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,
|
||||
id: steps.first.id
|
||||
), headers: @valid_headers
|
||||
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST step, #create' do
|
||||
before :all do
|
||||
@valid_headers['Content-Type'] = 'application/json'
|
||||
end
|
||||
|
||||
let(:action) do
|
||||
post(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: request_body.to_json,
|
||||
headers: @valid_headers)
|
||||
end
|
||||
|
||||
context 'when has valid params' do
|
||||
let(:request_body) do
|
||||
{
|
||||
data: {
|
||||
type: 'steps',
|
||||
attributes: {
|
||||
name: 'Step name',
|
||||
description: 'Description about step'
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates new step' do
|
||||
expect { action }.to change { Step.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: 'steps',
|
||||
attributes: hash_including(name: 'Step name')
|
||||
)
|
||||
)
|
||||
)
|
||||
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
|
||||
end
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe "Api::V1::TasksController", type: :request do
|
||||
RSpec.describe 'Api::V1::TasksController', type: :request do
|
||||
before :all do
|
||||
@user = create(:user)
|
||||
@teams = create_list(:team, 2, created_by: @user)
|
||||
|
@ -139,4 +139,101 @@ RSpec.describe "Api::V1::TasksController", type: :request do
|
|||
expect(hash_body['errors'][0]).to include('status': 404)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST tasks, #create' do
|
||||
before :all do
|
||||
create :user_project, :normal_user, user: @user, project: @valid_project
|
||||
@valid_headers['Content-Type'] = 'application/json'
|
||||
end
|
||||
|
||||
let(:request_body) do
|
||||
{
|
||||
data: {
|
||||
type: 'tasks',
|
||||
attributes: {
|
||||
name: 'task name',
|
||||
x: 1,
|
||||
y: 4
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
context 'when has valid params' do
|
||||
let(:action) do
|
||||
post(api_v1_team_project_experiment_tasks_path(
|
||||
team_id: @teams.first.id,
|
||||
project_id: @valid_project.id,
|
||||
experiment_id: @valid_experiment.id
|
||||
),
|
||||
params: request_body.to_json,
|
||||
headers: @valid_headers)
|
||||
end
|
||||
|
||||
it 'creates new my module' do
|
||||
expect { action }.to change { MyModule.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: 'tasks',
|
||||
attributes: hash_including(name: 'task name'),
|
||||
relationships: hash_including(outputs: { data: [] }, inputs: { data: [] })
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when has not valid params' do
|
||||
it 'renders 404 when project not found' do
|
||||
post(api_v1_team_project_experiment_tasks_path(
|
||||
team_id: @teams.first.id,
|
||||
project_id: -1,
|
||||
experiment_id: @valid_experiment.id
|
||||
),
|
||||
params: request_body.to_json,
|
||||
headers: @valid_headers)
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
|
||||
it 'renders 403 when user is not member of the team' do
|
||||
post(api_v1_team_project_experiment_tasks_path(
|
||||
team_id: @teams.second.id,
|
||||
project_id: @valid_project.id,
|
||||
experiment_id: @valid_experiment.id
|
||||
),
|
||||
params: request_body.to_json,
|
||||
headers: @valid_headers)
|
||||
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
|
||||
it 'renders 403 for use with view permissions' do
|
||||
up = UserProject.where(user: @user, project: @valid_project).first
|
||||
up.update_attribute(:role, :viewer)
|
||||
|
||||
post(api_v1_team_project_experiment_tasks_path(
|
||||
team_id: @teams.first.id,
|
||||
project_id: @valid_project.id,
|
||||
experiment_id: @valid_experiment.id
|
||||
),
|
||||
params: request_body.to_json,
|
||||
headers: @valid_headers)
|
||||
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
"updated_at": "2019-01-21T13:32:46.278Z",
|
||||
"workflow_order": -1,
|
||||
"x": 64,
|
||||
"y": 46
|
||||
"y": 22
|
||||
},
|
||||
"my_module_repository_rows": [],
|
||||
"my_module_tags": [],
|
||||
|
|
Loading…
Add table
Reference in a new issue