From f1124f9ea0f5e40d74dfa2afc0a3ff00c1e4ada2 Mon Sep 17 00:00:00 2001 From: Alex Kriuchykhin Date: Thu, 28 Jan 2021 12:57:04 +0100 Subject: [PATCH] Add experiments overview service [SCI-5430] (#3116) --- app/models/project.rb | 17 ++ app/services/experiments_overview_service.rb | 76 ++++++++ app/services/projects_overview_service.rb | 5 +- .../experiments_overview_service_spec.rb | 170 ++++++++++++++++++ 4 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 app/services/experiments_overview_service.rb create mode 100644 spec/services/experiments_overview_service_spec.rb diff --git a/app/models/project.rb b/app/models/project.rb index 6a6e8ebd9..329416f1b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2,6 +2,7 @@ class Project < ApplicationRecord include ArchivableModel include SearchableModel include SearchableByNameModel + include ViewableModel enum visibility: { hidden: 0, visible: 1 } @@ -150,6 +151,22 @@ class Project < ApplicationRecord .distinct end + def default_view_state + { + experiments: { + active: { sort: 'new' }, + archived: { sort: 'new' } + } + } + end + + def validate_view_state(view_state) + if %w(new old atoz ztoa).exclude?(view_state.state.dig('experiments', 'active', 'sort')) || + %w(new old atoz ztoa archived_new archived_old).exclude?(view_state.state.dig('experiments', 'archived', 'sort')) + view_state.errors.add(:state, :wrong_state) + end + end + def last_activities(count = Constants::ACTIVITY_AND_NOTIF_SEARCH_LIMIT) activities.order(created_at: :desc).first(count) end diff --git a/app/services/experiments_overview_service.rb b/app/services/experiments_overview_service.rb new file mode 100644 index 000000000..762723c0a --- /dev/null +++ b/app/services/experiments_overview_service.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +class ExperimentsOverviewService + def initialize(project, user, params) + @project = project + @user = user + @params = params + @view_state = @project.current_view_state(@user) + @view_mode = if @project.archived? + 'archived' + else + params[:view_mode] || 'active' + end + + # Update sort if changed + @sort = @view_state.state.dig('experiments', @view_mode, 'sort') || 'atoz' + if @params[:sort] && @sort != @params[:sort] && + %w(new old atoz ztoa archived_old archived_new).include?(@params[:sort]) + @view_state.state['experiments'].merge!(Hash[@view_mode, { 'sort': @params[:sort] }.stringify_keys]) + @view_state.save! + @sort = @view_state.state.dig('experiments', @view_mode, 'sort') + end + end + + def experiments + sort_records( + filter_records( + fetch_records + ) + ) + end + + private + + def fetch_records + @project.experiments.joins(:project) + end + + def filter_records(records) + records = records.archived if @view_mode == 'archived' && @project.active? + records = records.active if @view_mode == 'active' + if @params[:search].present? + records = records.where_attributes_like(%w(experiments.name experiments.description), @params[:search]) + end + if @params[:created_on_from].present? + records = records.where('experiments.created_at > ?', @params[:created_on_from]) + end + records = records.where('experiments.created_at < ?', @params[:created_on_to]) if @params[:created_on_to].present? + if @params[:archived_on_from].present? + records = records.where('COALESCE(experiments.archived_on, projects.archived_on) > ?', @params[:archived_on_from]) + end + if @params[:archived_on_to].present? + records = records.where('COALESCE(experiments.archived_on, projects.archived_on) < ?', @params[:archived_on_to]) + end + records + end + + def sort_records(records) + case @sort + when 'new' + records.order(created_at: :desc) + when 'old' + records.order(:created_at) + when 'atoz' + records.order(:name) + when 'ztoa' + records.order(name: :desc) + when 'archived_old' + records.order(Arel.sql('COALESCE(experiments.archived_on, projects.archived_on) ASC')) + when 'archived_new' + records.order(Arel.sql('COALESCE(experiments.archived_on, projects.archived_on) DESC')) + else + records + end + end +end diff --git a/app/services/projects_overview_service.rb b/app/services/projects_overview_service.rb index c5ec667e8..ce75a08bf 100644 --- a/app/services/projects_overview_service.rb +++ b/app/services/projects_overview_service.rb @@ -110,8 +110,9 @@ class ProjectsOverviewService records = records.archived if @view_mode == 'archived' records = records.active if @view_mode == 'active' records = records.where_attributes_like('projects.name', @params[:search]) if @params[:search].present? - records = records.where_attributes_like('projects.name', @params[:search]) if @params[:search].present? - records = records.joins(:user_projects).where('user_projects.user_id IN (?)', @params[:members]) if @params[:members]&.any? + if @params[:members].present? + records = records.joins(:user_projects).where('user_projects.user_id IN (?)', @params[:members]) + end records = records.where('projects.created_at > ?', @params[:created_on_from]) if @params[:created_on_from].present? records = records.where('projects.created_at < ?', @params[:created_on_to]) if @params[:created_on_to].present? records = records.where('projects.archived_on < ?', @params[:archived_on_to]) if @params[:archived_on_to].present? diff --git a/spec/services/experiments_overview_service_spec.rb b/spec/services/experiments_overview_service_spec.rb new file mode 100644 index 000000000..f0bdc04c3 --- /dev/null +++ b/spec/services/experiments_overview_service_spec.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ExperimentsOverviewService do + EXPERIMENTS_CNT = 26 + time = Time.new(2019, 8, 1, 14, 35, 0) + let!(:user) { create :user } + let!(:project) { create :project } + before do + @experiments_overview = ExperimentsOverviewService.new(project, user, params) + end + + let!(:experiment_1) do + create :experiment, name: 'test experiment D', project: project, + archived: false, created_at: time.advance(hours: 2) + end + let!(:experiment_2) do + create :experiment, name: 'test experiment B', project: project, + archived: true, archived_on: time.advance(hours: 9), archived_by: user, created_at: time + end + let!(:experiment_3) do + create :experiment, name: 'test experiment C', project: project, + archived: false, created_at: time.advance(hours: 3) + end + let!(:experiment_4) do + create :experiment, name: 'test experiment A', project: project, + archived: true, archived_on: time.advance(hours: 8), archived_by: user, + created_at: time.advance(hours: 1) + end + let!(:experiment_5) do + create :experiment, name: 'test experiment E', project: project, + archived: true, archived_on: time.advance(hours: 10), archived_by: user, + created_at: time.advance(hours: 5) + end + let!(:experiment_6) do + create :experiment, name: 'test experiment F', project: project, + archived: false, created_at: time.advance(hours: 4) + end + (7..EXPERIMENTS_CNT).each do |i| + let!("experiment_#{i}") do + create :experiment, name: "test experiment #{(64 + i).chr}", + project: project, created_at: time.advance(hours: 6, minutes: i) + end + end + + describe '#experiments' do + let(:params) { {} } + + context "with request parameters { filter: 'active' }" do + let(:params) { { view_mode: 'active' } } + + it 'returns all active experiments' do + experiments = @experiments_overview.experiments + expect(experiments.length).to eq EXPERIMENTS_CNT - 3 + expect(experiments.uniq.length).to eq experiments.length + expect(experiments).to include(experiment_1, experiment_3) + expect(experiments).not_to include(experiment_2, experiment_4, experiment_5) + end + + context "with request parameters { sort: 'old' }" do + let(:params) { super().merge(sort: 'old') } + + it 'returns all active experiments, sorted by ascending creation time attribute' do + experiments = @experiments_overview.experiments + expect(experiments.length).to eq EXPERIMENTS_CNT - 3 + expect(experiments.uniq.length).to eq experiments.length + expect(experiments.first(2)).to eq [experiment_1, experiment_3] + expect(experiments).not_to include(experiment_2, experiment_4, experiment_5) + end + end + + context "with request parameters { sort: 'new' }" do + let(:params) { super().merge(sort: 'new') } + + it 'returns all active experiments, sorted by descending creation time attribute' do + experiments = @experiments_overview.experiments + expect(experiments.length).to eq EXPERIMENTS_CNT - 3 + expect(experiments.uniq.length).to eq experiments.length + expect(experiments.last(2)).to eq [experiment_3, experiment_1] + expect(experiments).not_to include(experiment_2, experiment_4, experiment_5) + end + end + + context "with request parameters { sort: 'atoz' }" do + let(:params) { super().merge(sort: 'atoz') } + + it 'returns all active experiments, sorted by ascending name attribute' do + experiments = @experiments_overview.experiments + expect(experiments.length).to eq EXPERIMENTS_CNT - 3 + expect(experiments.uniq.length).to eq experiments.length + expect(experiments.first(2)).to eq [experiment_3, experiment_1] + expect(experiments).not_to include(experiment_2, experiment_4, experiment_5) + end + end + + context "with request parameters { sort: 'ztoa' }" do + let(:params) { super().merge(sort: 'ztoa') } + + it 'returns all active experiments, sorted by descending name attribute' do + experiments = @experiments_overview.experiments + expect(experiments.length).to eq EXPERIMENTS_CNT - 3 + expect(experiments.uniq.length).to eq experiments.length + expect(experiments.last(2)).to eq [experiment_1, experiment_3] + expect(experiments).not_to include(experiment_2, experiment_4, experiment_5) + end + end + end + + context "with request parameters { filter: 'archived' }" do + let(:params) { super().merge(view_mode: 'archived') } + + it 'returns all archived experiments' do + experiments = @experiments_overview.experiments + expect(experiments.length).to eq 3 + expect(experiments.uniq.length).to eq experiments.length + expect(experiments).to include(experiment_2, experiment_4, experiment_5) + expect(experiments).not_to include(experiment_1, experiment_3) + end + + context "with request parameters { sort: 'old' }" do + let(:params) { super().merge(sort: 'old') } + + it 'returns all archived experiments, sorted by ascending creation time attribute' do + experiments = @experiments_overview.experiments + expect(experiments.length).to eq 3 + expect(experiments.uniq.length).to eq experiments.length + expect(experiments.first(3)).to eq [experiment_2, experiment_4, experiment_5] + expect(experiments).not_to include(experiment_1, experiment_3, experiment_6) + end + end + + context "with request parameters { sort: 'new' }" do + let(:params) { super().merge(sort: 'new') } + + it 'returns all archived experiments, sorted by descending creation time attribute' do + experiments = @experiments_overview.experiments + expect(experiments.length).to eq 3 + expect(experiments.uniq.length).to eq experiments.length + expect(experiments.last(3)).to eq [experiment_5, experiment_4, experiment_2] + expect(experiments).not_to include(experiment_1, experiment_3, experiment_6) + end + end + + context "with request parameters { sort: 'atoz' }" do + let(:params) { super().merge(sort: 'atoz') } + + it 'returns all archived experiments, sorted by ascending name attribute' do + experiments = @experiments_overview.experiments + expect(experiments.length).to eq 3 + expect(experiments.uniq.length).to eq experiments.length + expect(experiments.first(3)).to eq [experiment_4, experiment_2, experiment_5] + expect(experiments).not_to include(experiment_1, experiment_3, experiment_6) + end + end + + context "with request parameters { sort: 'ztoa' }" do + let(:params) { super().merge(sort: 'ztoa') } + + it 'returns all archived experiments, sorted by descending name attribute' do + experiments = @experiments_overview.experiments + expect(experiments.length).to eq 3 + expect(experiments.uniq.length).to eq experiments.length + expect(experiments.last(3)).to eq [experiment_5, experiment_2, experiment_4] + expect(experiments).not_to include(experiment_1, experiment_3, experiment_6) + end + end + end + end +end