From 883016a3058f8566bb4c86665fa58fe1ad468f3a Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Wed, 6 Mar 2024 15:04:15 -0600 Subject: [PATCH 01/29] Aggregation migration, model, spec, factory --- app/models/aggregation.rb | 19 ++++------ ...240304201959_refactor_aggregation_model.rb | 26 ++++++++++++++ db/structure.sql | 36 +++++++++---------- spec/factories/aggregations.rb | 5 +-- spec/models/aggregation_spec.rb | 36 +++++-------------- 5 files changed, 61 insertions(+), 61 deletions(-) create mode 100644 db/migrate/20240304201959_refactor_aggregation_model.rb diff --git a/app/models/aggregation.rb b/app/models/aggregation.rb index d7471888f..526006e90 100644 --- a/app/models/aggregation.rb +++ b/app/models/aggregation.rb @@ -1,20 +1,13 @@ # frozen_string_literal: true class Aggregation < ApplicationRecord - belongs_to :workflow - belongs_to :subject - - validates_presence_of :workflow, :subject, :aggregation - validates_uniqueness_of :subject_id, scope: :workflow_id - validate :aggregation, :workflow_version_present + belongs_to :user + validates :workflow, presence: true + validates :user, presence: true + validates :user_id, uniqueness: { scope: :workflow_id } - private + self.ignored_columns = ["subject_id", "aggregation"] - def workflow_version_present - wv_key = :workflow_version - if aggregation && !aggregation.symbolize_keys.has_key?(wv_key) - errors.add(:aggregation, "must have #{wv_key} metadata") - end - end + enum status: [:pending, :completed, :failed] end diff --git a/db/migrate/20240304201959_refactor_aggregation_model.rb b/db/migrate/20240304201959_refactor_aggregation_model.rb new file mode 100644 index 000000000..5e0506696 --- /dev/null +++ b/db/migrate/20240304201959_refactor_aggregation_model.rb @@ -0,0 +1,26 @@ +class RefactorAggregationModel < ActiveRecord::Migration[6.1] + def up + # delete existing unused columns + safety_assured { remove_column :aggregations, :subject_id } + safety_assured { remove_column :aggregations, :aggregation } + + # and the new aggregations columns + add_column :aggregations, :user_id, :integer + add_foreign_key :aggregations, :users, column: :user_id, validate: false + + add_column :aggregations, :uuid, :string + add_column :aggregations, :task_id, :string + add_column :aggregations, :status, :integer + end + + def down + add_column :aggregations, :subject_id + add_column :aggregations, :aggregation + + remove_column :aggregations, :workflow_id + remove_column :aggregations, :user_id + remove_column :aggregations, :uuid + remove_column :aggregations, :task_id + remove_column :aggregations, :status + end +end diff --git a/db/structure.sql b/db/structure.sql index 0c3e5dc97..eecf1a925 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -53,6 +53,8 @@ COMMENT ON EXTENSION pg_trgm IS 'text similarity measurement and index searching SET default_tablespace = ''; +SET default_with_oids = false; + -- -- Name: access_control_lists; Type: TABLE; Schema: public; Owner: - -- @@ -94,10 +96,12 @@ ALTER SEQUENCE public.access_control_lists_id_seq OWNED BY public.access_control CREATE TABLE public.aggregations ( id integer NOT NULL, workflow_id integer, - subject_id integer, - aggregation jsonb DEFAULT '{}'::jsonb NOT NULL, created_at timestamp without time zone, - updated_at timestamp without time zone + updated_at timestamp without time zone, + user_id integer, + uuid character varying, + task_id character varying, + status integer ); @@ -2799,13 +2803,6 @@ CREATE INDEX index_access_control_lists_on_resource_id_and_resource_type ON publ CREATE INDEX index_access_control_lists_on_user_group_id ON public.access_control_lists USING btree (user_group_id); --- --- Name: index_aggregations_on_subject_id_and_workflow_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX index_aggregations_on_subject_id_and_workflow_id ON public.aggregations USING btree (subject_id, workflow_id); - - -- -- Name: index_aggregations_on_workflow_id; Type: INDEX; Schema: public; Owner: - -- @@ -3900,14 +3897,6 @@ ALTER TABLE ONLY public.subject_groups ADD CONSTRAINT fk_rails_283ede5252 FOREIGN KEY (project_id) REFERENCES public.projects(id); --- --- Name: aggregations fk_rails_28a7ada458; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.aggregations - ADD CONSTRAINT fk_rails_28a7ada458 FOREIGN KEY (subject_id) REFERENCES public.subjects(id) ON UPDATE CASCADE ON DELETE CASCADE; - - -- -- Name: project_contents fk_rails_305e6d8bf1; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4060,6 +4049,14 @@ ALTER TABLE ONLY public.collections_projects ADD CONSTRAINT fk_rails_895b025564 FOREIGN KEY (project_id) REFERENCES public.projects(id); +-- +-- Name: aggregations fk_rails_8eb620b6f6; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregations + ADD CONSTRAINT fk_rails_8eb620b6f6 FOREIGN KEY (user_id) REFERENCES public.users(id) NOT VALID; + + -- -- Name: set_member_subjects fk_rails_93073bf3b1; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4585,6 +4582,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20211201164326'), ('20221018032140'), ('20230613165746'), -('20231025200957'); +('20231025200957'), +('20240304201959'); diff --git a/spec/factories/aggregations.rb b/spec/factories/aggregations.rb index d1a0c1b53..7a9e1d8f7 100644 --- a/spec/factories/aggregations.rb +++ b/spec/factories/aggregations.rb @@ -1,7 +1,8 @@ FactoryBot.define do factory :aggregation do workflow - subject - aggregation { { data: "goes here", workflow_version: "1.1" } } + user + uuid { SecureRandom.uuid } + task_id { SecureRandom.uuid } end end diff --git a/spec/models/aggregation_spec.rb b/spec/models/aggregation_spec.rb index 608aa7137..173bbc1f2 100644 --- a/spec/models/aggregation_spec.rb +++ b/spec/models/aggregation_spec.rb @@ -7,52 +7,34 @@ expect(aggregation).to be_valid end - it 'should not be valid without an aggregation hash' do - expect(build(:aggregation, aggregation: nil)).to_not be_valid - end - it 'should not be valid without a workflow' do - expect(build(:aggregation, workflow: nil)).to_not be_valid - end - - it 'should not be valid without a subject' do - expect(build(:aggregation, subject: nil)).to_not be_valid + expect(build(:aggregation, workflow: nil)).not_to be_valid end - context "when missing the workflow_version aggregation metadata" do - let(:no_workflow_version) { build(:aggregation, aggregation: { test: 1}) } - - it 'should not be valid' do - expect(no_workflow_version).to_not be_valid - end - - it 'should not have the correct error message' do - no_workflow_version.valid? - error_message = "must have workflow_version metadata" - expect(no_workflow_version.errors[:aggregation]).to include(error_message) - end + it 'should not be valid without a user' do + expect(build(:aggregation, user: nil)).not_to be_valid end - context "when there is a duplicate subject_id workflow_id entry" do + context "when there is a duplicate user_id workflow_id entry" do before(:each) do aggregation.save end let(:duplicate) do build(:aggregation, workflow: aggregation.workflow, - subject: aggregation.subject) + user: aggregation.user) end it "should not be valid" do - expect(duplicate).to_not be_valid + expect(duplicate).not_to be_valid end - it "should have the correct error message on subject_id" do + it "should have the correct error message on user_id" do duplicate.valid? - expect(duplicate.errors[:subject_id]).to include("has already been taken") + expect(duplicate.errors[:user_id]).to include("has already been taken") end it "should raise a uniq index db error" do - expect{duplicate.save(validate: false)}.to raise_error(ActiveRecord::RecordNotUnique) + expect { duplicate.save! }.to raise_error(ActiveRecord::RecordInvalid) end end end From 5f2076b168746a6026d317886d3b7259d3e013e7 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Wed, 6 Mar 2024 16:09:07 -0600 Subject: [PATCH 02/29] Client & spec --- app/services/aggregation_client.rb | 59 ++++++++++++++++ spec/services/aggregation_client_spec.rb | 87 ++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 app/services/aggregation_client.rb create mode 100644 spec/services/aggregation_client_spec.rb diff --git a/app/services/aggregation_client.rb b/app/services/aggregation_client.rb new file mode 100644 index 000000000..957e4d1c7 --- /dev/null +++ b/app/services/aggregation_client.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +class AggregationClient + + class ConnectionError < StandardError; end + class ResourceNotFound < ConnectionError; end + class ServerError < ConnectionError; end + + attr_reader :connection + + def initialize(adapter=Faraday.default_adapter) + @connection = connect!(adapter) + @host ||= ENV.fetch('CELLECT_HOST', "http://test.example.com") + end + + def connect!(adapter) + Faraday.new(@host, ssl: {verify: false}) do |faraday| + faraday.request :json + faraday.response :json, content_type: /\bjson$/ + faraday.adapter(*adapter) + end + end + + def send_aggregation_request(project_id, workflow_id, user_id) + params = { project_id: project_id, workflow_id: workflow_id, user_id: user_id } + + request(:post, '/run_aggregation') do |req| + req.body = params.to_json + end + end + + private + + def request(http_method, params) + response = connection.send(http_method, *params) do |req| + req.headers['Accept'] = 'application/json' + req.headers['Content-Type'] = 'application/json' + req.options.timeout = 5 # open/read timeout in seconds + req.options.open_timeout = 2 # connection open timeout in seconds + yield req if block_given? + end + + handle_response(response) + rescue Faraday::TimeoutError, + Faraday::ConnectionFailed => e + raise ConnectionError, e.message + end + + def handle_response(response) + case response.status + when 404 + raise ResourceNotFound, status: response.status, body: response.body + when 400..600 + raise ServerError, response.body + else + response.body + end + end +end diff --git a/spec/services/aggregation_client_spec.rb b/spec/services/aggregation_client_spec.rb new file mode 100644 index 000000000..856620e54 --- /dev/null +++ b/spec/services/aggregation_client_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe AggregationClient do + describe "#send_aggregation_request" do + before do + allow(described_class).to receive(:host).and_return("http://test.example.com") + end + + let(:headers) { { 'Content-Type': 'application/json', 'Accept': 'application/json' } } + let(:params) { { project_id: 1, workflow_id: 10, user_id: 100 } } + let(:body) { { task_id: '1234-asdf-1234'} } + let(:path) { '/run_aggregation' } + + it 'was successful' do + stubs = Faraday::Adapter::Test::Stubs.new do |stub| + stub.post(path, params.to_json, headers) do + [200, { 'Content-Type': 'application/json' }, body.to_json] + end + end + + response = described_class.new([:test, stubs]).send_aggregation_request(1, 10, 100) + expect(response).to eq(body.with_indifferent_access) + end + + context 'when there is a problem' do + describe 'handles server errors' do + it 'raises if it cannot connect' do + stubs = Faraday::Adapter::Test::Stubs.new do |stub| + stub.post(path, params.to_json, headers) do + raise Faraday::ConnectionFailed, 'execution expired' + end + end + + expect do + described_class.new([:test, stubs]).send_aggregation_request(1, 10, 100) + end.to raise_error(AggregationClient::ConnectionError) + end + + it 'raises if it receives a 500' do + stubs = Faraday::Adapter::Test::Stubs.new do |stub| + stub.post(path, params.to_json, headers) do + [ + 500, + { 'Content-Type': 'application/json' }, + { 'errors' => { 'detail' => 'Server internal error' } }.to_json + ] + end + end + + expect do + described_class.new([:test, stubs]).send_aggregation_request(1, 10, 100) + end.to raise_error(AggregationClient::ServerError) + end + + it 'raises if it times out' do + stubs = Faraday::Adapter::Test::Stubs.new do |stub| + stub.post(path, params.to_json, headers) do + raise Faraday::TimeoutError + end + end + + expect do + described_class.new([:test, stubs]).send_aggregation_request(1, 10, 100) + end.to raise_error(AggregationClient::ConnectionError) + end + + it 'raises if response is an HTTP 404' do + stubs = Faraday::Adapter::Test::Stubs.new do |stub| + stub.post(path, params.to_json, headers) do + [ + 404, + { 'Content-Type' => 'application/json' }, + { 'errors' => 'Not Found' }.to_json + ] + end + end + + expect do + described_class.new([:test, stubs]).send_aggregation_request(1, 10, 100) + end.to raise_error(AggregationClient::ResourceNotFound) + end + end + end + end +end From 6683ba52bdba5dd51c0a46e7f4c54cd2b53a1c8d Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Tue, 26 Mar 2024 15:37:12 -0500 Subject: [PATCH 03/29] Policy and spec --- app/policies/aggregation_policy.rb | 24 +++------------- spec/policies/aggregation_policy_spec.rb | 36 +++++++++--------------- 2 files changed, 17 insertions(+), 43 deletions(-) diff --git a/app/policies/aggregation_policy.rb b/app/policies/aggregation_policy.rb index 4f9a5faf1..b9bea4b84 100644 --- a/app/policies/aggregation_policy.rb +++ b/app/policies/aggregation_policy.rb @@ -1,28 +1,12 @@ class AggregationPolicy < ApplicationPolicy - class ReadScope < Scope - # Allow access to public aggregations + class Scope < Scope def resolve(action) - updatable_parents = policy_for(Workflow).scope_for(:update) - updatable_scope = scope.joins(:workflow).merge(updatable_parents) - - public_aggregations = scope.joins(:workflow).where("workflows.aggregation ->> 'public' = 'true'") - Aggregation.union(updatable_scope, public_aggregations) + parent_scope = policy_for(Project).scope_for(:update) + scope.where(workflow_id: parent_scope.select(:workflow_id)) end end - class WriteScope < Scope - def resolve(action) - parent_scope = policy_for(Workflow).scope_for(action) - scope.where(workflow_id: parent_scope.select(:id)) - end - end - - scope :index, :show, with: ReadScope - scope :update, :destroy, :versions, :version, with: WriteScope - - def linkable_subjects - policy_for(Subject).scope_for(:show) - end + scope :index, :show, :update, :destroy, with: Scope def linkable_workflows policy_for(Workflow).scope_for(:update) diff --git a/spec/policies/aggregation_policy_spec.rb b/spec/policies/aggregation_policy_spec.rb index 8be7a2238..76cb9f89f 100644 --- a/spec/policies/aggregation_policy_spec.rb +++ b/spec/policies/aggregation_policy_spec.rb @@ -6,14 +6,12 @@ let(:logged_in_user) { create(:user) } let(:resource_owner) { create(:user) } - let(:project) { build(:project, owner: resource_owner) } + let(:project) { build(:project, owner: resource_owner) } - let(:public_aggregation) { build(:aggregation, workflow: build(:workflow, project: project, aggregation: {public: true})) } - let(:private_aggregation) { build(:aggregation, workflow: build(:workflow, project: project)) } + let(:aggregation) { build(:aggregation, workflow: build(:workflow, project: project)) } before do - public_aggregation.save! - private_aggregation.save! + aggregation.save! end describe 'index' do @@ -24,28 +22,24 @@ context 'for an anonymous user' do let(:api_user) { ApiUser.new(anonymous_user) } - it "includes aggregations from public projects" do - expect(resolved_scope).to match_array(public_aggregation) + it "returns nothing" do + expect(resolved_scope).to match_array([]) end end context 'for a normal user' do let(:api_user) { ApiUser.new(logged_in_user) } - it "includes aggregations from public projects" do - expect(resolved_scope).to match_array(public_aggregation) + it "returns nothing" do + expect(resolved_scope).to be_empty end end context 'for the resource owner' do let(:api_user) { ApiUser.new(resource_owner) } - it "includes aggregations from public projects" do - expect(resolved_scope).to include(public_aggregation) - end - - it 'includes aggregations from owned private projects' do - expect(resolved_scope).to include(private_aggregation) + it "includes aggregations" do + expect(resolved_scope).to include(aggregation) end end @@ -54,7 +48,7 @@ let(:api_user) { ApiUser.new(admin_user, admin: true) } it 'includes everything' do - expect(resolved_scope).to include(public_aggregation, private_aggregation) + expect(resolved_scope).to include(aggregation) end end end @@ -83,12 +77,8 @@ context 'for the resource owner' do let(:api_user) { ApiUser.new(resource_owner) } - it "includes aggregations from public projects" do - expect(resolved_scope).to include(public_aggregation) - end - - it 'includes aggregations from owned private projects' do - expect(resolved_scope).to include(private_aggregation) + it 'includes aggregations' do + expect(resolved_scope).to include(aggregation) end end @@ -97,7 +87,7 @@ let(:api_user) { ApiUser.new(admin_user, admin: true) } it 'includes everything' do - expect(resolved_scope).to include(public_aggregation, private_aggregation) + expect(resolved_scope).to include(aggregation) end end From 0adb3f11a05eaf731547d9a1511cf4fe01cdc1ae Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Tue, 26 Mar 2024 15:37:50 -0500 Subject: [PATCH 04/29] Schemas --- app/schemas/aggregation_create_schema.rb | 26 ++++++++++++++-------- app/schemas/aggregation_update_schema.rb | 28 +++++++++++++++--------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/app/schemas/aggregation_create_schema.rb b/app/schemas/aggregation_create_schema.rb index a649ae3a7..5313cf315 100644 --- a/app/schemas/aggregation_create_schema.rb +++ b/app/schemas/aggregation_create_schema.rb @@ -1,26 +1,34 @@ class AggregationCreateSchema < JsonSchema schema do type "object" - description "An Aggregation for a subject" + description "An Aggregation for a workflow" required "links" additional_properties false - property "aggregation" do - type "object" + property "uuid" do + type "string" + end + + property "task_id" do + type "string" + end + + property "user_id" do + type "string", "integer" + end + + property "status" do + type "string" end property "links" do type "object" + required "workflow" additional_properties false - required "subject", "workflow" - - property "subject" do - type "integer", "string" - end - property "workflow" do type "integer", "string" + pattern "^[0-9]*$" end end end diff --git a/app/schemas/aggregation_update_schema.rb b/app/schemas/aggregation_update_schema.rb index f3c41837f..e23824d9a 100644 --- a/app/schemas/aggregation_update_schema.rb +++ b/app/schemas/aggregation_update_schema.rb @@ -1,26 +1,34 @@ class AggregationUpdateSchema < JsonSchema schema do type "object" - description "An Aggregation for a subject" - required "aggregation", "links" + description "An Aggregation for a workflow" + required "links" additional_properties false - property "aggregation" do - type "object" + property "uuid" do + type "string" + end + + property "task_id" do + type "string" + end + + property "user_id" do + type "string", "integer" + end + + property "status" do + type "string" end property "links" do type "object" + required "workflow" additional_properties false - required "subject", "workflow" - - property "subject" do - type "integer", "string" - end - property "workflow" do type "integer", "string" + pattern "^[0-9]*$" end end end From 38e4efed0e92c43a556f7ef7b90290258e4f7bb3 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Wed, 27 Mar 2024 16:00:30 -0500 Subject: [PATCH 05/29] Update policy & schemas --- app/policies/aggregation_policy.rb | 10 +++++++--- app/schemas/aggregation_create_schema.rb | 9 +++++---- app/schemas/aggregation_update_schema.rb | 16 ---------------- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/app/policies/aggregation_policy.rb b/app/policies/aggregation_policy.rb index b9bea4b84..534b47463 100644 --- a/app/policies/aggregation_policy.rb +++ b/app/policies/aggregation_policy.rb @@ -1,14 +1,18 @@ class AggregationPolicy < ApplicationPolicy class Scope < Scope def resolve(action) - parent_scope = policy_for(Project).scope_for(:update) - scope.where(workflow_id: parent_scope.select(:workflow_id)) + parent_scope = policy_for(Workflow).scope_for(:update) + scope.where(workflow_id: parent_scope.select(:id)) end end - scope :index, :show, :update, :destroy, with: Scope + scope :index, :show, :create, :update, :destroy, with: Scope def linkable_workflows policy_for(Workflow).scope_for(:update) end + + def linkable_users + policy_for(User).scope_for(:update) + end end diff --git a/app/schemas/aggregation_create_schema.rb b/app/schemas/aggregation_create_schema.rb index 5313cf315..c8b2e29af 100644 --- a/app/schemas/aggregation_create_schema.rb +++ b/app/schemas/aggregation_create_schema.rb @@ -13,10 +13,6 @@ class AggregationCreateSchema < JsonSchema type "string" end - property "user_id" do - type "string", "integer" - end - property "status" do type "string" end @@ -30,6 +26,11 @@ class AggregationCreateSchema < JsonSchema type "integer", "string" pattern "^[0-9]*$" end + + property "user" do + type "integer", "string" + pattern "^[0-9]*$" + end end end end diff --git a/app/schemas/aggregation_update_schema.rb b/app/schemas/aggregation_update_schema.rb index e23824d9a..d4bd010dc 100644 --- a/app/schemas/aggregation_update_schema.rb +++ b/app/schemas/aggregation_update_schema.rb @@ -2,7 +2,6 @@ class AggregationUpdateSchema < JsonSchema schema do type "object" description "An Aggregation for a workflow" - required "links" additional_properties false property "uuid" do @@ -13,23 +12,8 @@ class AggregationUpdateSchema < JsonSchema type "string" end - property "user_id" do - type "string", "integer" - end - property "status" do type "string" end - - property "links" do - type "object" - required "workflow" - additional_properties false - - property "workflow" do - type "integer", "string" - pattern "^[0-9]*$" - end - end end end From d4067ecb44d12079d57de74a066cb424218df071 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Wed, 27 Mar 2024 16:00:57 -0500 Subject: [PATCH 06/29] Update status enum --- app/models/aggregation.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/aggregation.rb b/app/models/aggregation.rb index 526006e90..ba5941deb 100644 --- a/app/models/aggregation.rb +++ b/app/models/aggregation.rb @@ -9,5 +9,11 @@ class Aggregation < ApplicationRecord self.ignored_columns = ["subject_id", "aggregation"] - enum status: [:pending, :completed, :failed] + enum status: { + created: 0, + pending: 1, + completed: 2, + failed: 3 + } + end From 2d8f8e2e035d8e6b1c4f39ebd80c41c956cd7a3e Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Wed, 27 Mar 2024 16:01:19 -0500 Subject: [PATCH 07/29] Add default to status migration --- db/migrate/20240304201959_refactor_aggregation_model.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20240304201959_refactor_aggregation_model.rb b/db/migrate/20240304201959_refactor_aggregation_model.rb index 5e0506696..18f0b7d38 100644 --- a/db/migrate/20240304201959_refactor_aggregation_model.rb +++ b/db/migrate/20240304201959_refactor_aggregation_model.rb @@ -10,7 +10,7 @@ def up add_column :aggregations, :uuid, :string add_column :aggregations, :task_id, :string - add_column :aggregations, :status, :integer + add_column :aggregations, :status, :integer, default: 0 end def down From 7db71c955e8c1af3342bd95a86a0aec9b6737ed7 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Wed, 27 Mar 2024 16:01:31 -0500 Subject: [PATCH 08/29] Update serializer --- app/serializers/aggregation_serializer.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/serializers/aggregation_serializer.rb b/app/serializers/aggregation_serializer.rb index 498fc78c2..34e50d231 100644 --- a/app/serializers/aggregation_serializer.rb +++ b/app/serializers/aggregation_serializer.rb @@ -2,8 +2,8 @@ class AggregationSerializer include Serialization::PanoptesRestpack include CachedSerializer - attributes :id, :created_at, :updated_at, :aggregation, :href - can_include :workflow, :subject + attributes :id, :href, :created_at, :updated_at, :uuid, :task_id, :status + can_include :workflow, :user - can_filter_by :workflow, :subject + can_filter_by :workflow end From 3a88a0cf29cf66327e47a847e33862d09093198e Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Wed, 27 Mar 2024 16:01:46 -0500 Subject: [PATCH 09/29] Update factory --- spec/factories/aggregations.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/factories/aggregations.rb b/spec/factories/aggregations.rb index 7a9e1d8f7..16e165013 100644 --- a/spec/factories/aggregations.rb +++ b/spec/factories/aggregations.rb @@ -2,7 +2,6 @@ factory :aggregation do workflow user - uuid { SecureRandom.uuid } - task_id { SecureRandom.uuid } + status { Aggregation.statuses[:pending] } end end From 03ffb5142e2d517da683662cb4fbd2e8e820ca90 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Wed, 27 Mar 2024 16:02:02 -0500 Subject: [PATCH 10/29] Controller and spec --- .../api/v1/aggregations_controller.rb | 26 +-- .../api/v1/aggregations_controller_spec.rb | 166 +++++------------- 2 files changed, 57 insertions(+), 135 deletions(-) diff --git a/app/controllers/api/v1/aggregations_controller.rb b/app/controllers/api/v1/aggregations_controller.rb index b11427957..80f909434 100644 --- a/app/controllers/api/v1/aggregations_controller.rb +++ b/app/controllers/api/v1/aggregations_controller.rb @@ -1,19 +1,23 @@ +# frozen_string_literal: true + class Api::V1::AggregationsController < Api::ApiController include JsonApiController::PunditPolicy - require_authentication :create, :update, scopes: [:project] - resource_actions :create, :update, :show, :index + require_authentication :index, :show, :update, :create, scopes: [:workflow] + resource_actions :index, :show, :create, :update schema_type :json_schema - before_action :filter_by_subject_set, only: :index - - private - def filter_by_subject_set - subject_set_ids = params.delete(:subject_set_id).try(:split, ',') - unless subject_set_ids.blank? - @controlled_resources = controlled_resources - .joins(workflow: :subject_sets) - .where(workflows: { subject_set_id: subject_set_ids } ) + def create + workflow = Workflow.find(create_params['links']['workflow']) + response = AggregationClient.new.send_aggregation_request( + workflow.project.id, + workflow.id, + create_params['links']['user'] + ) + super do |agg| + agg.update({task_id: response.body[:task_id], status: 'pending'}) end + rescue AggregationClient::ConnectionError + json_api_render(:service_unavailable, response.body) end end diff --git a/spec/controllers/api/v1/aggregations_controller_spec.rb b/spec/controllers/api/v1/aggregations_controller_spec.rb index 354a049b5..7dab6a0a7 100644 --- a/spec/controllers/api/v1/aggregations_controller_spec.rb +++ b/spec/controllers/api/v1/aggregations_controller_spec.rb @@ -1,169 +1,87 @@ -require "spec_helper" +# frozen_string_literal: true + +require 'spec_helper' RSpec.describe Api::V1::AggregationsController, type: :controller do let(:api_resource_name) { 'aggregations' } - let(:api_resource_attributes) { %w(id created_at updated_at aggregation) } - let(:api_resource_links) { %w(aggregations.workflow aggregations.subject) } + let(:api_resource_attributes) { %w[id created_at updated_at uuid task_id status] } + let(:api_resource_links) { %w[aggregations.workflow] } - let(:scopes) { %w(public project) } + let(:scopes) { %w[public workflow] } let(:resource_class) { Aggregation } - describe "#index" do + describe '#index' do let(:workflow) { create(:workflow) } let!(:aggregations) { create_list(:aggregation, 2, workflow: workflow) } let!(:private_resource) { create(:aggregation) } let(:authorized_user) { workflow.project.owner } let(:n_visible) { 2 } - it_behaves_like "is indexable" - - context "non-logged in users" do - - context "when the workflow does not have public aggregation" do - let(:workflow) { create(:workflow) } - - it "should return an empty resource set" do - get :index, params: { workflow_id: workflow.id } - expect(json_response[api_resource_name].length).to eq(0) - end - end - - context "when the workflow has the public aggregation flag" do - let(:workflow) { create(:workflow, aggregation: { public: true }) } - let(:result_workflow_ids) do - json_response[api_resource_name].map { |r| r["links"]["workflow"] }.uniq - end - - it "should return all the aggregated data for the supplied workflow" do - get :index, params: { workflow_id: workflow.id } - expect(json_response[api_resource_name].length).to eq n_visible - end - - context "when not supplying a workflow id" do - - it "should return all the public workflow resources" do - get :index - expect(result_workflow_ids).to match_array( [ "#{workflow.id}" ]) - end - end - - context "when supplying just the non public workflow ids" do - - it "should return no results" do - get :index, params: { workflow_id: private_resource.workflow_id.to_s } - expect(json_response[api_resource_name].length).to eq(0) - end - end - - context "when supplying a mix of public and non public workflow ids" do - - it "should only return the resources for the public workflow" do - ids = [ workflow.id, private_resource.workflow_id ] - get :index, params: { workflow_ids: ids.join(',') } - expect(result_workflow_ids).to match_array( [ "#{workflow.id}" ]) - end - end - - context "filtering on subject_id" do - let(:subject_id) { "#{aggregations.last.subject_id}" } - - it "should return all the aggregated data for the supplied workflow" do - get :index, params: { workflow_id: workflow.id, subject_id: subject_id } - aggregate_failures "filtering" do - resources = json_response[api_resource_name] - expect(resources.length).to eq(1) - aggregation = resources.first - expect(aggregation["links"]["subject"]).to eq(subject_id) - end - end - end - end - end + it_behaves_like 'is indexable' end - describe "#show" do + describe '#show' do let(:resource) { create(:aggregation) } let(:authorized_user) { resource.workflow.project.owner } - it_behaves_like "is showable" - - context "non-logged in users" do - - context "when the workflow does not have public aggregation" do - let(:workflow) { create(:workflow) } - - it "should return not_found" do - get :show, params: { id: resource.id } - expect(response).to have_http_status(:not_found) - end - end - - context "when the workflow has the public aggregation flag" do - - it "should return the aggregated resource with a public workflow" do - resource.workflow.update_column(:aggregation, { public: true }) - get :show, params: { id: resource.id } - aggregate_failures "public show" do - expect(json_response[api_resource_name].length).to eq(1) - expect(created_instance(api_resource_name)["id"]).to eq("#{resource.id}") - end - end - end - end + it_behaves_like 'is showable' end - describe "create" do - let(:workflow) { create(:workflow_with_subjects) } - let(:subject) { workflow.subject_sets.first.subjects.first } + describe 'create' do + let(:workflow) { create(:workflow) } let(:authorized_user) { workflow.project.owner } - let(:test_attr) { :aggregation } - let(:test_attr_value) { { "something" => "HERE", - "workflow_version" => "1.1" } } + let(:test_attr) { :workflow_id } + let(:test_attr_value) { workflow.id } + let(:fake_response) {double } + let(:mock_agg) { instance_double(AggregationClient) } let(:create_params) do { aggregations: { - aggregation: test_attr_value, links: { - subject: subject.id.to_s, + user: authorized_user.id.to_s, workflow: workflow.id.to_s } } } end - it_behaves_like "is creatable" + before do + allow(AggregationClient).to receive(:new).and_return(mock_agg) + allow(fake_response).to receive(:body).and_return({'task_id': 'asdf-1234-asdf'}) + allow(mock_agg).to receive(:send_aggregation_request).and_return(fake_response) + default_request scopes: scopes, user_id: authorized_user.id + end + + it_behaves_like 'is creatable' + + it 'makes a request to the aggregation service' do + post :create, params: create_params + expect(mock_agg).to have_received(:send_aggregation_request) + end + + it 'stores the task_id from the client response' do + post :create, params: create_params + expect(Aggregation.first.task_id).to eq('asdf-1234-asdf') + end end describe '#update' do - let(:workflow) { create(:workflow_with_subjects) } - let(:subject) { workflow.subject_sets.first.subjects.first } + let(:workflow) { create(:workflow) } let(:authorized_user) { resource.workflow.project.owner } let(:resource) { create(:aggregation, workflow: workflow) } - let(:aggregation_results) do - { "mean" => "1", - "std" => "1", - "count" => ["1","1","1"], - "workflow_version" => "1.1" } - end - let(:test_attr) { :aggregation } - let(:test_attr_value) { aggregation_results } - let(:test_relation) { :subject } - let(:test_relation_ids) { [ subject.id ] } + let(:test_attr) { :uuid } + let(:test_attr_value) { '1234-asdf-1234' } + let(:update_params) do { aggregations: { - aggregation: aggregation_results, - links: { - subject: subject.id.to_s, - workflow: workflow.id.to_s - } + uuid: '1234-asdf-1234', + status: 'completed' } } end - it_behaves_like "is updatable" - - it_behaves_like "has updatable links" + it_behaves_like 'is updatable' end end From b3b72925c5323c19ce3aa8d95a641ba19191bb89 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Wed, 27 Mar 2024 16:16:10 -0500 Subject: [PATCH 11/29] Fix migration --- db/migrate/20240304201959_refactor_aggregation_model.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/db/migrate/20240304201959_refactor_aggregation_model.rb b/db/migrate/20240304201959_refactor_aggregation_model.rb index 18f0b7d38..daeb3c9ec 100644 --- a/db/migrate/20240304201959_refactor_aggregation_model.rb +++ b/db/migrate/20240304201959_refactor_aggregation_model.rb @@ -14,10 +14,9 @@ def up end def down - add_column :aggregations, :subject_id - add_column :aggregations, :aggregation + add_column :aggregations, :subject_id, :integer + add_column :aggregations, :aggregation, :jsonb - remove_column :aggregations, :workflow_id remove_column :aggregations, :user_id remove_column :aggregations, :uuid remove_column :aggregations, :task_id From 9b8d42366bb7a4b626cada5c86ba516fbf696c55 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Wed, 27 Mar 2024 16:20:35 -0500 Subject: [PATCH 12/29] Hound --- .../api/v1/aggregations_controller.rb | 2 +- app/schemas/aggregation_create_schema.rb | 36 +++++++++---------- app/schemas/aggregation_update_schema.rb | 16 ++++----- .../api/v1/aggregations_controller_spec.rb | 4 +-- spec/policies/aggregation_policy_spec.rb | 10 +++--- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/app/controllers/api/v1/aggregations_controller.rb b/app/controllers/api/v1/aggregations_controller.rb index 80f909434..32066d560 100644 --- a/app/controllers/api/v1/aggregations_controller.rb +++ b/app/controllers/api/v1/aggregations_controller.rb @@ -15,7 +15,7 @@ def create create_params['links']['user'] ) super do |agg| - agg.update({task_id: response.body[:task_id], status: 'pending'}) + agg.update({ task_id: response.body[:task_id], status: 'pending' }) end rescue AggregationClient::ConnectionError json_api_render(:service_unavailable, response.body) diff --git a/app/schemas/aggregation_create_schema.rb b/app/schemas/aggregation_create_schema.rb index c8b2e29af..fee920d54 100644 --- a/app/schemas/aggregation_create_schema.rb +++ b/app/schemas/aggregation_create_schema.rb @@ -1,35 +1,35 @@ class AggregationCreateSchema < JsonSchema schema do - type "object" - description "An Aggregation for a workflow" - required "links" + type 'object' + description 'An Aggregation for a workflow' + required 'links' additional_properties false - property "uuid" do - type "string" + property 'uuid' do + type 'string' end - property "task_id" do - type "string" + property 'task_id' do + type 'string' end - property "status" do - type "string" + property 'status' do + type 'string' end - property "links" do - type "object" - required "workflow" + property 'links' do + type 'object' + required 'workflow' additional_properties false - property "workflow" do - type "integer", "string" - pattern "^[0-9]*$" + property 'workflow' do + type 'integer', 'string' + pattern '^[0-9]*$' end - property "user" do - type "integer", "string" - pattern "^[0-9]*$" + property 'user' do + type 'integer', 'string' + pattern '^[0-9]*$' end end end diff --git a/app/schemas/aggregation_update_schema.rb b/app/schemas/aggregation_update_schema.rb index d4bd010dc..2830821ac 100644 --- a/app/schemas/aggregation_update_schema.rb +++ b/app/schemas/aggregation_update_schema.rb @@ -1,19 +1,19 @@ class AggregationUpdateSchema < JsonSchema schema do - type "object" - description "An Aggregation for a workflow" + type 'object' + description 'An Aggregation for a workflow' additional_properties false - property "uuid" do - type "string" + property 'uuid' do + type 'string' end - property "task_id" do - type "string" + property 'task_id' do + type 'string' end - property "status" do - type "string" + property 'status' do + type 'string' end end end diff --git a/spec/controllers/api/v1/aggregations_controller_spec.rb b/spec/controllers/api/v1/aggregations_controller_spec.rb index 7dab6a0a7..ea58a50c5 100644 --- a/spec/controllers/api/v1/aggregations_controller_spec.rb +++ b/spec/controllers/api/v1/aggregations_controller_spec.rb @@ -32,7 +32,7 @@ let(:authorized_user) { workflow.project.owner } let(:test_attr) { :workflow_id } let(:test_attr_value) { workflow.id } - let(:fake_response) {double } + let(:fake_response) { double } let(:mock_agg) { instance_double(AggregationClient) } let(:create_params) do @@ -48,7 +48,7 @@ before do allow(AggregationClient).to receive(:new).and_return(mock_agg) - allow(fake_response).to receive(:body).and_return({'task_id': 'asdf-1234-asdf'}) + allow(fake_response).to receive(:body).and_return({ 'task_id': 'asdf-1234-asdf' }) allow(mock_agg).to receive(:send_aggregation_request).and_return(fake_response) default_request scopes: scopes, user_id: authorized_user.id end diff --git a/spec/policies/aggregation_policy_spec.rb b/spec/policies/aggregation_policy_spec.rb index 76cb9f89f..42ca1e0aa 100644 --- a/spec/policies/aggregation_policy_spec.rb +++ b/spec/policies/aggregation_policy_spec.rb @@ -22,7 +22,7 @@ context 'for an anonymous user' do let(:api_user) { ApiUser.new(anonymous_user) } - it "returns nothing" do + it 'returns nothing' do expect(resolved_scope).to match_array([]) end end @@ -30,7 +30,7 @@ context 'for a normal user' do let(:api_user) { ApiUser.new(logged_in_user) } - it "returns nothing" do + it 'returns nothing' do expect(resolved_scope).to be_empty end end @@ -38,7 +38,7 @@ context 'for the resource owner' do let(:api_user) { ApiUser.new(resource_owner) } - it "includes aggregations" do + it 'includes aggregations' do expect(resolved_scope).to include(aggregation) end end @@ -61,7 +61,7 @@ context 'for an anonymous user' do let(:api_user) { ApiUser.new(anonymous_user) } - it "returns nothing" do + it 'returns nothing' do expect(resolved_scope).to be_empty end end @@ -69,7 +69,7 @@ context 'for a normal user' do let(:api_user) { ApiUser.new(logged_in_user) } - it "returns nothing" do + it 'returns nothing' do expect(resolved_scope).to be_empty end end From a16c421bcfb24a0aab6dffe3cf347a3b0ce7410e Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Wed, 27 Mar 2024 16:30:05 -0500 Subject: [PATCH 13/29] Hound again --- app/services/aggregation_client.rb | 3 +-- .../20240304201959_refactor_aggregation_model.rb | 2 ++ spec/models/aggregation_spec.rb | 10 +++++----- spec/services/aggregation_client_spec.rb | 10 +++++----- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/services/aggregation_client.rb b/app/services/aggregation_client.rb index 957e4d1c7..58035d3f4 100644 --- a/app/services/aggregation_client.rb +++ b/app/services/aggregation_client.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class AggregationClient - class ConnectionError < StandardError; end class ResourceNotFound < ConnectionError; end class ServerError < ConnectionError; end @@ -10,7 +9,7 @@ class ServerError < ConnectionError; end def initialize(adapter=Faraday.default_adapter) @connection = connect!(adapter) - @host ||= ENV.fetch('CELLECT_HOST', "http://test.example.com") + @host ||= ENV.fetch('AGGREGATION_HOST', 'http://test.example.com') end def connect!(adapter) diff --git a/db/migrate/20240304201959_refactor_aggregation_model.rb b/db/migrate/20240304201959_refactor_aggregation_model.rb index daeb3c9ec..83567daf1 100644 --- a/db/migrate/20240304201959_refactor_aggregation_model.rb +++ b/db/migrate/20240304201959_refactor_aggregation_model.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RefactorAggregationModel < ActiveRecord::Migration[6.1] def up # delete existing unused columns diff --git a/spec/models/aggregation_spec.rb b/spec/models/aggregation_spec.rb index 173bbc1f2..7daca187d 100644 --- a/spec/models/aggregation_spec.rb +++ b/spec/models/aggregation_spec.rb @@ -15,7 +15,7 @@ expect(build(:aggregation, user: nil)).not_to be_valid end - context "when there is a duplicate user_id workflow_id entry" do + context 'when there is a duplicate user_id workflow_id entry' do before(:each) do aggregation.save end @@ -24,16 +24,16 @@ user: aggregation.user) end - it "should not be valid" do + it 'should not be valid' do expect(duplicate).not_to be_valid end - it "should have the correct error message on user_id" do + it 'should have the correct error message on user_id' do duplicate.valid? - expect(duplicate.errors[:user_id]).to include("has already been taken") + expect(duplicate.errors[:user_id]).to include('has already been taken') end - it "should raise a uniq index db error" do + it 'should raise a uniq index db error' do expect { duplicate.save! }.to raise_error(ActiveRecord::RecordInvalid) end end diff --git a/spec/services/aggregation_client_spec.rb b/spec/services/aggregation_client_spec.rb index 856620e54..6185f3793 100644 --- a/spec/services/aggregation_client_spec.rb +++ b/spec/services/aggregation_client_spec.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require "spec_helper" +require 'spec_helper' RSpec.describe AggregationClient do - describe "#send_aggregation_request" do + describe '#send_aggregation_request' do before do - allow(described_class).to receive(:host).and_return("http://test.example.com") + allow(described_class).to receive(:host).and_return('http://test.example.com') end let(:headers) { { 'Content-Type': 'application/json', 'Accept': 'application/json' } } @@ -16,7 +16,7 @@ it 'was successful' do stubs = Faraday::Adapter::Test::Stubs.new do |stub| stub.post(path, params.to_json, headers) do - [200, { 'Content-Type': 'application/json' }, body.to_json] + [200, { 'Content-Type': 'application/json' }, body.to_json] end end @@ -43,7 +43,7 @@ stub.post(path, params.to_json, headers) do [ 500, - { 'Content-Type': 'application/json' }, + { 'Content-Type': 'application/json' }, { 'errors' => { 'detail' => 'Server internal error' } }.to_json ] end From ea565fb71cb6fad3029d3451a1d0772e480d66c3 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Wed, 27 Mar 2024 16:33:00 -0500 Subject: [PATCH 14/29] Third round hound --- app/models/aggregation.rb | 2 +- app/services/aggregation_client.rb | 2 +- spec/models/aggregation_spec.rb | 6 +++--- spec/services/aggregation_client_spec.rb | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/models/aggregation.rb b/app/models/aggregation.rb index ba5941deb..629147ac6 100644 --- a/app/models/aggregation.rb +++ b/app/models/aggregation.rb @@ -7,7 +7,7 @@ class Aggregation < ApplicationRecord validates :user, presence: true validates :user_id, uniqueness: { scope: :workflow_id } - self.ignored_columns = ["subject_id", "aggregation"] + self.ignored_columns = ['subject_id', 'aggregation'] enum status: { created: 0, diff --git a/app/services/aggregation_client.rb b/app/services/aggregation_client.rb index 58035d3f4..afe19be3e 100644 --- a/app/services/aggregation_client.rb +++ b/app/services/aggregation_client.rb @@ -13,7 +13,7 @@ def initialize(adapter=Faraday.default_adapter) end def connect!(adapter) - Faraday.new(@host, ssl: {verify: false}) do |faraday| + Faraday.new(@host, ssl: { verify: false }) do |faraday| faraday.request :json faraday.response :json, content_type: /\bjson$/ faraday.adapter(*adapter) diff --git a/spec/models/aggregation_spec.rb b/spec/models/aggregation_spec.rb index 7daca187d..5e2153775 100644 --- a/spec/models/aggregation_spec.rb +++ b/spec/models/aggregation_spec.rb @@ -24,16 +24,16 @@ user: aggregation.user) end - it 'should not be valid' do + it 'is not be valid' do expect(duplicate).not_to be_valid end - it 'should have the correct error message on user_id' do + it 'has the correct error message on user_id' do duplicate.valid? expect(duplicate.errors[:user_id]).to include('has already been taken') end - it 'should raise a uniq index db error' do + it 'raises a uniq index db error' do expect { duplicate.save! }.to raise_error(ActiveRecord::RecordInvalid) end end diff --git a/spec/services/aggregation_client_spec.rb b/spec/services/aggregation_client_spec.rb index 6185f3793..b5a03fb33 100644 --- a/spec/services/aggregation_client_spec.rb +++ b/spec/services/aggregation_client_spec.rb @@ -10,7 +10,7 @@ let(:headers) { { 'Content-Type': 'application/json', 'Accept': 'application/json' } } let(:params) { { project_id: 1, workflow_id: 10, user_id: 100 } } - let(:body) { { task_id: '1234-asdf-1234'} } + let(:body) { { task_id: '1234-asdf-1234' } } let(:path) { '/run_aggregation' } it 'was successful' do From f6440bdc332be8d7b98c9707ee1e5baa59f7504a Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Wed, 27 Mar 2024 16:34:58 -0500 Subject: [PATCH 15/29] Feed hound --- app/models/aggregation.rb | 2 +- spec/models/aggregation_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/aggregation.rb b/app/models/aggregation.rb index 629147ac6..0d3506a07 100644 --- a/app/models/aggregation.rb +++ b/app/models/aggregation.rb @@ -7,7 +7,7 @@ class Aggregation < ApplicationRecord validates :user, presence: true validates :user_id, uniqueness: { scope: :workflow_id } - self.ignored_columns = ['subject_id', 'aggregation'] + self.ignored_columns = %w[subject_id aggregation] enum status: { created: 0, diff --git a/spec/models/aggregation_spec.rb b/spec/models/aggregation_spec.rb index 5e2153775..89635e897 100644 --- a/spec/models/aggregation_spec.rb +++ b/spec/models/aggregation_spec.rb @@ -3,15 +3,15 @@ RSpec.describe Aggregation, :type => :model do let(:aggregation) { build(:aggregation) } - it 'should have a valid factory' do + it 'has a valid factory' do expect(aggregation).to be_valid end - it 'should not be valid without a workflow' do + it 'is not be valid without a workflow' do expect(build(:aggregation, workflow: nil)).not_to be_valid end - it 'should not be valid without a user' do + it 'is not be valid without a user' do expect(build(:aggregation, user: nil)).not_to be_valid end From 25e1843f5456bd96e89fb52f590e49d3a9020bc9 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Wed, 27 Mar 2024 16:37:15 -0500 Subject: [PATCH 16/29] Consistency --- spec/policies/aggregation_policy_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/policies/aggregation_policy_spec.rb b/spec/policies/aggregation_policy_spec.rb index 42ca1e0aa..314cd3253 100644 --- a/spec/policies/aggregation_policy_spec.rb +++ b/spec/policies/aggregation_policy_spec.rb @@ -23,7 +23,7 @@ let(:api_user) { ApiUser.new(anonymous_user) } it 'returns nothing' do - expect(resolved_scope).to match_array([]) + expect(resolved_scope).to be_empty end end From 50f14c13ca570c73f8f53e24271a2a8feceec21c Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Fri, 24 May 2024 17:01:18 -0500 Subject: [PATCH 17/29] Add project_id to Aggregations to allow Doorkeeper to scope by project --- app/controllers/api/v1/aggregations_controller.rb | 6 ++++-- app/models/aggregation.rb | 3 ++- app/policies/aggregation_policy.rb | 4 ++++ app/serializers/aggregation_serializer.rb | 4 ++-- .../20240304201959_refactor_aggregation_model.rb | 4 ++++ db/structure.sql | 11 ++++++++++- .../api/v1/aggregations_controller_spec.rb | 7 ++++++- spec/factories/aggregations.rb | 1 + spec/models/aggregation_spec.rb | 8 +++++--- 9 files changed, 38 insertions(+), 10 deletions(-) diff --git a/app/controllers/api/v1/aggregations_controller.rb b/app/controllers/api/v1/aggregations_controller.rb index 32066d560..213605eb9 100644 --- a/app/controllers/api/v1/aggregations_controller.rb +++ b/app/controllers/api/v1/aggregations_controller.rb @@ -3,14 +3,16 @@ class Api::V1::AggregationsController < Api::ApiController include JsonApiController::PunditPolicy - require_authentication :index, :show, :update, :create, scopes: [:workflow] + require_authentication :index, :show, :update, :create, scopes: [:project] resource_actions :index, :show, :create, :update schema_type :json_schema def create workflow = Workflow.find(create_params['links']['workflow']) + project_id = workflow.project.id + create_params['links']['project'] = project_id response = AggregationClient.new.send_aggregation_request( - workflow.project.id, + project_id, workflow.id, create_params['links']['user'] ) diff --git a/app/models/aggregation.rb b/app/models/aggregation.rb index 0d3506a07..15e45326d 100644 --- a/app/models/aggregation.rb +++ b/app/models/aggregation.rb @@ -2,7 +2,9 @@ class Aggregation < ApplicationRecord belongs_to :workflow + belongs_to :project belongs_to :user + validates :project, presence: true validates :workflow, presence: true validates :user, presence: true validates :user_id, uniqueness: { scope: :workflow_id } @@ -15,5 +17,4 @@ class Aggregation < ApplicationRecord completed: 2, failed: 3 } - end diff --git a/app/policies/aggregation_policy.rb b/app/policies/aggregation_policy.rb index 534b47463..7582f5b78 100644 --- a/app/policies/aggregation_policy.rb +++ b/app/policies/aggregation_policy.rb @@ -12,6 +12,10 @@ def linkable_workflows policy_for(Workflow).scope_for(:update) end + def linkable_projects + policy_for(Project).scope_for(:update) + end + def linkable_users policy_for(User).scope_for(:update) end diff --git a/app/serializers/aggregation_serializer.rb b/app/serializers/aggregation_serializer.rb index 34e50d231..73db29b54 100644 --- a/app/serializers/aggregation_serializer.rb +++ b/app/serializers/aggregation_serializer.rb @@ -3,7 +3,7 @@ class AggregationSerializer include CachedSerializer attributes :id, :href, :created_at, :updated_at, :uuid, :task_id, :status - can_include :workflow, :user + can_include :project, :workflow, :user - can_filter_by :workflow + can_filter_by :project, :workflow end diff --git a/db/migrate/20240304201959_refactor_aggregation_model.rb b/db/migrate/20240304201959_refactor_aggregation_model.rb index 83567daf1..68a86aa34 100644 --- a/db/migrate/20240304201959_refactor_aggregation_model.rb +++ b/db/migrate/20240304201959_refactor_aggregation_model.rb @@ -7,6 +7,9 @@ def up safety_assured { remove_column :aggregations, :aggregation } # and the new aggregations columns + add_column :aggregations, :project_id, :integer + add_foreign_key :aggregations, :projects, column: :project_id, validate: false + add_column :aggregations, :user_id, :integer add_foreign_key :aggregations, :users, column: :user_id, validate: false @@ -20,6 +23,7 @@ def down add_column :aggregations, :aggregation, :jsonb remove_column :aggregations, :user_id + remove_column :aggregations, :project_id remove_column :aggregations, :uuid remove_column :aggregations, :task_id remove_column :aggregations, :status diff --git a/db/structure.sql b/db/structure.sql index eecf1a925..5273c031f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -98,10 +98,11 @@ CREATE TABLE public.aggregations ( workflow_id integer, created_at timestamp without time zone, updated_at timestamp without time zone, + project_id integer, user_id integer, uuid character varying, task_id character varying, - status integer + status integer DEFAULT 0 ); @@ -4209,6 +4210,14 @@ ALTER TABLE ONLY public.organization_versions ADD CONSTRAINT fk_rails_be858ed31d FOREIGN KEY (organization_id) REFERENCES public.organizations(id); +-- +-- Name: aggregations fk_rails_c7d229ada4; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregations + ADD CONSTRAINT fk_rails_c7d229ada4 FOREIGN KEY (project_id) REFERENCES public.projects(id) NOT VALID; + + -- -- Name: subject_set_imports fk_rails_d596712569; Type: FK CONSTRAINT; Schema: public; Owner: - -- diff --git a/spec/controllers/api/v1/aggregations_controller_spec.rb b/spec/controllers/api/v1/aggregations_controller_spec.rb index ea58a50c5..12e957931 100644 --- a/spec/controllers/api/v1/aggregations_controller_spec.rb +++ b/spec/controllers/api/v1/aggregations_controller_spec.rb @@ -7,7 +7,7 @@ let(:api_resource_attributes) { %w[id created_at updated_at uuid task_id status] } let(:api_resource_links) { %w[aggregations.workflow] } - let(:scopes) { %w[public workflow] } + let(:scopes) { %w[project] } let(:resource_class) { Aggregation } describe '#index' do @@ -55,6 +55,11 @@ it_behaves_like 'is creatable' + it 'saves the project id' do + post :create, params: create_params + expect(Aggregation.first.project_id).to eq(workflow.project.id) + end + it 'makes a request to the aggregation service' do post :create, params: create_params expect(mock_agg).to have_received(:send_aggregation_request) diff --git a/spec/factories/aggregations.rb b/spec/factories/aggregations.rb index 16e165013..df01cb93f 100644 --- a/spec/factories/aggregations.rb +++ b/spec/factories/aggregations.rb @@ -1,6 +1,7 @@ FactoryBot.define do factory :aggregation do workflow + project user status { Aggregation.statuses[:pending] } end diff --git a/spec/models/aggregation_spec.rb b/spec/models/aggregation_spec.rb index 89635e897..ef61b0b2d 100644 --- a/spec/models/aggregation_spec.rb +++ b/spec/models/aggregation_spec.rb @@ -11,14 +11,16 @@ expect(build(:aggregation, workflow: nil)).not_to be_valid end + it 'is not be valid without a project' do + expect(build(:aggregation, project: nil)).not_to be_valid + end + it 'is not be valid without a user' do expect(build(:aggregation, user: nil)).not_to be_valid end context 'when there is a duplicate user_id workflow_id entry' do - before(:each) do - aggregation.save - end + before { aggregation.save } let(:duplicate) do build(:aggregation, workflow: aggregation.workflow, user: aggregation.user) From ee4774799c1858d9c6ed03c4d95694a1f1a50058 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Fri, 24 May 2024 17:13:30 -0500 Subject: [PATCH 18/29] Clarify admin specs, add collab spec --- spec/policies/aggregation_policy_spec.rb | 36 +++++++++++++++--------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/spec/policies/aggregation_policy_spec.rb b/spec/policies/aggregation_policy_spec.rb index 314cd3253..5ebd50629 100644 --- a/spec/policies/aggregation_policy_spec.rb +++ b/spec/policies/aggregation_policy_spec.rb @@ -5,6 +5,7 @@ let(:anonymous_user) { nil } let(:logged_in_user) { create(:user) } let(:resource_owner) { create(:user) } + let(:collaborator) { create(:user) } let(:project) { build(:project, owner: resource_owner) } @@ -19,7 +20,7 @@ Pundit.policy!(api_user, Aggregation).scope_for(:index) end - context 'for an anonymous user' do + context 'when the user is an anonymous user' do let(:api_user) { ApiUser.new(anonymous_user) } it 'returns nothing' do @@ -27,7 +28,7 @@ end end - context 'for a normal user' do + context 'when the user is a normal user' do let(:api_user) { ApiUser.new(logged_in_user) } it 'returns nothing' do @@ -35,19 +36,29 @@ end end - context 'for the resource owner' do + context 'when the user is a resource owner' do + let(:api_user) { ApiUser.new(collaborator) } + + before { create :access_control_list, user_group: collaborator.identity_group, resource: project, roles: ['collaborator'] } + + it 'includes aggregation' do + expect(resolved_scope).to include(aggregation) + end + end + + context 'when the user is a resource collaborators' do let(:api_user) { ApiUser.new(resource_owner) } - it 'includes aggregations' do + it 'includes the aggregation' do expect(resolved_scope).to include(aggregation) end end - context 'for an admin' do + context 'when the user is an admin' do let(:admin_user) { create :user, admin: true } let(:api_user) { ApiUser.new(admin_user, admin: true) } - it 'includes everything' do + it 'includes the aggregation' do expect(resolved_scope).to include(aggregation) end end @@ -58,7 +69,7 @@ Pundit.policy!(api_user, Aggregation).scope_for(:update) end - context 'for an anonymous user' do + context 'when the user is an anonymous user' do let(:api_user) { ApiUser.new(anonymous_user) } it 'returns nothing' do @@ -66,7 +77,7 @@ end end - context 'for a normal user' do + context 'when the user is a normal user' do let(:api_user) { ApiUser.new(logged_in_user) } it 'returns nothing' do @@ -74,23 +85,22 @@ end end - context 'for the resource owner' do + context 'when the user is the resource owner' do let(:api_user) { ApiUser.new(resource_owner) } - it 'includes aggregations' do + it 'includes the aggregation' do expect(resolved_scope).to include(aggregation) end end - context 'for an admin' do + context 'when the user is an admin' do let(:admin_user) { create :user, admin: true } let(:api_user) { ApiUser.new(admin_user, admin: true) } - it 'includes everything' do + it 'includes the aggregation' do expect(resolved_scope).to include(aggregation) end end - end end end From 0271b4c0008b8a9103c40471cbbd5aa661fd36f3 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Fri, 24 May 2024 17:18:27 -0500 Subject: [PATCH 19/29] Remove ignored_columns --- app/models/aggregation.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/models/aggregation.rb b/app/models/aggregation.rb index 15e45326d..ccc588eef 100644 --- a/app/models/aggregation.rb +++ b/app/models/aggregation.rb @@ -9,8 +9,6 @@ class Aggregation < ApplicationRecord validates :user, presence: true validates :user_id, uniqueness: { scope: :workflow_id } - self.ignored_columns = %w[subject_id aggregation] - enum status: { created: 0, pending: 1, From 2fccdb74c3d35e6136b48ca7035e89dc514fd997 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Fri, 24 May 2024 17:54:48 -0500 Subject: [PATCH 20/29] Add spec for failed service connections --- app/controllers/api/v1/aggregations_controller.rb | 2 +- spec/controllers/api/v1/aggregations_controller_spec.rb | 9 +++++++++ spec/models/aggregation_spec.rb | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/aggregations_controller.rb b/app/controllers/api/v1/aggregations_controller.rb index 213605eb9..16164987b 100644 --- a/app/controllers/api/v1/aggregations_controller.rb +++ b/app/controllers/api/v1/aggregations_controller.rb @@ -20,6 +20,6 @@ def create agg.update({ task_id: response.body[:task_id], status: 'pending' }) end rescue AggregationClient::ConnectionError - json_api_render(:service_unavailable, response.body) + json_api_render(:service_unavailable, 'The aggregation service is unavailable or not responding') end end diff --git a/spec/controllers/api/v1/aggregations_controller_spec.rb b/spec/controllers/api/v1/aggregations_controller_spec.rb index 12e957931..7484d185c 100644 --- a/spec/controllers/api/v1/aggregations_controller_spec.rb +++ b/spec/controllers/api/v1/aggregations_controller_spec.rb @@ -65,6 +65,15 @@ expect(mock_agg).to have_received(:send_aggregation_request) end + context 'when the aggregation service is unavailable' do + before { allow(mock_agg).to receive(:send_aggregation_request).and_raise(AggregationClient::ConnectionError) } + + it 'sends back an error response' do + post :create, params: create_params + expect(response.status).to eq(503) + end + end + it 'stores the task_id from the client response' do post :create, params: create_params expect(Aggregation.first.task_id).to eq('asdf-1234-asdf') diff --git a/spec/models/aggregation_spec.rb b/spec/models/aggregation_spec.rb index ef61b0b2d..e043ee635 100644 --- a/spec/models/aggregation_spec.rb +++ b/spec/models/aggregation_spec.rb @@ -21,6 +21,7 @@ context 'when there is a duplicate user_id workflow_id entry' do before { aggregation.save } + let(:duplicate) do build(:aggregation, workflow: aggregation.workflow, user: aggregation.user) From 4f1d46c0227abc839b74a35977bb0701b83802b7 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Tue, 28 May 2024 16:33:26 -0500 Subject: [PATCH 21/29] Add serializer spec --- spec/serializers/aggregation_serializer_spec.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 spec/serializers/aggregation_serializer_spec.rb diff --git a/spec/serializers/aggregation_serializer_spec.rb b/spec/serializers/aggregation_serializer_spec.rb new file mode 100644 index 000000000..7b5a49cbd --- /dev/null +++ b/spec/serializers/aggregation_serializer_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AggregationSerializer do + let(:aggregation) { create(:aggregation) } + + it_behaves_like 'a panoptes restpack serializer' do + let(:resource) { aggregation } + let(:includes) { %i[project user workflow] } + let(:preloads) { [] } + end +end From a08dbd1ccbdf2cc9f48cc2dcc3d3f74473d83a9e Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Tue, 28 May 2024 16:34:06 -0500 Subject: [PATCH 22/29] Implement #destroy, add error specs for collisions --- .../api/v1/aggregations_controller.rb | 4 +-- .../api/v1/aggregations_controller_spec.rb | 35 +++++++++++++------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/app/controllers/api/v1/aggregations_controller.rb b/app/controllers/api/v1/aggregations_controller.rb index 16164987b..9a52a4331 100644 --- a/app/controllers/api/v1/aggregations_controller.rb +++ b/app/controllers/api/v1/aggregations_controller.rb @@ -3,8 +3,8 @@ class Api::V1::AggregationsController < Api::ApiController include JsonApiController::PunditPolicy - require_authentication :index, :show, :update, :create, scopes: [:project] - resource_actions :index, :show, :create, :update + require_authentication :index, :show, :update, :destroy, :create, scopes: [:project] + resource_actions :index, :show, :create, :update, :destroy schema_type :json_schema def create diff --git a/spec/controllers/api/v1/aggregations_controller_spec.rb b/spec/controllers/api/v1/aggregations_controller_spec.rb index 7484d185c..df9052ac5 100644 --- a/spec/controllers/api/v1/aggregations_controller_spec.rb +++ b/spec/controllers/api/v1/aggregations_controller_spec.rb @@ -10,8 +10,11 @@ let(:scopes) { %w[project] } let(:resource_class) { Aggregation } + let(:workflow) { create(:workflow) } + let(:authorized_user) { workflow.project.owner } + let(:resource) { create(:aggregation, workflow: workflow) } + describe '#index' do - let(:workflow) { create(:workflow) } let!(:aggregations) { create_list(:aggregation, 2, workflow: workflow) } let!(:private_resource) { create(:aggregation) } let(:authorized_user) { workflow.project.owner } @@ -21,15 +24,10 @@ end describe '#show' do - let(:resource) { create(:aggregation) } - let(:authorized_user) { resource.workflow.project.owner } - it_behaves_like 'is showable' end describe 'create' do - let(:workflow) { create(:workflow) } - let(:authorized_user) { workflow.project.owner } let(:test_attr) { :workflow_id } let(:test_attr_value) { workflow.id } let(:fake_response) { double } @@ -65,6 +63,20 @@ expect(mock_agg).to have_received(:send_aggregation_request) end + context 'when there is an existing aggregation for that user and workflow' do + let!(:existing_agg) { create(:aggregation, workflow: workflow, user: authorized_user) } + + before { post :create, params: create_params } + + it 'returns an error' do + expect(response).to have_http_status(:bad_request) + end + + it 'includes a validation error' do + expect(response.body).to include('Validation failed: User has already been taken') + end + end + context 'when the aggregation service is unavailable' do before { allow(mock_agg).to receive(:send_aggregation_request).and_raise(AggregationClient::ConnectionError) } @@ -81,16 +93,13 @@ end describe '#update' do - let(:workflow) { create(:workflow) } - let(:authorized_user) { resource.workflow.project.owner } - let(:resource) { create(:aggregation, workflow: workflow) } let(:test_attr) { :uuid } - let(:test_attr_value) { '1234-asdf-1234' } + let(:test_attr_value) { '557ebcfa3c29496787336bfbd7c4d856' } let(:update_params) do { aggregations: { - uuid: '1234-asdf-1234', + uuid: '557ebcfa3c29496787336bfbd7c4d856', status: 'completed' } } @@ -98,4 +107,8 @@ it_behaves_like 'is updatable' end + + describe '#destroy' do + it_behaves_like 'is destructable' + end end From d97e0274b90d5f26ef3bb0ac83191d0bb9da4bad Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Tue, 28 May 2024 16:59:18 -0500 Subject: [PATCH 23/29] Aggregation documentation --- docs/source/includes/_aggregations.md | 139 ++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 docs/source/includes/_aggregations.md diff --git a/docs/source/includes/_aggregations.md b/docs/source/includes/_aggregations.md new file mode 100644 index 000000000..c8d22c0eb --- /dev/null +++ b/docs/source/includes/_aggregations.md @@ -0,0 +1,139 @@ +# Aggregations + +```json +{ + "aggregations": [ + { + "id": "1", + "href": "/aggregations/1", + "created_at": "2024-05-22T22:52:59.173Z", + "updated_at": "2024-05-22T22:52:59.181Z", + "uuid": "557ebcfa3c29496787336bfbd7c4d856", + "task_id": "9c963646-e7cd-4c5a-8749-c97086d416bd", + "status": "completed", + "links": { + "workflow": "2", + "user": "2" + } + } + ], + "links": { + "aggregations.workflow": { + "href": "/workflows/{aggregations.workflow}", + "type": "workflows" + }, + "aggregations.user": { + "href": "/users/{aggregations.user}", + "type": "users" + } + } +} +``` + +A single Aggregation resource object. This represents the automated aggregation by the external service by a _user_ of a single _workflow_ via the online Aggregation service. + +An Aggregation has the following attributes: + +Attribute | Type | Description +--------- | ---- | ----------- +id | integer | read-only +workflow_id | integer | read-only +project_id | integer | read-only +created_at | string | read-only +updated_at | string | read-only +user_id | integer | read-only +uuid | string | The unique identifier of the aggregation run, used in the URLs of output files +task_id | string | The Celery task ID, used to query the status of the backgrounded task on the aggregation service +status | integer | created, pending, completed, failed + + +## List aggregations + +```http +GET /api/aggregations HTTP/1.1 +Accept: application/vnd.api+json; version=1 +Content-Type: application/json +``` + +Only lists aggregations where the active user has has edit permissions on the related project. + +Any of the scopes may be further filtered using the *project_id*, *workflow_id* +and *user_id* parameters. + +### Parameters + ++ page (optional, integer) ... index of the page to retrieve, 1 is default ++ page_size (optional, integer) ... number of items on a page. 20 is default ++ sort (optional, string) ... fields to sort by. updated_at is default ++ project_id (optional, integer) ... only retrieve aggregations for a specific project ++ workflow_id (optional, integer) ... only retrieve aggregations for a specific workflow ++ user_id (optional, integer) ... only retrieve aggregations for a specific user ++ include (optional, string) ... comma separated list of linked resources to return in the response + + +## Retrieve a single Aggregation + +```http +GET /api/aggregations/123 HTTP/1.1 +Accept: application/vnd.api+json; version=1 +Content-Type: application/json +``` + +A User may only retrieve Aggregations they have permission to access. + +### Parameters + ++ `id` (required, integer) ... integer id of the resource to retrieve + + +## Create an Aggregation [POST] + +```http +POST /api/aggregations HTTP/1.1 +Accept: application/vnd.api+json; version=1 +Content-Type: application/json + +{ + "aggregations": { + "links": { + "user": "3", + "workflow": "2" + } + } +} +``` + +Creating a new Aggregation will initiate a new batch aggregation run. +A call out to the Aggregation Service is made and the newly created Aggregation resource +is updated with the Aggregation Service's Celery background task ID so that its status can be checked. + + +## Edit a single Aggregation [PUT] + +```http +PUT /api/aggregations/123 HTTP/1.1 +Accept: application/vnd.api+json; version=1 +Content-Type: application/json + +{ + "aggregations": { + "uuid": "557ebcfa3c29496787336bfbd7c4d856", + "status": "completed" + } +} +``` + +Aggregations are updated by the Aggregation Service when a run has succeeded or failed. A successful run +will send a request to this endpoint and update the UUID generated by that run. This UUID is a 32-character +lowercase hexadecimal string and can be used to construct the URL to download that run's output data. + +The status can also be updated via this route (see the _aggregations_ model) and can be the only attribute included. + +## Destroy a single Aggregation [DELETE] +```http +DELETE /api/Aggregations/123 HTTP/1.1 +Accept: application/vnd.api+json; version=1 +Content-Type: application/json +``` + +A user may destroy an Aggregation by ID if they have destroy permissions for the parent project. \ No newline at end of file From b90bdcc36ac60fed65c520d89907b88d6ea04131 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Tue, 28 May 2024 17:04:17 -0500 Subject: [PATCH 24/29] reorder spec --- .../controllers/api/v1/aggregations_controller_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/controllers/api/v1/aggregations_controller_spec.rb b/spec/controllers/api/v1/aggregations_controller_spec.rb index df9052ac5..50b08d047 100644 --- a/spec/controllers/api/v1/aggregations_controller_spec.rb +++ b/spec/controllers/api/v1/aggregations_controller_spec.rb @@ -63,6 +63,11 @@ expect(mock_agg).to have_received(:send_aggregation_request) end + it 'stores the task_id from the client response' do + post :create, params: create_params + expect(Aggregation.first.task_id).to eq('asdf-1234-asdf') + end + context 'when there is an existing aggregation for that user and workflow' do let!(:existing_agg) { create(:aggregation, workflow: workflow, user: authorized_user) } @@ -85,11 +90,6 @@ expect(response.status).to eq(503) end end - - it 'stores the task_id from the client response' do - post :create, params: create_params - expect(Aggregation.first.task_id).to eq('asdf-1234-asdf') - end end describe '#update' do From b175f36261254ce66b9af7ce28e614aeb0c0b628 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Tue, 28 May 2024 17:07:22 -0500 Subject: [PATCH 25/29] Fix hash alignment --- spec/controllers/api/v1/aggregations_controller_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/controllers/api/v1/aggregations_controller_spec.rb b/spec/controllers/api/v1/aggregations_controller_spec.rb index 50b08d047..1ce0378b7 100644 --- a/spec/controllers/api/v1/aggregations_controller_spec.rb +++ b/spec/controllers/api/v1/aggregations_controller_spec.rb @@ -34,7 +34,8 @@ let(:mock_agg) { instance_double(AggregationClient) } let(:create_params) do - { aggregations: + { + aggregations: { links: { user: authorized_user.id.to_s, From cc1fbfe0ff652cedf7e6d9b805a0efbe0d79a677 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Thu, 30 May 2024 15:55:03 -0500 Subject: [PATCH 26/29] Remove aggregation association from subject --- app/models/subject.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/subject.rb b/app/models/subject.rb index 2203c45bc..581baef30 100644 --- a/app/models/subject.rb +++ b/app/models/subject.rb @@ -17,7 +17,6 @@ class Subject < ApplicationRecord class_name: "Medium", as: :linked has_many :recents, dependent: :destroy - has_many :aggregations, dependent: :destroy has_many :tutorial_workflows, class_name: 'Workflow', foreign_key: 'tutorial_subject_id', From 871a1479394d63679732c9f3a106318cd3aeb45b Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Thu, 30 May 2024 15:57:24 -0500 Subject: [PATCH 27/29] Remove user uniqueness constraint --- app/models/aggregation.rb | 6 ++---- app/models/workflow.rb | 2 +- .../api/v1/aggregations_controller_spec.rb | 13 +++++++------ spec/models/aggregation_spec.rb | 13 +++++++------ 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/models/aggregation.rb b/app/models/aggregation.rb index ccc588eef..39cec2728 100644 --- a/app/models/aggregation.rb +++ b/app/models/aggregation.rb @@ -4,10 +4,8 @@ class Aggregation < ApplicationRecord belongs_to :workflow belongs_to :project belongs_to :user - validates :project, presence: true - validates :workflow, presence: true - validates :user, presence: true - validates :user_id, uniqueness: { scope: :workflow_id } + validates :project, :workflow, :user, presence: true + validates :workflow, uniqueness: true enum status: { created: 0, diff --git a/app/models/workflow.rb b/app/models/workflow.rb index 33878a391..2ac340f96 100644 --- a/app/models/workflow.rb +++ b/app/models/workflow.rb @@ -24,7 +24,7 @@ class Workflow < ApplicationRecord has_many :user_seen_subjects, dependent: :destroy has_many :workflow_tutorials, dependent: :destroy has_many :tutorials, through: :workflow_tutorials - has_many :aggregations, dependent: :destroy + has_one :aggregation, dependent: :destroy has_many :attached_images, -> { where(type: "workflow_attached_image") }, class_name: "Medium", as: :linked has_one :classifications_export, -> { where(type: "workflow_classifications_export").order(created_at: :desc) }, diff --git a/spec/controllers/api/v1/aggregations_controller_spec.rb b/spec/controllers/api/v1/aggregations_controller_spec.rb index 1ce0378b7..71a494dfe 100644 --- a/spec/controllers/api/v1/aggregations_controller_spec.rb +++ b/spec/controllers/api/v1/aggregations_controller_spec.rb @@ -15,10 +15,11 @@ let(:resource) { create(:aggregation, workflow: workflow) } describe '#index' do - let!(:aggregations) { create_list(:aggregation, 2, workflow: workflow) } - let!(:private_resource) { create(:aggregation) } + let(:other_workflow) { create(:workflow) } + let!(:aggregations) { create(:aggregation, workflow: workflow) } + let!(:private_resource) { create(:aggregation, workflow: other_workflow) } let(:authorized_user) { workflow.project.owner } - let(:n_visible) { 2 } + let(:n_visible) { 1 } it_behaves_like 'is indexable' end @@ -69,8 +70,8 @@ expect(Aggregation.first.task_id).to eq('asdf-1234-asdf') end - context 'when there is an existing aggregation for that user and workflow' do - let!(:existing_agg) { create(:aggregation, workflow: workflow, user: authorized_user) } + context 'when there is an existing aggregation for that workflow' do + let!(:existing_agg) { create(:aggregation, workflow: workflow) } before { post :create, params: create_params } @@ -79,7 +80,7 @@ end it 'includes a validation error' do - expect(response.body).to include('Validation failed: User has already been taken') + expect(response.body).to include('Validation failed: Workflow has already been taken') end end diff --git a/spec/models/aggregation_spec.rb b/spec/models/aggregation_spec.rb index e043ee635..f657cf2e2 100644 --- a/spec/models/aggregation_spec.rb +++ b/spec/models/aggregation_spec.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + require 'spec_helper' -RSpec.describe Aggregation, :type => :model do +RSpec.describe Aggregation, type: :model do let(:aggregation) { build(:aggregation) } it 'has a valid factory' do @@ -19,21 +21,20 @@ expect(build(:aggregation, user: nil)).not_to be_valid end - context 'when there is a duplicate user_id workflow_id entry' do + context 'when there is a duplicate workflow_id' do before { aggregation.save } let(:duplicate) do - build(:aggregation, workflow: aggregation.workflow, - user: aggregation.user) + build(:aggregation, workflow: aggregation.workflow, user: aggregation.user) end it 'is not be valid' do expect(duplicate).not_to be_valid end - it 'has the correct error message on user_id' do + it 'has the correct error message' do duplicate.valid? - expect(duplicate.errors[:user_id]).to include('has already been taken') + expect(duplicate.errors.full_messages.first).to include('has already been taken') end it 'raises a uniq index db error' do From 99cb844f6df52f5f2b3e72fd65773fcddcf453dd Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Mon, 10 Jun 2024 17:06:26 -0500 Subject: [PATCH 28/29] MIgrate database, update structure.sql --- ...model.rb => 20240610220553_refactor_aggregation_model.rb} | 0 db/structure.sql | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) rename db/migrate/{20240304201959_refactor_aggregation_model.rb => 20240610220553_refactor_aggregation_model.rb} (100%) diff --git a/db/migrate/20240304201959_refactor_aggregation_model.rb b/db/migrate/20240610220553_refactor_aggregation_model.rb similarity index 100% rename from db/migrate/20240304201959_refactor_aggregation_model.rb rename to db/migrate/20240610220553_refactor_aggregation_model.rb diff --git a/db/structure.sql b/db/structure.sql index b38796f1e..e62018dd6 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1973,7 +1973,8 @@ CREATE TABLE public.workflows ( published_version_id integer, steps jsonb DEFAULT '[]'::jsonb NOT NULL, serialize_with_project boolean DEFAULT true, - real_set_member_subjects_count integer DEFAULT 0 NOT NULL + real_set_member_subjects_count integer DEFAULT 0 NOT NULL, + aggregation jsonb ); @@ -4602,6 +4603,6 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240216142515'), ('20240216171653'), ('20240216171937'), -('20240531184258'); +('20240304201959'); From 7009cf4d98d6b75b51e2b3970c6d69dbe5f202a2 Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Mon, 10 Jun 2024 17:27:29 -0500 Subject: [PATCH 29/29] Resolve migrations --- ...odel.rb => 20240304201959_refactor_aggregation_model.rb} | 0 db/structure.sql | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename db/migrate/{20240610220553_refactor_aggregation_model.rb => 20240304201959_refactor_aggregation_model.rb} (100%) diff --git a/db/migrate/20240610220553_refactor_aggregation_model.rb b/db/migrate/20240304201959_refactor_aggregation_model.rb similarity index 100% rename from db/migrate/20240610220553_refactor_aggregation_model.rb rename to db/migrate/20240304201959_refactor_aggregation_model.rb diff --git a/db/structure.sql b/db/structure.sql index e62018dd6..87c94a278 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1973,8 +1973,7 @@ CREATE TABLE public.workflows ( published_version_id integer, steps jsonb DEFAULT '[]'::jsonb NOT NULL, serialize_with_project boolean DEFAULT true, - real_set_member_subjects_count integer DEFAULT 0 NOT NULL, - aggregation jsonb + real_set_member_subjects_count integer DEFAULT 0 NOT NULL ); @@ -4603,6 +4602,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240216142515'), ('20240216171653'), ('20240216171937'), -('20240304201959'); +('20240304201959'), +('20240531184258');