diff --git a/.github/workflows/lint_yard_docs.yml b/.github/workflows/lint_yard_docs.yml new file mode 100644 index 0000000000..a4a3685061 --- /dev/null +++ b/.github/workflows/lint_yard_docs.yml @@ -0,0 +1,18 @@ +name: Lint documentation +on: + - push + - pull_request + +jobs: + yard-junk: + runs-on: ubuntu-latest + env: + BUNDLE_WITHOUT: "cucumber deployment profile development default test" + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run yard-junk + run: bundle exec yard-junk --sanity diff --git a/.release-version b/.release-version index 288cc1f283..9d68c6c43b 100644 --- a/.release-version +++ b/.release-version @@ -1 +1 @@ -14.38.0 +14.39.0 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1b778979b1..22c06f6bf5 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -913,7 +913,6 @@ RSpec/NamedSubject: - 'spec/resources/api/v2/receptacle_resource_spec.rb' - 'spec/resources/api/v2/request_resource_spec.rb' - 'spec/resources/api/v2/request_type_resource_spec.rb' - - 'spec/resources/api/v2/sample_metadata_resource_spec.rb' - 'spec/resources/api/v2/sample_resource_spec.rb' - 'spec/resources/api/v2/study_resource_spec.rb' - 'spec/resources/api/v2/submission_resource_spec.rb' diff --git a/Gemfile b/Gemfile index 850f6d5d36..51bb14aaab 100644 --- a/Gemfile +++ b/Gemfile @@ -132,10 +132,6 @@ group :development do # Detect n+1 queries gem 'bullet' - # Automatically generate documentation - gem 'yard', require: false - gem 'yard-activerecord', '~> 0.0.16' - # MiniProfiler allows you to see the speed of a request conveniently on the page. # It also shows the SQL queries performed and allows you to profile a specific block of code. gem 'rack-mini-profiler' @@ -158,6 +154,11 @@ group :development, :linting do gem 'syntax_tree', require: false gem 'syntax_tree-haml', require: false gem 'syntax_tree-rbs', require: false + + # Automatically generate documentation + gem 'yard', require: false + gem 'yard-activerecord', '~> 0.0.16', require: false + gem 'yard-junk', '~> 0.0.9', require: false end group :linting, :test do diff --git a/Gemfile.lock b/Gemfile.lock index d4e644d26e..6fe00f7f6e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -115,6 +115,7 @@ GEM ast (2.4.2) avro (1.11.3) multi_json (~> 1.0) + backports (3.25.0) base64 (0.2.0) bigdecimal (3.1.8) bootsnap (1.18.3) @@ -555,6 +556,10 @@ GEM yard (0.9.36) yard-activerecord (0.0.16) yard (>= 0.8.3) + yard-junk (0.0.9) + backports (>= 3.18) + rainbow + yard zeitwerk (2.6.16) PLATFORMS @@ -652,6 +657,7 @@ DEPENDENCIES will_paginate-bootstrap yard yard-activerecord (~> 0.0.16) + yard-junk (~> 0.0.9) BUNDLED WITH 2.5.9 diff --git a/README.md b/README.md index 50c46b789b..4630426d0e 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,14 @@ You can then access the Sequencescape documentation through: [http://localhost:8 Yard will also try and document the installed gems: [http://localhost:8808/docs](http://localhost:8808/docs) +### Linting + +Yard-Junk is used to check for missing or incorrect documentation. To run the checks: + +```shell +bundle exec yard-junk --sanity +``` + ## Requirements The following tools are required for development: diff --git a/app/api/core/service/error_handling.rb b/app/api/core/service/error_handling.rb index c5722ddcb6..cbce1a0529 100644 --- a/app/api/core/service/error_handling.rb +++ b/app/api/core/service/error_handling.rb @@ -99,5 +99,10 @@ class IllegalOperation < RuntimeError self.api_error_message = 'requested action is not supported on this resource' end -Aliquot::TagClash.include Core::Service::Error::Behaviour -Aliquot::TagClash.api_error_code = 422 +class Aliquot::TagClash + include Core::Service::Error::Behaviour + + def self.api_error_code + 422 + end +end diff --git a/app/controllers/api/v2/plate_purposes_controller.rb b/app/controllers/api/v2/plate_purposes_controller.rb index 82702603ff..9cace3d1bb 100644 --- a/app/controllers/api/v2/plate_purposes_controller.rb +++ b/app/controllers/api/v2/plate_purposes_controller.rb @@ -2,10 +2,10 @@ module Api module V2 - # Provides a JSON API controller for receptacle + # Provides a JSON API controller for plate purposes. # See: http://jsonapi-resources.com/ for JSONAPI::Resource documentation class PlatePurposesController < JSONAPI::ResourceController - # By default JSONAPI::ResourceController provides most the standard + # By default JSONAPI::ResourceController provides most of the standard # behaviour, and in many cases this file may be left empty. end end diff --git a/app/controllers/api/v2/submission_templates_controller.rb b/app/controllers/api/v2/submission_templates_controller.rb new file mode 100644 index 0000000000..47776643e5 --- /dev/null +++ b/app/controllers/api/v2/submission_templates_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Api + module V2 + # Provides a JSON API controller for submission templates. + # See: http://jsonapi-resources.com/ for JSONAPI::Resource documentation. + class SubmissionTemplatesController < JSONAPI::ResourceController + # By default JSONAPI::ResourceController provides most of the standard + # behaviour, and in many cases this file may be left empty. + end + end +end diff --git a/app/controllers/api/v2/transfer_templates_controller.rb b/app/controllers/api/v2/transfer_templates_controller.rb new file mode 100644 index 0000000000..71a2af0278 --- /dev/null +++ b/app/controllers/api/v2/transfer_templates_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Api + module V2 + # Provides a JSON API controller for transfer templates. + # See: http://jsonapi-resources.com/ for JSONAPI::Resource documentation. + class TransferTemplatesController < JSONAPI::ResourceController + # By default JSONAPI::ResourceController provides most of the standard + # behaviour, and in many cases this file may be left empty. + end + end +end diff --git a/app/controllers/api/v2/tube_purposes_controller.rb b/app/controllers/api/v2/tube_purposes_controller.rb new file mode 100644 index 0000000000..6fa22e10c0 --- /dev/null +++ b/app/controllers/api/v2/tube_purposes_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Api + module V2 + # Provides a JSON API controller for tube purposes. + # See: http://jsonapi-resources.com/ for JSONAPI::Resource documentation + class TubePurposesController < JSONAPI::ResourceController + # By default JSONAPI::ResourceController provides most of the standard + # behaviour, and in many cases this file may be left empty. + end + end +end diff --git a/app/controllers/tag_layout_templates_controller.rb b/app/controllers/tag_layout_templates_controller.rb index e7249d8181..94c623b80c 100644 --- a/app/controllers/tag_layout_templates_controller.rb +++ b/app/controllers/tag_layout_templates_controller.rb @@ -6,18 +6,6 @@ # that they should be laid out column by column across a plate. # NB. Not all combinations will be valid. class TagLayoutTemplatesController < ApplicationController - DIRECTIONS = { - 'InColumns (A1,B1,C1...)': 'TagLayout::InColumns', - 'InRows (A1,A2,A3...)': 'TagLayout::InRows', - 'InInverseColumns (H12,G12,F12...)': 'TagLayout::InInverseColumns', - 'InInverseRows (H12,H11,H10...)': 'TagLayout::InInverseRows', - # These next two directions are used with the 'quadrants' walking by algorithm and the layout - # described refers to the 4 wells adjacent to one another, one from each of the 4 quadrants. - # The two directions order the tag distribution to the 4 quadrants differently. - 'InColumnsThenRows (A1,A2,B1,B2...)': 'TagLayout::InColumnsThenRows', - 'InColumnsThenColumns (A1,B1,A2,B2...)': 'TagLayout::InColumnsThenColumns' - }.freeze - authorize_resource def index @@ -36,7 +24,8 @@ def show # Allows for the passing in of tag group id using a link from the tag group show page. def new @tag_layout_template = TagLayoutTemplate.new(tag_group_id: params[:tag_group_id]) - @direction_algorithms = DIRECTIONS + @direction_algorithms = TagLayout::DIRECTION_ALGORITHMS + @walking_algorithms = TagLayout::WALKING_ALGORITHMS respond_to { |format| format.html } end @@ -49,7 +38,8 @@ def create flash[:notice] = I18n.t('tag_groups.success') format.html { redirect_to(@tag_layout_template) } else - @direction_algorithms = DIRECTIONS + @direction_algorithms = TagLayout::DIRECTION_ALGORITHMS + @walking_algorithms = TagLayout::WALKING_ALGORITHMS format.html { render action: 'new' } end end diff --git a/app/frontend/stylesheets/all/sequencescape.scss b/app/frontend/stylesheets/all/sequencescape.scss index 352e677226..3994f98d73 100644 --- a/app/frontend/stylesheets/all/sequencescape.scss +++ b/app/frontend/stylesheets/all/sequencescape.scss @@ -134,6 +134,10 @@ h3.card-header-custom { } } +.alert-error ul { + margin: 0; +} + .alert-notice, .alert-passed { @extend .alert-success; diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1ce818406b..9db8efe1ab 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -66,12 +66,21 @@ def apple_icon end def render_flashes - flash.each do |key, message| - concat(alert(key, id: "message_#{key}") { Array(message).each { |m| concat tag.div(m) } }) - end + flash.each { |key, message| concat(alert(key, id: "message_#{key}") { render_message(message) }) } nil end + # A helper method for render_flashes - If multiple messages, render them as a list, else render as a single div + # @param messages [Array, String] The flash message or messages to be rendered + def render_message(messages) + messages = Array(messages) + if messages.size > 1 + tag.ul { messages.each { |m| concat tag.li(m) } } + else + tag.div(messages.first) + end + end + def api_data { api_version: RELEASE.api_version } end diff --git a/app/jobs/asset_link/builder_job.rb b/app/jobs/asset_link/builder_job.rb index 75cc32edf9..67d745bbfe 100644 --- a/app/jobs/asset_link/builder_job.rb +++ b/app/jobs/asset_link/builder_job.rb @@ -2,8 +2,8 @@ # Enables the bulk creation of the asset links defined by the pairs passed as edges. require_dependency 'asset_link' -# An AssetLink::BuilderJob receives an array of [parent_id, child_id] and builds -# asset links between them +# An AssetLink::BuilderJob receives an array of [parent_id, child_id] and builds asset links between them +# @return [] AssetLink::BuilderJob = Struct.new(:links) do # For memory reasons we need to limit transaction size to 10 links at a time diff --git a/app/models/cherrypick_task.rb b/app/models/cherrypick_task.rb index c641b3f603..13304cb801 100644 --- a/app/models/cherrypick_task.rb +++ b/app/models/cherrypick_task.rb @@ -34,11 +34,9 @@ def new_control_locator(batch_id, total_wells, num_control_wells, wells_to_leave # Cherrypick tasks are directly coupled to the previous task, due to the awkward # way in which the WorkflowsController operates. See issues#2831 for aims to help improve some of this # - # @param batch [Batch] The batch on which the action will be performed - # # @return [false,'Can only be accessed via the previous step'>] Array indicating this action can't be linked # - def can_link_directly?(_batch) + def can_link_directly? [false, 'Can only be accessed via the previous step'] end diff --git a/app/models/illumina_htp/mx_tube_purpose.rb b/app/models/illumina_htp/mx_tube_purpose.rb index 99bea208ec..cd6a76307a 100644 --- a/app/models/illumina_htp/mx_tube_purpose.rb +++ b/app/models/illumina_htp/mx_tube_purpose.rb @@ -20,7 +20,7 @@ class IlluminaHtp::MxTubePurpose < Tube::Purpose # limber pipelines this will actually return the plate on which you charge and pass. # See https://github.com/sanger/sequencescape/issues/3040 for more information # - # @deprecate Do not use this for new behaviour. + # @deprecated Do not use this for new behaviour. # # @param tube [Tube] The tube for which to find the stock_plate # diff --git a/app/models/illumina_htp/stock_tube_purpose.rb b/app/models/illumina_htp/stock_tube_purpose.rb index dc17ef263c..5f35a29174 100644 --- a/app/models/illumina_htp/stock_tube_purpose.rb +++ b/app/models/illumina_htp/stock_tube_purpose.rb @@ -28,7 +28,7 @@ def create_with_request_options(_tube) # with that in plate, and deprecate it. # See https://github.com/sanger/sequencescape/issues/3040 for more information # - # @deprecate Do not use this for new behaviour. + # @deprecated Do not use this for new behaviour. # # @param tube [Tube] The tube for which to find the stock_plate # diff --git a/app/models/plate.rb b/app/models/plate.rb index cf599c54d3..0655990c19 100644 --- a/app/models/plate.rb +++ b/app/models/plate.rb @@ -326,7 +326,7 @@ def stock_plate? # JG: 2021-02-11: # See https://github.com/sanger/sequencescape/issues/3040 for more information # - # @deprecate Do not use this for new behaviour. + # @deprecated Do not use this for new behaviour. # # # @return [Plate, nil] The stock plate if found diff --git a/app/models/plate/quad_creator.rb b/app/models/plate/quad_creator.rb index 3444bf6266..46dc85f7af 100644 --- a/app/models/plate/quad_creator.rb +++ b/app/models/plate/quad_creator.rb @@ -125,7 +125,7 @@ def target_coordinate_for(source_coordinate_name, quadrant_index) # # Converts a well or tube location name to its co-ordinates # - # @param [] Location name of the well or tube. Eg. A3 + # @param [] locn_name name of the well or tube. Eg. A3 # # @return [Array] An array of two integers indicating column and row. eg. [0, 2] # diff --git a/app/models/tag_layout.rb b/app/models/tag_layout.rb index cd4d6bfaf7..f4976bad9f 100644 --- a/app/models/tag_layout.rb +++ b/app/models/tag_layout.rb @@ -15,7 +15,7 @@ class TagLayout < ApplicationRecord UnknownDirection = Struct.new(:direction) UnknownWalking = Struct.new(:walking_by) - DIRECTIONS = { + DIRECTION_ALGORITHMS = { 'column' => 'TagLayout::InColumns', 'row' => 'TagLayout::InRows', 'inverse column' => 'TagLayout::InInverseColumns', @@ -50,7 +50,7 @@ class TagLayout < ApplicationRecord # The plate we'll be laying out the tags into belongs_to :plate, optional: false - validates :direction, inclusion: { in: DIRECTIONS.keys } + validates :direction, inclusion: { in: DIRECTION_ALGORITHMS.keys } validates :walking_by, inclusion: { in: WALKING_ALGORITHMS.keys } validates :direction_algorithm, presence: true @@ -65,7 +65,7 @@ class TagLayout < ApplicationRecord delegate :walking_by, :walk_wells, :apply_tags, to: :walking_algorithm_helper def direction=(new_direction) - self.direction_algorithm = DIRECTIONS.fetch(new_direction) { UnknownDirection.new(new_direction) } + self.direction_algorithm = DIRECTION_ALGORITHMS.fetch(new_direction) { UnknownDirection.new(new_direction) } end def walking_by=(walk) diff --git a/app/models/tube/purpose.rb b/app/models/tube/purpose.rb index 6bc0134d1d..21b1dfbe6c 100644 --- a/app/models/tube/purpose.rb +++ b/app/models/tube/purpose.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true # Base class for the all tube purposes, describes the role the associated -# {Tube} is playing within the lab, and my modify its behaviour. +# {Tube} is playing within the lab, and modifies its behaviour. # This is not an abstract class, and can be used directly. # @see Purpose class Tube::Purpose < Purpose diff --git a/app/resources/api/v2/asset_audit_resource.rb b/app/resources/api/v2/asset_audit_resource.rb index f7a03a4452..a68ec83fb0 100644 --- a/app/resources/api/v2/asset_audit_resource.rb +++ b/app/resources/api/v2/asset_audit_resource.rb @@ -33,7 +33,7 @@ class AssetAuditResource < BaseResource # Sets the Asset on the model using the UUID provided in the API create/update request. # - # @param name [String] the uuid of the associated asset. + # @param uuid [String] the uuid of the associated asset. # @return [void] def asset_uuid=(uuid) @model.asset = Uuid.with_external_id(uuid).include_resource.map(&:resource).first diff --git a/app/resources/api/v2/plate_purpose_resource.rb b/app/resources/api/v2/plate_purpose_resource.rb index c76e9b26ef..f4ba76f83e 100644 --- a/app/resources/api/v2/plate_purpose_resource.rb +++ b/app/resources/api/v2/plate_purpose_resource.rb @@ -13,34 +13,34 @@ class PlatePurposeResource < BaseResource # The following attributes are sent by Limber for a new plate purpose. - # @!attribute name - # @return [String] gets or sets the name of the plate purpose + # @!attribute [rw] + # @return [String] The name of the plate purpose. attribute :name - # @!attribute stock_plate - # @return [Boolean] gets or sets whether the plates of this purpose are stock plates + # @!attribute [rw] + # @return [Boolean] Whether the plates of this purpose are stock plates. attribute :stock_plate - # @!attribute cherrypickable_target - # @return [Boolean] gets or sets whether the plates of this purpose are cherrypickable + # @!attribute [rw] + # @return [Boolean] Whether the plates of this purpose are cherrypickable. attribute :cherrypickable_target - # @!attribute input_plate - # @return [Boolean] gets or sets whether the plates of this purpose are input plates + # @!attribute [rw] + # @return [Boolean] Whether the plates of this purpose are input plates. attribute :input_plate - # @!attribute size - # @return [Integer] gets or sets the size of the plates of this purpose + # @!attribute [rw] + # @return [Integer] The size of the plates of this purpose. attribute :size - # @!attribute asset_shape - # @return [String] gets or sets the name of the shape of the plates of this purpose + # @!attribute [rw] + # @return [String] The name of the shape of the plates of this purpose. attribute :asset_shape # The following attribute is required by Limber to store purposes. - # @!attribute [r] uuid - # @return [String] gets the UUID of the plate purpose + # @!attribute [r] + # @return [String] gets the UUID of the plate purpose. attribute :uuid # Sets the asset shape of the plate purpose by name if given. diff --git a/app/resources/api/v2/sample_metadata_resource.rb b/app/resources/api/v2/sample_metadata_resource.rb index 8c55c63897..f96d48393e 100644 --- a/app/resources/api/v2/sample_metadata_resource.rb +++ b/app/resources/api/v2/sample_metadata_resource.rb @@ -4,20 +4,54 @@ module Api module V2 # SampleMetadataResource class SampleMetadataResource < BaseResource - attribute :sample_common_name - attribute :supplier_name - attribute :collected_by + # Set add_model_hint true to allow updates from Limber, otherwise get a + # 500 error as it looks for resource Api::V2::MetadatumResource + model_name 'Sample::Metadata', add_model_hint: true + + ### + # Attributes + ### + + # @!attribute [rw] + # @return [String] The sample cohort. attribute :cohort - attribute :sample_description - attribute :donor_id + + # @!attribute [rw] + # @return [String] The name of the body collecting the sample. + attribute :collected_by + + # @!attribute [rw] + # @return [String] The sample concentration. attribute :concentration - attribute :volume - # set add_model_hint true to allow updates from Limber, otherwise get a - # 500 error as it looks for resource Api::V2::MetadatumResource - model_name 'Sample::Metadata', add_model_hint: true + # @!attribute [rw] + # @return [String] The ID of the sample donor. + attribute :donor_id + + # @!attribute [rw] + # @return [String] The gender of the organism providing the sample. + attribute :gender + # @!attribute [rw] + # @return [String] The common name for the sample. + attribute :sample_common_name + + # @!attribute [rw] + # @return [String] A description of the sample. + attribute :sample_description + + # @!attribute [rw] + # @return [String] The supplier name for the sample. + attribute :supplier_name + + # @!attribute [rw] + # @return [String] The volume of the sample. + attribute :volume + + ### # Filters + ### + filter :sample_id end end diff --git a/app/resources/api/v2/submission_template_resource.rb b/app/resources/api/v2/submission_template_resource.rb new file mode 100644 index 0000000000..f72d3fbaa7 --- /dev/null +++ b/app/resources/api/v2/submission_template_resource.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Api + module V2 + # Provides a JSON API representation of a submission template. + # See: http://jsonapi-resources.com/ for JSONAPI::Resource documentation + class SubmissionTemplateResource < BaseResource + immutable + + default_includes :uuid_object + + ### + # Attributes + ### + + # @!attribute [r] + # @return [String] The name of the submission template. + attribute :name + + # @!attribute [r] + # @return [String] The UUID of the submission template. + attribute :uuid, readonly: true + end + end +end diff --git a/app/resources/api/v2/transfer_template_resource.rb b/app/resources/api/v2/transfer_template_resource.rb new file mode 100644 index 0000000000..6a41f76373 --- /dev/null +++ b/app/resources/api/v2/transfer_template_resource.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Api + module V2 + # Provides a JSON API representation of a transfer template. + # See: http://jsonapi-resources.com/ for JSONAPI::Resource documentation + class TransferTemplateResource < BaseResource + immutable + + default_includes :uuid_object + + ### + # Attributes + ### + + # @!attribute [r] + # @return [String] The name of the transfer template. + attribute :name + + # @!attribute [r] + # @return [String] The UUID of the transfer template. + attribute :uuid, readonly: true + end + end +end diff --git a/app/resources/api/v2/tube_purpose_resource.rb b/app/resources/api/v2/tube_purpose_resource.rb new file mode 100644 index 0000000000..aa08d628d1 --- /dev/null +++ b/app/resources/api/v2/tube_purpose_resource.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Api + module V2 + # Provides a JSON API representation of a tube purpose. + # See: http://jsonapi-resources.com/ for JSONAPI::Resource documentation + class TubePurposeResource < BaseResource + model_name 'Tube::Purpose' + + ##### + # Attributes + ##### + + # @!attribute [rw] + # @return [String] The name of the tube purpose. + attribute :name + + # @!attribute [rw] + # @return [String] The purpose type. This is mapped to the type attribute on the model. + attribute :purpose_type, delegate: :type + + # @!attribute [rw] + # @return [String] The target type. + attribute :target_type + + # @!attribute [r] + # @return [String] The UUID of the tube purpose. + attribute :uuid + + # Gets the list of fields which are creatable on a TubePurpose. + # + # @param _context [JSONAPI::Resource::Context] not used + # @return [Array] the list of creatable fields. + def self.creatable_fields(_context) + super - %i[uuid] # Do not allow creating with any readonly fields + end + + # Gets the list of fields which are updatable on an existing TubePurpose. + # + # @param _context [JSONAPI::Resource::Context] not used + # @return [Array] the list of updatable fields. + def self.updatable_fields(_context) + super - %i[uuid] # Do not allow creating with any readonly fields + end + end + end +end diff --git a/app/resources/api/v2/volume_update_resource.rb b/app/resources/api/v2/volume_update_resource.rb index 78db317197..9c8ea04f0c 100644 --- a/app/resources/api/v2/volume_update_resource.rb +++ b/app/resources/api/v2/volume_update_resource.rb @@ -21,7 +21,7 @@ class VolumeUpdateResource < BaseResource # Sets the target Labware on the model using the UUID provided in the API create/update request. # - # @param name [String] the uuid of the associated target Labware. + # @param uuid [String] the uuid of the associated target Labware. # @return [void] def target_uuid=(uuid) @model.target = Uuid.with_external_id(uuid).include_resource.map(&:resource).first diff --git a/app/sequencescape_excel/sequencescape_excel/validation.rb b/app/sequencescape_excel/sequencescape_excel/validation.rb index 346526ddd8..ad0b7e0843 100644 --- a/app/sequencescape_excel/sequencescape_excel/validation.rb +++ b/app/sequencescape_excel/sequencescape_excel/validation.rb @@ -68,7 +68,7 @@ def inspect ## # formula1 is defined within the options, however it needs to be updated # with: - # 1) A provided rage in the case of lists + # 1) A provided range in the case of lists # 2) Proper cell names in the case of custom formulas # 3) AXLSX doesn't escape text fields for us, so we do that ourselves def formula1 diff --git a/app/uat_actions/uat_actions/generate_tag_layout_template.rb b/app/uat_actions/uat_actions/generate_tag_layout_template.rb index 4ca88ea5d7..bf5559666d 100644 --- a/app/uat_actions/uat_actions/generate_tag_layout_template.rb +++ b/app/uat_actions/uat_actions/generate_tag_layout_template.rb @@ -30,7 +30,7 @@ class UatActions::GenerateTagLayoutTemplate < UatActions :select, label: 'Direction', help: 'Direction the tags are laid out by', - select_options: -> { TagLayoutTemplatesController::DIRECTIONS }, + select_options: -> { TagLayout::DIRECTION_ALGORITHMS }, options: { include_blank: 'Select a direction...' } @@ -38,7 +38,7 @@ class UatActions::GenerateTagLayoutTemplate < UatActions :select, label: 'Walking By', help: 'Walking by algorithms, will default to TagLayout::WalkWellsOfPlate if left blank', - select_options: -> { TagLayoutTemplatesController::WALKING_ALGORITHMS }, + select_options: -> { TagLayout::WALKING_ALGORITHMS }, options: { include_blank: 'Select a walking by...' } diff --git a/app/uat_actions/uat_actions/generate_tagged_plates.rb b/app/uat_actions/uat_actions/generate_tagged_plates.rb index 77df89ee04..790f587fdc 100644 --- a/app/uat_actions/uat_actions/generate_tagged_plates.rb +++ b/app/uat_actions/uat_actions/generate_tagged_plates.rb @@ -38,7 +38,7 @@ class UatActions::GenerateTaggedPlates < UatActions::GeneratePlates help: 'The order in which tags will be laid out on the plate. ' \ 'Most commonly \'column\'.', - select_options: -> { TagLayout::DIRECTIONS.keys } + select_options: -> { TagLayout::DIRECTION_ALGORITHMS.keys } form_field :walking_by, :select, label: 'Tag layout pattern', @@ -51,7 +51,7 @@ class UatActions::GenerateTaggedPlates < UatActions::GeneratePlates # driven by submission information select_options: -> { TagLayout::WALKING_ALGORITHMS.keys - EXCLUDED_WALKING } - validates :direction, inclusion: { in: TagLayout::DIRECTIONS.keys }, presence: true + validates :direction, inclusion: { in: TagLayout::DIRECTION_ALGORITHMS.keys }, presence: true validates :walking_by, inclusion: { in: TagLayout::WALKING_ALGORITHMS.keys - EXCLUDED_WALKING }, presence: true validates :tag_group_name, presence: true validates :tag_group, presence: { message: 'could not be found' }, if: :tag_group_name diff --git a/app/views/tag_layout_templates/new.html.erb b/app/views/tag_layout_templates/new.html.erb index a7c700f085..9fed6ffeb8 100644 --- a/app/views/tag_layout_templates/new.html.erb +++ b/app/views/tag_layout_templates/new.html.erb @@ -18,12 +18,41 @@ <%= f.select :tag2_group_id, TagGroup.visible.pluck(:name, :id), { prompt: 'Select tag2 group...' }, { class: 'form-control select2' } %> +
+

Direction Algorithms and what they mean:

+
    +
  • In columns: A1,B1,C1...
  • +
  • In rows: A1,A2,A3...
  • +
  • In inverse columns: H12,G12,F12...
  • +
  • In inverse rows: H12,H11,H10...
  • +
  • In columns then rows (use with quadrants direction): A1,A2,B1,B2...
  • +
  • In columns then columns (use with quadrants direction): A1,B1,A2,B2...
  • +
  • Combinatorial by row (4 x tag1 per well): [A1,A2,A3,A4],[A5,A6,A7,A8],...
  • +
+
<%= f.label :direction, 'Direction the tags are laid out by' %>
<%= f.select :direction_algorithm, options_for_select(@direction_algorithms), { prompt: 'Select direction algorithm...' }, { class: 'form-control select2' } %>
- <%= f.hidden_field 'walking_algorithm', value: 'TagLayout::WalkWellsOfPlate' %> +
+

Walking Algorithms and what they mean:

+
    +
  • Wells in Pools: Groups by pools
  • +
  • Wells of Plate: All wells in plate
  • +
  • Manual by pool: By pool id
  • +
  • As group by plate: Assigns 4 tags per well
  • +
  • Manual by plate: All wells with aliquots in plate
  • +
  • Quadrants: By quadrants for 384-well plates
  • +
  • As fixed group by plate: Assigns 4 tags per well sequentially
  • +
  • Combinatorial sequential: Handles arbitrary layouts of 2 tags (i7 and i5)
  • +
+
+ +
+ <%= f.label :walking_by, 'Walking by algorithm the tags are laid out by' %>
+ <%= f.select :walking_algorithm, options_for_select(@walking_algorithms, 'TagLayout::WalkWellsOfPlate'), { prompt: 'Select walking algorithm...' }, { class: 'form-control select2' } %> +
<%= f.submit('Create tag layout template', class: 'btn btn-success') %> diff --git a/config/default_records/request_types/014_limber_scrna_core_cdna_prep_request_types.wip.yml b/config/default_records/request_types/014_limber_scrna_core_cdna_prep_request_types.wip.yml index 4516533dd3..76d872940a 100644 --- a/config/default_records/request_types/014_limber_scrna_core_cdna_prep_request_types.wip.yml +++ b/config/default_records/request_types/014_limber_scrna_core_cdna_prep_request_types.wip.yml @@ -4,11 +4,11 @@ # - acceptable_purposes is where the request type can be used. # --- -limber_scrna_core_cdna_prep_v2: +limber_scrna_core_cdna_prep_gem_x_5p: name: scRNA Core cDNA Prep asset_type: SampleTube order: 1 - request_class_name: IlluminaHtp::Requests::StdLibraryRequest + request_class_name: CustomerRequest for_multiplexing: false billable: true product_line_name: Short Read @@ -16,8 +16,6 @@ limber_scrna_core_cdna_prep_v2: - LRC Bank Seq - LRC Bank Spare - LRC Bank Input - library_types: - - Chromium single cell GEM-X 5p v3 GE limber_scrna_core_cdna_prep_input: name: scRNA Core cDNA Prep Input asset_type: Well diff --git a/config/default_records/submission_templates/011_scrna_core_cdna_prep_submission_templates.wip.yml b/config/default_records/submission_templates/011_scrna_core_cdna_prep_submission_templates.wip.yml index 7c3fe929fa..9ef460adef 100644 --- a/config/default_records/submission_templates/011_scrna_core_cdna_prep_submission_templates.wip.yml +++ b/config/default_records/submission_templates/011_scrna_core_cdna_prep_submission_templates.wip.yml @@ -10,10 +10,10 @@ # - product_catalogue_name is the name of the product catalogue that has a # selection_behaviour as 'SingleProduct'. --- -Limber-Htp - scRNA Core cDNA Prep: +Limber-Htp - scRNA Core cDNA Prep GEM-X 5p: submission_class_name: "LinearSubmission" related_records: - request_type_keys: ["limber_scrna_core_cdna_prep_v2"] + request_type_keys: ["limber_scrna_core_cdna_prep_gem_x_5p"] product_line_name: Short Read product_catalogue_name: scRNA Core Limber-Htp - scRNA Core cDNA Prep Input: diff --git a/config/routes.rb b/config/routes.rb index 35202e5c47..036d3cb635 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -14,7 +14,7 @@ end end - mount Api::RootService.new => '/api/1' + mount Api::RootService.new => '/api/1' unless ENV['DISABLE_V1_API'] namespace :api do namespace :v2 do @@ -48,10 +48,13 @@ jsonapi_resources :sample_manifests jsonapi_resources :sample_metadata jsonapi_resources :studies + jsonapi_resources :submission_templates jsonapi_resources :submissions jsonapi_resources :tag_groups jsonapi_resources :tag_layout_templates jsonapi_resources :transfer_requests + jsonapi_resources :transfer_templates + jsonapi_resources :tube_purposes jsonapi_resources :tube_rack_statuses jsonapi_resources :tube_racks jsonapi_resources :tubes diff --git a/config/sample_manifest_excel/columns.yml b/config/sample_manifest_excel/columns.yml index 54f8febbcf..6bd16407f1 100644 --- a/config/sample_manifest_excel/columns.yml +++ b/config/sample_manifest_excel/columns.yml @@ -731,6 +731,26 @@ sample_description_specimen_plate_barcode: operator: ">" operand: 20 is_number: +sample_description_specimen_plate_barcode_mandatory: + heading: SAMPLE DESCRIPTION + updates: sample_description + unlocked: true + validation: + options: + type: :custom + operator: :between + formula1: "=AND(LEN(A1)>=7,LEN(A1)<=11)" + allowBlank: false + showInputMessage: true + promptTitle: "Sample Description - Specimen Plate Barcode" + prompt: "Please enter the specimen plate barcode (NB. should be the same for all wells on the input plate, and between 7 and 11 characters in length)." + showErrorMessage: true + errorStyle: :stop + errorTitle: "Sample Description - Specimen Plate Barcode" + error: "The barcode length must be between 7 and 11 characters in length." + conditional_formattings: + empty_mandatory_cell: + is_number: sample_strain_att: heading: STRAIN unlocked: true diff --git a/config/sample_manifest_excel/manifest_types.yml b/config/sample_manifest_excel/manifest_types.yml index 3dd6ddd915..4c29a4c456 100644 --- a/config/sample_manifest_excel/manifest_types.yml +++ b/config/sample_manifest_excel/manifest_types.yml @@ -273,7 +273,7 @@ plate_anospp: - :date_of_sample_collection - :sample_taxon_id - :sample_common_name - - :sample_description_specimen_plate_barcode + - :sample_description_specimen_plate_barcode_mandatory - :sample_type - :donor_id - :control_type diff --git a/lib/accession/accession/configuration.rb b/lib/accession/accession/configuration.rb index 31f00202a2..67dbac8d2d 100644 --- a/lib/accession/accession/configuration.rb +++ b/lib/accession/accession/configuration.rb @@ -4,6 +4,8 @@ class Configuration include Accession::Helpers include Accession::Equality + # This constant defines a list of tags for loading + # @return [Array] a list of symbols FILES = [:tags].freeze attr_accessor :folder, *FILES diff --git a/lib/nested_validation.rb b/lib/nested_validation.rb index 865854d85e..21122483ec 100644 --- a/lib/nested_validation.rb +++ b/lib/nested_validation.rb @@ -38,7 +38,7 @@ def validate_each(record, attribute, value) # # Records of this class will call valid? on any associations provided # as attr_names. Errors on these records will be propagated out - # @param *attr_names [Symbol] One or more associations to validate + # @param attr_names [Symbol] One or more associations to validate # # @return [NestedValidator] def validates_nested(*attr_names) diff --git a/spec/data/sample_manifest_excel/columns.yml b/spec/data/sample_manifest_excel/columns.yml index 405efbb1dc..8199b9d64c 100644 --- a/spec/data/sample_manifest_excel/columns.yml +++ b/spec/data/sample_manifest_excel/columns.yml @@ -686,6 +686,26 @@ sample_description_specimen_plate_barcode: operator: ">" operand: 20 is_number: +sample_description_specimen_plate_barcode_mandatory: + heading: SAMPLE DESCRIPTION + updates: sample_description + unlocked: true + validation: + options: + type: :custom + operator: :between + formula1: "=AND(LEN(A1)>=7,LEN(A1)<=11)" + allowBlank: false + showInputMessage: true + promptTitle: "Sample Description - Specimen Plate Barcode" + prompt: "Please enter the specimen plate barcode (NB. should be the same for all wells on the input plate, and between 7 and 11 characters in length)." + showErrorMessage: true + errorStyle: :stop + errorTitle: "Sample Description - Specimen Plate Barcode" + error: "The barcode length must be between 7 and 11 characters in length." + conditional_formattings: + empty_mandatory_cell: + is_number: sample_strain_att: heading: STRAIN unlocked: true diff --git a/spec/data/sample_manifest_excel/manifest_types.yml b/spec/data/sample_manifest_excel/manifest_types.yml index 40e08b82c8..d6c5c5aeae 100644 --- a/spec/data/sample_manifest_excel/manifest_types.yml +++ b/spec/data/sample_manifest_excel/manifest_types.yml @@ -206,7 +206,7 @@ plate_anospp: - :date_of_sample_collection - :sample_taxon_id - :sample_common_name - - :sample_description_specimen_plate_barcode + - :sample_description_specimen_plate_barcode_mandatory - :sample_type - :donor_id - :control_type diff --git a/spec/factories/sample_metadata.rb b/spec/factories/sample_metadata.rb index c1d7d32217..824baf75ae 100644 --- a/spec/factories/sample_metadata.rb +++ b/spec/factories/sample_metadata.rb @@ -12,6 +12,7 @@ cohort { 'cohort' } country_of_origin { 'country_of_origin' } geographical_region { 'geographical_region' } + gender { :male } ethnicity { 'ethnicity' } volume { 'volume' } mother { 'mother' } diff --git a/spec/features/tag_layout_template_spec.rb b/spec/features/tag_layout_template_spec.rb index 5dd6411f53..1d9d8fc963 100644 --- a/spec/features/tag_layout_template_spec.rb +++ b/spec/features/tag_layout_template_spec.rb @@ -20,7 +20,7 @@ expect(page).to have_content 'Tag Layout Template New' within('#new_tag_layout_template') do fill_in('tag_layout_template_name', with: 'Test tag layout template') - select('InColumns (A1,B1,C1...)', from: 'tag_layout_template_direction_algorithm') + select('column', from: 'tag_layout_template_direction_algorithm') click_on 'Create tag layout template' end expect(page).to have_content 'The Tag Layout Template has been successfully created.' @@ -40,7 +40,7 @@ fill_in('tag_layout_template_name', with: 'Test tag layout template') select(tag_group_1.name, from: 'tag_layout_template_tag_group_id') select(tag_group_2.name, from: 'tag_layout_template_tag2_group_id') - select('InColumns (A1,B1,C1...)', from: 'tag_layout_template_direction_algorithm') + select('column', from: 'tag_layout_template_direction_algorithm') click_on 'Create tag layout template' end expect(page).to have_content 'The Tag Layout Template has been successfully created.' @@ -59,7 +59,7 @@ within('#new_tag_layout_template') do fill_in('tag_layout_template_name', with: 'Test tag layout template') - select('InColumns (A1,B1,C1...)', from: 'tag_layout_template_direction_algorithm') + select('column', from: 'tag_layout_template_direction_algorithm') click_on 'Create tag layout template' end diff --git a/spec/models/transfer_request_spec.rb b/spec/models/transfer_request_spec.rb index 84747f7b74..662d6d9d02 100644 --- a/spec/models/transfer_request_spec.rb +++ b/spec/models/transfer_request_spec.rb @@ -232,7 +232,9 @@ let(:merge) { false } it 'will throw a TagClash exception' do - expect { transfer_request.save }.to raise_error(Aliquot::TagClash) + expect { transfer_request.save }.to raise_error(Aliquot::TagClash) do |error| + expect(error.api_error_code).to eq(422) + end end end end diff --git a/spec/requests/api/v2/sample_metadata_spec.rb b/spec/requests/api/v2/sample_metadata_spec.rb new file mode 100644 index 0000000000..55251c6b5b --- /dev/null +++ b/spec/requests/api/v2/sample_metadata_spec.rb @@ -0,0 +1,199 @@ +# frozen_string_literal: true + +require 'rails_helper' +require './spec/requests/api/v2/shared_examples/api_key_authenticatable' + +describe 'SampleMetadata API', with: :api_v2 do + let(:base_endpoint) { '/api/v2/sample_metadata' } + + it_behaves_like 'ApiKeyAuthenticatable' + + context 'with multiple metadata resources' do + before { create_list(:sample_metadata_for_api, 5) } + + it 'gets a list of metadata resources' do + api_get base_endpoint + + expect(response).to have_http_status(:success) + expect(json['data'].length).to eq(5) + end + end + + context 'with a single metadata resource' do + let(:resource_model) { create :sample_metadata_for_api } + + describe '#get' do + it 'generates a success response' do + api_get "#{base_endpoint}/#{resource_model.id}" + + expect(response).to have_http_status(:success) + end + + it 'returns the expected JSON' do + api_get "#{base_endpoint}/#{resource_model.id}" + + expect(json.dig('data', 'type')).to eq('sample_metadata') + expect(json.dig('data', 'attributes', 'cohort')).to eq resource_model.cohort + expect(json.dig('data', 'attributes', 'collected_by')).to eq resource_model.collected_by + expect(json.dig('data', 'attributes', 'concentration')).to eq resource_model.concentration + expect(json.dig('data', 'attributes', 'donor_id')).to eq resource_model.donor_id + expect(json.dig('data', 'attributes', 'gender')).to eq resource_model.gender + expect(json.dig('data', 'attributes', 'sample_common_name')).to eq resource_model.sample_common_name + expect(json.dig('data', 'attributes', 'sample_description')).to eq resource_model.sample_description + expect(json.dig('data', 'attributes', 'supplier_name')).to eq resource_model.supplier_name + expect(json.dig('data', 'attributes', 'volume')).to eq resource_model.volume + end + end + + describe '#patch' do + context 'with a valid payload' do + let(:payload) do + { + 'data' => { + 'id' => resource_model.id, + 'type' => 'sample_metadata', + 'attributes' => { + cohort: 'updated cohort', + collected_by: 'updated collected_by', + concentration: 'updated concentration', + donor_id: 'updated donor_id', + gender: 'female', + sample_common_name: 'updated sample_common_name', + sample_description: 'updated sample_description', + supplier_name: 'updated supplier_name', + volume: 'updated volume' + } + } + } + end + + it 'gives a success response' do + api_patch "#{base_endpoint}/#{resource_model.id}", payload + + expect(response).to have_http_status(:success) + end + + it 'responds with the expected attributes' do + api_patch "#{base_endpoint}/#{resource_model.id}", payload + + expect(response).to have_http_status(:success) + expect(json.dig('data', 'attributes', 'cohort')).to eq 'updated cohort' + expect(json.dig('data', 'attributes', 'collected_by')).to eq 'updated collected_by' + expect(json.dig('data', 'attributes', 'concentration')).to eq 'updated concentration' + expect(json.dig('data', 'attributes', 'donor_id')).to eq 'updated donor_id' + expect(json.dig('data', 'attributes', 'gender')).to eq 'Female' + expect(json.dig('data', 'attributes', 'sample_common_name')).to eq 'updated sample_common_name' + expect(json.dig('data', 'attributes', 'sample_description')).to eq 'updated sample_description' + expect(json.dig('data', 'attributes', 'supplier_name')).to eq 'updated supplier_name' + expect(json.dig('data', 'attributes', 'volume')).to eq 'updated volume' + end + + it 'updates the model correctly' do + # Apply the patch which replaced all the metadata + api_patch "#{base_endpoint}/#{resource_model.id}", payload + + # Check that the model was modified + resource_model.reload + expect(resource_model.cohort).to eq 'updated cohort' + expect(resource_model.collected_by).to eq 'updated collected_by' + expect(resource_model.concentration).to eq 'updated concentration' + expect(resource_model.donor_id).to eq 'updated donor_id' + expect(resource_model.gender).to eq 'Female' + expect(resource_model.sample_common_name).to eq 'updated sample_common_name' + expect(resource_model.sample_description).to eq 'updated sample_description' + expect(resource_model.supplier_name).to eq 'updated supplier_name' + expect(resource_model.volume).to eq 'updated volume' + end + end + + context 'with a missing type in the payload' do + let(:payload) { { 'data' => { 'id' => resource_model.id, 'attributes' => { cohort: 'updated cohort' } } } } + + it 'does not update the collection' do + original_cohort = resource_model.cohort + + api_patch "#{base_endpoint}/#{resource_model.id}", payload + expect(response).to have_http_status(:bad_request) + expect(json['errors'][0]['title']).to eq('Missing Parameter') + + # Check that the model was not modified + resource_model.reload + expect(resource_model.cohort).to eq original_cohort + end + end + end + + describe '#post' do + context 'with a valid payload' do + let(:payload) do + { + 'data' => { + 'type' => 'sample_metadata', + 'attributes' => { + cohort: 'posted cohort', + collected_by: 'posted collected_by', + concentration: 'posted concentration', + donor_id: 'posted donor_id', + gender: 'mixed', + sample_common_name: 'posted sample_common_name', + sample_description: 'posted sample_description', + supplier_name: 'posted supplier_name', + volume: 'posted volume' + } + } + } + end + + it 'gives a success response' do + api_post base_endpoint, payload + + expect(response).to have_http_status(:success) + end + + it 'creates the resource' do + expect { api_post base_endpoint, payload }.to change(Sample::Metadata, :count).by(1) + end + + it 'responds with the correct attributes' do + api_post base_endpoint, payload + + expect(json.dig('data', 'attributes', 'cohort')).to eq 'posted cohort' + expect(json.dig('data', 'attributes', 'collected_by')).to eq 'posted collected_by' + expect(json.dig('data', 'attributes', 'concentration')).to eq 'posted concentration' + expect(json.dig('data', 'attributes', 'donor_id')).to eq 'posted donor_id' + expect(json.dig('data', 'attributes', 'gender')).to eq 'Mixed' + expect(json.dig('data', 'attributes', 'sample_common_name')).to eq 'posted sample_common_name' + expect(json.dig('data', 'attributes', 'sample_description')).to eq 'posted sample_description' + expect(json.dig('data', 'attributes', 'supplier_name')).to eq 'posted supplier_name' + expect(json.dig('data', 'attributes', 'volume')).to eq 'posted volume' + end + + it 'populates the model correctly' do + api_post base_endpoint, payload + + new_model = Sample::Metadata.last + expect(new_model.cohort).to eq 'posted cohort' + expect(new_model.collected_by).to eq 'posted collected_by' + expect(new_model.concentration).to eq 'posted concentration' + expect(new_model.donor_id).to eq 'posted donor_id' + expect(new_model.gender).to eq 'Mixed' + expect(new_model.sample_common_name).to eq 'posted sample_common_name' + expect(new_model.sample_description).to eq 'posted sample_description' + expect(new_model.supplier_name).to eq 'posted supplier_name' + expect(new_model.volume).to eq 'posted volume' + end + end + + context 'with unexpected "birthday" attribute in the payload' do + let(:payload) { { 'data' => { 'type' => 'sample_metadata', 'attributes' => { birthday: '14-04-1954' } } } } + + it 'does not create the resource' do + expect { api_post base_endpoint, payload }.not_to change(Sample::Metadata, :count) + + expect(response).to have_http_status(:bad_request) + expect(json['errors'][0]['detail']).to eq('birthday is not allowed.') # Sad times :( + end + end + end + end +end diff --git a/spec/requests/api/v2/submission_templates_spec.rb b/spec/requests/api/v2/submission_templates_spec.rb new file mode 100644 index 0000000000..bc024514fc --- /dev/null +++ b/spec/requests/api/v2/submission_templates_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'rails_helper' +require './spec/requests/api/v2/shared_examples/api_key_authenticatable' + +describe 'Submission Templates API', with: :api_v2 do + let(:base_endpoint) { '/api/v2/submission_templates' } + + it_behaves_like 'ApiKeyAuthenticatable' + + describe '#get all Submission Templates' do + before { create_list(:submission_template, 5) } + + it 'returns the list of Submission Templates' do + api_get base_endpoint + + expect(response).to have_http_status(:success) + expect(json['data'].length).to eq(5) + end + end + + describe '#get a specific Submission Template' do + let(:resource_model) { create(:submission_template) } + + it 'returns the template' do + api_get "#{base_endpoint}/#{resource_model.id}" + expect(response).to have_http_status(:success) + expect(json.dig('data', 'type')).to eq('submission_templates') + expect(json.dig('data', 'attributes', 'name')).to eq(resource_model.name) + end + end + + describe '#patch a specific Submission Template' do + let(:resource_model) { create(:submission_template) } + let(:payload) do + { + 'data' => { + 'id' => resource_model.id, + 'type' => 'submission_templates', + 'attributes' => { + 'name' => 'Updated Name' + } + } + } + end + + it 'finds no route for the method' do + expect { api_patch "#{base_endpoint}/#{resource_model.id}", payload }.to raise_error( + ActionController::RoutingError + ) + end + end + + describe '#post a new Submission Template' do + let(:payload) { { 'data' => { 'type' => 'submission_templates', 'attributes' => { 'name' => 'New Name' } } } } + + it 'finds no routes for the method' do + expect { api_post base_endpoint, payload }.to raise_error(ActionController::RoutingError) + end + end +end diff --git a/spec/requests/api/v2/transfer_templates_spec.rb b/spec/requests/api/v2/transfer_templates_spec.rb new file mode 100644 index 0000000000..8123fb5625 --- /dev/null +++ b/spec/requests/api/v2/transfer_templates_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'rails_helper' +require './spec/requests/api/v2/shared_examples/api_key_authenticatable' + +describe 'Transfer Templates API', with: :api_v2 do + let(:base_endpoint) { '/api/v2/transfer_templates' } + + it_behaves_like 'ApiKeyAuthenticatable' + + describe '#get all Transfer Templates' do + before { create_list(:transfer_template, 5) } + + it 'returns the list of Transfer Templates' do + api_get base_endpoint + + expect(response).to have_http_status(:success) + expect(json['data'].length).to eq(5) + end + end + + describe '#get a specific Transfer Template' do + let(:resource_model) { create(:transfer_template) } + + it 'returns the template' do + api_get "#{base_endpoint}/#{resource_model.id}" + expect(response).to have_http_status(:success) + expect(json.dig('data', 'type')).to eq('transfer_templates') + expect(json.dig('data', 'attributes', 'name')).to eq(resource_model.name) + end + end + + describe '#patch a specific Transfer Template' do + let(:resource_model) { create(:transfer_template) } + let(:payload) do + { + 'data' => { + 'id' => resource_model.id, + 'type' => 'transfer_templates', + 'attributes' => { + 'name' => 'Updated Name' + } + } + } + end + + it 'finds no route for the method' do + expect { api_patch "#{base_endpoint}/#{resource_model.id}", payload }.to raise_error( + ActionController::RoutingError + ) + end + end + + describe '#post a new Transfer Template' do + let(:payload) { { 'data' => { 'type' => 'transfer_templates', 'attributes' => { 'name' => 'New Name' } } } } + + it 'finds no routes for the method' do + expect { api_post base_endpoint, payload }.to raise_error(ActionController::RoutingError) + end + end +end diff --git a/spec/requests/api/v2/tube_purposes_spec.rb b/spec/requests/api/v2/tube_purposes_spec.rb new file mode 100644 index 0000000000..1bcf67ae58 --- /dev/null +++ b/spec/requests/api/v2/tube_purposes_spec.rb @@ -0,0 +1,200 @@ +# frozen_string_literal: true + +require 'rails_helper' +require './spec/requests/api/v2/shared_examples/api_key_authenticatable' + +describe 'Tube Purposes API', with: :api_v2 do + let(:base_endpoint) { '/api/v2/tube_purposes' } + + it_behaves_like 'ApiKeyAuthenticatable' + + describe '#get all Tube Purposes' do + before { create_list(:tube_purpose, 5) } + + it 'returns the list of Tube Purposes' do + api_get base_endpoint + + expect(response).to have_http_status(:success) + expect(json['data'].length).to eq(5) + end + end + + describe '#get a specific Tube Purpose' do + let(:resource_model) { create(:tube_purpose) } + + it 'returns the template' do + api_get "#{base_endpoint}/#{resource_model.id}" + expect(response).to have_http_status(:success) + expect(json.dig('data', 'type')).to eq('tube_purposes') + expect(json.dig('data', 'attributes', 'name')).to eq(resource_model.name) + expect(json.dig('data', 'attributes', 'purpose_type')).to eq(resource_model.type) + expect(json.dig('data', 'attributes', 'target_type')).to eq(resource_model.target_type) + expect(json.dig('data', 'attributes', 'uuid')).to eq(resource_model.uuid) + end + end + + describe '#patch a specific Tube Purpose' do + let(:resource_model) { create(:tube_purpose) } + + context 'when patching the name' do + let(:updated_name) { 'Updated Name' } + let(:payload) do + { + 'data' => { + 'id' => resource_model.id, + 'type' => 'tube_purposes', + 'attributes' => { + 'name' => updated_name + } + } + } + end + + it 'patches correctly' do + api_patch "#{base_endpoint}/#{resource_model.id}", payload + expect(response).to have_http_status(:success) + expect(json.dig('data', 'type')).to eq('tube_purposes') + expect(json.dig('data', 'attributes', 'name')).to eq(updated_name) + expect(json.dig('data', 'attributes', 'purpose_type')).to eq(resource_model.type) + expect(json.dig('data', 'attributes', 'target_type')).to eq(resource_model.target_type) + expect(json.dig('data', 'attributes', 'uuid')).to eq(resource_model.uuid) + end + end + + context 'when patching the purpose_type' do + let(:updated_purpose_type) { 'Updated Purpose Type' } + let(:payload) do + { + 'data' => { + 'id' => resource_model.id, + 'type' => 'tube_purposes', + 'attributes' => { + 'purpose_type' => updated_purpose_type + } + } + } + end + + it 'patches correctly' do + api_patch "#{base_endpoint}/#{resource_model.id}", payload + expect(response).to have_http_status(:success) + expect(json.dig('data', 'type')).to eq('tube_purposes') + expect(json.dig('data', 'attributes', 'name')).to eq(resource_model.name) + expect(json.dig('data', 'attributes', 'purpose_type')).to eq(updated_purpose_type) + expect(json.dig('data', 'attributes', 'target_type')).to eq(resource_model.target_type) + expect(json.dig('data', 'attributes', 'uuid')).to eq(resource_model.uuid) + end + end + + context 'when patching the target_type' do + let(:updated_target_type) { 'SampleTube' } + let(:payload) do + { + 'data' => { + 'id' => resource_model.id, + 'type' => 'tube_purposes', + 'attributes' => { + 'target_type' => updated_target_type + } + } + } + end + + it 'patches correctly' do + api_patch "#{base_endpoint}/#{resource_model.id}", payload + expect(response).to have_http_status(:success) + expect(json.dig('data', 'type')).to eq('tube_purposes') + expect(json.dig('data', 'attributes', 'name')).to eq(resource_model.name) + expect(json.dig('data', 'attributes', 'purpose_type')).to eq(resource_model.type) + expect(json.dig('data', 'attributes', 'target_type')).to eq(updated_target_type) + expect(json.dig('data', 'attributes', 'uuid')).to eq(resource_model.uuid) + end + end + + context 'when patching the uuid' do + let(:updated_uuid) { 'new-uuid' } + let(:payload) do + { + 'data' => { + 'id' => resource_model.id, + 'type' => 'tube_purposes', + 'attributes' => { + 'uuid' => updated_uuid + } + } + } + end + + it 'responds with 400 bad request, because uuid is read-only' do + api_patch "#{base_endpoint}/#{resource_model.id}", payload + expect(response).to have_http_status(:bad_request) + end + end + end + + describe '#post a new Tube Purpose' do + context 'with a valid payload' do + let(:payload) do + { + 'data' => { + 'type' => 'tube_purposes', + 'attributes' => { + 'name' => 'New Name', + 'purpose_type' => 'Test Purpose Type', + 'target_type' => 'MultiplexedLibraryTube' + } + } + } + end + + it 'creates the Tube Purpose' do + api_post base_endpoint, payload + expect(response).to have_http_status(:created) + expect(json.dig('data', 'type')).to eq('tube_purposes') + expect(json.dig('data', 'attributes', 'name')).to eq('New Name') + expect(json.dig('data', 'attributes', 'purpose_type')).to eq('Test Purpose Type') + expect(json.dig('data', 'attributes', 'target_type')).to eq('MultiplexedLibraryTube') + end + end + + context 'with an invalid payload (missing target_type)' do + let(:payload) do + { + 'data' => { + 'type' => 'tube_purposes', + 'attributes' => { + 'name' => 'New Name', + 'purpose_type' => 'Test Purpose Type' + } + } + } + end + + it 'responds with 422 unprocessable entity' do + api_post base_endpoint, payload + expect(response).to have_http_status(:unprocessable_entity) + end + end + + context 'with an invalid payload (includes uuid)' do + let(:payload) do + { + 'data' => { + 'type' => 'tube_purposes', + 'attributes' => { + 'name' => 'New Name', + 'purpose_type' => 'Test Purpose Type', + 'target_type' => 'MultiplexedLibraryTube', + 'uuid' => 'new-uuid' + } + } + } + end + + it 'responds with 400 bad request, because uuid is read-only' do + api_post base_endpoint, payload + expect(response).to have_http_status(:bad_request) + end + end + end +end diff --git a/spec/resources/api/v2/plate_template_resource_spec.rb b/spec/resources/api/v2/plate_template_resource_spec.rb index a43b1e0cec..89c0955f88 100644 --- a/spec/resources/api/v2/plate_template_resource_spec.rb +++ b/spec/resources/api/v2/plate_template_resource_spec.rb @@ -9,14 +9,15 @@ let(:resource_model) { build_stubbed :plate_template } # Test attributes - it 'works', :aggregate_failures do # rubocop:todo RSpec/ExampleWording + it 'has the expected attributes', :aggregate_failures do + expect(resource).not_to have_attribute :id expect(resource).to have_attribute :uuid - expect(resource).not_to have_updatable_field(:id) - expect(resource).not_to have_updatable_field(:uuid) end # Updatable fields - # eg. it { is_expected.to have_updatable_field(:state) } + it 'disallows updating of read only fields', :aggregate_failures do + expect(resource).not_to have_updatable_field :uuid + end # Filters # eg. it { is_expected.to filter(:order_type) } diff --git a/spec/resources/api/v2/sample_metadata_resource_spec.rb b/spec/resources/api/v2/sample_metadata_resource_spec.rb index 7781cef4c1..aa113a2bf6 100644 --- a/spec/resources/api/v2/sample_metadata_resource_spec.rb +++ b/spec/resources/api/v2/sample_metadata_resource_spec.rb @@ -4,18 +4,38 @@ require './app/resources/api/v2/sample_metadata_resource' RSpec.describe Api::V2::SampleMetadataResource, type: :resource do - describe 'it works' do - subject { described_class.new(sample_metadata, {}) } + subject(:resource) { described_class.new(sample_metadata, {}) } - let(:sample_metadata) { create(:sample_metadata) } + let(:sample_metadata) { create(:sample_metadata) } - it 'has the expected attributes' do - expect(subject).to have_attribute :sample_common_name - expect(subject).to have_attribute :supplier_name - expect(subject).to have_attribute :collected_by - expect(subject).to have_attribute :donor_id - expect(subject).to have_attribute :concentration - expect(subject).to have_attribute :volume - end + # Test attributes + it 'has the expected attributes', :aggregate_failures do + expect(resource).to have_attribute :cohort + expect(resource).to have_attribute :collected_by + expect(resource).to have_attribute :concentration + expect(resource).to have_attribute :donor_id + expect(resource).to have_attribute :gender + expect(resource).to have_attribute :sample_common_name + expect(resource).to have_attribute :sample_description + expect(resource).to have_attribute :supplier_name + expect(resource).to have_attribute :volume end + + # Updatable fields + it 'allows updating of read-write fields', :aggregate_failures do + expect(resource).to have_updatable_field :cohort + expect(resource).to have_updatable_field :collected_by + expect(resource).to have_updatable_field :concentration + expect(resource).to have_updatable_field :donor_id + expect(resource).to have_updatable_field :gender + expect(resource).to have_updatable_field :sample_common_name + expect(resource).to have_updatable_field :sample_description + expect(resource).to have_updatable_field :supplier_name + expect(resource).to have_updatable_field :volume + end + + # Non-updatable fields -- uncomment to use + # it 'disallows updating of read only fields', :aggregate_failures do + # expect(resource).not_to have_updatable_field :uuid + # end end diff --git a/spec/resources/api/v2/submission_template_resource_spec.rb b/spec/resources/api/v2/submission_template_resource_spec.rb new file mode 100644 index 0000000000..2e3fae719f --- /dev/null +++ b/spec/resources/api/v2/submission_template_resource_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' +require './app/resources/api/v2/submission_template_resource' + +RSpec.describe Api::V2::SubmissionTemplateResource, type: :resource do + subject(:resource) { described_class.new(resource_model, {}) } + + let(:resource_model) { build_stubbed :submission_template } + + # Test attributes + it 'has the expected attributes', :aggregate_failures do + expect(resource).not_to have_attribute :id + expect(resource).to have_attribute :uuid + expect(resource).to have_attribute :name + end + + # Updatable fields + it 'allows updating of read-write fields', :aggregate_failures do + expect(resource).to have_updatable_field :name + end + + it 'disallows updating of read only fields', :aggregate_failures do + expect(resource).not_to have_updatable_field :uuid + end + + # Filters + # eg. it { is_expected.to filter(:order_type) } + + # Associations + # eg. it { is_expected.to have_many(:samples).with_class_name('Sample') } + + # Custom method tests + # Add tests for any custom methods you've added. +end diff --git a/spec/resources/api/v2/transfer_template_resource_spec.rb b/spec/resources/api/v2/transfer_template_resource_spec.rb new file mode 100644 index 0000000000..e1caf97c62 --- /dev/null +++ b/spec/resources/api/v2/transfer_template_resource_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' +require './app/resources/api/v2/transfer_template_resource' + +RSpec.describe Api::V2::TransferTemplateResource, type: :resource do + subject(:resource) { described_class.new(resource_model, {}) } + + let(:resource_model) { build_stubbed :transfer_template } + + # Test attributes + it 'has the expected attributes', :aggregate_failures do + expect(resource).not_to have_attribute :id + expect(resource).to have_attribute :uuid + expect(resource).to have_attribute :name + end + + # Updatable fields + it 'allows updating of read-write fields', :aggregate_failures do + expect(resource).to have_updatable_field :name + end + + it 'disallows updating of read only fields', :aggregate_failures do + expect(resource).not_to have_updatable_field :uuid + end + + # Filters + # eg. it { is_expected.to filter(:order_type) } + + # Associations + # eg. it { is_expected.to have_many(:samples).with_class_name('Sample') } + + # Custom method tests + # Add tests for any custom methods you've added. +end diff --git a/spec/resources/api/v2/tube_purpose_resource_spec.rb b/spec/resources/api/v2/tube_purpose_resource_spec.rb new file mode 100644 index 0000000000..8dff614633 --- /dev/null +++ b/spec/resources/api/v2/tube_purpose_resource_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'rails_helper' +require './app/resources/api/v2/tube_purpose_resource' + +RSpec.describe Api::V2::TubePurposeResource, type: :resource do + subject(:resource) { described_class.new(resource_model, {}) } + + let(:resource_model) { build_stubbed :tube_purpose } + + # Test attributes + it 'has the expected attributes', :aggregate_failures do + expect(resource).not_to have_attribute :id + expect(resource).to have_attribute :name + expect(resource).to have_attribute :purpose_type + expect(resource).to have_attribute :target_type + expect(resource).to have_attribute :uuid + end + + # Updatable fields + it 'allows updating of read-write fields', :aggregate_failures do + expect(resource).to have_updatable_field :name + expect(resource).to have_updatable_field :purpose_type + expect(resource).to have_updatable_field :target_type + expect(resource).not_to have_updatable_field :uuid + end + + # Filters + # eg. it { is_expected.to filter(:order_type) } + + # Associations + # eg. it { is_expected.to have_many(:samples).with_class_name('Sample') } + + # Custom method tests + # Add tests for any custom methods you've added. +end