diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 78ca0369d6..65ef893d7c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -294,7 +294,7 @@ Lint/UselessAssignment: # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max. Metrics/AbcSize: Exclude: - - 'app/controllers/api/v2/transfers/transfers_controller.rb' + - 'app/controllers/api/v2/transfers_controller.rb' - 'app/jobs/export_pool_xp_to_traction_job.rb' - 'app/sample_manifest_excel/sample_manifest_excel/manifest_type_list.rb' @@ -717,7 +717,7 @@ RSpec/ExampleLength: - 'spec/requests/api/v2/shared_examples/api_key_authenticatable.rb' - 'spec/requests/api/v2/tag_layout_templates_spec.rb' - 'spec/requests/api/v2/tag_layouts_spec.rb' - - 'spec/requests/api/v2/transfers/transfers_spec.rb' + - 'spec/requests/api/v2/transfers_spec.rb' - 'spec/requests/api/v2/tube_purposes_spec.rb' - 'spec/requests/api/v2/users_spec.rb' - 'spec/requests/api/v2/volume_update_spec.rb' @@ -736,7 +736,6 @@ RSpec/ExampleLength: - 'spec/resources/api/v2/sample_metadata_resource_spec.rb' - 'spec/resources/api/v2/submission_resource_spec.rb' - 'spec/resources/api/v2/tag_group_resource_spec.rb' - - 'spec/resources/api/v2/transfers/transfer_resource_spec.rb' - 'spec/resources/api/v2/tube_rack_resource_spec.rb' - 'spec/resources/api/v2/tube_resource_spec.rb' - 'spec/sample_manifest_excel/configuration_spec.rb' @@ -1085,7 +1084,7 @@ RSpec/MultipleExpectations: - 'spec/requests/api/v2/transfer_request_collections_spec.rb' - 'spec/requests/api/v2/transfer_requests_spec.rb' - 'spec/requests/api/v2/transfer_templates_spec.rb' - - 'spec/requests/api/v2/transfers/transfers_spec.rb' + - 'spec/requests/api/v2/transfers_spec.rb' - 'spec/requests/api/v2/tube_purposes_spec.rb' - 'spec/requests/api/v2/tube_racks_spec.rb' - 'spec/requests/api/v2/tubes_spec.rb' @@ -1271,7 +1270,7 @@ RSpec/MultipleMemoizedHelpers: - 'spec/requests/api/v2/state_changes_spec.rb' - 'spec/requests/api/v2/tag_layouts_spec.rb' - 'spec/requests/api/v2/transfer_request_collections_spec.rb' - - 'spec/requests/api/v2/transfers/transfers_spec.rb' + - 'spec/requests/api/v2/transfers_spec.rb' - 'spec/requests/api/v2/tube_from_tube_creations_spec.rb' - 'spec/requests/api/v2/wells_spec.rb' - 'spec/requests/plate_picks_request_spec.rb' @@ -1434,7 +1433,7 @@ RSpec/NestedGroups: - 'spec/requests/api/v2/state_changes_spec.rb' - 'spec/requests/api/v2/tag_layouts_spec.rb' - 'spec/requests/api/v2/transfer_request_collections_spec.rb' - - 'spec/requests/api/v2/transfers/transfers_spec.rb' + - 'spec/requests/api/v2/transfers_spec.rb' - 'spec/requests/api/v2/tube_from_tube_creations_spec.rb' - 'spec/requests/api/v2/users_spec.rb' - 'spec/sample_manifest_excel/upload/processor_spec.rb' @@ -2424,7 +2423,7 @@ Style/StringConcatenation: Style/SuperArguments: Exclude: - 'app/api/model_extensions/order.rb' - - 'app/controllers/api/v2/transfers/transfers_controller.rb' + - 'app/controllers/api/v2/transfers_controller.rb' - 'app/models/broadcast_event/plate_cherrypicked.rb' - 'app/models/pipeline.rb' - 'app/models/plate_purpose/additional_input.rb' diff --git a/app/controllers/api/v2/transfers/transfers_controller.rb b/app/controllers/api/v2/transfers/transfers_controller.rb deleted file mode 100644 index 6d9550eb2e..0000000000 --- a/app/controllers/api/v2/transfers/transfers_controller.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -module Api - module V2 - module Transfers - # Provides a JSON API controller for Transfers. - # See: http://jsonapi-resources.com/ for JSONAPI::Resource documentation. - class TransfersController < JSONAPI::ResourceController - # By default JSONAPI::ResourceController provides most of the standard behaviour. - # However in this case we want to redirect create and update operations to the correct polymorphic type. - - def process_operations(operations) - # We need to determine the polymorphic type of the transfer to create based on any template provided. - operations.each do |operation| - # Neither data nor attributes are guaranteed among the operation options. - attributes = operation.options.fetch(:data, {}).fetch(:attributes, {}) - - # Skip the operation if it does not contain a transfer template. - next unless attributes.key?(:transfer_template_uuid) - - # Get the transfer template and use it to update the context and attributes. - template = TransferTemplate.with_uuid(attributes[:transfer_template_uuid]).first - operation.options[:context][:polymorphic_type] = template.transfer_class_name - attributes[:transfers] = template.transfers if template.transfers.present? - - # Remove the UUID of the transfer template from the attributes. - attributes.delete(:transfer_template_uuid) - end - - super(operations) - end - end - - # Provides a controller for BetweenPlateAndTubes transfers routed using jsonapi_resources endpoints. - class BetweenPlateAndTubesController < JSONAPI::ResourceController - end - - # Provides a controller for BetweenPlatesBySubmissions transfers routed using jsonapi_resources endpoints. - class BetweenPlatesBySubmissionsController < JSONAPI::ResourceController - end - - # Provides a controller for BetweenPlates transfers routed using jsonapi_resources endpoints. - class BetweenPlatesController < JSONAPI::ResourceController - end - - # Provides a controller for BetweenSpecificTubes transfers routed using jsonapi_resources endpoints. - class BetweenSpecificTubesController < JSONAPI::ResourceController - end - - # Provides a controller for BetweenTubesBySubmissions transfers routed using jsonapi_resources endpoints. - class BetweenTubesBySubmissionsController < JSONAPI::ResourceController - end - - # Provides a controller for FromPlateToSpecificTubes transfers routed using jsonapi_resources endpoints. - class FromPlateToSpecificTubesByPoolsController < JSONAPI::ResourceController - end - - # Provides a controller for FromPlateToSpecificTubes transfers routed using jsonapi_resources endpoints. - class FromPlateToSpecificTubesController < JSONAPI::ResourceController - end - - # Provides a controller for FromPlateToTubeByMultiplexes transfers routed using jsonapi_resources endpoints. - class FromPlateToTubeByMultiplexesController < JSONAPI::ResourceController - end - - # Provides a controller for FromPlateToTubeBySubmissions transfers routed using jsonapi_resources endpoints. - class FromPlateToTubeBySubmissionsController < JSONAPI::ResourceController - end - - # Provides a controller for FromPlateToTubes transfers routed using jsonapi_resources endpoints. - class FromPlateToTubesController < JSONAPI::ResourceController - end - end - end -end diff --git a/app/controllers/api/v2/transfers_controller.rb b/app/controllers/api/v2/transfers_controller.rb new file mode 100644 index 0000000000..e474bcb388 --- /dev/null +++ b/app/controllers/api/v2/transfers_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Api + module V2 + # Provides a JSON API controller for Transfers. + # See: http://jsonapi-resources.com/ for JSONAPI::Resource documentation. + class TransfersController < JSONAPI::ResourceController + # By default JSONAPI::ResourceController provides most of the standard behaviour. + # However in this case we want to redirect create and update operations to the correct polymorphic type. + + def process_operations(operations) + # We need to determine the polymorphic type of the transfer to create based on any template provided. + operations.each do |operation| + # Neither data nor attributes are guaranteed among the operation options. + attributes = operation.options.fetch(:data, {}).fetch(:attributes, {}) + + # Skip the operation if it does not contain a transfer template. + next unless attributes.key?(:transfer_template_uuid) + + # Get the transfer template and use it to update the context and attributes. + template = TransferTemplate.with_uuid(attributes[:transfer_template_uuid]).first + operation.options[:context][:polymorphic_type] = template.transfer_class_name + attributes[:transfers] = template.transfers if template.transfers.present? + + # Remove the UUID of the transfer template from the attributes. + attributes.delete(:transfer_template_uuid) + end + + super(operations) + end + end + end +end diff --git a/app/models/transfer.rb b/app/models/transfer.rb index 5b82f32bee..86d9f3210e 100644 --- a/app/models/transfer.rb +++ b/app/models/transfer.rb @@ -65,3 +65,15 @@ def should_well_not_be_transferred?(well) well.nil? or well.aliquots.empty? or well.failed? or well.cancelled? end end + +# Required for the descendants method to work when eager loading is off in test +require_dependency 'transfer/between_plate_and_tubes' +require_dependency 'transfer/between_plates_by_submission' +require_dependency 'transfer/between_plates' +require_dependency 'transfer/between_specific_tubes' +require_dependency 'transfer/between_tubes_by_submission' +require_dependency 'transfer/from_plate_to_specific_tubes_by_pool' +require_dependency 'transfer/from_plate_to_specific_tubes' +require_dependency 'transfer/from_plate_to_tube_by_multiplex' +require_dependency 'transfer/from_plate_to_tube_by_submission' +require_dependency 'transfer/from_plate_to_tube' diff --git a/app/resources/api/v2/aliquot_resource.rb b/app/resources/api/v2/aliquot_resource.rb index fae4ea85dd..79644e6959 100644 --- a/app/resources/api/v2/aliquot_resource.rb +++ b/app/resources/api/v2/aliquot_resource.rb @@ -27,13 +27,13 @@ class AliquotResource < BaseResource has_one :library # Attributes - attribute :tag_oligo, readonly: true - attribute :tag_index, readonly: true - attribute :tag2_oligo, readonly: true - attribute :tag2_index, readonly: true - attribute :suboptimal, readonly: true - attribute :library_type, readonly: true - attribute :insert_size_to, readonly: true + attribute :tag_oligo, write_once: true + attribute :tag_index, write_once: true + attribute :tag2_oligo, write_once: true + attribute :tag2_index, write_once: true + attribute :suboptimal, write_once: true + attribute :library_type, write_once: true + attribute :insert_size_to, write_once: true # Filters diff --git a/app/resources/api/v2/bait_library_layout_resource.rb b/app/resources/api/v2/bait_library_layout_resource.rb index 6b99434dfd..493bbbe69f 100644 --- a/app/resources/api/v2/bait_library_layout_resource.rb +++ b/app/resources/api/v2/bait_library_layout_resource.rb @@ -22,7 +22,7 @@ class BaitLibraryLayoutResource < BaseResource # @param value [String] The UUID of the plate for this bait library layout. # @return [Void] # @see #plate - attribute :plate_uuid + attribute :plate_uuid, writeonly: true def plate_uuid=(value) @model.plate = Plate.with_uuid(value).first @@ -35,7 +35,7 @@ def plate_uuid=(value) # @param value [String] The UUID of the user who created this bait library layout. # @return [Void] # @see #user - attribute :user_uuid + attribute :user_uuid, writeonly: true def user_uuid=(value) @model.user = User.with_uuid(value).first @@ -64,17 +64,6 @@ def user_uuid=(value) # @return [PlateResource] The plate for this bait library layout. # @note This relationship is required. has_one :plate - - def self.creatable_fields(context) - # The layout is generated by the system. - # The UUID is set by the system. - super - %i[layout uuid] - end - - def fetchable_fields - # UUIDs for relationships are not fetchable. They should be accessed via the relationship itself. - super - %i[plate_uuid user_uuid] - end end end end diff --git a/app/resources/api/v2/barcode_printer_resource.rb b/app/resources/api/v2/barcode_printer_resource.rb index b3c32cc22b..2f7abdff51 100644 --- a/app/resources/api/v2/barcode_printer_resource.rb +++ b/app/resources/api/v2/barcode_printer_resource.rb @@ -25,7 +25,7 @@ class BarcodePrinterResource < BaseResource # @!attribute [r] name # @return [String] the name of the barcode printer. - attribute :name + attribute :name, readonly: true # @!attribute [r] uuid # @return [String] the UUID of the barcode printer. diff --git a/app/resources/api/v2/base_resource.rb b/app/resources/api/v2/base_resource.rb index 9b43fe9940..0a9dbd2495 100644 --- a/app/resources/api/v2/base_resource.rb +++ b/app/resources/api/v2/base_resource.rb @@ -21,18 +21,27 @@ class BaseResource < JSONAPI::Resource Plate.descendants.each { |subclass| model_hint model: subclass, resource: :plate } Tube.descendants.each { |subclass| model_hint model: subclass, resource: :tube } Request.descendants.each { |subclass| model_hint model: subclass, resource: :request } + Transfer.descendants.each { |subclass| model_hint model: subclass, resource: :transfer } - # This extension allows the readonly property to be used on attributes/relationships - # prior to the 0.10 upgrade. This avoids the need to override updatable_fields on - # every resource. Readonly does not work on attributes in 0.9 by default + # These extensions allow the use of readonly, write_once and writeonly properties. + # readonly - The attribute/relationship can be read but not written to. + # write_once - The attribute/relationship can be written to once on creation but not updated. + # writeonly - The attribute/relationship can be written to but not read. + # This avoids the need to override self.creatable_fields, self.updatable_fields and fetchable_fields on every + # resource. + # readonly does not work on attributes in JSONAPI:Resources 0.9 by default. # This can be removed as soon as we update to 0.10, which is currently only in alpha - def self.updatable_fields(context) + + def self.creatable_fields(context) super - _attributes.select { |_attr, options| options[:readonly] }.keys - _relationships.select { |_rel_key, rel| rel.options[:readonly] }.keys end - # This extension allows the writeonly property to be used on attributes/relationships. - # This avoids the need to override fetchable_fields on every resource. + def self.updatable_fields(context) + super - _attributes.select { |_attr, options| options[:readonly] || options[:write_once] }.keys - + _relationships.select { |_rel_key, rel| rel.options[:readonly] || rel.options[:write_once] }.keys + end + def fetchable_fields super - self.class._attributes.select { |_attr, options| options[:writeonly] }.keys - self.class._relationships.select { |_rel_key, rel| rel.options[:writeonly] }.keys diff --git a/app/resources/api/v2/comment_resource.rb b/app/resources/api/v2/comment_resource.rb index 19f8a3c9b0..5546c971e1 100644 --- a/app/resources/api/v2/comment_resource.rb +++ b/app/resources/api/v2/comment_resource.rb @@ -23,8 +23,8 @@ class CommentResource < BaseResource has_one :commentable, polymorphic: true # Attributes - attribute :title, readonly: true - attribute :description, readonly: true + attribute :title, write_once: true + attribute :description, write_once: true attribute :created_at, readonly: true attribute :updated_at, readonly: true diff --git a/app/resources/api/v2/custom_metadatum_collection_resource.rb b/app/resources/api/v2/custom_metadatum_collection_resource.rb index 491f69fedd..9654e144b7 100644 --- a/app/resources/api/v2/custom_metadatum_collection_resource.rb +++ b/app/resources/api/v2/custom_metadatum_collection_resource.rb @@ -22,33 +22,19 @@ class CustomMetadatumCollectionResource < BaseResource # @!attribute [r] uuid # @return [String] The UUID of the collection. - attribute :uuid + attribute :uuid, readonly: true # @!attribute [rw] user_id # @return [Int] The ID of the user who created this collection. Can only and must be set on creation. - attribute :user_id + attribute :user_id, write_once: true # @!attribute [rw] asset_id # @return [Int] The ID of the labware the metadata corresponds to. Can only and must be set on creation. - attribute :asset_id + attribute :asset_id, write_once: true # @!attribute [rw] metadata # @return [Hash] All metadata in this collection. attribute :metadata - - ### - # Allowable fields (defining read/write permissions for POST and PATCH) - ### - - # @return [Array] Fields that can be created in a POST request. - def self.creatable_fields(context) - super - %i[uuid] - end - - # @return [Array] Fields that can be updated in a PATCH request. - def self.updatable_fields(context) - super - %i[uuid user_id asset_id] # PATCH should only update metadata - end end end end diff --git a/app/resources/api/v2/lot_resource.rb b/app/resources/api/v2/lot_resource.rb index 20c8ee55b5..5941649dc9 100644 --- a/app/resources/api/v2/lot_resource.rb +++ b/app/resources/api/v2/lot_resource.rb @@ -28,7 +28,7 @@ class LotResource < BaseResource # Attributes attribute :uuid, readonly: true - attribute :lot_number, readonly: true + attribute :lot_number, write_once: true # Filters diff --git a/app/resources/api/v2/lot_type_resource.rb b/app/resources/api/v2/lot_type_resource.rb index c0eceeccfd..b67d2e15ae 100644 --- a/app/resources/api/v2/lot_type_resource.rb +++ b/app/resources/api/v2/lot_type_resource.rb @@ -24,12 +24,12 @@ class LotTypeResource < BaseResource default_includes :uuid_object # Associations: - has_one :target_purpose, readonly: true, class_name: 'Purpose' + has_one :target_purpose, write_once: true, class_name: 'Purpose' # Attributes attribute :uuid, readonly: true - attribute :name, readonly: true - attribute :template_type, readonly: true + attribute :name, write_once: true + attribute :template_type, write_once: true # Filters diff --git a/app/resources/api/v2/order_resource.rb b/app/resources/api/v2/order_resource.rb index e603d7e8a2..e06622ea27 100644 --- a/app/resources/api/v2/order_resource.rb +++ b/app/resources/api/v2/order_resource.rb @@ -24,7 +24,7 @@ class OrderResource < BaseResource # Attributes attribute :uuid, readonly: true - attribute :request_options, readonly: true + attribute :request_options, write_once: true # Filters diff --git a/app/resources/api/v2/pick_list_resource.rb b/app/resources/api/v2/pick_list_resource.rb index 874c1e163d..7d8e4483d4 100644 --- a/app/resources/api/v2/pick_list_resource.rb +++ b/app/resources/api/v2/pick_list_resource.rb @@ -25,8 +25,8 @@ class PickListResource < BaseResource # Attributes attribute :created_at, readonly: true attribute :updated_at, readonly: true - attribute :state, readonly: true - attribute :links, readonly: true + attribute :state, write_once: true + attribute :links, write_once: true attribute :pick_attributes attribute :labware_pick_attributes, writeonly: true diff --git a/app/resources/api/v2/plate_purpose_resource.rb b/app/resources/api/v2/plate_purpose_resource.rb index 58edb41b51..6ae23acd29 100644 --- a/app/resources/api/v2/plate_purpose_resource.rb +++ b/app/resources/api/v2/plate_purpose_resource.rb @@ -6,6 +6,7 @@ module V2 # @todo This documentation does not yet include detailed descriptions for relationships, attributes and filters. # @todo This documentation does not yet include any example usage of the API via cURL or similar. # + # @note This resource cannot be modified after creation: its endpoint will not accept `PATCH` requests. # @note Access this resource via the `/api/v2/plate_purposes/` endpoint. # # Provides a JSON:API representation of {PlatePurpose}. @@ -50,7 +51,7 @@ class PlatePurposeResource < BaseResource # @!attribute [r] uuid # @return [String] the UUID of the plate purpose. - attribute :uuid + attribute :uuid, readonly: true # Sets the asset shape of the plate purpose by name if given. # 'asset_shape' can be given via the Limber purpose configuration and @@ -88,22 +89,6 @@ def input_plate=(is_input) def input_plate @model.type == 'PlatePurpose::Input' end - - # Gets the list of fields which are creatable on a PlatePurpose. - # - # @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 PlatePurpose. - # - # @param _context [JSONAPI::Resource::Context] not used - # @return [Array] the list of updatable fields. - def self.updatable_fields(_context) - [] # Do not allow updating any fields. - end end end end diff --git a/app/resources/api/v2/plate_resource.rb b/app/resources/api/v2/plate_resource.rb index 9a0cfc13cf..c584f0845e 100644 --- a/app/resources/api/v2/plate_resource.rb +++ b/app/resources/api/v2/plate_resource.rb @@ -6,6 +6,7 @@ module V2 # @todo This documentation does not yet include detailed descriptions for relationships, attributes and filters. # @todo This documentation does not yet include any example usage of the API via cURL or similar. # + # @note This resource cannot be modified after creation: its endpoint will not accept `PATCH` requests. # @note Access this resource via the `/api/v2/plates/` endpoint. # # Provides a JSON:API representation of {Plate}. @@ -25,12 +26,12 @@ class PlateResource < BaseResource default_includes :uuid_object, :barcodes, :plate_purpose, :transfer_requests # Associations: - has_many :wells, readonly: true + has_many :wells, write_once: true # Attributes - attribute :number_of_rows, readonly: true, delegate: :height - attribute :number_of_columns, readonly: true, delegate: :width - attribute :size, readonly: true + attribute :number_of_rows, write_once: true, delegate: :height + attribute :number_of_columns, write_once: true, delegate: :width + attribute :size, write_once: true # Filters diff --git a/app/resources/api/v2/pooled_plate_creation_resource.rb b/app/resources/api/v2/pooled_plate_creation_resource.rb index 52f8ec97d1..1e96f55919 100644 --- a/app/resources/api/v2/pooled_plate_creation_resource.rb +++ b/app/resources/api/v2/pooled_plate_creation_resource.rb @@ -22,7 +22,7 @@ class PooledPlateCreationResource < BaseResource # @!attribute [w] child_purpose_uuid # @param value [String] The UUID of a child purpose to use in the creation of the child plate. # @return [Void] - attribute :child_purpose_uuid + attribute :child_purpose_uuid, writeonly: true def child_purpose_uuid=(value) @model.child_purpose = Purpose.with_uuid(value).first @@ -35,7 +35,7 @@ def child_purpose_uuid=(value) # @param value [Array] The UUIDs of labware that will be the parents for the created plate. # @return [Void] # @see #parents - attribute :parent_uuids + attribute :parent_uuids, writeonly: true def parent_uuids=(value) @model.parents = value.map { |uuid| Labware.with_uuid(uuid).first } @@ -48,7 +48,7 @@ def parent_uuids=(value) # @param value [String] The UUID of the user who initiated the creation of this pooled plate. # @return [Void] # @see #user - attribute :user_uuid + attribute :user_uuid, writeonly: true def user_uuid=(value) @model.user = User.with_uuid(value).first @@ -64,7 +64,7 @@ def user_uuid=(value) # @!attribute [r] child # @return [PlateResource] The child plate which was created. - has_one :child, class_name: 'Plate' + has_one :child, class_name: 'Plate', readonly: true # @!attribute [rw] parents # Setting this relationship alongside the `parent_uuids` attribute will override the attribute value. @@ -77,16 +77,6 @@ def user_uuid=(value) # @return [UserResource] The user who initiated the creation of the pooled plate. # @note This relationship is required. has_one :user - - def self.creatable_fields(context) - # UUID is set by the system. - super - %i[child uuid] - end - - def fetchable_fields - # UUIDs for relationships are not fetchable. They should be accessed via the relationship itself. - super - %i[child_purpose_uuid parent_uuids user_uuid] - end end end end diff --git a/app/resources/api/v2/primer_panel_resource.rb b/app/resources/api/v2/primer_panel_resource.rb index f273a97f80..3e00591960 100644 --- a/app/resources/api/v2/primer_panel_resource.rb +++ b/app/resources/api/v2/primer_panel_resource.rb @@ -17,8 +17,8 @@ class PrimerPanelResource < BaseResource # Constants... # model_name / model_hint if required - attribute :name, readonly: true - attribute :programs, readonly: true + attribute :name, write_once: true + attribute :programs, write_once: true # Associations: # Attributes diff --git a/app/resources/api/v2/purpose_resource.rb b/app/resources/api/v2/purpose_resource.rb index 0c26a6b88d..1877eec38c 100644 --- a/app/resources/api/v2/purpose_resource.rb +++ b/app/resources/api/v2/purpose_resource.rb @@ -24,9 +24,9 @@ class PurposeResource < BaseResource # Attributes attribute :uuid, readonly: true - attribute :name, readonly: true - attribute :size, readonly: true - attribute :lifespan, readonly: true + attribute :name, write_once: true + attribute :size, write_once: true + attribute :lifespan, write_once: true # Filters filter :name diff --git a/app/resources/api/v2/qcable_resource.rb b/app/resources/api/v2/qcable_resource.rb index 5e9f45e178..3ffc30d7b7 100644 --- a/app/resources/api/v2/qcable_resource.rb +++ b/app/resources/api/v2/qcable_resource.rb @@ -26,8 +26,8 @@ class QcableResource < BaseResource # Attributes attribute :uuid, readonly: true - attribute :state, readonly: true - attribute :labware_barcode, readonly: true + attribute :state, write_once: true + attribute :labware_barcode, write_once: true # Filters filter :barcode, apply: ->(records, value, _options) { records.with_barcode(value) } diff --git a/app/resources/api/v2/racked_tube_resource.rb b/app/resources/api/v2/racked_tube_resource.rb index 452c60fd22..778523cdef 100644 --- a/app/resources/api/v2/racked_tube_resource.rb +++ b/app/resources/api/v2/racked_tube_resource.rb @@ -23,7 +23,7 @@ class RackedTubeResource < BaseResource has_one :tube_rack # Attributes - attribute :coordinate, readonly: true + attribute :coordinate, write_once: true # Filters diff --git a/app/resources/api/v2/request_metadata_resource.rb b/app/resources/api/v2/request_metadata_resource.rb index ba29d757bc..dc765f21a2 100644 --- a/app/resources/api/v2/request_metadata_resource.rb +++ b/app/resources/api/v2/request_metadata_resource.rb @@ -30,11 +30,11 @@ class RequestMetadataResource < BaseResource # @!attribute [r] number_of_samples_per_pool # @return [Int] the number_of_samples_per_pool. - attribute :number_of_samples_per_pool, readonly: true + attribute :number_of_samples_per_pool, write_once: true # @!attribute [r] cells_per_chip_well # @return [Int] the cells_per_chip_well. - attribute :cells_per_chip_well, readonly: true + attribute :cells_per_chip_well, write_once: true # Filters diff --git a/app/resources/api/v2/request_resource.rb b/app/resources/api/v2/request_resource.rb index bf12598965..dda38f6312 100644 --- a/app/resources/api/v2/request_resource.rb +++ b/app/resources/api/v2/request_resource.rb @@ -35,11 +35,11 @@ class RequestResource < BaseResource # Attributes attribute :uuid, readonly: true - attribute :role, readonly: true + attribute :role, write_once: true attribute :state - attribute :priority, readonly: true + attribute :priority, write_once: true attribute :options - attribute :library_type, readonly: true + attribute :library_type, write_once: true # Filters diff --git a/app/resources/api/v2/request_type_resource.rb b/app/resources/api/v2/request_type_resource.rb index 8b70c4569d..d0b0b5b716 100644 --- a/app/resources/api/v2/request_type_resource.rb +++ b/app/resources/api/v2/request_type_resource.rb @@ -27,9 +27,9 @@ class RequestTypeResource < BaseResource # Attributes attribute :uuid, readonly: true - attribute :name, readonly: true - attribute :key, readonly: true - attribute :for_multiplexing, readonly: true + attribute :name, write_once: true + attribute :key, write_once: true + attribute :for_multiplexing, write_once: true # Filters diff --git a/app/resources/api/v2/shared_behaviour/labware.rb b/app/resources/api/v2/shared_behaviour/labware.rb index 5ad2156de4..be2c11aeb3 100644 --- a/app/resources/api/v2/shared_behaviour/labware.rb +++ b/app/resources/api/v2/shared_behaviour/labware.rb @@ -15,12 +15,12 @@ module Labware included do # Associations: - has_one :purpose, readonly: true, foreign_key: :plate_purpose_id, class_name: 'Purpose' + has_one :purpose, write_once: true, foreign_key: :plate_purpose_id, class_name: 'Purpose' has_one :custom_metadatum_collection, foreign_key_on: :related - has_many :samples, readonly: true - has_many :studies, readonly: true - has_many :projects, readonly: true + has_many :samples, write_once: true + has_many :studies, write_once: true + has_many :projects, write_once: true has_many :comments, readonly: true # If we are using api/v2/labware to pull back a list of labware, we may diff --git a/app/resources/api/v2/shared_behaviour/receptacle.rb b/app/resources/api/v2/shared_behaviour/receptacle.rb index 0b77436614..5443643048 100644 --- a/app/resources/api/v2/shared_behaviour/receptacle.rb +++ b/app/resources/api/v2/shared_behaviour/receptacle.rb @@ -17,8 +17,8 @@ module Receptacle # Associations: has_many :samples, readonly: true - has_many :studies, readonly: true - has_many :projects, readonly: true + has_many :studies, write_once: true + has_many :projects, write_once: true has_many :requests_as_source, readonly: true has_many :requests_as_target, readonly: true @@ -38,11 +38,11 @@ module Receptacle has_many :transfer_requests_as_source, readonly: true has_many :transfer_requests_as_target, readonly: true - has_one :labware, readonly: true + has_one :labware, write_once: true # Attributes attribute :uuid, readonly: true - attribute :name, delegate: :display_name, readonly: true + attribute :name, delegate: :display_name, write_once: true attributes :pcr_cycles, :submit_for_sequencing, :sub_pool, :coverage, :diluent_volume attribute :state, readonly: true diff --git a/app/resources/api/v2/specific_tube_creation_resource.rb b/app/resources/api/v2/specific_tube_creation_resource.rb index d3cfae2529..bd4d7602ac 100644 --- a/app/resources/api/v2/specific_tube_creation_resource.rb +++ b/app/resources/api/v2/specific_tube_creation_resource.rb @@ -22,7 +22,7 @@ class SpecificTubeCreationResource < BaseResource # @!attribute [w] child_purpose_uuids # @param value [Array] Array of UUIDs for child purposes to use in the creation of tubes. # @return [Void] - attribute :child_purpose_uuids + attribute :child_purpose_uuids, writeonly: true def child_purpose_uuids=(value) @model.child_purposes = value.map { |uuid| Purpose.with_uuid(uuid).first } @@ -35,7 +35,7 @@ def child_purpose_uuids=(value) # @param value [Array] The UUIDs of labware that will be the parents for all tubes created. # @return [Void] # @see #parents - attribute :parent_uuids + attribute :parent_uuids, writeonly: true def parent_uuids=(value) @model.parents = value.map { |uuid| Labware.with_uuid(uuid).first } @@ -47,7 +47,7 @@ def parent_uuids=(value) # @example Setting the name of the tubes being created. # [{ name: 'Tube one' }, { name: 'Tube two' }] # @return [Void] - attribute :tube_attributes + attribute :tube_attributes, writeonly: true def tube_attributes=(value) return if value.nil? @@ -63,7 +63,7 @@ def tube_attributes=(value) # @param value [String] The UUID of the user who initiated the creation of tubes. # @return [Void] # @see #user - attribute :user_uuid + attribute :user_uuid, writeonly: true def user_uuid=(value) @model.user = User.with_uuid(value).first @@ -79,7 +79,7 @@ def user_uuid=(value) # @!attribute [r] children # @return [Array] An array of tubes that were created. - has_many :children, class_name: 'Tube' + has_many :children, class_name: 'Tube', readonly: true # @!attribute [rw] parents # Setting this relationship alongside the `parent_uuids` attribute will override the attribute value. @@ -92,17 +92,6 @@ def user_uuid=(value) # @return [UserResource] The user who initiated the creation of tubes. # @note This relationship is required. has_one :user - - def self.creatable_fields(context) - # UUID is set by the system. - super - %i[uuid] - end - - def fetchable_fields - # The tube_attributes attribute is only available during resource creation. - # UUIDs for relationships are not fetchable. They should be accessed via the relationship itself. - super - %i[child_purpose_uuids parent_uuids tube_attributes user_uuid] - end end end end diff --git a/app/resources/api/v2/state_change_resource.rb b/app/resources/api/v2/state_change_resource.rb index 53f8388aad..3f29a0cba8 100644 --- a/app/resources/api/v2/state_change_resource.rb +++ b/app/resources/api/v2/state_change_resource.rb @@ -27,7 +27,7 @@ class StateChangeResource < BaseResource # @param value [Boolean] Sets whether the customer proceeded against advice and will still be charged in the # event of a failure. # @return [Void] - attribute :customer_accepts_responsibility + attribute :customer_accepts_responsibility, writeonly: true # @!attribute [r] previous_state # @return [String] The previous state of the target before this state change. @@ -49,7 +49,7 @@ class StateChangeResource < BaseResource # @param value [String] The UUID of the target labware this state change applies to. # @return [Void] # @see #target - attribute :target_uuid + attribute :target_uuid, writeonly: true def target_uuid=(value) @model.target = Labware.with_uuid(value).first @@ -62,7 +62,7 @@ def target_uuid=(value) # @param value [String] The UUID of the user who initiated this state change. # @return [Void] # @see #user - attribute :user_uuid + attribute :user_uuid, writeonly: true def user_uuid=(value) @model.user = User.with_uuid(value).first @@ -87,17 +87,6 @@ def user_uuid=(value) # @return [LabwareResource] The target labware this state change applies to. # @note This relationship is required. has_one :target, class_name: 'Labware' - - def self.creatable_fields(context) - # Previous state and UUID are set by the system. - super - %i[previous_state uuid] - end - - def fetchable_fields - # The customer_accepts_responsibility attribute is only available during resource creation. - # UUIDs for relationships are not fetchable. They should be accessed via the relationship itself. - super - %i[customer_accepts_responsibility target_uuid user_uuid] - end end end end diff --git a/app/resources/api/v2/submission_resource.rb b/app/resources/api/v2/submission_resource.rb index f3a5335d65..aa17be7fe6 100644 --- a/app/resources/api/v2/submission_resource.rb +++ b/app/resources/api/v2/submission_resource.rb @@ -31,12 +31,12 @@ class SubmissionResource < BaseResource # for field filtering, otherwise newly added attributes # will not show by default. attribute :uuid, readonly: true - attribute :name, readonly: true + attribute :name, write_once: true attribute :state, readonly: true attribute :created_at, readonly: true attribute :updated_at, readonly: true - attribute :used_tags, readonly: true - attribute :lanes_of_sequencing, readonly: true + attribute :used_tags, write_once: true + attribute :lanes_of_sequencing, write_once: true # Filters filter :uuid, apply: ->(records, value, _options) { records.with_uuid(value) } diff --git a/app/resources/api/v2/tag_group_adapter_type_resource.rb b/app/resources/api/v2/tag_group_adapter_type_resource.rb index fc2cb5047e..f28b990bde 100644 --- a/app/resources/api/v2/tag_group_adapter_type_resource.rb +++ b/app/resources/api/v2/tag_group_adapter_type_resource.rb @@ -19,7 +19,7 @@ class TagGroupAdapterTypeResource < BaseResource # Constants... # Associations: - has_many :tag_groups, readonly: true, class_name: 'TagGroup' + has_many :tag_groups, write_once: true, class_name: 'TagGroup' # Attributes attribute :name, readonly: true diff --git a/app/resources/api/v2/tag_group_resource.rb b/app/resources/api/v2/tag_group_resource.rb index 7b2409b61f..63678f2ed3 100644 --- a/app/resources/api/v2/tag_group_resource.rb +++ b/app/resources/api/v2/tag_group_resource.rb @@ -21,12 +21,15 @@ class TagGroupResource < BaseResource default_includes :uuid_object, :tags # Associations: - has_one :tag_group_adapter_type, foreign_key: :adapter_type_id, readonly: true, class_name: 'TagGroupAdapterType' + has_one :tag_group_adapter_type, + foreign_key: :adapter_type_id, + write_once: true, + class_name: 'TagGroupAdapterType' # Attributes attribute :uuid, readonly: true - attribute :name, readonly: true - attribute :tags, readonly: true + attribute :name, write_once: true + attribute :tags, write_once: true # Filters filter :visible, default: true diff --git a/app/resources/api/v2/tag_layout_resource.rb b/app/resources/api/v2/tag_layout_resource.rb index edbf0661a8..b9c29a0e72 100644 --- a/app/resources/api/v2/tag_layout_resource.rb +++ b/app/resources/api/v2/tag_layout_resource.rb @@ -37,7 +37,7 @@ class TagLayoutResource < BaseResource # @param value [String] The UUID of the {Plate} this tag layout applies to. # @return [Void] # @see #plate - attribute :plate_uuid + attribute :plate_uuid, writeonly: true def plate_uuid=(value) @model.plate = Plate.with_uuid(value).first @@ -64,7 +64,7 @@ def substitutions=(value) # @param value [String] The UUID of the {TagGroup} used in this tag layout. # @return [Void] # @see #tag_group - attribute :tag_group_uuid + attribute :tag_group_uuid, writeonly: true def tag_group_uuid=(value) @model.tag_group = TagGroup.with_uuid(value).first @@ -77,7 +77,7 @@ def tag_group_uuid=(value) # @param value [String] The UUID of the second {TagGroup} used in this tag layout. # @return [Void] # @see #tag2_group - attribute :tag2_group_uuid + attribute :tag2_group_uuid, writeonly: true def tag2_group_uuid=(value) @model.tag2_group = TagGroup.with_uuid(value).first @@ -97,7 +97,7 @@ def tag2_group_uuid=(value) # @param value [String] The UUID of the {User} who initiated this state change. # @return [Void] # @see #user - attribute :user_uuid + attribute :user_uuid, writeonly: true def user_uuid=(value) @model.user = User.with_uuid(value).first @@ -142,16 +142,6 @@ def user_uuid=(value) # @return [Api::V2::UserResource] The user who initiated this state change. # @note This relationship is required. has_one :user - - def self.creatable_fields(context) - # UUID is set by the system. - super - %i[uuid] - end - - def fetchable_fields - # UUIDs for relationships are not fetchable. They should be accessed via the relationship itself. - super - %i[plate_uuid tag_group_uuid tag2_group_uuid user_uuid] - end end end end diff --git a/app/resources/api/v2/tag_resource.rb b/app/resources/api/v2/tag_resource.rb index 4c75af8311..6f494d1720 100644 --- a/app/resources/api/v2/tag_resource.rb +++ b/app/resources/api/v2/tag_resource.rb @@ -25,8 +25,8 @@ class TagResource < BaseResource has_one :tag_group # Attributes - attribute :oligo, readonly: true - attribute :map_id, readonly: true + attribute :oligo, write_once: true + attribute :map_id, write_once: true # Filters diff --git a/app/resources/api/v2/transfer_request_collection_resource.rb b/app/resources/api/v2/transfer_request_collection_resource.rb index 78d6b2251a..360523d932 100644 --- a/app/resources/api/v2/transfer_request_collection_resource.rb +++ b/app/resources/api/v2/transfer_request_collection_resource.rb @@ -36,7 +36,7 @@ class TransferRequestCollectionResource < BaseResource # @param value [Array] An array of hashes containing the attributes for transfer request to be created. # @return [Void] # @see #transfer_requests - attribute :transfer_requests_attributes + attribute :transfer_requests_attributes, writeonly: true def transfer_requests_attributes=(value) return if value.nil? @@ -52,7 +52,7 @@ def transfer_requests_attributes=(value) # @param value [String] The UUID of the user who initiated the creation of this pooled plate. # @return [Void] # @see #user - attribute :user_uuid + attribute :user_uuid, writeonly: true def user_uuid=(value) @model.user = User.with_uuid(value).first @@ -68,28 +68,17 @@ def user_uuid=(value) # @!attribute [r] target_tubes # @return [Array] An array of tubes targeted by the transfer requests in this collection. - has_many :target_tubes, class_name: 'Tube' + has_many :target_tubes, class_name: 'Tube', readonly: true # @!attribute [r] transfer_requests # @return [Array] An array of transfer requests in this collection. - has_many :transfer_requests + has_many :transfer_requests, readonly: true # @!attribute [rw] user # Setting this relationship alongside the `user_uuid` attribute will override the attribute value. # @return [UserResource] The user who initiated the creation of the pooled plate. # @note This relationship is required. has_one :user - - def self.creatable_fields(context) - # UUID is set by the system. - super - %i[target_tubes transfer_requests uuid] - end - - def fetchable_fields - # The transfer_requests_attributes attribute is only available during resource creation. - # UUIDs for relationships are not fetchable. They should be accessed via the relationship itself. - super - %i[transfer_requests_attributes user_uuid] - end end end end diff --git a/app/resources/api/v2/transfer_request_resource.rb b/app/resources/api/v2/transfer_request_resource.rb index 084f34a07c..b56e5c62f9 100644 --- a/app/resources/api/v2/transfer_request_resource.rb +++ b/app/resources/api/v2/transfer_request_resource.rb @@ -31,7 +31,7 @@ class TransferRequestResource < BaseResource # Attributes attribute :uuid, readonly: true attribute :state, readonly: true - attribute :volume, readonly: true + attribute :volume, write_once: true # Filters diff --git a/app/resources/api/v2/transfer_resource.rb b/app/resources/api/v2/transfer_resource.rb new file mode 100644 index 0000000000..208c7a2516 --- /dev/null +++ b/app/resources/api/v2/transfer_resource.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +module Api + module V2 + # @todo This documentation does not yet include a detailed description of what this resource represents. + # @todo This documentation does not yet include detailed descriptions for relationships, attributes and filters. + # @todo This documentation does not yet include any example usage of the API via cURL or similar. + # + # @note Access this resource via the `/api/v2/transfers/` endpoint. + # + # Provides a JSON:API representation of {Transfer}. + # + # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) + # or look at the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation + # of the JSON:API standard. + class TransferResource < BaseResource + ### + # Attributes + ### + + # @!attribute [rw] destination_uuid + # @return [String, void] the UUID of the destination labware. + attribute :destination_uuid + + def destination_uuid + @model.destination&.uuid + end + + def destination_uuid=(uuid) + @model.destination = Labware.with_uuid(uuid).first if uuid + end + + # @!attribute [rw] source_uuid + # @return [String] the UUID of the source labware. + # The type of the labware varies by the type of transfer. + attribute :source_uuid + + def source_uuid + @model.source.uuid + end + + def source_uuid=(uuid) + @model.source = Labware.with_uuid(uuid).first + end + + # @!attribute [rw] transfers + # @return [Hash] a hash of the transfers made. + # This is usually populated by the TransferTemplate used during creation of the Transfer. + attribute :transfers + + def transfers + # Only some transfer types have the :transfers method. + # This gets implemented differently, depending on the type of transfer being performed. + return nil unless @model.respond_to?(:transfers) + + @model.transfers + end + + def transfers=(transfers) + # This setter is invoked by the TransferTemplate populating the attributes for transfers. + @model.transfers = + if transfers.is_a?(ActionController::Parameters) + transfers.to_unsafe_h # We must unwrap the parameters to a real Hash. + else + transfers + end + end + + # @!attribute [w] transfer_template_uuid + # @return [String] the UUID of a TransferTemplate to create a transfer from. + # This must be provided or the Transfer creation will raise an error. + attribute :transfer_template_uuid, writeonly: true + + # @!attribute [r] transfer_type + # @return [String] The STI type of the transfer. + attribute :transfer_type, delegate: :sti_type, readonly: true + + # @!attribute [w] user_uuid + # This is declared for convenience where the user is not available to set as a relationship. + # Setting this attribute alongside the `user` relationship will prefer the relationship value. + # @deprecated Use the `user` relationship instead. + # @param value [String] The UUID of the user who requested the transfer. + # @return [Void] + # @see #user + attribute :user_uuid, writeonly: true + + def user_uuid + @model.user&.uuid # Some old data may not have a User relationship even though it's required for new records. + end + + def user_uuid=(uuid) + @model.user = User.with_uuid(uuid).first + end + + # @!attribute [r] uuid + # @return [String] the UUID of the transfer. + attribute :uuid, readonly: true + + ### + # Relationships + ### + + # @!attribute [rw] user + # Setting this relationship alongside the `user_uuid` attribute will override the attribute value. + # @return [UserResource] The user who requested the transfer. + # @note This relationship is required. + has_one :user + + ### + # Filters + ### + + # @!method transfer_type + # A filter to restrict the type of transfer to retrieve. + # One of the following types: + # - 'Transfer::BetweenPlateAndTubes' + # - 'Transfer::BetweenPlatesBySubmission' + # - 'Transfer::BetweenPlates' + # - 'Transfer::BetweenSpecificTubes' + # - 'Transfer::BetweenTubesBySubmission' + # - 'Transfer::FromPlateToSpecificTubesByPool' + # - 'Transfer::FromPlateToSpecificTubes' + # - 'Transfer::FromPlateToTubeByMultiplex' + # - 'Transfer::FromPlateToTubeBySubmission' + # - 'Transfer::FromPlateToTube' + filter :transfer_type, apply: ->(records, value, _options) { records.where(sti_type: value) } + + def self.create(context) + # Create the polymorphic type, not the base class. + new(context[:polymorphic_type].constantize.new, context) + end + end + end +end diff --git a/app/resources/api/v2/transfers/transfer_resource.rb b/app/resources/api/v2/transfers/transfer_resource.rb deleted file mode 100644 index 58d5bdd61e..0000000000 --- a/app/resources/api/v2/transfers/transfer_resource.rb +++ /dev/null @@ -1,265 +0,0 @@ -# frozen_string_literal: true - -module Api - module V2 - module Transfers - # @todo This documentation does not yet include a detailed description of what this resource represents. - # @todo This documentation does not yet include detailed descriptions for relationships, attributes and filters. - # @todo This documentation does not yet include any example usage of the API via cURL or similar. - # - # @note Access this resource via the `/api/v2/transfers/transfers/` endpoint. - # - # Provides a JSON:API representation of {Transfer}. - # - # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) - # or look at the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation - # of the JSON:API standard. - class TransferResource < BaseResource - # @!attribute [r] uuid - # @return [String] the UUID of the transfer. - attribute :uuid - - # @!attribute [rw] source_uuid - # @return [String] the UUID of the source labware. - # The type of the labware varies by the type of transfer. - attribute :source_uuid - - def source_uuid - @model.source.uuid - end - - def source_uuid=(uuid) - @model.source = Labware.with_uuid(uuid).first - end - - # @!attribute [rw] destination_uuid - # @return [String, void] the UUID of the destination labware. - attribute :destination_uuid - - def destination_uuid - @model.destination&.uuid - end - - def destination_uuid=(uuid) - @model.destination = Labware.with_uuid(uuid).first if uuid - end - - # @!attribute [rw] user_uuid - # @return [String] the UUID of the user who requested the transfer. - attribute :user_uuid - - def user_uuid - @model.user&.uuid # Some old data may not have a User relationship even though it's required for new records. - end - - def user_uuid=(uuid) - @model.user = User.with_uuid(uuid).first - end - - # @!attribute [rw] transfers - # @return [Hash] a hash of the transfers made. - # This is usually populated by the TransferTemplate used during creation of the Transfer. - attribute :transfers - - def transfers - # Only some transfer types have the :transfers method. - # This gets implemented differently, depending on the type of transfer being performed. - return nil unless @model.respond_to?(:transfers) - - @model.transfers - end - - def transfers=(transfers) - # This setter is invoked by the TransferTemplate populating the attributes for transfers. - @model.transfers = - if transfers.is_a?(ActionController::Parameters) - transfers.to_unsafe_h # We must unwrap the parameters to a real Hash. - else - transfers - end - end - - # @!attribute [w] transfer_template_uuid - # @return [String] the UUID of a TransferTemplate to create a transfer from. - # This must be provided or the Transfer creation will raise an error. - attribute :transfer_template_uuid - - def fetchable_fields - # Do not fetch the transfer template. - # It is only submitted when creating a new transfer and not stored. - super - %i[transfer_template_uuid] - end - - def self.creatable_fields(context) - # Do not allow the UUID to be declared by the client. - super - %i[uuid] - end - - def self.create(context) - # Create the polymorphic type, not the base class. - new(context[:polymorphic_type].constantize.new, context) - end - end - - # @todo This documentation does not yet include a detailed description of what this resource represents. - # @todo This documentation does not yet include any example usage of the API via cURL or similar. - # - # @note This resource is immutable: its endpoint will not accept `POST`, `PATCH`, or `DELETE` requests. - # @note Access this resource via the `/api/v2/transfers/between_plate_and_tubes/` endpoint. - # - # Provides a JSON:API representation of {Transfer::BetweenPlateAndTubes}. - # - # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) - # or look at the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation - # of the JSON:API standard. - class BetweenPlateAndTubeResource < TransferResource - immutable - filter :sti_type, default: 'Transfer::BetweenPlateAndTubes' - end - - # @todo This documentation does not yet include a detailed description of what this resource represents. - # @todo This documentation does not yet include any example usage of the API via cURL or similar. - # - # @note This resource is immutable: its endpoint will not accept `POST`, `PATCH`, or `DELETE` requests. - # @note Access this resource via the `/api/v2/transfers/between_plates_by_submissions/` endpoint. - # - # Provides a JSON:API representation of {Transfer::BetweenPlatesBySubmission}. - # - # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) - # or look at the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation - # of the JSON:API standard. - class BetweenPlatesBySubmissionResource < TransferResource - immutable - filter :sti_type, default: 'Transfer::BetweenPlatesBySubmission' - end - - # @todo This documentation does not yet include a detailed description of what this resource represents. - # @todo This documentation does not yet include any example usage of the API via cURL or similar. - # - # @note This resource is immutable: its endpoint will not accept `POST`, `PATCH`, or `DELETE` requests. - # @note Access this resource via the `/api/v2/transfers/between_plates/` endpoint. - # - # Provides a JSON:API representation of {Transfer::BetweenPlates}. - # - # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) - # or look at the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation - # of the JSON:API standard. - class BetweenPlateResource < TransferResource - immutable - filter :sti_type, default: 'Transfer::BetweenPlates' - end - - # @todo This documentation does not yet include a detailed description of what this resource represents. - # @todo This documentation does not yet include any example usage of the API via cURL or similar. - # - # @note This resource is immutable: its endpoint will not accept `POST`, `PATCH`, or `DELETE` requests. - # @note Access this resource via the `/api/v2/transfers/between_specific_tubes/` endpoint. - # - # Provides a JSON:API representation of {Transfer::BetweenSpecificTubes}. - # - # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) - # or look at the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation - # of the JSON:API standard. - class BetweenSpecificTubeResource < TransferResource - immutable - filter :sti_type, default: 'Transfer::BetweenSpecificTubes' - end - - # @todo This documentation does not yet include a detailed description of what this resource represents. - # @todo This documentation does not yet include any example usage of the API via cURL or similar. - # - # @note This resource is immutable: its endpoint will not accept `POST`, `PATCH`, or `DELETE` requests. - # @note Access this resource via the `/api/v2/transfers/between_tubes_by_submissions/` endpoint. - # - # Provides a JSON:API representation of {Transfer::BetweenTubesBySubmission}. - # - # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) - # or look at the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation - # of the JSON:API standard. - class BetweenTubesBySubmissionResource < TransferResource - immutable - filter :sti_type, default: 'Transfer::BetweenTubesBySubmission' - end - - # @todo This documentation does not yet include a detailed description of what this resource represents. - # @todo This documentation does not yet include any example usage of the API via cURL or similar. - # - # @note This resource is immutable: its endpoint will not accept `POST`, `PATCH`, or `DELETE` requests. - # @note Access this resource via the `/api/v2/transfers/from_plate_to_specific_tubes_by_pools/` endpoint. - # - # Provides a JSON:API representation of {Transfer::FromPlateToSpecificTubesByPool}. - # - # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) - # or look at the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation - # of the JSON:API standard. - class FromPlateToSpecificTubesByPoolResource < TransferResource - immutable - filter :sti_type, default: 'Transfer::FromPlateToSpecificTubesByPool' - end - - # @todo This documentation does not yet include a detailed description of what this resource represents. - # @todo This documentation does not yet include any example usage of the API via cURL or similar. - # - # @note This resource is immutable: its endpoint will not accept `POST`, `PATCH`, or `DELETE` requests. - # @note Access this resource via the `/api/v2/transfers/from_plate_to_specific_tubes/` endpoint. - # - # Provides a JSON:API representation of {Transfer::FromPlateToSpecificTubes}. - # - # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) - # or look at the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation - # of the JSON:API standard. - class FromPlateToSpecificTubeResource < TransferResource - immutable - filter :sti_type, default: 'Transfer::FromPlateToSpecificTubes' - end - - # @todo This documentation does not yet include a detailed description of what this resource represents. - # @todo This documentation does not yet include any example usage of the API via cURL or similar. - # - # @note This resource is immutable: its endpoint will not accept `POST`, `PATCH`, or `DELETE` requests. - # @note Access this resource via the `/api/v2/transfers/from_plate_to_tube_by_multiplexes/` endpoint. - # - # Provides a JSON:API representation of {Transfer::FromPlateToTubeByMultiplex}. - # - # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) - # or look at the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation - # of the JSON:API standard. - class FromPlateToTubeByMultiplexResource < TransferResource - immutable - filter :sti_type, default: 'Transfer::FromPlateToTubeByMultiplex' - end - - # @todo This documentation does not yet include a detailed description of what this resource represents. - # @todo This documentation does not yet include any example usage of the API via cURL or similar. - # - # @note This resource is immutable: its endpoint will not accept `POST`, `PATCH`, or `DELETE` requests. - # @note Access this resource via the `/api/v2/transfers/from_plate_to_tube_by_submissions/` endpoint. - # - # Provides a JSON:API representation of {Transfer::FromPlateToTubeBySubmission}. - # - # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) - # or look at the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation - # of the JSON:API standard. - class FromPlateToTubeBySubmissionResource < TransferResource - immutable - filter :sti_type, default: 'Transfer::FromPlateToTubeBySubmission' - end - - # @todo This documentation does not yet include a detailed description of what this resource represents. - # @todo This documentation does not yet include any example usage of the API via cURL or similar. - # - # @note This resource is immutable: its endpoint will not accept `POST`, `PATCH`, or `DELETE` requests. - # @note Access this resource via the `/api/v2/transfers/from_plate_to_tubes/` endpoint. - # - # Provides a JSON:API representation of {Transfer::FromPlateToTube}. - # - # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) - # or look at the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation - # of the JSON:API standard. - class FromPlateToTubeResource < TransferResource - immutable - filter :sti_type, default: 'Transfer::FromPlateToTube' - end - end - end -end diff --git a/app/resources/api/v2/tube_from_tube_creation_resource.rb b/app/resources/api/v2/tube_from_tube_creation_resource.rb index cd1cff5564..cee651efe2 100644 --- a/app/resources/api/v2/tube_from_tube_creation_resource.rb +++ b/app/resources/api/v2/tube_from_tube_creation_resource.rb @@ -64,7 +64,7 @@ class TubeFromTubeCreationResource < BaseResource # @param value [String] The UUID of a child purpose to use in the creation of the child tube. # @return [Void] # @see #child_purpose - attribute :child_purpose_uuid + attribute :child_purpose_uuid, writeonly: true def child_purpose_uuid=(value) @model.child_purpose = Purpose.with_uuid(value).first @@ -77,7 +77,7 @@ def child_purpose_uuid=(value) # @param value [String] The UUID of tube that will be the parent for the created tube. # @return [Void] # @see #parent - attribute :parent_uuid + attribute :parent_uuid, writeonly: true def parent_uuid=(value) @model.parent = Labware.with_uuid(value).first @@ -90,7 +90,7 @@ def parent_uuid=(value) # @param value [String] The UUID of the user who initiated the creation of this tube from a parent tube. # @return [Void] # @see #user - attribute :user_uuid + attribute :user_uuid, writeonly: true def user_uuid=(value) @model.user = User.with_uuid(value).first @@ -106,7 +106,7 @@ def user_uuid=(value) # @!attribute [r] child # @return [TubeResource] The child tube which was created. - has_one :child, class_name: 'Tube' + has_one :child, class_name: 'Tube', readonly: true # @!attribute [rw] child_purpose # Setting this relationship alongside the `child_purpose_uuid` attribute will override the attribute value. @@ -125,17 +125,6 @@ def user_uuid=(value) # @return [UserResource] The user who initiated the creation of the pooled plate. # @note This relationship is required. has_one :user - - def self.creatable_fields(context) - # The child relationship can only be read after the creation has happened. - # UUID is set by the system. - super - %i[child uuid] - end - - def fetchable_fields - # UUIDs for relationships are not fetchable. They should be accessed via the relationship itself. - super - %i[child_purpose_uuid parent_uuid user_uuid] - end end end end diff --git a/app/resources/api/v2/tube_purpose_resource.rb b/app/resources/api/v2/tube_purpose_resource.rb index 44f34e4345..909494bedc 100644 --- a/app/resources/api/v2/tube_purpose_resource.rb +++ b/app/resources/api/v2/tube_purpose_resource.rb @@ -36,22 +36,6 @@ class TubePurposeResource < BaseResource # @return [String] the UUID of the tube purpose. attribute :uuid, readonly: true - # 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 - filter :type, default: 'Tube::Purpose' end end diff --git a/app/resources/api/v2/tube_rack_resource.rb b/app/resources/api/v2/tube_rack_resource.rb index 72004ae850..f3811d108f 100644 --- a/app/resources/api/v2/tube_rack_resource.rb +++ b/app/resources/api/v2/tube_rack_resource.rb @@ -35,11 +35,11 @@ class TubeRackResource < BaseResource attribute :uuid, readonly: true attribute :created_at, readonly: true attribute :updated_at, readonly: true - attribute :labware_barcode, readonly: true + attribute :labware_barcode, write_once: true attribute :size - attribute :number_of_rows, readonly: true - attribute :number_of_columns, readonly: true - attribute :name, readonly: true + attribute :number_of_rows, write_once: true + attribute :number_of_columns, write_once: true + attribute :name, write_once: true attribute :tube_locations # Filters diff --git a/app/resources/api/v2/well_resource.rb b/app/resources/api/v2/well_resource.rb index d3f69d1e3b..5eb6904441 100644 --- a/app/resources/api/v2/well_resource.rb +++ b/app/resources/api/v2/well_resource.rb @@ -23,7 +23,7 @@ class WellResource < BaseResource # Associations: # Attributes - attribute :position, readonly: true + attribute :position, write_once: true # Custom methods diff --git a/app/resources/api/v2/work_order_resource.rb b/app/resources/api/v2/work_order_resource.rb index 3bda24a115..00f4df752f 100644 --- a/app/resources/api/v2/work_order_resource.rb +++ b/app/resources/api/v2/work_order_resource.rb @@ -21,13 +21,13 @@ class WorkOrderResource < BaseResource default_includes [{ example_request: :request_metadata }, :work_order_type] - has_one :study, readonly: true - has_one :project, readonly: true - has_one :source_receptacle, readonly: true, polymorphic: true - has_many :samples, readonly: true + has_one :study, write_once: true + has_one :project, write_once: true + has_one :source_receptacle, write_once: true, polymorphic: true + has_many :samples, write_once: true - attribute :order_type, readonly: true - attribute :quantity, readonly: true + attribute :order_type, write_once: true + attribute :quantity, write_once: true attribute :state attribute :options attribute :at_risk diff --git a/config/routes.rb b/config/routes.rb index 0641359bc9..2332c28b8c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -35,7 +35,7 @@ jsonapi_resources :lots jsonapi_resources :orders jsonapi_resources :pick_lists - jsonapi_resources :plate_purposes + jsonapi_resources :plate_purposes, except: %i[update] jsonapi_resources :plate_templates jsonapi_resources :plates jsonapi_resources :poly_metadata @@ -68,6 +68,7 @@ jsonapi_resources :transfer_request_collections, except: %i[update] jsonapi_resources :transfer_requests jsonapi_resources :transfer_templates + jsonapi_resources :transfers, except: %i[update] jsonapi_resources :tube_from_tube_creations, except: %i[update] jsonapi_resources :tube_purposes jsonapi_resources :tube_rack_statuses @@ -78,21 +79,6 @@ jsonapi_resources :wells jsonapi_resources :work_orders - namespace :transfers do - jsonapi_resources :transfers, except: %i[update] - - jsonapi_resources :between_plate_and_tubes - jsonapi_resources :between_plates_by_submissions - jsonapi_resources :between_plates - jsonapi_resources :between_specific_tubes - jsonapi_resources :between_tubes_by_submissions - jsonapi_resources :from_plate_to_specific_tubes_by_pools - jsonapi_resources :from_plate_to_specific_tubes - jsonapi_resources :from_plate_to_tube_by_multiplexes - jsonapi_resources :from_plate_to_tube_by_submissions - jsonapi_resources :from_plate_to_tubes - end - namespace :heron do resources :tube_rack_statuses, only: [:create] resources :tube_racks, only: [:create] diff --git a/spec/requests/api/v2/plate_purposes_spec.rb b/spec/requests/api/v2/plate_purposes_spec.rb index 2522656370..85530b73e0 100644 --- a/spec/requests/api/v2/plate_purposes_spec.rb +++ b/spec/requests/api/v2/plate_purposes_spec.rb @@ -44,9 +44,9 @@ end it 'does not allow update of a PlatePurpose' do - api_patch "#{base_endpoint}/#{resource_model.id}", payload - expect(response).to have_http_status(:bad_request) - expect(json.dig('errors', 0, 'detail')).to eq('size is not allowed.') + expect { api_patch "#{base_endpoint}/#{resource_model.id}", payload }.to raise_error( + ActionController::RoutingError + ) end end diff --git a/spec/requests/api/v2/transfers/transfers_spec.rb b/spec/requests/api/v2/transfers/transfers_spec.rb deleted file mode 100644 index 49484e4893..0000000000 --- a/spec/requests/api/v2/transfers/transfers_spec.rb +++ /dev/null @@ -1,221 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require './spec/requests/api/v2/shared_examples/api_key_authenticatable' -require './spec/requests/api/v2/shared_examples/post_requests' - -describe 'Transfer API', with: :api_v2 do - let(:base_endpoint) { '/api/v2/transfers/transfers' } - let(:model_class) { Transfer::BetweenPlates } - - it_behaves_like 'ApiKeyAuthenticatable' - - context 'with a list of Transfers' do - before { create_list(:transfer_between_plates, 5) } - - describe '#get all Transfers' do - before { api_get base_endpoint } - - it 'responds with a success http code' do - expect(response).to have_http_status(:success) - end - - it 'returns the full list of Transfers' do - expect(json['data'].length).to eq(5) - end - end - - describe '#get Transfer by ID' do - context 'with all relationships' do - let(:transfer) { create(:transfer_between_plates) } - - before { api_get "#{base_endpoint}/#{transfer.id}" } - - it 'responds with a success http code' do - expect(response).to have_http_status(:success) - end - - it 'returns the Transfer' do - expect(json.dig('data', 'id')).to eq(transfer.id.to_s) - expect(json.dig('data', 'type')).to eq('between_plates') - expect(json.dig('data', 'attributes', 'uuid')).to eq(transfer.uuid) - expect(json.dig('data', 'attributes', 'user_uuid')).to eq(transfer.user.uuid) - expect(json.dig('data', 'attributes', 'source_uuid')).to eq(transfer.source.uuid) - expect(json.dig('data', 'attributes', 'destination_uuid')).to eq(transfer.destination.uuid) - expect(json.dig('data', 'attributes', 'transfers')).to eq(transfer.transfers) - - # We don't want to see the TransferTemplate UUID as it's not fetchable. - expect(json.dig('data', 'attributes', 'transfer_template_uuid')).not_to be_present - end - end - - # Some old data may not have a User relationship even though it's required for new records. - context 'without a User relationship' do - let(:transfer) { create(:transfer_between_plates) } - - before do - # We need to remove the user relationship without invoking validations. - # The validations prevent new records from being created without a User. - transfer.user = nil - transfer.save(validate: false) - - api_get "#{base_endpoint}/#{transfer.id}" - end - - it 'responds with a success http code' do - expect(response).to have_http_status(:success) - end - - it 'returns the Transfer' do - expect(json.dig('data', 'id')).to eq(transfer.id.to_s) - expect(json.dig('data', 'type')).to eq('between_plates') - expect(json.dig('data', 'attributes', 'uuid')).to eq(transfer.uuid) - expect(json.dig('data', 'attributes', 'user_uuid')).not_to be_present - expect(json.dig('data', 'attributes', 'source_uuid')).to eq(transfer.source.uuid) - expect(json.dig('data', 'attributes', 'destination_uuid')).to eq(transfer.destination.uuid) - expect(json.dig('data', 'attributes', 'transfers')).to eq(transfer.transfers) - - # We don't want to see the TransferTemplate UUID as it's not fetchable. - expect(json.dig('data', 'attributes', 'transfer_template_uuid')).not_to be_present - end - end - end - end - - describe '#patch a Transfer' do - let(:resource_model) { create(:transfer_between_plates) } - let(:payload) do - { - 'data' => { - 'id' => resource_model.id, - 'type' => 'transfers', - 'attributes' => { - 'user_uuid' => '111111-2222-3333-4444-555555666666' - } - } - } - 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' do - let(:user) { create(:user) } - let(:source) { create(:transfer_plate) } - let(:destination) { create(:plate_with_empty_wells) } - let(:transfer_template) { create(:transfer_template) } # BetweenPlates - let(:base_attributes) do - { - user_uuid: user.uuid, - source_uuid: source.uuid, - destination_uuid: destination.uuid, - transfer_template_uuid: transfer_template.uuid - } - end - - context 'with a valid payload' do - let(:payload) { { 'data' => { 'type' => 'transfers', 'attributes' => base_attributes } } } - - it 'creates a new resource' do - expect { api_post base_endpoint, payload }.to change(model_class, :count).by(1) - end - - it 'responds with success' do - api_post base_endpoint, payload - - expect(response).to have_http_status(:success) - end - - it 'responds with the correct attributes' do - api_post base_endpoint, payload - - expect(json.dig('data', 'type')).to eq('transfers') - expect(json.dig('data', 'attributes', 'uuid')).to be_present - expect(json.dig('data', 'attributes', 'user_uuid')).to eq(user.uuid) - expect(json.dig('data', 'attributes', 'source_uuid')).to eq(source.uuid) - expect(json.dig('data', 'attributes', 'destination_uuid')).to eq(destination.uuid) - expect(json.dig('data', 'attributes', 'transfers')).to eq(transfer_template.transfers) - end - end - - context 'with a read-only attribute in the payload' do - context 'with uuid' do - let(:disallowed_value) { 'uuid' } - let(:payload) do - { - 'data' => { - 'type' => 'transfers', - 'attributes' => base_attributes.merge({ 'uuid' => '111111-2222-3333-4444-555555666666' }) - } - } - end - - it_behaves_like 'a POST request with a disallowed value' - end - end - - context 'without a required attribute' do - let(:payload) do - { 'data' => { 'type' => 'transfers', 'attributes' => base_attributes.merge({ attribute_to_remove => nil }) } } - end - - context 'without user_uuid' do - let(:attribute_to_remove) { 'user_uuid' } - let(:error_detail_message) { "user - can't be blank" } - - it_behaves_like 'an unprocessable POST request with a specific error' - end - - context 'without source_uuid' do - let(:attribute_to_remove) { 'source_uuid' } - let(:error_detail_message) { "source - can't be blank" } - - it_behaves_like 'an unprocessable POST request with a specific error' - end - - context 'without destination_uuid' do - let(:attribute_to_remove) { 'destination_uuid' } - let(:error_detail_message) { "destination - can't be blank" } - - it_behaves_like 'an unprocessable POST request with a specific error' - end - end - - context 'when providing an invalid payload' do - context 'without "transfer_template_uuid"' do - let(:payload) do - { - 'data' => { - 'type' => 'transfers', - 'attributes' => { - user_uuid: user.uuid, - source_uuid: source.uuid, - destination_uuid: destination.uuid - } - } - } - end - - it 'does not change the number of Transfers' do - expect { api_post base_endpoint, payload }.not_to change(model_class, :count) - end - - it 'responds with server error' do - api_post base_endpoint, payload - - expect(response).to have_http_status(:server_error) - end - - it 'gives an informative error message' do - api_post base_endpoint, payload - - expect(json.dig('errors', 0, 'detail')).to eq('Internal Server Error') - end - end - end - end -end diff --git a/spec/requests/api/v2/transfers_spec.rb b/spec/requests/api/v2/transfers_spec.rb new file mode 100644 index 0000000000..354bd557a4 --- /dev/null +++ b/spec/requests/api/v2/transfers_spec.rb @@ -0,0 +1,307 @@ +# frozen_string_literal: true + +require 'rails_helper' +require './spec/requests/api/v2/shared_examples/api_key_authenticatable' +require './spec/requests/api/v2/shared_examples/post_requests' + +describe 'Transfer API', with: :api_v2 do + let(:model_class) { Transfer::BetweenPlates } + let(:base_endpoint) { '/api/v2/transfers' } + let(:resource_type) { 'transfers' } + + it_behaves_like 'ApiKeyAuthenticatable' + + context 'with a list of resources' do + let(:filtered_type_count) { 5 } + let(:other_type_count) { 3 } + + before do + create_list(:transfer_between_plates, filtered_type_count) + create_list(:transfer_from_plate_to_tube, other_type_count) + end + + describe '#GET all resources' do + before { api_get base_endpoint } + + it 'responds with a success http code' do + expect(response).to have_http_status(:success) + end + + it 'returns all the resources' do + expect(json['data'].length).to eq(filtered_type_count + other_type_count) + end + end + + describe '#GET filtered resources' do + before { api_get base_endpoint + "?filter[transfer_type]=#{model_class}" } + + it 'responds with a success http code' do + expect(response).to have_http_status(:success) + end + + it 'returns all the resources' do + expect(json['data'].length).to eq(filtered_type_count) + end + end + end + + context 'with a single resource' do + describe '#GET resource by ID' do + let(:resource) { create(:transfer_between_plates) } + + context 'without included relationships' do + before { api_get "#{base_endpoint}/#{resource.id}" } + + it 'responds with a success http code' do + expect(response).to have_http_status(:success) + end + + it 'returns the resource with the correct id' do + expect(json.dig('data', 'id')).to eq(resource.id.to_s) + end + + it 'returns the resource with the correct type' do + expect(json.dig('data', 'type')).to eq(resource_type) + end + + it 'returns the correct attributes' do + expect(json.dig('data', 'attributes', 'destination_uuid')).to eq(resource.destination.uuid) + expect(json.dig('data', 'attributes', 'source_uuid')).to eq(resource.source.uuid) + expect(json.dig('data', 'attributes', 'transfer_type')).to eq(model_class.to_s) + expect(json.dig('data', 'attributes', 'transfers')).to eq(resource.transfers) + expect(json.dig('data', 'attributes', 'uuid')).to eq(resource.uuid) + end + + it 'excludes the unfetchable transfer_template_uuid' do + expect(json.dig('data', 'attributes', 'transfer_template_uuid')).not_to be_present + end + + it 'excludes the unfetchable user_uuid' do + expect(json.dig('data', 'attributes', 'user_uuid')).not_to be_present + end + + it 'returns a reference to the user relationship' do + expect(json.dig('data', 'relationships', 'user')).to be_present + end + + it 'does not include attributes for related resources' do + expect(json['included']).not_to be_present + end + end + + context 'with included relationships' do + context 'with user' do + let(:related_name) { 'user' } + + it_behaves_like 'a POST request including a has_one relationship' + end + end + end + end + + # Some old data may not have a User relationship even though it's required for new records. + # Note that the user relationship will still be shown in the response. We're only checking that the response + # is successful and contains expected attributes. + context 'with a single resource without a User relationship' do + describe '#GET resource by ID' do + let(:resource) { create(:transfer_between_plates) } + + before do + # We need to remove the user relationship without invoking validations. + # The validations prevent new records from being created without a User. + resource.user = nil + resource.save(validate: false) + + api_get "#{base_endpoint}/#{resource.id}" + end + + it 'responds with a success http code' do + expect(response).to have_http_status(:success) + end + + it 'returns the resource with the correct id' do + expect(json.dig('data', 'id')).to eq(resource.id.to_s) + end + + it 'returns the resource with the correct type' do + expect(json.dig('data', 'type')).to eq(resource_type) + end + + it 'returns the correct attributes' do + expect(json.dig('data', 'attributes', 'destination_uuid')).to eq(resource.destination.uuid) + expect(json.dig('data', 'attributes', 'source_uuid')).to eq(resource.source.uuid) + expect(json.dig('data', 'attributes', 'transfer_type')).to eq(model_class.to_s) + expect(json.dig('data', 'attributes', 'transfers')).to eq(resource.transfers) + expect(json.dig('data', 'attributes', 'uuid')).to eq(resource.uuid) + end + end + end + + describe '#PATCH a resource' do + let(:resource_model) { create(:transfer_between_plates) } + let(:purpose) { create(:tube_purpose) } + let(:payload) do + { data: { id: resource_model.id, type: resource_type, attributes: { child_purpose_uuid: [purpose.uuid] } } } + 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 create request' do + let(:destination) { create(:plate_with_empty_wells) } + let(:source) { create(:transfer_plate) } + let(:transfer_template) { create(:transfer_template) } # BetweenPlates + let(:user) { create(:user) } + + let(:user_relationship) { { data: { id: user.id, type: 'users' } } } + + context 'with a valid payload' do + shared_examples 'a valid request' do + # We can't perform the request in a `before` block because it can only be submitted once and some tests need to + # confirm expectations before the request is made. + def perform_request + api_post base_endpoint, payload + end + + it 'creates a new resource' do + expect { perform_request }.to change(model_class, :count).by(1) + end + + it 'responds with success' do + perform_request + expect(response).to have_http_status(:success) + end + + it 'responds with a resource of the correct type' do + perform_request + expect(json.dig('data', 'type')).to eq(resource_type) + end + + it 'responds with a uuid matching the new record' do + perform_request + new_record = model_class.last + expect(json.dig('data', 'attributes', 'destination_uuid')).to eq(new_record.destination.uuid) + expect(json.dig('data', 'attributes', 'source_uuid')).to eq(new_record.source.uuid) + expect(json.dig('data', 'attributes', 'transfer_type')).to eq(model_class.to_s) + expect(json.dig('data', 'attributes', 'transfers')).to eq(new_record.transfers) + expect(json.dig('data', 'attributes', 'uuid')).to eq(new_record.uuid) + end + + it 'excludes the unfetchable transfer_template_uuid' do + perform_request + expect(json.dig('data', 'attributes', 'transfer_template_uuid')).not_to be_present + end + + it 'excludes the unfetchable user_uuid' do + perform_request + expect(json.dig('data', 'attributes', 'user_uuid')).not_to be_present + end + + it 'returns a reference to the user relationship' do + perform_request + expect(json.dig('data', 'relationships', 'user')).to be_present + end + + it 'associates the user with the new record' do + perform_request + new_record = model_class.last + expect(new_record.user).to eq(user) + end + end + + context 'with complete attributes' do + let(:payload) do + { + data: { + type: resource_type, + attributes: { + destination_uuid: destination.uuid, + source_uuid: source.uuid, + transfer_template_uuid: transfer_template.uuid, + user_uuid: user.uuid + } + } + } + end + + it_behaves_like 'a valid request' + end + + context 'with user as a relationship' do + let(:payload) do + { + data: { + type: resource_type, + attributes: { + destination_uuid: destination.uuid, + source_uuid: source.uuid, + transfer_template_uuid: transfer_template.uuid + }, + relationships: { + user: user_relationship + } + } + } + end + + it_behaves_like 'a valid request' + end + + context 'with conflicting user definitions' do + let(:other_user) { create(:user) } + let(:payload) do + { + data: { + type: resource_type, + attributes: { + destination_uuid: destination.uuid, + source_uuid: source.uuid, + transfer_template_uuid: transfer_template.uuid, + user_uuid: other_user.uuid + }, + relationships: { + user: user_relationship + } + } + } + end + + # This test should pass because the relationships are preferred over the attributes. + it_behaves_like 'a valid request' + end + end + + context 'with a read-only attribute in the payload' do + context 'with uuid' do + let(:disallowed_value) { 'uuid' } + let(:payload) { { data: { type: resource_type, attributes: { uuid: '111111-2222-3333-4444-555555666666' } } } } + + it_behaves_like 'a POST request with a disallowed value' + end + end + + context 'without a required relationship' do + context 'without a user or user_uuid' do + let(:error_detail_message) { "user - can't be blank" } + let(:payload) do + { + data: { + type: resource_type, + attributes: { + destination_uuid: destination.uuid, + source_uuid: source.uuid, + transfer_template_uuid: transfer_template.uuid + } + } + } + end + + it_behaves_like 'an unprocessable POST request with a specific error' + end + end + end +end diff --git a/spec/resources/api/v2/barcode_printer_resource_spec.rb b/spec/resources/api/v2/barcode_printer_resource_spec.rb index 1026e8ffa9..4215dc6dae 100644 --- a/spec/resources/api/v2/barcode_printer_resource_spec.rb +++ b/spec/resources/api/v2/barcode_printer_resource_spec.rb @@ -4,14 +4,16 @@ require './app/resources/api/v2/barcode_printer_resource' RSpec.describe Api::V2::BarcodePrinterResource, type: :resource do - subject(:resource) { described_class.new(resource_model, {}) } + subject { described_class.new(resource_model, {}) } let(:resource_model) { build_stubbed(:barcode_printer) } - # Test attributes - it { is_expected.to have_readonly_attribute :uuid } - it { is_expected.to have_readonly_attribute :print_service } - it { is_expected.to have_readonly_attribute :barcode_type } + # Model Name + it { is_expected.to have_model_name 'BarcodePrinter' } - it { is_expected.to have_readwrite_attribute :name } + # Attributes + it { is_expected.to have_readonly_attribute :barcode_type } + it { is_expected.to have_readonly_attribute :name } + it { is_expected.to have_readonly_attribute :print_service } + it { is_expected.to have_readonly_attribute :uuid } end diff --git a/spec/resources/api/v2/custom_metadatum_collection_resource_spec.rb b/spec/resources/api/v2/custom_metadatum_collection_resource_spec.rb index 41c02ad640..05ec4dce84 100644 --- a/spec/resources/api/v2/custom_metadatum_collection_resource_spec.rb +++ b/spec/resources/api/v2/custom_metadatum_collection_resource_spec.rb @@ -4,14 +4,13 @@ require './app/resources/api/v2/custom_metadatum_collection_resource' RSpec.describe Api::V2::CustomMetadatumCollectionResource, type: :resource do - subject(:resource) { described_class.new(resource_model, {}) } + subject { described_class.new(resource_model, {}) } let(:resource_model) { build_stubbed(:custom_metadatum_collection) } - # Test attributes - it { is_expected.to have_readonly_attribute :uuid } - it { is_expected.to have_readonly_attribute :user_id } - it { is_expected.to have_readonly_attribute :asset_id } - + # Attributes + it { is_expected.to have_write_once_attribute :asset_id } it { is_expected.to have_readwrite_attribute :metadata } + it { is_expected.to have_readonly_attribute :uuid } + it { is_expected.to have_write_once_attribute :user_id } end diff --git a/spec/resources/api/v2/state_change_resource_spec.rb b/spec/resources/api/v2/state_change_resource_spec.rb index 91a4ce2af0..65e058c48b 100644 --- a/spec/resources/api/v2/state_change_resource_spec.rb +++ b/spec/resources/api/v2/state_change_resource_spec.rb @@ -4,21 +4,22 @@ require './app/resources/api/v2/state_change_resource' RSpec.describe Api::V2::StateChangeResource, type: :resource do - subject(:resource) { described_class.new(resource_model, {}) } + subject { described_class.new(resource_model, {}) } let(:resource_model) { build_stubbed(:state_change) } - # Attributes - it { is_expected.to have_readonly_attribute :uuid } - it { is_expected.to have_readonly_attribute :previous_state } + # Model Name + it { is_expected.to have_model_name 'StateChange' } + # Attributes it { is_expected.to have_readwrite_attribute :contents } + it { is_expected.to have_writeonly_attribute :customer_accepts_responsibility } + it { is_expected.to have_readonly_attribute :previous_state } it { is_expected.to have_readwrite_attribute :reason } it { is_expected.to have_readwrite_attribute :target_state } - - it { is_expected.to have_writeonly_attribute :user_uuid } it { is_expected.to have_writeonly_attribute :target_uuid } - it { is_expected.to have_writeonly_attribute :customer_accepts_responsibility } + it { is_expected.to have_writeonly_attribute :user_uuid } + it { is_expected.to have_readonly_attribute :uuid } # Relationships it { is_expected.to have_one(:target).with_class_name('Labware') } diff --git a/spec/resources/api/v2/tag_layout_resource_spec.rb b/spec/resources/api/v2/tag_layout_resource_spec.rb index 87a0cafc66..88a652179c 100644 --- a/spec/resources/api/v2/tag_layout_resource_spec.rb +++ b/spec/resources/api/v2/tag_layout_resource_spec.rb @@ -4,23 +4,24 @@ require './app/resources/api/v2/tag_layout_resource' RSpec.describe Api::V2::TagLayoutResource, type: :resource do - subject(:resource) { described_class.new(resource_model, {}) } + subject { described_class.new(resource_model, {}) } let(:resource_model) { build_stubbed(:tag_layout) } - # Attributes - it { is_expected.to have_readonly_attribute :uuid } + # Model Name + it { is_expected.to have_model_name 'TagLayout' } + # Attributes it { is_expected.to have_readwrite_attribute :direction } it { is_expected.to have_readwrite_attribute :initial_tag } - it { is_expected.to have_readwrite_attribute :substitutions } - it { is_expected.to have_readwrite_attribute :tags_per_well } - it { is_expected.to have_readwrite_attribute :walking_by } - it { is_expected.to have_writeonly_attribute :plate_uuid } + it { is_expected.to have_readwrite_attribute :substitutions } it { is_expected.to have_writeonly_attribute :tag_group_uuid } it { is_expected.to have_writeonly_attribute :tag2_group_uuid } + it { is_expected.to have_readwrite_attribute :tags_per_well } it { is_expected.to have_writeonly_attribute :user_uuid } + it { is_expected.to have_readonly_attribute :uuid } + it { is_expected.to have_readwrite_attribute :walking_by } # Relationships it { is_expected.to have_one(:plate).with_class_name('Plate') } diff --git a/spec/resources/api/v2/transfer_resource_spec.rb b/spec/resources/api/v2/transfer_resource_spec.rb new file mode 100644 index 0000000000..3f91a21502 --- /dev/null +++ b/spec/resources/api/v2/transfer_resource_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'rails_helper' +require './app/resources/api/v2/transfer_resource' + +RSpec.describe Api::V2::TransferResource, type: :resource do + subject { described_class.new(resource_model, {}) } + + let(:resource_model) { build_stubbed(:transfer_between_plates) } + + # Model Name + it { is_expected.to have_model_name 'Transfer' } + + # Attributes + it { is_expected.to have_readwrite_attribute :destination_uuid } + it { is_expected.to have_readwrite_attribute :source_uuid } + it { is_expected.to have_writeonly_attribute :transfer_template_uuid } + it { is_expected.to have_readonly_attribute :transfer_type } + it { is_expected.to have_readwrite_attribute :transfers } + it { is_expected.to have_writeonly_attribute :user_uuid } + it { is_expected.to have_readonly_attribute :uuid } + + # Relationships + it { is_expected.to have_one(:user).with_class_name('User') } + + # Filters + it { is_expected.to filter(:transfer_type) } +end diff --git a/spec/resources/api/v2/transfer_template_resource_spec.rb b/spec/resources/api/v2/transfer_template_resource_spec.rb index 107bce78eb..1877267a53 100644 --- a/spec/resources/api/v2/transfer_template_resource_spec.rb +++ b/spec/resources/api/v2/transfer_template_resource_spec.rb @@ -4,13 +4,16 @@ require './app/resources/api/v2/transfer_template_resource' RSpec.describe Api::V2::TransferTemplateResource, type: :resource do - subject(:resource) { described_class.new(resource_model, {}) } + subject { described_class.new(resource_model, {}) } let(:resource_model) { build_stubbed(:transfer_template) } + # Model Name + it { is_expected.to have_model_name 'TransferTemplate' } + # Attributes - it { is_expected.to have_readonly_attribute :uuid } it { is_expected.to have_readwrite_attribute :name } + it { is_expected.to have_readonly_attribute :uuid } # Filters it { is_expected.to filter(:uuid) } diff --git a/spec/resources/api/v2/transfers/transfer_resource_spec.rb b/spec/resources/api/v2/transfers/transfer_resource_spec.rb deleted file mode 100644 index e63cd10711..0000000000 --- a/spec/resources/api/v2/transfers/transfer_resource_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require './app/resources/api/v2/transfers/transfer_resource' - -RSpec.describe Api::V2::Transfers::TransferResource, type: :resource do - subject(:resource) { described_class.new(resource_model, {}) } - - let(:resource_model) { build_stubbed(:transfer_between_plates) } - - # Test attributes - it 'allows fetching the expected attributes', :aggregate_failures do - expect(resource).not_to have_attribute :id - expect(resource).to have_attribute :uuid - expect(resource).to have_attribute :source_uuid - expect(resource).to have_attribute :destination_uuid - expect(resource).to have_attribute :user_uuid - expect(resource).to have_attribute :transfers - expect(resource).not_to have_attribute :transfer_template_uuid - end - - # Updatable fields - it 'allows updating of read-write fields', :aggregate_failures do - expect(resource).to have_updatable_field :source_uuid - expect(resource).to have_updatable_field :destination_uuid - expect(resource).to have_updatable_field :user_uuid - expect(resource).to have_updatable_field :transfers - expect(resource).to have_updatable_field :transfer_template_uuid - end - - # Filters - # it { is_expected.to filter(:uuid) } - - # 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/support/api_v2_resource_matchers.rb b/spec/support/api_v2_resource_matchers.rb index cb081e288e..8699c60be5 100644 --- a/spec/support/api_v2_resource_matchers.rb +++ b/spec/support/api_v2_resource_matchers.rb @@ -9,6 +9,7 @@ module ApiV2Matchers match do |resource| expect(resource).to have_attribute attribute + expect(resource).not_to have_creatable_field attribute expect(resource).not_to have_updatable_field attribute end end @@ -21,6 +22,7 @@ module ApiV2Matchers match do |resource| expect(resource).to have_attribute attribute + expect(resource).to have_creatable_field attribute expect(resource).to have_updatable_field attribute end end @@ -33,7 +35,21 @@ module ApiV2Matchers match do |resource| expect(resource).not_to have_attribute attribute + expect(resource).to have_creatable_field attribute expect(resource).to have_updatable_field attribute end end + + RSpec::Matchers.define :have_write_once_attribute do |attribute| + description { "have write-once attribute `#{attribute}`" } + + failure_message { "expected #{resource.class.name.demodulize} to #{description}" } + failure_message_when_negated { "expected #{resource.class.name.demodulize} not to #{description}" } + + match do |resource| + expect(resource).to have_attribute attribute + expect(resource).to have_creatable_field attribute + expect(resource).not_to have_updatable_field attribute + end + end end