From 02eb7df85cd7d414a01121e158aa320c0c82f539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alu=C3=ADzio=20Oliveira=20Gon=C3=A7alves=20Filho?= <80135392+aluizi0@users.noreply.github.com> Date: Sat, 18 Nov 2023 23:28:10 -0300 Subject: [PATCH 01/29] Create 4_Adicionar_experimento.feature --- .../4_Adicionar_experimento.feature | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 test/integration/4_Adicionar_experimento.feature diff --git a/test/integration/4_Adicionar_experimento.feature b/test/integration/4_Adicionar_experimento.feature new file mode 100644 index 00000000..840bfbe9 --- /dev/null +++ b/test/integration/4_Adicionar_experimento.feature @@ -0,0 +1,44 @@ +Funcionalidade: Criar um experimento baseado em planos locais + +**Contexto:** +Dado que sou um usuário +E estou logado na plataforma +E estou na tela para adicionar um novo experimento +E não existe nenhum robô + +**Cenário: Registrar um novo robô (Happy)** +Dado que o usuário deseja registrar um novo robô +Quando clico para adicionar os parâmetros (ID e nome) e confirmo +Então um novo robô é registrado com sucesso. + +**Cenário: Registrar um novo robô (Sad)** +Dado que o usuário deseja registrar um novo robô +Quando clico para adicionar os parâmetros (ID e nome) e confirmo +Então o robô não é adicionado devido a um erro ou à sintaxe inválida dos parâmetros. + +**Contexto:** +Dado que sou um usuário +E estou logado na plataforma +E estou na tela para adicionar um novo experimento +E já existem um ou mais robôs + +**Cenário: Registrar um novo robô (Sad - Robô Existente)** +Dado que o usuário deseja registrar um novo robô +Quando clico para adicionar os parâmetros (ID e nome) e confirmo +Então o robô não é adicionado devido a um erro ou ao fornecimento de um ID/nome já existente. + +**Contexto:** +Dado que sou um usuário +E estou logado na plataforma +E estou na tela para adicionar um novo experimento +E um robô já existe. + +**Cenário: Registrar um novo experimento (Happy)** +Dado que o usuário deseja adicionar um novo experimento +Quando forneço os parâmetros corretamente e especifico a quantidade de robôs +Então o robô executa a operação como esperado. + +**Cenário: Registrar um novo experimento (Sad)** +Dado que o usuário deseja adicionar um novo experimento +Quando forneço os parâmetros de forma inválida e especifico a quantidade de robôs +Então o robô não executa a operação como esperado. From 8034bea3108a210128e41c282c259c82671119f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alu=C3=ADzio=20Oliveira=20Gon=C3=A7alves=20Filho?= <80135392+aluizi0@users.noreply.github.com> Date: Sat, 18 Nov 2023 23:31:09 -0300 Subject: [PATCH 02/29] Create 5_Classificar_experimento.feature --- .../Survey/5_Classificar_experimento.feature | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 test/integration/frontend/tests/e2e/integration/Survey/5_Classificar_experimento.feature diff --git a/test/integration/frontend/tests/e2e/integration/Survey/5_Classificar_experimento.feature b/test/integration/frontend/tests/e2e/integration/Survey/5_Classificar_experimento.feature new file mode 100644 index 00000000..ed1d56a4 --- /dev/null +++ b/test/integration/frontend/tests/e2e/integration/Survey/5_Classificar_experimento.feature @@ -0,0 +1,26 @@ +Funcionalidade: Classificar experimentos com uma tag + +**Contexto:** +Dado que sou um usuário +E estou logado na plataforma +E estou na tela de lista dos experimentos + +**Cenário: Classificar um experimento (Happy)** +Dado que eu desejo classificar um novo experimento +Quando seleciono o experimento desejado e atribuo o nome da tag +Então o experimento é classificado com sucesso e a cor é atribuída ao experimento. + +**Cenário: Classificar um experimento (Sad)** +Dado que eu desejo classificar um novo experimento +Quando seleciono o experimento desejado e atribuo o nome da tag +Então o experimento não pode ser classificado, pois a tag não existe. + +**Cenário: Classificar um experimento (Sad)** +Dado que eu desejo classificar um novo experimento +Quando tento classificar um experimento que não existe +Então o sistema informa que o experimento não pode ser encontrado. + +**Cenário: Classificar um experimento (Sad)** +Dado que eu desejo classificar um novo experimento +Quando seleciono o experimento desejado e atribuo um nome de tag inválido +Então o experimento não pode ser classificado devido à tag inválida. From 012f86550571d45dec6f07905141afa5fd28e241 Mon Sep 17 00:00:00 2001 From: Aluizio Date: Sat, 18 Nov 2023 23:36:29 -0300 Subject: [PATCH 03/29] Adicionando arquivo 5_Classificar_experimento.feature --- .../e2e/integration/Survey => }/5_Classificar_experimento.feature | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/integration/{frontend/tests/e2e/integration/Survey => }/5_Classificar_experimento.feature (100%) diff --git a/test/integration/frontend/tests/e2e/integration/Survey/5_Classificar_experimento.feature b/test/integration/5_Classificar_experimento.feature similarity index 100% rename from test/integration/frontend/tests/e2e/integration/Survey/5_Classificar_experimento.feature rename to test/integration/5_Classificar_experimento.feature From 837db0c4005a50840c941efb26e49021e92a46de Mon Sep 17 00:00:00 2001 From: Fernando <45339608+fernandodealcantara@users.noreply.github.com> Date: Sat, 2 Dec 2023 06:53:26 +0000 Subject: [PATCH 04/29] Applied requested changes --- .../4_Adicionar_experimento.feature | 32 +++++++++---------- .../5_Classificar_experimento.feature | 13 +++----- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/test/integration/4_Adicionar_experimento.feature b/test/integration/4_Adicionar_experimento.feature index 840bfbe9..e56cfe7e 100644 --- a/test/integration/4_Adicionar_experimento.feature +++ b/test/integration/4_Adicionar_experimento.feature @@ -1,44 +1,44 @@ Funcionalidade: Criar um experimento baseado em planos locais -**Contexto:** +Contexto: Dado que sou um usuário E estou logado na plataforma E estou na tela para adicionar um novo experimento E não existe nenhum robô -**Cenário: Registrar um novo robô (Happy)** -Dado que o usuário deseja registrar um novo robô -Quando clico para adicionar os parâmetros (ID e nome) e confirmo +Cenário: Registrar um novo robô (Happy) +Dado que o usuário clique no botão adicionar um novo robô +Quando clico para registra o parâmetro nome e confirmo Então um novo robô é registrado com sucesso. -**Cenário: Registrar um novo robô (Sad)** -Dado que o usuário deseja registrar um novo robô -Quando clico para adicionar os parâmetros (ID e nome) e confirmo +Cenário: Registrar um novo robô (Sad) +Dado que o usuário clique no botão adicionar um novo robô +Quando clico para adicionar o parâmetro nome e confirmo Então o robô não é adicionado devido a um erro ou à sintaxe inválida dos parâmetros. -**Contexto:** +Contexto: Dado que sou um usuário E estou logado na plataforma E estou na tela para adicionar um novo experimento E já existem um ou mais robôs -**Cenário: Registrar um novo robô (Sad - Robô Existente)** +Cenário: Registrar um novo robô (Sad - Robô Existente) Dado que o usuário deseja registrar um novo robô -Quando clico para adicionar os parâmetros (ID e nome) e confirmo -Então o robô não é adicionado devido a um erro ou ao fornecimento de um ID/nome já existente. +Quando clico para adicionar o parâmetro nome e confirmo +Então o robô não é adicionado devido a um erro ou ao fornecimento de um nome já existente. -**Contexto:** +Contexto: Dado que sou um usuário E estou logado na plataforma E estou na tela para adicionar um novo experimento E um robô já existe. -**Cenário: Registrar um novo experimento (Happy)** +Cenário: Registrar um novo experimento (Happy) Dado que o usuário deseja adicionar um novo experimento Quando forneço os parâmetros corretamente e especifico a quantidade de robôs -Então o robô executa a operação como esperado. +Então os testes são gerados como esperado. -**Cenário: Registrar um novo experimento (Sad)** +Cenário: Registrar um novo experimento (Sad) Dado que o usuário deseja adicionar um novo experimento Quando forneço os parâmetros de forma inválida e especifico a quantidade de robôs -Então o robô não executa a operação como esperado. +Então os testes não são gerados como esperado. diff --git a/test/integration/5_Classificar_experimento.feature b/test/integration/5_Classificar_experimento.feature index ed1d56a4..66e9662f 100644 --- a/test/integration/5_Classificar_experimento.feature +++ b/test/integration/5_Classificar_experimento.feature @@ -1,26 +1,21 @@ Funcionalidade: Classificar experimentos com uma tag -**Contexto:** +Contexto: Dado que sou um usuário E estou logado na plataforma E estou na tela de lista dos experimentos -**Cenário: Classificar um experimento (Happy)** +Cenário: Classificar um experimento (Happy) Dado que eu desejo classificar um novo experimento Quando seleciono o experimento desejado e atribuo o nome da tag Então o experimento é classificado com sucesso e a cor é atribuída ao experimento. -**Cenário: Classificar um experimento (Sad)** +Cenário: Classificar um experimento (Sad) Dado que eu desejo classificar um novo experimento Quando seleciono o experimento desejado e atribuo o nome da tag Então o experimento não pode ser classificado, pois a tag não existe. -**Cenário: Classificar um experimento (Sad)** -Dado que eu desejo classificar um novo experimento -Quando tento classificar um experimento que não existe -Então o sistema informa que o experimento não pode ser encontrado. - -**Cenário: Classificar um experimento (Sad)** +Cenário: Classificar um experimento (Sad) Dado que eu desejo classificar um novo experimento Quando seleciono o experimento desejado e atribuo um nome de tag inválido Então o experimento não pode ser classificado devido à tag inválida. From 52f3fe37390d5af11bc5a60d14aaf6eea5c4463c Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 3 Dec 2023 08:15:55 -0300 Subject: [PATCH 05/29] Criar experimento Controladora de experimentos e endpoint para criar experimento --- app/controllers/experiment_controller.rb | 62 +++++++++++++++++++ app/helpers/experiment_helper.rb | 2 + config/routes.rb | 5 +- db/schema.rb | 2 + .../controllers/experiment_controller_test.rb | 9 +++ 5 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 app/controllers/experiment_controller.rb create mode 100644 app/helpers/experiment_helper.rb create mode 100644 test/controllers/experiment_controller_test.rb diff --git a/app/controllers/experiment_controller.rb b/app/controllers/experiment_controller.rb new file mode 100644 index 00000000..53e1af00 --- /dev/null +++ b/app/controllers/experiment_controller.rb @@ -0,0 +1,62 @@ +class ExperimentController < ApplicationController + skip_forgery_protection + def index + render + end + + def create + # Check if params are valid + if params['nameExperiment'].nil? || params['factors'].nil? || params['trials'].nil? + render json: { error: 'Invalid params' }, status: :unprocessable_entity + return + end + + # Check if factors are valid + factors_ = params['factors'] + factors_.each do |name, value| + if name.nil? || value.nil? + render json: { error: 'Invalid factors' }, status: :unprocessable_entity + return + end + end + + # Check if trials are valid + trials = params['trials'] + trials.each do |name, factors| + if name.nil? || factors.nil? + render json: { error: 'Invalid trials' }, status: :unprocessable_entity + return + end + factors.each do |factor| + if factors_[factor].nil? + render json: { error: 'Invalid trials' }, status: :unprocessable_entity + return + end + end + end + + # Save factors + factors = params['factors'] + saved_factors = {} + factors.each do |name, value| + saved_factors[name] = Factor.create(name: name, value: value) + end + + # Save experiment + experiment = Experiment.create(name: params['nameExperiment'], disabled: false) + + # Save trials + trials = params['trials'] + trials.each do |name, factors| + trial = Trial.create(name: name, experiment_id: experiment.id, disabled: false, deleted: false, runs: 0) + factors.each do |factor| + TrialFactor.create(trial_id: trial.id, factor_id: saved_factors[factor].id) + end + end + + render json: { message: 'success', experiment: experiment }, status: :ok + + rescue StandardError => e + render json: { error: e.message }, status: :unprocessable_entity + end +end diff --git a/app/helpers/experiment_helper.rb b/app/helpers/experiment_helper.rb new file mode 100644 index 00000000..438b6654 --- /dev/null +++ b/app/helpers/experiment_helper.rb @@ -0,0 +1,2 @@ +module ExperimentHelper +end diff --git a/config/routes.rb b/config/routes.rb index a27857a6..59acf59e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,9 @@ Rails.application.routes.draw do + resources :create_experiments get 'hello/world' - get 'hello/worldGet', to: "hello#index" + get 'hello/worldGet', to: "hello#index" + get 'experiment/create', to: "experiment#index" + post 'experiment/create', to: "experiment#create" # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. diff --git a/db/schema.rb b/db/schema.rb index 183d56d5..6763f89a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -44,8 +44,10 @@ create_table "trial_executions", force: :cascade do |t| t.string "status" t.text "log" + t.integer "trial_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["trial_id"], name: "index_trial_executions_on_trial_id" end create_table "trial_factors", force: :cascade do |t| diff --git a/test/controllers/experiment_controller_test.rb b/test/controllers/experiment_controller_test.rb new file mode 100644 index 00000000..4e9b0163 --- /dev/null +++ b/test/controllers/experiment_controller_test.rb @@ -0,0 +1,9 @@ +require "test_helper" + +class ExperimentControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end + + +end From 45faacc51045210a1d475afad254ee9d4eb4a349 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 3 Dec 2023 08:17:33 -0300 Subject: [PATCH 06/29] =?UTF-8?q?Testes=20do=20m=C3=A9todo=20de=20criar=20?= =?UTF-8?q?experimento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .rspec | 1 + Gemfile | 1 + Gemfile.lock | 18 ++++ .../controllers/experiment_controller_spec.rb | 31 ++++++ spec/rails_helper.rb | 65 +++++++++++++ spec/spec_helper.rb | 94 +++++++++++++++++++ 6 files changed, 210 insertions(+) create mode 100644 .rspec create mode 100644 spec/controllers/experiment_controller_spec.rb create mode 100644 spec/rails_helper.rb create mode 100644 spec/spec_helper.rb diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/Gemfile b/Gemfile index b4f7b825..ac9a049c 100644 --- a/Gemfile +++ b/Gemfile @@ -45,6 +45,7 @@ gem "bootsnap", require: false # gem "image_processing", "~> 1.2" group :development, :test do + gem 'rspec-rails' # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri mswin mswin64 mingw x64_mingw ] end diff --git a/Gemfile.lock b/Gemfile.lock index f5dbc2bb..67f5c029 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -243,6 +243,23 @@ GEM reline (0.4.0) io-console (~> 0.5) rexml (3.2.6) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.6) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-rails (6.1.0) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) + rspec-support (3.12.1) ruby2_keywords (0.0.5) rubyzip (2.3.2) selenium-webdriver (4.9.0) @@ -305,6 +322,7 @@ DEPENDENCIES jsbundling-rails puma (>= 5.0) rails (~> 7.1.1) + rspec-rails selenium-webdriver sprockets-rails sqlite3 (~> 1.4) diff --git a/spec/controllers/experiment_controller_spec.rb b/spec/controllers/experiment_controller_spec.rb new file mode 100644 index 00000000..136f8c56 --- /dev/null +++ b/spec/controllers/experiment_controller_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +RSpec.describe ExperimentController, type: :controller do + context 'POST #create' do + it 'should create an experiment' do + post :create, params: { nameExperiment: 'test', factors: { 'velocidade' => '15' }, trials: { 'test' => ['velocidade'] } } + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['message']).to eq('success') + end + + it 'should not create an experiment with a trial with invalid factor' do + post :create, params: { nameExperiment: 'test', factors: { 'velocidade' => '10' }, trials: { 'test' => ['velocidade2'] } } + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'should not create an experiment without trials' do + post :create, params: { nameExperiment: 'test', factors: { 'test' => 'test' } } + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'should not create an experiment without factors and trials' do + post :create, params: { nameExperiment: 'test' } + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'should not create an experiment without name' do + post :create, params: { factors: { 'test' => 'test' }, trials: { 'test' => ['test'] } } + expect(response).to have_http_status(:unprocessable_entity) + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 00000000..a15455f3 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,65 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Rails.root.glob('spec/support/**/*.rb').sort.each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_paths = [ + Rails.root.join('spec/fixtures') + ] + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://rspec.info/features/6-0/rspec-rails + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..327b58ea --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,94 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end From be2224e59b91e3f75ad2d51674d104891cb1591d Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 3 Dec 2023 08:18:24 -0300 Subject: [PATCH 07/29] Componente para criar experimento --- app/assets/stylesheets/application.css | 267 ++++++++++++++++++ .../react/src/components/CreateExperiment.jsx | 230 +++++++++++++++ app/javascript/react/src/index.js | 3 +- app/views/experiment/index.html.erb | 1 + 4 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 app/javascript/react/src/components/CreateExperiment.jsx create mode 100644 app/views/experiment/index.html.erb diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 288b9ab7..e6003baf 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -13,3 +13,270 @@ *= require_tree . *= require_self */ + +ul { + margin: 0; + padding: 0; + list-style-type: none; +} + +.outer-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + border-radius: 10px; /* Cantos arredondados de 10px */ + gap: 20px; /* Espaçamento entre os elementos */ + padding: 20px; /* Espaçamento interno */ +} + +.container { + padding: 0 20px; + background-color: #f5f5f5; + min-width: 300px; /* Ajustar a largura conforme necessário */ + border-radius: 8px; /* Cantos arredondados de 8px */ +} + +.container form { + display: flex; + flex-direction: column; + gap: 10px; /* Espaçamento entre os elementos */ + width: fit-content; /* Ajustar a largura conforme necessário */ + box-sizing: border-box; /* Incluir padding e border na largura e altura */ +} + +.outer-container h1, h2, h3, h4, h5, h6 { + font-family: "Roboto", sans-serif; /* Substitua pelo nome real da sua fonte */ + font-size: 24px; /* Tamanho da fonte do título */ + font-weight: bold; /* Negrito */ + color: #333; /* Cor do texto */ + margin-bottom: 20px; /* Espaçamento inferior para dar mais espaço */ + letter-spacing: 2px; /* Espaçamento entre as letras */ + line-height: 1.2; /* Altura da linha para melhor legibilidade */ + /* Adiciona bordas arredondadas às letras */ + border-radius: 10px; /* Valor de border-radius para ajustar a curvatura */ + display: inline-block; /* Para que o border-radius seja aplicado corretamente */ + background-color: #f0f0f0; /* Cor de fundo para cobrir os espaços vazios após arredondar */ + padding: 5px 10px; /* Adiciona algum preenchimento para espaçamento interno */ +} + +.outer-container { + font-family: "Roboto", sans-serif; /* Substitua pelo nome real da sua fonte */ + font-size: 15px; /* Tamanho da fonte do título */ + color: #333; /* Cor do texto */ + margin-bottom: 20px; /* Espaçamento inferior para dar mais espaço */ + letter-spacing: 2px; /* Espaçamento entre as letras */ + line-height: 1.2; /* Altura da linha para melhor legibilidade */ +} + +.container button { + width: fit-content; +} + +.container form div { + display: flex; + flex-direction: column; + flex-wrap: wrap; + gap: 5px; +} + +.list-container { + border-radius: 8px; + padding: 0 20px; + background-color: #f5f5f5; + min-width: 300px; /* Ajustar a largura conforme necessário */ + width: fit-content; /* Ajustar a largura conforme necessário */ + box-sizing: border-box; /* Incluir padding e border na largura e altura */ +} + +.list-container li { + display:flex; + flex-direction: row; + justify-content: space-between; + padding: 5px; /* Espaçamento interno */ + border-radius: 8px; /* Cantos arredondados de 8px */ +} + +.list-container li:nth-child(odd) { + background-color: lightgray; /* Set the background color for odd items */ +} + +.trial-container { + display: flex; + flex-direction: column; + gap: 5px; + padding: 0 20px; + min-width: 300px; /* Ajustar a largura conforme necessário */ + border-radius: 8px; /* Cantos arredondados de 8px */ + background-color: #f5f5f5; +} + +.trail { + display:flex; + flex-direction: column; + justify-content: space-between; + padding: 5px; /* Espaçamento interno */ + border-radius: 8px; /* Cantos arredondados de 8px */ +} + +.trial-container .trail:nth-child(odd) { + background-color: lightgray; /* Set the background color for odd items */ +} + +.trial-container .trail:nth-child(even) { + background-color: #f0f0f0; /* Set the background color for odd items */ +} + +.trial-container .trail:nth-child(even) .trail-row { + background-color: lightgray; /* Set the background color for odd items */ +} +.trial-container .trail:nth-child(odd) .trail-row { + background-color: #f0f0f0; /* Set the background color for odd items */ +} + +.trail-row { + padding: 5px; +} + +.trail-row > div { + display: flex; + flex-direction: row; + gap: 5px; +} + +.trail-row { + display:flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + border-radius: 8px; /* Cantos arredondados de 8px */ + margin-bottom: 10px; +} + +.factor-trail { + display:flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + border-radius: 8px; /* Cantos arredondados de 8px */ + margin-bottom: 10px; + padding: 5px; /* Espaçamento interno */ +} + +.trail .factor-trail:nth-child(odd) { + background-color: #f0f0f0;; /* Set the background color for odd items */ +} + +.trail .factor-trail:nth-child(even) { + background-color: lightgray; /* Set the background color for odd items */ +} + +.label-title { + font-family: "Roboto", sans-serif; /* Substitua pelo nome real da sua fonte */ + font-size: 24px; /* Tamanho da fonte do título */ + font-weight: bold; /* Negrito */ + color: #333; /* Cor do texto */ + margin-bottom: 20px; /* Espaçamento inferior para dar mais espaço */ + letter-spacing: 2px; /* Espaçamento entre as letras */ + line-height: 1.2; /* Altura da linha para melhor legibilidade */ + /* Adiciona bordas arredondadas às letras */ + border-radius: 10px; /* Valor de border-radius para ajustar a curvatura */ + display: inline-block; /* Para que o border-radius seja aplicado corretamente */ + background-color: #f0f0f0; /* Cor de fundo para cobrir os espaços vazios após arredondar */ + padding: 5px 10px; /* Adiciona algum preenchimento para espaçamento interno */ +} + +.label-menor { + font-family: "Roboto", sans-serif; /* Substitua pelo nome real da sua fonte */ + font-size: 15px; /* Tamanho menor da fonte */ + color: #333; /* Cor do texto para diferenciar */ + margin-bottom: 13px; /* Espaçamento inferior menor */ + letter-spacing: 0.5px; /* Espaçamento entre as letras */ + line-height: 1.5; /* Altura da linha para melhor legibilidade */ +} + +.input-arredondado { + font-family: "Roboto", sans-serif; /* Substitua pelo nome real da sua fonte */ + font-size: 10px; /* Tamanho da fonte do input */ + padding: 10px; /* Espaçamento interno */ + border: 1px solid #ddd; /* Cor da borda */ + border-radius: 8px; /* Cantos arredondados */ + outline: none; /* Remover a borda de foco padrão */ + box-sizing: border-box; /* Incluir padding e border na largura e altura */ + transition: border-color 0.3s ease; /* Adiciona uma transição suave para a cor da borda */ +} + +.input-arredondado:focus { + border-color: #3498db; /* Cor da borda quando em foco */ +} + +.botao-responsivo { + font-family: "Roboto", sans-serif; + font-size: 9px; + padding: 5px 10px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s ease; /* Adiciona uma transição suave para a cor de fundo */ + + /* Adiciona estilos padrão */ + background-color: #3498db; + color: #fff; +} + +/* Efeito quando o mouse passa por cima */ +.botao-responsivo:hover { + background-color: #2980b9; /* Nova cor de fundo ao passar o mouse */ +} + +.botao-responsivo-secundario { + font-family: "Roboto", sans-serif; + font-size: 9px; + padding: 5px 10px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s ease; /* Adiciona uma transição suave para a cor de fundo */ + + /* Adiciona estilos padrão */ + background-color: #f0f0f0; + color: #333; +} + +/* Efeito quando o mouse passa por cima */ +.botao-responsivo-secundario:hover { + background-color: #ddd; /* Nova cor de fundo ao passar o mouse */ +} + +/* DROPDOWN PARA SELECIONAR OS FATORES */ +.dropdown { + position: relative; + display: inline-block; +} + +.dropdown-content { + display: none; + position: absolute; + background-color: #f1f1f1; + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + z-index: 1; + border-radius: 8px; + padding: 5px; +} + +.dropdown-content span { + color: black; + padding: 2px 10px; + text-decoration: none; + display: block; + cursor: pointer; + border-radius: 8px; +} + +.dropdown-content span:hover { + background-color: #ddd; +} + +.dropdown:hover .dropdown-content { + display: block; +} diff --git a/app/javascript/react/src/components/CreateExperiment.jsx b/app/javascript/react/src/components/CreateExperiment.jsx new file mode 100644 index 00000000..2b5a9940 --- /dev/null +++ b/app/javascript/react/src/components/CreateExperiment.jsx @@ -0,0 +1,230 @@ +import React, { useEffect, useState } from "react"; + +const CreateExperiment = () => { + const [nameExperiment, setNameExperiment] = useState(localStorage.getItem("nameExperiment") || ""); + const [factors, setFactors] = useState(JSON.parse(localStorage.getItem("factors")) || {}); + const [trials, setTrials] = useState(JSON.parse(localStorage.getItem("trials")) || {}); + + addFactor = (name, value) => { + // if name and value are not empty and name is not already a factor + if (name.length > 0 && value.length > 0 && !Object.keys(factors).includes(name)) { + setFactors({ ...factors, [name]: value }); + } + }; + + removeFactor = (name) => { + const newFactors = { ...factors }; + + // remove factor from trials + const newTrials = { ...trials }; + Object.keys(trials).forEach((trial) => { + if (newTrials[trial].includes(name)) { + newTrials[trial].splice(newTrials[trial].indexOf(name), 1); + } + }); + + delete newFactors[name]; + + setFactors(newFactors); + setTrials(newTrials); + }; + + addTrial = (name) => { + if (name.length > 0 && !Object.keys(trials).includes(name)) { + setTrials({ ...trials, [name]: [] }); + } + }; + + removeTrial = (name) => { + const newTrials = { ...trials }; + delete newTrials[name]; + setTrials(newTrials); + }; + + addFactorToTrial = (trialName, factorName) => { + const newTrials = { ...trials }; + // add if not already in trial and factor exists + if (!newTrials[trialName]?.includes(factorName) && Object.keys(factors).includes(factorName)) { + newTrials[trialName].push(factorName); + } + setTrials(newTrials); + }; + + removeFactorFromTrial = (trialName, factorName) => { + const newTrials = { ...trials }; + if (newTrials[trialName]?.includes(factorName)) { + newTrials[trialName].splice(newTrials[trialName].indexOf(factorName), 1); + } + setTrials(newTrials); + }; + + submitExperiment = () => { + const data = { nameExperiment, factors, trials }; + + // post /experiments + fetch("/experiment/create", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + credentials: "same-origin", + }) + .then(async (res) => { + const parse = await res.text(); + console.log(parse); + return parse; + }) + .then((res) => { + console.log(res); + // clearExperiment(); + }); + }; + + clearExperiment = () => { + setNameExperiment(""); + setFactors({}); + setTrials({}); + }; + + // Cache values in local storage so they are not lost on page refresh + useEffect(() => { + localStorage.setItem("nameExperiment", nameExperiment); + localStorage.setItem("factors", JSON.stringify(factors)); + localStorage.setItem("trials", JSON.stringify(trials)); + }, [nameExperiment, factors, trials]); + + return ( +
+
+

Criar Experimento

+
+
+ setNameExperiment(e.target.value)} + /> +
+

Adicionar fator

+
+ + + +
+

Adicionar teste

+
+ + +
+ + +
+
+
+

Fatores

+
    + {Object.keys(factors).map((factor) => ( +
  • + {factor}: {factors[factor]}{" "} + +
  • + ))} +
+
+
+

Testes

+ {Object.keys(trials).map((trial) => ( +
+
+ {trial} +
+
+
+ Adicionar fator +
+
+ {Object.keys(factors).map((factor) => ( + addFactorToTrial(trial, factor)} + > + {factor} + + ))} +
+
+ +
+
+ {trials[trial].map((factor) => ( +
+ {factor} + +
+ ))} +
+ ))} +
+
+ ); +}; + +export default CreateExperiment; diff --git a/app/javascript/react/src/index.js b/app/javascript/react/src/index.js index a9759c53..aacf9634 100644 --- a/app/javascript/react/src/index.js +++ b/app/javascript/react/src/index.js @@ -1,5 +1,6 @@ import { define } from 'remount' import Hello from "./components/Hello" import Graph from "./components/Graph" +import CreateExperiment from "./components/CreateExperiment" -define({ 'hello-component': Hello, 'graph-component': Graph }) +define({ 'hello-component': Hello, 'graph-component': Graph, 'create-experiment-component': CreateExperiment }) \ No newline at end of file diff --git a/app/views/experiment/index.html.erb b/app/views/experiment/index.html.erb new file mode 100644 index 00000000..e3a951c7 --- /dev/null +++ b/app/views/experiment/index.html.erb @@ -0,0 +1 @@ + \ No newline at end of file From d62b8577eb7410c19e4e9bb734b2b0ad3458dfa1 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 3 Dec 2023 14:08:07 -0300 Subject: [PATCH 08/29] atualiza testes de criar o experimento --- .../controllers/experiment_controller_spec.rb | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/spec/controllers/experiment_controller_spec.rb b/spec/controllers/experiment_controller_spec.rb index 136f8c56..5d6aff08 100644 --- a/spec/controllers/experiment_controller_spec.rb +++ b/spec/controllers/experiment_controller_spec.rb @@ -3,29 +3,38 @@ RSpec.describe ExperimentController, type: :controller do context 'POST #create' do it 'should create an experiment' do - post :create, params: { nameExperiment: 'test', factors: { 'velocidade' => '15' }, trials: { 'test' => ['velocidade'] } } + post :create, params: { experimentName: 'test', factors: { 'velocidade' => ['1', '2'], 'temperatura' => ['4', '5'] } } + expect(response).to have_http_status(:ok) expect(JSON.parse(response.body)['message']).to eq('success') end - it 'should not create an experiment with a trial with invalid factor' do - post :create, params: { nameExperiment: 'test', factors: { 'velocidade' => '10' }, trials: { 'test' => ['velocidade2'] } } + it 'should not create an experiment if experimentName is nil' do + post :create, params: {factors: { 'velocidade' => ['1', '2'], 'temperatura' => ['4', '5'] } } + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') end - it 'should not create an experiment without trials' do - post :create, params: { nameExperiment: 'test', factors: { 'test' => 'test' } } + it 'should not create an experiment if factors is nil' do + post :create, params: { experimentName: 'test' } + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') end - it 'should not create an experiment without factors and trials' do - post :create, params: { nameExperiment: 'test' } + it 'should not create an experiment if factors is not a hash' do + post :create, params: { experimentName: 'test', factors: 'velocidade' } + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') end - it 'should not create an experiment without name' do - post :create, params: { factors: { 'test' => 'test' }, trials: { 'test' => ['test'] } } + it 'should not create an experiment if factors is a hash with values that are not arrays' do + post :create, params: { experimentName: 'test', factors: { 'velocidade' => '1', 'temperatura' => ['4', '5'] } } + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') end end end From 6fa7fa2f01f037f2e2802a1ad89dd0466f7e6511 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 3 Dec 2023 14:08:26 -0300 Subject: [PATCH 09/29] BDD criar o experimento --- features/4_Adicionar_experimento.feature | 18 +++++++ .../4_Adicionar_experimento_step.rb | 50 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 features/4_Adicionar_experimento.feature create mode 100644 features/step_definitions/4_Adicionar_experimento_step.rb diff --git a/features/4_Adicionar_experimento.feature b/features/4_Adicionar_experimento.feature new file mode 100644 index 00000000..cf559908 --- /dev/null +++ b/features/4_Adicionar_experimento.feature @@ -0,0 +1,18 @@ +Feature: Add a new experiment + @javascript + Scenario: Add a new experiment (Happy) + When the user is on the add experiment screen + And the user clicks on the experiment name field and types "Experiment A" + And the user clicks on the factor name field and types "Factor 1" + And the user clicks on the add factor button + And the user clicks on the factor value field and types "Value 1" + And the user clicks on the add value button + And the user clicks on the create experiment button + Then the user should see the message "Experimento criado com sucesso!" + + @javascript + Scenario: Add a new experiment (Sad) + When the user is on the add experiment screen - fail + And the user dont fill the fields + And the user tries to clicks on the create experiment button + Then the user should see the fail message "Erro ao criar o experimento!" \ No newline at end of file diff --git a/features/step_definitions/4_Adicionar_experimento_step.rb b/features/step_definitions/4_Adicionar_experimento_step.rb new file mode 100644 index 00000000..0c0b81e5 --- /dev/null +++ b/features/step_definitions/4_Adicionar_experimento_step.rb @@ -0,0 +1,50 @@ +When("the user is on the add experiment screen") do + visit '/experiment/create' +end + +And("the user clicks on the experiment name field and types {string}") do |string| + fill_in 'experimentName', with: string +end + +And("the user clicks on the factor name field and types {string}") do |string| + fill_in 'factorName', with: string +end + +And("the user clicks on the add factor button") do + click_button 'addFactor' +end + + +And("the user clicks on the factor value field and types {string}") do |string| + fill_in 'factorValue', with: string +end + +And("the user clicks on the add value button") do + find(:css, '.trail-row .botao-responsivo').click +end + +And("the user clicks on the create experiment button") do + click_button 'createExperiment' +end + +Then("the user should see the message {string}") do |string| + expect(page).to have_content(string) +end + +When("the user is on the add experiment screen - fail") do + visit '/experiment/create' +end + +And("the user dont fill the fields") do + fill_in 'experimentName', with: '' + fill_in 'factorName', with: '' + fill_in 'factorValue', with: '' +end + +And("the user tries to clicks on the create experiment button") do + click_button 'createExperiment' +end + +Then("the user should see the fail message {string}") do |string| + expect(page).to have_content(string) +end \ No newline at end of file From ab5579bf1994b50af8d7a4d73ef79f29bc2b9671 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 3 Dec 2023 14:09:54 -0300 Subject: [PATCH 10/29] Atualiza componente de criar o experimento MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove criação manual das trials --- app/assets/stylesheets/application.css | 65 +++--- .../react/src/components/CreateExperiment.jsx | 191 +++++++----------- bun.lockb | Bin 4660 -> 5086 bytes package.json | 1 + 4 files changed, 103 insertions(+), 154 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index e6003baf..9d82a19e 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -49,7 +49,8 @@ ul { font-size: 24px; /* Tamanho da fonte do título */ font-weight: bold; /* Negrito */ color: #333; /* Cor do texto */ - margin-bottom: 20px; /* Espaçamento inferior para dar mais espaço */ + margin-top: 5px; + margin-bottom: 5px; /* Espaçamento inferior para dar mais espaço */ letter-spacing: 2px; /* Espaçamento entre as letras */ line-height: 1.2; /* Altura da linha para melhor legibilidade */ /* Adiciona bordas arredondadas às letras */ @@ -59,11 +60,14 @@ ul { padding: 5px 10px; /* Adiciona algum preenchimento para espaçamento interno */ } +.outer-container p { + margin: 5px 0; /* Espaçamento superior e inferior */ +} + .outer-container { font-family: "Roboto", sans-serif; /* Substitua pelo nome real da sua fonte */ font-size: 15px; /* Tamanho da fonte do título */ color: #333; /* Cor do texto */ - margin-bottom: 20px; /* Espaçamento inferior para dar mais espaço */ letter-spacing: 2px; /* Espaçamento entre as letras */ line-height: 1.2; /* Altura da linha para melhor legibilidade */ } @@ -100,6 +104,30 @@ ul { background-color: lightgray; /* Set the background color for odd items */ } +.outer-container { + padding: 20px; + border-radius: 4px; + border: 1px solid #ddd; +} + +.outer-container h2 { + font-size: 1.5em; + margin-bottom: 10px; +} + +.outer-container ul { + list-style: none; + padding: 0; + margin: 0; +} + +.outer-container li { + margin-bottom: 5px; + padding: 5px; + border-radius: 4px; + background-color: #f5f5f5; +} + .trial-container { display: flex; flex-direction: column; @@ -247,36 +275,3 @@ ul { background-color: #ddd; /* Nova cor de fundo ao passar o mouse */ } -/* DROPDOWN PARA SELECIONAR OS FATORES */ -.dropdown { - position: relative; - display: inline-block; -} - -.dropdown-content { - display: none; - position: absolute; - background-color: #f1f1f1; - min-width: 160px; - box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); - z-index: 1; - border-radius: 8px; - padding: 5px; -} - -.dropdown-content span { - color: black; - padding: 2px 10px; - text-decoration: none; - display: block; - cursor: pointer; - border-radius: 8px; -} - -.dropdown-content span:hover { - background-color: #ddd; -} - -.dropdown:hover .dropdown-content { - display: block; -} diff --git a/app/javascript/react/src/components/CreateExperiment.jsx b/app/javascript/react/src/components/CreateExperiment.jsx index 2b5a9940..8f23e2cc 100644 --- a/app/javascript/react/src/components/CreateExperiment.jsx +++ b/app/javascript/react/src/components/CreateExperiment.jsx @@ -1,65 +1,47 @@ import React, { useEffect, useState } from "react"; +import toast from "react-simple-toasts"; +import 'react-simple-toasts/dist/theme/success.css'; +import 'react-simple-toasts/dist/theme/failure.css'; + const CreateExperiment = () => { - const [nameExperiment, setNameExperiment] = useState(localStorage.getItem("nameExperiment") || ""); + const [experimentName, setExperimentName] = useState( + localStorage.getItem("experimentName") || "" + ); const [factors, setFactors] = useState(JSON.parse(localStorage.getItem("factors")) || {}); - const [trials, setTrials] = useState(JSON.parse(localStorage.getItem("trials")) || {}); + const [value, setValue] = useState(""); - addFactor = (name, value) => { - // if name and value are not empty and name is not already a factor - if (name.length > 0 && value.length > 0 && !Object.keys(factors).includes(name)) { - setFactors({ ...factors, [name]: value }); + addFactor = (name) => { + if (name.length > 0 && !Object.keys(factors).includes(name)) { + setFactors({ ...factors, [name]: [] }); } }; removeFactor = (name) => { const newFactors = { ...factors }; - - // remove factor from trials - const newTrials = { ...trials }; - Object.keys(trials).forEach((trial) => { - if (newTrials[trial].includes(name)) { - newTrials[trial].splice(newTrials[trial].indexOf(name), 1); - } - }); - delete newFactors[name]; - setFactors(newFactors); - setTrials(newTrials); }; - addTrial = (name) => { - if (name.length > 0 && !Object.keys(trials).includes(name)) { - setTrials({ ...trials, [name]: [] }); + addValueToFactor = (factorName, value) => { + const newFactors = { ...factors }; + if (newFactors[factorName] && !newFactors[factorName].includes(value) && value.length > 0) { + newFactors[factorName].push(value); } + setFactors(newFactors); }; - removeTrial = (name) => { - const newTrials = { ...trials }; - delete newTrials[name]; - setTrials(newTrials); - }; - - addFactorToTrial = (trialName, factorName) => { - const newTrials = { ...trials }; - // add if not already in trial and factor exists - if (!newTrials[trialName]?.includes(factorName) && Object.keys(factors).includes(factorName)) { - newTrials[trialName].push(factorName); + removeValueFromFactor = (factorName, value) => { + const newFactors = { ...factors }; + if (newFactors[factorName]?.includes(value)) { + newFactors[factorName] = newFactors[factorName].filter((v) => v !== value); } - setTrials(newTrials); + setFactors(newFactors); }; - removeFactorFromTrial = (trialName, factorName) => { - const newTrials = { ...trials }; - if (newTrials[trialName]?.includes(factorName)) { - newTrials[trialName].splice(newTrials[trialName].indexOf(factorName), 1); - } - setTrials(newTrials); - }; submitExperiment = () => { - const data = { nameExperiment, factors, trials }; + const data = { experimentName, factors }; // post /experiments fetch("/experiment/create", { @@ -71,28 +53,36 @@ const CreateExperiment = () => { credentials: "same-origin", }) .then(async (res) => { - const parse = await res.text(); + const parse = JSON.parse(await res.text()); console.log(parse); return parse; }) .then((res) => { - console.log(res); - // clearExperiment(); - }); + if (res?.error) { + toast("Erro ao criar o experimento!", { + position: "top-right", theme: "failure" + }) + return; + } + toast("Experimento criado com sucesso!", { position: "top-right", theme: "success" }); + clearExperiment(); + }).catch((err) => { + toast("Erro ao criar o experimento!", { + position: "top-right", theme: "failure" + }) + }) }; clearExperiment = () => { - setNameExperiment(""); + setExperimentName(""); setFactors({}); - setTrials({}); }; // Cache values in local storage so they are not lost on page refresh useEffect(() => { - localStorage.setItem("nameExperiment", nameExperiment); + localStorage.setItem("experimentName", experimentName); localStorage.setItem("factors", JSON.stringify(factors)); - localStorage.setItem("trials", JSON.stringify(trials)); - }, [nameExperiment, factors, trials]); + }, [experimentName, factors]); return (
@@ -101,57 +91,46 @@ const CreateExperiment = () => {
setNameExperiment(e.target.value)} + value={experimentName} + onChange={(e) => setExperimentName(e.target.value)} />
-

Adicionar fator

+

Adicionar fator

- -
-

Adicionar teste

-
+

Valor para fator

setValue(e.target.value)} /> -
-
-
-

Fatores

-
    - {Object.keys(factors).map((factor) => ( -
  • - {factor}: {factors[factor]}{" "} - -
  • - ))} -
-
-

Testes

- {Object.keys(trials).map((trial) => ( -
+

Fatores

+ {Object.keys(factors).map((factor) => ( +
- {trial} + {factor}
-
-
- Adicionar fator -
-
- {Object.keys(factors).map((factor) => ( - addFactorToTrial(trial, factor)} - > - {factor} - - ))} -
+
addValueToFactor(factor, value)} + role="button" + className="botao-responsivo" + > + Adicionar valor ao fator
- {trials[trial].map((factor) => ( -
- {factor} + {factors[factor].map((value) => ( +
+ {value} diff --git a/bun.lockb b/bun.lockb index fbc672dd154cb18acb41450a2a515be6a9e0742e..a65d1c3340d1bb7269a66b2f66fea919af9925a7 100755 GIT binary patch delta 1031 zcmbVKT}V_>5WeTE>;B!{y{6Ib;vY+7Dtq;2`M0^dWfmnRkzzp^mWC^a)^6Y;(i&2v z5ZXML1SS?`L?!J)t+ZlK;X~RV_7b)I5JJ7wgQR6P$L@V8BWPgw&dhu>XU?1nuYQ%$ zBW9lMnesPmKUlWcmPYpJ`e&Ohd#;qeOs*<#dUN9Qz2Wn_61gSed#lsve!jPLtrSHE z&vC&j!f`sp3A7pJA|gAu0&yi`fN~t3gN?lt+qCb!6|HTNF%bmPKuRG%+~gEQh+F*> z*?ABpPO2tQOkKJfEL)6eanL~u$OVW{xA7VJD$u7!vK}Oo4J|}V?G)yzi*!Sg%B_Bk zw{3(4>ZH61MpeZp>Qmp+YG$Jx#$owW$#usE{Phs*qGT%Sy=l|aY zq5il|>alc_hKTi2)A8(CCC(9f~Cn8%3}>O zsy%^aD<8mytAU+-hK+p(v-GGeaYRn)G{b2=3&MPzRaq!ycPQgCfmKFFNdOOQ?J{I+ zvpDCyXDW{fM2RSiN?#lc_HA01O-YR;N+On>fDU8cj=#s1)t(vdGEYy(w@^}s>w#|# zE}sK2KFKW*SBaD_7K*ulwxmQF@d;)u^X1U(oSQYV-Plrrn8#WQK#Iw#pTr=ONi%+g zNB>newkxrxW?YY+P!KJU?%rh13Jxz^|Rg<_@~#IZ~u$C0y)*ResAKosnu IZs9ul3HssU1^@s6 delta 663 zcma)3J!n%=6u##sG4H+Py_cle5(=h+v_@1)D-{~a3mqzLkhY7(DO)HAwu?|fFoF&( zB6)li2m7-)lq?BNX_rzvwu6I+(WRXWopezLwdcfF2M6)M$9K-T-#wh4qrT0H)oa<# z&*2qa-+Jw6cJI7jAwJKh^jUEn+`v4i8yxSLl Date: Sun, 3 Dec 2023 15:03:32 -0300 Subject: [PATCH 11/29] =?UTF-8?q?Atualiza=20cria=C3=A7=C3=A3o=20do=20exper?= =?UTF-8?q?imento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Realiza as permutações do ensaio --- app/controllers/experiment_controller.rb | 78 +++++++++++++----------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/app/controllers/experiment_controller.rb b/app/controllers/experiment_controller.rb index 53e1af00..9ce2f5e4 100644 --- a/app/controllers/experiment_controller.rb +++ b/app/controllers/experiment_controller.rb @@ -4,53 +4,53 @@ def index render end - def create + def create # Check if params are valid - if params['nameExperiment'].nil? || params['factors'].nil? || params['trials'].nil? + if params['experimentName'].nil? || params['factors'].nil? render json: { error: 'Invalid params' }, status: :unprocessable_entity return end - # Check if factors are valid - factors_ = params['factors'] - factors_.each do |name, value| - if name.nil? || value.nil? - render json: { error: 'Invalid factors' }, status: :unprocessable_entity - return - end + # Check if factors is a hash with arrays as values + if params['factors'].class != ActionController::Parameters || params['factors'].values.any? { |value| value.class != Array } + puts params['factors'].class + render json: { error: 'Invalid params' }, status: :unprocessable_entity + return end - # Check if trials are valid - trials = params['trials'] - trials.each do |name, factors| - if name.nil? || factors.nil? - render json: { error: 'Invalid trials' }, status: :unprocessable_entity - return - end - factors.each do |factor| - if factors_[factor].nil? - render json: { error: 'Invalid trials' }, status: :unprocessable_entity - return + # Save experiment + experiment = Experiment.create(name: params['experimentName'], disabled: false) + + # Save factors (should not save if factor with same name and value already exists) + params['factors'].each do |factor| + factor[1].each do |value| + if Factor.where(name: factor[0], value: value).empty? + Factor.create(name: factor[0], value: value) end end end - # Save factors - factors = params['factors'] - saved_factors = {} - factors.each do |name, value| - saved_factors[name] = Factor.create(name: name, value: value) - end + # Generate all possible combinations of factors and save them + # as [{factor1: value1, factor2: value1}, {factor1: value1, factor2: value2}, ...] - # Save experiment - experiment = Experiment.create(name: params['nameExperiment'], disabled: false) - - # Save trials - trials = params['trials'] - trials.each do |name, factors| - trial = Trial.create(name: name, experiment_id: experiment.id, disabled: false, deleted: false, runs: 0) - factors.each do |factor| - TrialFactor.create(trial_id: trial.id, factor_id: saved_factors[factor].id) + # Get all factors from the request + factors = params['factors'].keys + + # Get all values from the request + values = params['factors'].values + + # Get all possible combinations of values + combinations = values[0].product(*values[1..-1]) + + # Save all combinations + combinations.each do |combination| + # Create trial + trial = Trial.create(name: combination.join('-'), disabled: false, deleted: false, runs: 0, experiment_id: experiment.id) + + # Create trial factors + combination.each do |value| + factor = Factor.where(name: factors[combination.index(value)], value: value).first + TrialFactor.create(factor_id: factor.id, trial_id: trial.id) end end @@ -59,4 +59,12 @@ def create rescue StandardError => e render json: { error: e.message }, status: :unprocessable_entity end + + + # GET all experiments + def get_all + experiments = Experiment.all + render json: { experiments: experiments }, status: :ok + end + end From 33aabb2b88e4539b308a0fc4fc2288958ea30bb1 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 3 Dec 2023 15:23:51 -0300 Subject: [PATCH 12/29] Tabela de tags de experimentos --- db/migrate/20231203174203_create_experiment_tags.rb | 9 +++++++++ db/schema.rb | 11 ++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20231203174203_create_experiment_tags.rb diff --git a/db/migrate/20231203174203_create_experiment_tags.rb b/db/migrate/20231203174203_create_experiment_tags.rb new file mode 100644 index 00000000..204f79d8 --- /dev/null +++ b/db/migrate/20231203174203_create_experiment_tags.rb @@ -0,0 +1,9 @@ +class CreateExperimentTags < ActiveRecord::Migration[7.1] + def change + create_table :experiment_tags do |t| + t.belongs_to :experiment + t.belongs_to :tag + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 6763f89a..df8612a9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2023_11_13_225719) do +ActiveRecord::Schema[7.1].define(version: 2023_12_03_174203) do create_table "classifications", force: :cascade do |t| t.integer "trial_id" t.integer "tag_id" @@ -20,6 +20,15 @@ t.index ["trial_id"], name: "index_classifications_on_trial_id" end + create_table "experiment_tags", force: :cascade do |t| + t.integer "experiment_id" + t.integer "tag_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["experiment_id"], name: "index_experiment_tags_on_experiment_id" + t.index ["tag_id"], name: "index_experiment_tags_on_tag_id" + end + create_table "experiments", force: :cascade do |t| t.string "name" t.boolean "disabled" From 8fd618ce3724bc2f57a0f9259466cea191e68331 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 3 Dec 2023 15:25:36 -0300 Subject: [PATCH 13/29] =?UTF-8?q?Atualiza=20rela=C3=A7=C3=B5es=20do=20expe?= =?UTF-8?q?rimento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/experiment.rb | 1 + app/models/experiment_tag.rb | 4 ++++ app/models/tag.rb | 1 + 3 files changed, 6 insertions(+) create mode 100644 app/models/experiment_tag.rb diff --git a/app/models/experiment.rb b/app/models/experiment.rb index 5cf63272..46ba6406 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -1,3 +1,4 @@ class Experiment < ApplicationRecord has_many :trials + has_many :tags, through: :experiment_tag end diff --git a/app/models/experiment_tag.rb b/app/models/experiment_tag.rb new file mode 100644 index 00000000..8f6879bc --- /dev/null +++ b/app/models/experiment_tag.rb @@ -0,0 +1,4 @@ +class ExperimentTag < ApplicationRecord + belongs_to :experiment + belongs_to :tag +end diff --git a/app/models/tag.rb b/app/models/tag.rb index 98eba85b..5843a35c 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -1,3 +1,4 @@ class Tag < ApplicationRecord has_many :trials, through: :classification + has_many :experiments, through: :experiment_tag end From d9fe44fd7f72b71ee1f02aa71dd2d693dd517ebe Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 3 Dec 2023 15:26:20 -0300 Subject: [PATCH 14/29] Tag endpoints --- app/controllers/experiment_controller.rb | 33 ++++++++++++++++++++++++ app/controllers/tags_controller.rb | 16 ++++++++++++ config/routes.rb | 5 ++++ 3 files changed, 54 insertions(+) create mode 100644 app/controllers/tags_controller.rb diff --git a/app/controllers/experiment_controller.rb b/app/controllers/experiment_controller.rb index 9ce2f5e4..c1c51f3a 100644 --- a/app/controllers/experiment_controller.rb +++ b/app/controllers/experiment_controller.rb @@ -67,4 +67,37 @@ def get_all render json: { experiments: experiments }, status: :ok end + def add_tag + # Check if params are valid + if params['experiment_id'].nil? || params['tag_id'].nil? + render json: { error: 'Invalid params' }, status: :unprocessable_entity + return + end + + # Check if experiment exists + if Experiment.where(id: params['experiment_id']).empty? + render json: { error: 'Experiment not found' }, status: :not_found + return + end + + # Check if tag exists + if Tag.where(id: params['tag_id']).empty? + render json: { error: 'Tag not found' }, status: :not_found + return + end + + # Check if experiment already has tag + if ExperimentTag.where(experiment_id: params['experiment_id'], tag_id: params['tag_id']).any? + render json: { error: 'Experiment already has tag' }, status: :unprocessable_entity + return + end + + # Add tag to experiment + experiment_tag = ExperimentTag.create(experiment_id: params['experiment_id'], tag_id: params['tag_id']) + render json: { message: 'success', experiment_tag: experiment_tag }, status: :ok + + rescue StandardError => e + render json: { error: e.message }, status: :unprocessable_entity + end + end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb new file mode 100644 index 00000000..ef6a8073 --- /dev/null +++ b/app/controllers/tags_controller.rb @@ -0,0 +1,16 @@ +class TagsController < ApplicationController + skip_forgery_protection + # get all tags + def get_all + render json: Tag.all + end + + def create + tag = Tag.new(params.require(:tag).permit(:name, :color)) + if tag.save + render json: tag + else + render json: tag.errors + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 59acf59e..085d33ed 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,6 +4,11 @@ get 'hello/worldGet', to: "hello#index" get 'experiment/create', to: "experiment#index" post 'experiment/create', to: "experiment#create" + get 'experiment/classification', to: "classification#index" + get 'experiment/get_all', to: "experiment#get_all" + post 'tags/create', to: "tags#create" + get 'tags/get_all', to: "tags#get_all" + post 'experiment/add_tag', to: "experiment#add_tag" # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. From a04fd83445a664aab7bd41039271ef6869646bc7 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 3 Dec 2023 15:26:48 -0300 Subject: [PATCH 15/29] Componente de adicionar tag ao experimento --- .../components/ClassificationExperiment.jsx | 45 +++++++++++++++++++ app/javascript/react/src/index.js | 3 +- app/views/classification/index.html.erb | 1 + 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 app/javascript/react/src/components/ClassificationExperiment.jsx create mode 100644 app/views/classification/index.html.erb diff --git a/app/javascript/react/src/components/ClassificationExperiment.jsx b/app/javascript/react/src/components/ClassificationExperiment.jsx new file mode 100644 index 00000000..b469a7a8 --- /dev/null +++ b/app/javascript/react/src/components/ClassificationExperiment.jsx @@ -0,0 +1,45 @@ +import React, { useEffect, useState } from "react"; + +const ClassificationExperiment = (props) => { + const [experimentList, setExperimentList] = useState([]); + const [searchText, setSearchText] = useState(""); + + useEffect(() => { + // Fetch experiments from the API + const fetchExperiments = async () => { + const response = await fetch("/experiment/get_all"); + const data = await response.json(); + + if (response.ok) { + setExperimentList(data.experiments); + } else { + console.error("Error fetching experiments:", data.error); + } + }; + + fetchExperiments(); + }, []); + + const filteredExperiments = experimentList.filter((experiment) => + experiment.name.toLowerCase().includes(searchText.toLowerCase()) + ); + + return ( +
+

Classificação de Experimentos

+ setSearchText(e.target.value)} + /> +
    + {filteredExperiments.map((experiment) => ( +
  • {experiment.name}
  • + ))} +
+
+ ); +}; + +export default ClassificationExperiment; diff --git a/app/javascript/react/src/index.js b/app/javascript/react/src/index.js index aacf9634..68a83910 100644 --- a/app/javascript/react/src/index.js +++ b/app/javascript/react/src/index.js @@ -2,5 +2,6 @@ import { define } from 'remount' import Hello from "./components/Hello" import Graph from "./components/Graph" import CreateExperiment from "./components/CreateExperiment" +import ClassificationExperiment from './components/ClassificationExperiment' -define({ 'hello-component': Hello, 'graph-component': Graph, 'create-experiment-component': CreateExperiment }) \ No newline at end of file +define({ 'hello-component': Hello, 'graph-component': Graph, 'create-experiment-component': CreateExperiment, 'create-classification-experiment-component': ClassificationExperiment }) \ No newline at end of file diff --git a/app/views/classification/index.html.erb b/app/views/classification/index.html.erb new file mode 100644 index 00000000..b9c62a6e --- /dev/null +++ b/app/views/classification/index.html.erb @@ -0,0 +1 @@ + \ No newline at end of file From e3861fd2da27e2aab4c9cea81fca3117b8aaf99d Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 3 Dec 2023 15:29:48 -0300 Subject: [PATCH 16/29] Create classification_controller.rb --- app/controllers/classification_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/controllers/classification_controller.rb diff --git a/app/controllers/classification_controller.rb b/app/controllers/classification_controller.rb new file mode 100644 index 00000000..9da042e8 --- /dev/null +++ b/app/controllers/classification_controller.rb @@ -0,0 +1,5 @@ +class ClassificationController < ApplicationController + def index + render + end +end From 7189655c10eaa8d12a3383134b750a3d90468c52 Mon Sep 17 00:00:00 2001 From: dyesimontagner Date: Sun, 3 Dec 2023 20:59:31 -0300 Subject: [PATCH 17/29] =?UTF-8?q?adicionado=20o=20bot=C3=A3o=20de=20adicio?= =?UTF-8?q?nar=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../react/src/components/AddTag.jsx | 13 +++++ .../components/ClassificationExperiment.jsx | 45 +++++++++++++++--- bun.lockb | Bin 5086 -> 5086 bytes 3 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 app/javascript/react/src/components/AddTag.jsx diff --git a/app/javascript/react/src/components/AddTag.jsx b/app/javascript/react/src/components/AddTag.jsx new file mode 100644 index 00000000..9b123734 --- /dev/null +++ b/app/javascript/react/src/components/AddTag.jsx @@ -0,0 +1,13 @@ +import React from "react"; + +const AddTag = (props) => { + return ( +
+ + + +
+ ) +} + +export default AddTag \ No newline at end of file diff --git a/app/javascript/react/src/components/ClassificationExperiment.jsx b/app/javascript/react/src/components/ClassificationExperiment.jsx index b469a7a8..32b3c7c7 100644 --- a/app/javascript/react/src/components/ClassificationExperiment.jsx +++ b/app/javascript/react/src/components/ClassificationExperiment.jsx @@ -1,7 +1,9 @@ import React, { useEffect, useState } from "react"; +import AddTag from "./AddTag"; const ClassificationExperiment = (props) => { const [experimentList, setExperimentList] = useState([]); + const [filteredExperiments, setFilteredExperiments] = useState([]); const [searchText, setSearchText] = useState(""); useEffect(() => { @@ -12,17 +14,42 @@ const ClassificationExperiment = (props) => { if (response.ok) { setExperimentList(data.experiments); + return data.experiments } else { console.error("Error fetching experiments:", data.error); } }; - fetchExperiments(); + const result = fetchExperiments(); + result.then((result) => { + for (let index = 0; index < result.length; index++) { + result[index]["show"] = false; + } + setExperimentList(result) + setFilteredExperiments(result) + }); + }, []); - const filteredExperiments = experimentList.filter((experiment) => - experiment.name.toLowerCase().includes(searchText.toLowerCase()) - ); + const toggleHide = (idx) => { + const tmp = [] + for (let index = 0; index < filteredExperiments.length; index++) { + tmp.push(filteredExperiments[index]) + if (idx == index) { + tmp[index]["show"] = true + } + + } + setFilteredExperiments(tmp) + console.log(filteredExperiments) + } + + const handleChange = (e) => { + setSearchText(e.target.value) + setFilteredExperiments(experimentList.filter((experiment) => + experiment.name.toLowerCase().includes(searchText.toLowerCase())) + ) + } return (
@@ -31,11 +58,15 @@ const ClassificationExperiment = (props) => { type="text" placeholder="Pesquisar experimento..." value={searchText} - onChange={(e) => setSearchText(e.target.value)} + onChange={(e) => handleChange(e)} />
    - {filteredExperiments.map((experiment) => ( -
  • {experiment.name}
  • + {filteredExperiments.map((experiment, idx) => ( +
  • + {experiment.name} + + {experiment.show && } +
  • ))}
diff --git a/bun.lockb b/bun.lockb index a65d1c3340d1bb7269a66b2f66fea919af9925a7..17db9e53e2a245b5f9fa41d8dbf00c6d3a7c4438 100755 GIT binary patch delta 21 ccmcboeouXaCLaf5oT;9nfu6x;L%y}F089V|9{>OV delta 21 YcmcboeouXaCLaeA0|aa~ From e4d4d5e80c6f919165aa10cd5287fc4634b77b40 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 3 Dec 2023 23:46:05 -0300 Subject: [PATCH 18/29] Atualiza metodos das tags dos experimentos --- app/controllers/experiment_controller.rb | 53 +++++++++++++++++++++++- app/models/experiment.rb | 1 + config/routes.rb | 1 + 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app/controllers/experiment_controller.rb b/app/controllers/experiment_controller.rb index c1c51f3a..e8ee2904 100644 --- a/app/controllers/experiment_controller.rb +++ b/app/controllers/experiment_controller.rb @@ -4,7 +4,7 @@ def index render end - def create + def create # Check if params are valid if params['experimentName'].nil? || params['factors'].nil? render json: { error: 'Invalid params' }, status: :unprocessable_entity @@ -63,7 +63,24 @@ def create # GET all experiments def get_all + # Join experiment tags experiments = Experiment.all + + # Get all experiment_tags + experiment_tags = ExperimentTag.all + + # Get all tags + tags = Tag.all + + # Join experiment_tags and tags + experiments = experiments.map do |experiment| + experiment_tags = experiment_tags.select { |experiment_tag| experiment_tag.experiment_id == experiment.id } + tags = experiment_tags.map do |experiment_tag| + tags.select { |tag| tag.id == experiment_tag.tag_id }.first + end + { experiment: experiment, tags: tags } + end + render json: { experiments: experiments }, status: :ok end @@ -100,4 +117,38 @@ def add_tag render json: { error: e.message }, status: :unprocessable_entity end + def remove_tag + # Check if params are valid + if params['experiment_id'].nil? || params['tag_id'].nil? + render json: { error: 'Invalid params' }, status: :unprocessable_entity + return + end + + # Check if experiment exists + if Experiment.where(id: params['experiment_id']).empty? + render json: { error: 'Experiment not found' }, status: :not_found + return + end + + # Check if tag exists + if Tag.where(id: params['tag_id']).empty? + render json: { error: 'Tag not found' }, status: :not_found + return + end + + # Check if experiment has tag + if ExperimentTag.where(experiment_id: params['experiment_id'], tag_id: params['tag_id']).empty? + render json: { error: 'Experiment does not have tag' }, status: :unprocessable_entity + return + end + + # Remove tag from experiment + experiment_tag = ExperimentTag.where(experiment_id: params['experiment_id'], tag_id: params['tag_id']).first + experiment_tag.destroy + render json: { message: 'success' }, status: :ok + + rescue StandardError => e + render json: { error: e.message }, status: :unprocessable_entity + end + end diff --git a/app/models/experiment.rb b/app/models/experiment.rb index 46ba6406..e7f5e492 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -1,4 +1,5 @@ class Experiment < ApplicationRecord has_many :trials + has_many :experiment_tag has_many :tags, through: :experiment_tag end diff --git a/config/routes.rb b/config/routes.rb index 085d33ed..ab880e20 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,6 +9,7 @@ post 'tags/create', to: "tags#create" get 'tags/get_all', to: "tags#get_all" post 'experiment/add_tag', to: "experiment#add_tag" + delete 'experiment/remove_tag', to: "experiment#remove_tag" # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. From 6c6aff0eb1bc417a90ae9f6730a4977a0e200baa Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 3 Dec 2023 23:46:24 -0300 Subject: [PATCH 19/29] Componente para classificar experimentos --- .../react/src/components/AddTag.jsx | 13 -- .../components/ClassificationExperiment.jsx | 113 ++++++++++++------ 2 files changed, 76 insertions(+), 50 deletions(-) delete mode 100644 app/javascript/react/src/components/AddTag.jsx diff --git a/app/javascript/react/src/components/AddTag.jsx b/app/javascript/react/src/components/AddTag.jsx deleted file mode 100644 index 9b123734..00000000 --- a/app/javascript/react/src/components/AddTag.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; - -const AddTag = (props) => { - return ( -
- - - -
- ) -} - -export default AddTag \ No newline at end of file diff --git a/app/javascript/react/src/components/ClassificationExperiment.jsx b/app/javascript/react/src/components/ClassificationExperiment.jsx index 32b3c7c7..bafed9d8 100644 --- a/app/javascript/react/src/components/ClassificationExperiment.jsx +++ b/app/javascript/react/src/components/ClassificationExperiment.jsx @@ -1,49 +1,38 @@ import React, { useEffect, useState } from "react"; -import AddTag from "./AddTag"; +import toast from "react-simple-toasts"; +import 'react-simple-toasts/dist/theme/success.css'; +import 'react-simple-toasts/dist/theme/failure.css'; const ClassificationExperiment = (props) => { const [experimentList, setExperimentList] = useState([]); const [filteredExperiments, setFilteredExperiments] = useState([]); const [searchText, setSearchText] = useState(""); + const [tags, setTags] = useState([]) - useEffect(() => { - // Fetch experiments from the API - const fetchExperiments = async () => { + const fetchExperiments = async () => { + try { const response = await fetch("/experiment/get_all"); - const data = await response.json(); + const data = (await response.json()).experiments; + setExperimentList(data) + setFilteredExperiments(data) + } catch (error) { + console.error("Error fetching experiments:", error); + } + }; - if (response.ok) { - setExperimentList(data.experiments); - return data.experiments - } else { - console.error("Error fetching experiments:", data.error); - } - }; + useEffect(() => { + fetchExperiments(); - const result = fetchExperiments(); - result.then((result) => { - for (let index = 0; index < result.length; index++) { - result[index]["show"] = false; - } - setExperimentList(result) - setFilteredExperiments(result) - }); - + fetch("/tags/get_all") + .then((response) => response.json()) + .then((data) => { + setTags(data) + return data + }).catch((error) => { + console.error("Error fetching tags:", error) + }) }, []); - const toggleHide = (idx) => { - const tmp = [] - for (let index = 0; index < filteredExperiments.length; index++) { - tmp.push(filteredExperiments[index]) - if (idx == index) { - tmp[index]["show"] = true - } - - } - setFilteredExperiments(tmp) - console.log(filteredExperiments) - } - const handleChange = (e) => { setSearchText(e.target.value) setFilteredExperiments(experimentList.filter((experiment) => @@ -51,6 +40,42 @@ const ClassificationExperiment = (props) => { ) } + const addTag = (experimentId, tagId) => { + const data = { experiment_id: experimentId, tag_id: parseInt(tagId) } + + fetch("/experiment/add_tag", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }).then((response) => { + fetchExperiments(); + toast("Tag adicionada com sucesso!", { theme: "success", position: "top-right" }); + }).catch((error) => { + console.error("Error adding tag:", error) + toast("Erro ao adicionar tag!", { theme: "failure", position: "top-right" }); + }) + } + + const removeTag = (experimentId, tagId) => { + const data = { experiment_id: experimentId, tag_id: parseInt(tagId) } + + fetch("/experiment/remove_tag", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }).then((response) => { + fetchExperiments(); + toast("Tag removida com sucesso!", { theme: "success", position: "top-right" }); + }).catch((error) => { + console.error("Error removing tag:", error) + toast("Erro ao remover tag!", { theme: "failure", position: "top-right" }); + }) + } + return (

Classificação de Experimentos

@@ -61,11 +86,25 @@ const ClassificationExperiment = (props) => { onChange={(e) => handleChange(e)} />
    - {filteredExperiments.map((experiment, idx) => ( + {filteredExperiments.map(({experiment, tags: tags_}) => (
  • {experiment.name} - - {experiment.show && } + + +
      + {tags_.map((tag) => ( +
    • + {tag.name} + +
    • + ))} +
  • ))}
From de4f7cbde404d0a5f487a8c0f36dbb22f73ce029 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 3 Dec 2023 23:46:30 -0300 Subject: [PATCH 20/29] Testes --- features/5_Classificar_experimentos.feature | 6 +++ .../5_Classificar_experimentos_step.rb | 20 +++++++ .../controllers/experiment_controller_spec.rb | 52 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 features/5_Classificar_experimentos.feature create mode 100644 features/step_definitions/5_Classificar_experimentos_step.rb diff --git a/features/5_Classificar_experimentos.feature b/features/5_Classificar_experimentos.feature new file mode 100644 index 00000000..62c87120 --- /dev/null +++ b/features/5_Classificar_experimentos.feature @@ -0,0 +1,6 @@ +Feature: Classify experiments with tags + @javascript + Scenario: Add tag to experiment + Given I am on the "/experiment/classification" page + When I select tag with id "tag-2" + Then I should see the tag "Tag adicionada com sucesso!" \ No newline at end of file diff --git a/features/step_definitions/5_Classificar_experimentos_step.rb b/features/step_definitions/5_Classificar_experimentos_step.rb new file mode 100644 index 00000000..9e2a001a --- /dev/null +++ b/features/step_definitions/5_Classificar_experimentos_step.rb @@ -0,0 +1,20 @@ +Before do + Experiment.create(name: 'Experimento 1') + Tag.create(name: 'Tag 1', color: 'red') + Tag.create(name: 'Tag 2', color: 'blue') +end + +Given("I am on the {string} page") do |string| + visit string +end + +When("I select tag with id {string}") do |string| + # click the select + find(:css, '#select-1').click + # click the option + find(:css, '#' + string).click +end + +Then("I should see the tag {string}") do |string| + expect(page).to have_content(string) +end diff --git a/spec/controllers/experiment_controller_spec.rb b/spec/controllers/experiment_controller_spec.rb index 5d6aff08..0970e40b 100644 --- a/spec/controllers/experiment_controller_spec.rb +++ b/spec/controllers/experiment_controller_spec.rb @@ -37,4 +37,56 @@ expect(JSON.parse(response.body)['error']).to eq('Invalid params') end end + + context 'POST #add_tag' do + # setup + before(:each) do + Experiment.create(id: 1, name: 'test') + Tag.create(id: 1, name: 'tag') + end + + it 'should add tag to experiment' do + post :add_tag, params: { experiment_id: 1, tag_id: 1 } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['message']).to eq('success') + end + + it 'should not add tag to experiment if experiment_id is nil' do + post :add_tag, params: { tag_id: 1 } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') + end + + it 'should not add tag to experiment if tag_id is nil' do + post :add_tag, params: { experiment_id: 1 } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') + end + + it 'should not add tag to experiment if experiment does not exist' do + post :add_tag, params: { experiment_id: 2, tag_id: 1 } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Experiment not found') + end + + it 'should not add tag to experiment if tag does not exist' do + post :add_tag, params: { experiment_id: 1, tag_id: 2 } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Tag not found') + end + + it 'should not add tag to experiment if experiment already has tag' do + ExperimentTag.create(experiment_id: 1, tag_id: 1) + post :add_tag, params: { experiment_id: 1, tag_id: 1 } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Experiment already has tag') + end + + end end From e2154a0af73122a96a3b9f2ead818212ab6984c6 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 17 Dec 2023 22:53:53 -0300 Subject: [PATCH 21/29] Melhora os testes BDD --- features/4_Adicionar_experimento.feature | 26 ++++++++++++------- features/5_Classificar_experimentos.feature | 14 +++++++--- .../4_Adicionar_experimento_step.rb | 26 ++++++++----------- .../5_Classificar_experimentos_step.rb | 16 ++++++++---- 4 files changed, 50 insertions(+), 32 deletions(-) diff --git a/features/4_Adicionar_experimento.feature b/features/4_Adicionar_experimento.feature index cf559908..ece7753b 100644 --- a/features/4_Adicionar_experimento.feature +++ b/features/4_Adicionar_experimento.feature @@ -1,18 +1,26 @@ -Feature: Add a new experiment + Feature: Add a new experiment and Confirmation @javascript Scenario: Add a new experiment (Happy) When the user is on the add experiment screen - And the user clicks on the experiment name field and types "Experiment A" - And the user clicks on the factor name field and types "Factor 1" + And the user clicks on the experiment name field and types "Experiment B1" + And the user clicks on the factor name field and types "Velocidade" And the user clicks on the add factor button - And the user clicks on the factor value field and types "Value 1" - And the user clicks on the add value button + And the user clicks on the factor name field and types "Temperatura" + And the user clicks on the add factor button + And the user clicks on the factor value field and types "10" + And the user clicks on the button with id "add-to-Velocidade" + And the user clicks on the factor value field and types "20" + And the user clicks on the button with id "add-to-Velocidade" + And the user clicks on the factor value field and types "30" + And the user clicks on the button with id "add-to-Temperatura" + And the user clicks on the factor value field and types "40" + And the user clicks on the button with id "add-to-Temperatura" And the user clicks on the create experiment button - Then the user should see the message "Experimento criado com sucesso!" + Then the user should see a modal with the experiment details @javascript Scenario: Add a new experiment (Sad) - When the user is on the add experiment screen - fail + When the user is on the add experiment screen And the user dont fill the fields - And the user tries to clicks on the create experiment button - Then the user should see the fail message "Erro ao criar o experimento!" \ No newline at end of file + And the user clicks on the create experiment button + Then the user should see the fail message "Erro ao criar o experimento!" diff --git a/features/5_Classificar_experimentos.feature b/features/5_Classificar_experimentos.feature index 62c87120..4a06870b 100644 --- a/features/5_Classificar_experimentos.feature +++ b/features/5_Classificar_experimentos.feature @@ -1,6 +1,14 @@ Feature: Classify experiments with tags @javascript - Scenario: Add tag to experiment + Scenario: Add tag to experiment (Happy) Given I am on the "/experiment/classification" page - When I select tag with id "tag-2" - Then I should see the tag "Tag adicionada com sucesso!" \ No newline at end of file + When I click on the tag with id "tag-2" + And I click on the add button with id "add-tag-1" + Then I should see the message "Tag adicionada com sucesso!" + And I should see the tag "Tag 2" on the experiment "experiment-1-tags" + + @javascript + Scenario: Add tag to experiment without selecting a tag (Sad) + Given I am on the "/experiment/classification" page + And I click on the add button with id "add-tag-1" + Then I should see the message "Erro ao adicionar tag!" \ No newline at end of file diff --git a/features/step_definitions/4_Adicionar_experimento_step.rb b/features/step_definitions/4_Adicionar_experimento_step.rb index 0c0b81e5..bd3172eb 100644 --- a/features/step_definitions/4_Adicionar_experimento_step.rb +++ b/features/step_definitions/4_Adicionar_experimento_step.rb @@ -14,37 +14,33 @@ click_button 'addFactor' end - And("the user clicks on the factor value field and types {string}") do |string| fill_in 'factorValue', with: string end -And("the user clicks on the add value button") do - find(:css, '.trail-row .botao-responsivo').click +And("the user clicks on the button with id {string}") do |string| + find(:css, '#' + string).click end And("the user clicks on the create experiment button") do click_button 'createExperiment' end -Then("the user should see the message {string}") do |string| - expect(page).to have_content(string) -end - -When("the user is on the add experiment screen - fail") do - visit '/experiment/create' +Then('the user should see a modal with the experiment details') do + expect(page).to have_content('Experimento criado') + expect(page).to have_content('Experiment B1') + expect(page).to have_content('Ensaio 10-30') + expect(page).to have_content('Ensaio 10-40') + expect(page).to have_content('Ensaio 20-30') + expect(page).to have_content('Ensaio 20-40') end -And("the user dont fill the fields") do +And("the user dont fill the fields") do fill_in 'experimentName', with: '' fill_in 'factorName', with: '' fill_in 'factorValue', with: '' end -And("the user tries to clicks on the create experiment button") do - click_button 'createExperiment' -end - Then("the user should see the fail message {string}") do |string| expect(page).to have_content(string) -end \ No newline at end of file +end diff --git a/features/step_definitions/5_Classificar_experimentos_step.rb b/features/step_definitions/5_Classificar_experimentos_step.rb index 9e2a001a..dc1969cc 100644 --- a/features/step_definitions/5_Classificar_experimentos_step.rb +++ b/features/step_definitions/5_Classificar_experimentos_step.rb @@ -8,13 +8,19 @@ visit string end -When("I select tag with id {string}") do |string| - # click the select - find(:css, '#select-1').click - # click the option +When("I click on the tag with id {string}") do |string| find(:css, '#' + string).click end -Then("I should see the tag {string}") do |string| +And("I click on the add button with id {string}") do |string| + find(:css, '#' + string).click +end + + +Then("I should see the message {string}") do |string| expect(page).to have_content(string) end + +And("I should see the tag {string} on the experiment {string}") do |string, string2| + expect(page).to have_css('#'+string2, text: string) +end From e960dbbfe26d53e2dec91baef8f81473e76a18a4 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 17 Dec 2023 22:54:32 -0300 Subject: [PATCH 22/29] =?UTF-8?q?Refatora=20formulario=20de=20cria=C3=A7?= =?UTF-8?q?=C3=A3o=20do=20experimento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../react/src/components/CreateExperiment.jsx | 175 ++++++++++-------- 1 file changed, 99 insertions(+), 76 deletions(-) diff --git a/app/javascript/react/src/components/CreateExperiment.jsx b/app/javascript/react/src/components/CreateExperiment.jsx index 8f23e2cc..46005893 100644 --- a/app/javascript/react/src/components/CreateExperiment.jsx +++ b/app/javascript/react/src/components/CreateExperiment.jsx @@ -1,8 +1,6 @@ import React, { useEffect, useState } from "react"; import toast from "react-simple-toasts"; -import 'react-simple-toasts/dist/theme/success.css'; -import 'react-simple-toasts/dist/theme/failure.css'; - +import { useClickAway } from "@uidotdev/usehooks"; const CreateExperiment = () => { const [experimentName, setExperimentName] = useState( @@ -10,6 +8,15 @@ const CreateExperiment = () => { ); const [factors, setFactors] = useState(JSON.parse(localStorage.getItem("factors")) || {}); const [value, setValue] = useState(""); + const [open, setOpen] = useState(null); + + const handleOpen = ({ experiment, trials }) => { + setOpen({ experiment, trials }); + }; + const handleClose = () => { + setOpen(null); + }; + const ref = useClickAway(handleClose); addFactor = (name) => { if (name.length > 0 && !Object.keys(factors).includes(name)) { @@ -39,7 +46,6 @@ const CreateExperiment = () => { setFactors(newFactors); }; - submitExperiment = () => { const data = { experimentName, factors }; @@ -54,23 +60,26 @@ const CreateExperiment = () => { }) .then(async (res) => { const parse = JSON.parse(await res.text()); - console.log(parse); return parse; }) .then((res) => { if (res?.error) { toast("Erro ao criar o experimento!", { - position: "top-right", theme: "failure" - }) + position: "top-right", + theme: "failure", + }); return; } + handleOpen(res); toast("Experimento criado com sucesso!", { position: "top-right", theme: "success" }); clearExperiment(); - }).catch((err) => { - toast("Erro ao criar o experimento!", { - position: "top-right", theme: "failure" - }) }) + .catch((err) => { + toast("Erro ao criar o experimento!", { + position: "top-right", + theme: "failure", + }); + }); }; clearExperiment = () => { @@ -85,31 +94,63 @@ const CreateExperiment = () => { }, [experimentName, factors]); return ( -
-
-

Criar Experimento

-
-
+ <> + {/* Modal de experimento criado */} +
+
+
+

Experimento criado

+ +
+
+
+

{open?.experiment?.name}

+ {open?.experiment?.id} +
+

Ensaios

+ {open?.trials?.map((trial) => ( +
+
+

Ensaio {trial.trial.name}

+ {trial.trial.id} +
+
Fatores
+ + + {trial.factors.map((factor) => ( + + + + + ))} + +
{factor.name}{factor.value}
+
+ ))} +
+
+
+ {/* Fim do modal de experimento criado */} + + {/* Formulário de criação de experimento */} +
+
+

Criar Experimento

+ setExperimentName(e.target.value)} /> -
-
+

Adicionar fator

- +
- - - -
-
-

Fatores

- {Object.keys(factors).map((factor) => ( -
-
- {factor} -
-
addValueToFactor(factor, value)} - role="button" - className="botao-responsivo" - > - Adicionar valor ao fator + + Para adicionar o valor inserido ao fator, clique no botão 'Adicionar valor' ao lado do + nome do fator na seção 'Fatores'. + + + + +
+
+

Fatores

+ {Object.keys(factors).map((factor) => ( +
+
+ {factor} +
+ +
-
+ {factors[factor].map((value) => ( +
+ {value} + +
+ ))}
- {factors[factor].map((value) => ( -
- {value} - -
- ))} -
- ))} + ))} +
-
+ ); }; From 3b37dbac8dcd47389426788b3f848b48a971113c Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 17 Dec 2023 22:55:35 -0300 Subject: [PATCH 23/29] Refatora componente de add tag ao experimento --- .../components/ClassificationExperiment.jsx | 270 +++++++++++------- 1 file changed, 160 insertions(+), 110 deletions(-) diff --git a/app/javascript/react/src/components/ClassificationExperiment.jsx b/app/javascript/react/src/components/ClassificationExperiment.jsx index bafed9d8..a572b35b 100644 --- a/app/javascript/react/src/components/ClassificationExperiment.jsx +++ b/app/javascript/react/src/components/ClassificationExperiment.jsx @@ -1,115 +1,165 @@ import React, { useEffect, useState } from "react"; import toast from "react-simple-toasts"; -import 'react-simple-toasts/dist/theme/success.css'; -import 'react-simple-toasts/dist/theme/failure.css'; - -const ClassificationExperiment = (props) => { - const [experimentList, setExperimentList] = useState([]); - const [filteredExperiments, setFilteredExperiments] = useState([]); - const [searchText, setSearchText] = useState(""); - const [tags, setTags] = useState([]) - - const fetchExperiments = async () => { - try { - const response = await fetch("/experiment/get_all"); - const data = (await response.json()).experiments; - setExperimentList(data) - setFilteredExperiments(data) - } catch (error) { - console.error("Error fetching experiments:", error); - } - }; - - useEffect(() => { - fetchExperiments(); - - fetch("/tags/get_all") - .then((response) => response.json()) - .then((data) => { - setTags(data) - return data - }).catch((error) => { - console.error("Error fetching tags:", error) - }) - }, []); - - const handleChange = (e) => { - setSearchText(e.target.value) - setFilteredExperiments(experimentList.filter((experiment) => - experiment.name.toLowerCase().includes(searchText.toLowerCase())) - ) - } - - const addTag = (experimentId, tagId) => { - const data = { experiment_id: experimentId, tag_id: parseInt(tagId) } - - fetch("/experiment/add_tag", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }).then((response) => { - fetchExperiments(); - toast("Tag adicionada com sucesso!", { theme: "success", position: "top-right" }); - }).catch((error) => { - console.error("Error adding tag:", error) - toast("Erro ao adicionar tag!", { theme: "failure", position: "top-right" }); - }) - } - - const removeTag = (experimentId, tagId) => { - const data = { experiment_id: experimentId, tag_id: parseInt(tagId) } - - fetch("/experiment/remove_tag", { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }).then((response) => { - fetchExperiments(); - toast("Tag removida com sucesso!", { theme: "success", position: "top-right" }); - }).catch((error) => { - console.error("Error removing tag:", error) - toast("Erro ao remover tag!", { theme: "failure", position: "top-right" }); - }) - } - - return ( -
-

Classificação de Experimentos

- handleChange(e)} - /> -
    - {filteredExperiments.map(({experiment, tags: tags_}) => ( -
  • - {experiment.name} - - -
      - {tags_.map((tag) => ( -
    • - {tag.name} - -
    • - ))} -
    -
  • - ))} -
-
- ); + +const ClassificationExperiment = () => { + const [experimentList, setExperimentList] = useState([]); + const [filteredExperiments, setFilteredExperiments] = useState([]); + const [tags, setTags] = useState([]); + const [searchText, setSearchText] = useState(""); + const [selectedTagId, setSelectedTagId] = useState(null); + + const fetchExperiments = async () => { + try { + const response = await fetch("/experiment/get_all"); + const data = (await response.json()).experiments; + setExperimentList(data); + setFilteredExperiments( + data.filter(({ experiment }) => experiment.name?.toLowerCase().includes(searchText)) + ); + } catch (error) { + console.error("Error fetching experiments:", error); + } + }; + + useEffect(() => { + fetchExperiments(); + + fetch("/tags/get_all") + .then((response) => response.json()) + .then((data) => { + setTags(data); + return data; + }) + .catch((error) => { + console.error("Error fetching tags:", error); + }); + }, []); + + const handleChange = (e) => { + const searchText = e.target.value.toLowerCase(); + setSearchText(searchText); + setFilteredExperiments( + experimentList.filter(({ experiment }) => experiment.name?.toLowerCase().includes(searchText)) + ); + }; + + const handleSelectedTagId = (tagId) => { + if (selectedTagId === tagId) { + setSelectedTagId(null); + } else { + setSelectedTagId(tagId); + } + }; + + const addTag = async (experimentId, tagId) => { + try { + const data = { experiment_id: experimentId, tag_id: parseInt(tagId) }; + const response = await fetch("/experiment/add_tag", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }).then(async (res) => { + const parse = JSON.parse(await res.text()); + return parse; + }); + + if (response?.error) { + toast("Erro ao adicionar tag!", { theme: "failure", position: "top-right" }); + return; + } + + fetchExperiments(); + + toast("Tag adicionada com sucesso!", { theme: "success", position: "top-right" }); + } catch (error) { + console.error("Error adding tag:", error); + toast("Erro ao adicionar tag!", { theme: "failure", position: "top-right" }); + } + }; + + const removeTag = async (experimentId, tagId) => { + try { + const data = { experiment_id: experimentId, tag_id: parseInt(tagId) }; + const response = await fetch("/experiment/remove_tag", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }).then(async (res) => { + const parse = JSON.parse(await res.text()); + return parse; + }); + + if (response?.error) { + toast("Erro ao remover tag!", { theme: "failure", position: "top-right" }); + return; + } + + fetchExperiments(); + + toast("Tag removida com sucesso!", { theme: "success", position: "top-right" }); + } catch (error) { + console.error("Error removing tag:", error); + toast("Erro ao remover tag!", { theme: "failure", position: "top-right" }); + } + }; + + return ( +
+

Classificação de Experimentos

+ handleChange(e)} + /> +
+

Tags

+
    + {tags.map((tag) => ( +
  • handleSelectedTagId(tag.id)} + style={{ + backgroundColor: tag.color, + opacity: selectedTagId === tag.id ? 1 : 0.5, + }} + > + {tag.name} +
  • + ))} +
+
+

Experimentos

+
    + {filteredExperiments.map(({ experiment, tags: tags_ }) => ( +
  • +
    +

    {experiment.name}

    + +
    +
      + {tags_.map((tag) => ( +
    • + {tag.name} + +
    • + ))} +
    +
  • + ))} +
+
+ ); }; export default ClassificationExperiment; From 8f83b1207f2d37082e82a2cce83e3fad4de618e8 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 17 Dec 2023 22:56:04 -0300 Subject: [PATCH 24/29] Refatora controladora dos experimentos --- app/controllers/experiment_controller.rb | 298 ++++++++++++++--------- 1 file changed, 187 insertions(+), 111 deletions(-) diff --git a/app/controllers/experiment_controller.rb b/app/controllers/experiment_controller.rb index e8ee2904..1838ba19 100644 --- a/app/controllers/experiment_controller.rb +++ b/app/controllers/experiment_controller.rb @@ -1,154 +1,230 @@ +# Purpose: Handle requests for experiments class ExperimentController < ApplicationController skip_forgery_protection - def index - render - end + # POST create experiment + # Params: + # experimentName: string + # factors: { factorName: [factorValue1, factorValue2, ...], ... } + # Returns: + # message: string + # experiment: { id: integer, name: string, disabled: boolean, created_at: datetime, updated_at: datetime } + # trials: [ + # { + # trial: { id: integer, name: string, disabled: boolean, deleted: boolean, runs: integer, experiment_id: integer, created_at: datetime, updated_at: datetime }, + # factors: [ + # { id: integer, name: string, value: string, created_at: datetime, updated_at: datetime }, + # ... + # ] + # }, + # ... + # ] + # error: string + # status: integer def create - # Check if params are valid - if params['experimentName'].nil? || params['factors'].nil? - render json: { error: 'Invalid params' }, status: :unprocessable_entity - return - end + experiment_name = params['experimentName'] + factors = params['factors'] + return render_unprocessable_entity("Invalid params") unless create_params_are_valid(experiment_name, factors) - # Check if factors is a hash with arrays as values - if params['factors'].class != ActionController::Parameters || params['factors'].values.any? { |value| value.class != Array } - puts params['factors'].class - render json: { error: 'Invalid params' }, status: :unprocessable_entity - return - end + experiment = save_experiment(experiment_name, factors) - # Save experiment - experiment = Experiment.create(name: params['experimentName'], disabled: false) + render_experiment_and_trials(experiment.id) + rescue StandardError => error + render_unprocessable_entity(error.message) + end - # Save factors (should not save if factor with same name and value already exists) - params['factors'].each do |factor| - factor[1].each do |value| - if Factor.where(name: factor[0], value: value).empty? - Factor.create(name: factor[0], value: value) - end - end + + # GET all experiments + # Returns: + # experiments: [ + # { + # experiment: { id: integer, name: string, disabled: boolean, created_at: datetime, updated_at: datetime }, + # tags: [ + # { id: integer, name: string, color: string, created_at: datetime, updated_at: datetime }, + # ... + # ] + # }, + # ... + # ] + def get_all + experiments_with_tags = Experiment.includes(:experiment_tag => :tag) + + experiments = experiments_with_tags.map do |experiment| + experiment_tags = experiment.experiment_tag.map(&:tag) + { experiment: experiment, tags: experiment_tags } end - # Generate all possible combinations of factors and save them - # as [{factor1: value1, factor2: value1}, {factor1: value1, factor2: value2}, ...] + render json: { experiments: experiments }, status: :ok + end - # Get all factors from the request - factors = params['factors'].keys + # POST add tag to experiment + # Params: + # experiment_id: integer + # tag_id: integer + # Returns: + # message: string + # experiment_tag: { id: integer, experiment_id: integer, tag_id: integer, created_at: datetime, updated_at: datetime } + # error: string + # status: integer + def add_tag + exp_id = params['experiment_id'] + tag_id = params['tag_id'] - # Get all values from the request - values = params['factors'].values + return render_not_found('Experiment not found') if Experiment.where(id: exp_id).empty? - # Get all possible combinations of values - combinations = values[0].product(*values[1..-1]) + return render_not_found('Tag not found') if Tag.where(id: tag_id).empty? - # Save all combinations - combinations.each do |combination| - # Create trial - trial = Trial.create(name: combination.join('-'), disabled: false, deleted: false, runs: 0, experiment_id: experiment.id) + return render_unprocessable_entity('Experiment already has tag') \ + if ExperimentTag.where(experiment_id: exp_id, tag_id: tag_id).any? - # Create trial factors - combination.each do |value| - factor = Factor.where(name: factors[combination.index(value)], value: value).first - TrialFactor.create(factor_id: factor.id, trial_id: trial.id) - end - end - render json: { message: 'success', experiment: experiment }, status: :ok + # Add tag to experiment + experiment_tag = ExperimentTag.create(experiment_id: exp_id, tag_id: tag_id) + render json: { message: 'success', experiment_tag: experiment_tag }, status: :ok - rescue StandardError => e - render json: { error: e.message }, status: :unprocessable_entity + rescue StandardError => error + return render_unprocessable_entity(error.message) end + # DELETE remove tag from experiment + # Params: + # experiment_id: integer + # tag_id: integer + # Returns: + # message: string + # error: string + # status: integer + def remove_tag + exp_id = params['experiment_id'] + tag_id = params['tag_id'] - # GET all experiments - def get_all - # Join experiment tags - experiments = Experiment.all + return render_not_found('Experiment not found') if Experiment.where(id: exp_id).empty? - # Get all experiment_tags - experiment_tags = ExperimentTag.all + return render_not_found('Tag not found') if Tag.where(id: tag_id).empty? - # Get all tags - tags = Tag.all + experiment_tag = ExperimentTag.where(experiment_id: exp_id, tag_id: tag_id).first - # Join experiment_tags and tags - experiments = experiments.map do |experiment| - experiment_tags = experiment_tags.select { |experiment_tag| experiment_tag.experiment_id == experiment.id } - tags = experiment_tags.map do |experiment_tag| - tags.select { |tag| tag.id == experiment_tag.tag_id }.first - end - { experiment: experiment, tags: tags } - end + return render_unprocessable_entity('Experiment does not have tag') unless experiment_tag&.present? - render json: { experiments: experiments }, status: :ok - end - def add_tag - # Check if params are valid - if params['experiment_id'].nil? || params['tag_id'].nil? - render json: { error: 'Invalid params' }, status: :unprocessable_entity - return - end + experiment_tag.destroy + render json: { message: 'success' }, status: :ok - # Check if experiment exists - if Experiment.where(id: params['experiment_id']).empty? - render json: { error: 'Experiment not found' }, status: :not_found - return - end + rescue StandardError => error + render_unprocessable_entity(error.message) + end - # Check if tag exists - if Tag.where(id: params['tag_id']).empty? - render json: { error: 'Tag not found' }, status: :not_found - return - end + private + + # Check if params are valid + # Params: + # experiment_name: string + # factors: { factorName: [factorValue1, factorValue2, ...], ... } + # Returns: + # boolean + def create_params_are_valid(experiment_name, factors) + factors&.is_a?(ActionController::Parameters) \ + && factors.values.all? { |value| value.is_a?(Array) && value.length >= 1 } \ + && factors.keys.all? { |key| key.is_a?(String) } \ + && experiment_name.is_a?(String) \ + && experiment_name.length >= 1 + end - # Check if experiment already has tag - if ExperimentTag.where(experiment_id: params['experiment_id'], tag_id: params['tag_id']).any? - render json: { error: 'Experiment already has tag' }, status: :unprocessable_entity - return - end + def render_not_found(message) + render json: { error: message }, status: :not_found + end - # Add tag to experiment - experiment_tag = ExperimentTag.create(experiment_id: params['experiment_id'], tag_id: params['tag_id']) - render json: { message: 'success', experiment_tag: experiment_tag }, status: :ok + def render_unprocessable_entity(message) + render json: { error: message }, status: :unprocessable_entity + end - rescue StandardError => e - render json: { error: e.message }, status: :unprocessable_entity + # Save experiment and its factors and combinations + # Params: + # experiment_name: string + # factors: { factorName: [factorValue1, factorValue2, ...], ... } + # Returns: + # experiment: { id: integer, name: string, disabled: boolean, created_at: datetime, updated_at: datetime } + def save_experiment(experiment_name, factors) + experiment = Experiment.create(name: experiment_name, disabled: false) + save_factors(factors) + values = factors.values + save_combinations(experiment.id, factors.keys, values[0].product(*values[1..-1])) + return experiment end - def remove_tag - # Check if params are valid - if params['experiment_id'].nil? || params['tag_id'].nil? - render json: { error: 'Invalid params' }, status: :unprocessable_entity - return + # Save factors + # Params: + # factors: { factorName: [factorValue1, factorValue2, ...], ... } + # Returns: + # void + def save_factors(factors) + # Save factors (should not save if factor with same name and value already exists) + factors.each do |factor_name, factor_values| + save_factor(factor_name, factor_values) end + end - # Check if experiment exists - if Experiment.where(id: params['experiment_id']).empty? - render json: { error: 'Experiment not found' }, status: :not_found - return + def save_factor(factor_name, factor_values) + factor_values.each do |value| + if Factor.where(name: factor_name, value: value).empty? + Factor.create(name: factor_name, value: value) + end end + end - # Check if tag exists - if Tag.where(id: params['tag_id']).empty? - render json: { error: 'Tag not found' }, status: :not_found - return + # Save combinations + # Generate all possible combinations of factors and save them + # as [{factor1: value1, factor2: value1}, {factor1: value1, factor2: value2}, ...] + # Params: + # experiment_id: integer + # factors: [factorName1, factorName2, ...] + # values: [[factorValue1, factorValue2, ...], [factorValue1, factorValue2, ...], ...] + # Returns: + # void + def save_combinations(experiment_id, keys, combinations) + # combinations_keys looks like: + # [ [ [factor1: value1, factor2: value1], [factor1: value1, factor2: value2], ... ], + combinations_keys = combinations.map do |combination| + keys.zip(combination).map { |key, value| { key => value } } end - # Check if experiment has tag - if ExperimentTag.where(experiment_id: params['experiment_id'], tag_id: params['tag_id']).empty? - render json: { error: 'Experiment does not have tag' }, status: :unprocessable_entity - return + combinations_keys.each do |combination| + save_trial(experiment_id, combination) end + end - # Remove tag from experiment - experiment_tag = ExperimentTag.where(experiment_id: params['experiment_id'], tag_id: params['tag_id']).first - experiment_tag.destroy - render json: { message: 'success' }, status: :ok + def save_trial(experiment_id, combination) + # Name is the combination of values e.g. value1-value2-value3 + name = combination.map { |factor| factor.values[0] }.join('-') + trial = Trial.create(name: name, disabled: false, deleted: false, runs: 0, experiment_id: experiment_id) + + combination.each do |value| + factor = Factor.where(name: value.keys[0], value: value.values[0]).first + TrialFactor.create(factor_id: factor.id, trial_id: trial.id) + end + end + + + def render_experiment_and_trials(experiment_id) + experiment = Experiment.find(experiment_id) + trials = get_trials(experiment_id) + render json: { + message: 'success', + experiment: experiment, + trials: trials + }, status: :ok + end + + def get_trials(experiment_id) + Trial.where(experiment_id: experiment_id).map do |trial| + factors = get_factors(trial.id) + { trial: trial, factors: factors } + end + end - rescue StandardError => e - render json: { error: e.message }, status: :unprocessable_entity + def get_factors(trial_id) + trial_factors = TrialFactor.where(trial_id: trial_id) + trial_factors.map { |trial_factor| Factor.find(trial_factor.factor_id) } end end From 44f1b7adfd78d68bcec727953a6bf50bdd25b4ff Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 17 Dec 2023 22:56:19 -0300 Subject: [PATCH 25/29] =?UTF-8?q?Remove=20m=C3=A9todo=20desnecessario?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/classification_controller.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/controllers/classification_controller.rb b/app/controllers/classification_controller.rb index 9da042e8..8c6707f0 100644 --- a/app/controllers/classification_controller.rb +++ b/app/controllers/classification_controller.rb @@ -1,5 +1,2 @@ class ClassificationController < ApplicationController - def index - render - end end From ef15ad7ad2c4a0fe630ab20b9dbd1892227b8889 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 17 Dec 2023 22:56:41 -0300 Subject: [PATCH 26/29] =?UTF-8?q?Nova=20estiliza=C3=A7=C3=A3o=20para=20os?= =?UTF-8?q?=20componentes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/stylesheets/application.css | 504 ++++++++++++++++--------- 1 file changed, 332 insertions(+), 172 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 9d82a19e..09961503 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -14,264 +14,424 @@ *= require_self */ -ul { +/* Reset margins and paddings for specific elements */ + +h1, +h2, +h3, +h4, +h5, +h6, +p, +a, +ul, +ol, +li { margin: 0; padding: 0; - list-style-type: none; } -.outer-container { - display: flex; - flex-direction: row; - flex-wrap: wrap; - border-radius: 10px; /* Cantos arredondados de 10px */ - gap: 20px; /* Espaçamento entre os elementos */ - padding: 20px; /* Espaçamento interno */ +:root { + --background-color: #edf2f4; + --form-background-color: #8d99ae; + --trial-background-color: #8d99ae; + --class-exp-background-color: #8d99ae; + --class-exp-tag-background-color: #edf2f4; } -.container { - padding: 0 20px; - background-color: #f5f5f5; - min-width: 300px; /* Ajustar a largura conforme necessário */ - border-radius: 8px; /* Cantos arredondados de 8px */ +* { + + font-family: "Roboto", sans-serif; } -.container form { - display: flex; - flex-direction: column; - gap: 10px; /* Espaçamento entre os elementos */ - width: fit-content; /* Ajustar a largura conforme necessário */ - box-sizing: border-box; /* Incluir padding e border na largura e altura */ +/* Classificate Experiment Component */ +.classification-experiment { + color: #2b2d42; + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px; + border-radius: 10px; + background-color: var(--class-exp-background-color); + max-width: 300px; + width: 100%; + max-height: 600px; + box-sizing: border-box; } -.outer-container h1, h2, h3, h4, h5, h6 { - font-family: "Roboto", sans-serif; /* Substitua pelo nome real da sua fonte */ - font-size: 24px; /* Tamanho da fonte do título */ - font-weight: bold; /* Negrito */ - color: #333; /* Cor do texto */ - margin-top: 5px; - margin-bottom: 5px; /* Espaçamento inferior para dar mais espaço */ - letter-spacing: 2px; /* Espaçamento entre as letras */ - line-height: 1.2; /* Altura da linha para melhor legibilidade */ - /* Adiciona bordas arredondadas às letras */ - border-radius: 10px; /* Valor de border-radius para ajustar a curvatura */ - display: inline-block; /* Para que o border-radius seja aplicado corretamente */ - background-color: #f0f0f0; /* Cor de fundo para cobrir os espaços vazios após arredondar */ - padding: 5px 10px; /* Adiciona algum preenchimento para espaçamento interno */ +.classification-experiment > h1 { + margin-bottom: 10px; } -.outer-container p { - margin: 5px 0; /* Espaçamento superior e inferior */ +.classification-experiment > input { + padding: 10px; + border: 1px solid #ddd; + border-radius: 8px; + outline: none; + box-sizing: border-box; + transition: border-color 0.3s ease; + font-size: 10px; } -.outer-container { - font-family: "Roboto", sans-serif; /* Substitua pelo nome real da sua fonte */ - font-size: 15px; /* Tamanho da fonte do título */ - color: #333; /* Cor do texto */ - letter-spacing: 2px; /* Espaçamento entre as letras */ - line-height: 1.2; /* Altura da linha para melhor legibilidade */ +.classification-experiment > input:focus { + border-color: #3498db; +} + +.classification-experiment > ul { + list-style: none; + display: flex; + flex-direction: column; + gap: 10px; + max-height: 100%; + overflow-y: auto; } -.container button { - width: fit-content; +.classification-experiment > ul > li { + background-color: var(--class-exp-tag-background-color); + padding: 10px; + border-radius: 10px; + margin-right: 10px; } -.container form div { +.class-exp-tags ul { + list-style: none; display: flex; - flex-direction: column; + flex-direction: row; flex-wrap: wrap; - gap: 5px; + gap: 10px; + max-height: 100%; + overflow-y: auto; +} + +.class-exp-tags ul > li { + background-color: var(--class-exp-tag-background-color); + padding: 5px; + border-radius: 10px; + font-size: 10px; + cursor: pointer; +} + +.experiment-tags-header { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #e0e1dd; + padding: 6px; + border-radius: 10px; + margin-bottom: 10px; } -.list-container { +.experiment-tags-header > button { + font-size: 15px; + padding: 5px 10px; + border: none; border-radius: 8px; - padding: 0 20px; - background-color: #f5f5f5; - min-width: 300px; /* Ajustar a largura conforme necessário */ - width: fit-content; /* Ajustar a largura conforme necessário */ - box-sizing: border-box; /* Incluir padding e border na largura e altura */ + cursor: pointer; + transition: background-color 0.3s ease; + background-color: #8d99ae; + color: #000; } -.list-container li { - display:flex; - flex-direction: row; +.experiment-tags { + list-style: none; + display: flex; + flex-direction: column; + gap: 10px; +} + +.experiment-tags li { + display: flex; justify-content: space-between; - padding: 5px; /* Espaçamento interno */ - border-radius: 8px; /* Cantos arredondados de 8px */ + align-items: center; + padding: 6px; + background-color: #d6ccc2; + border-radius: 10px; } -.list-container li:nth-child(odd) { - background-color: lightgray; /* Set the background color for odd items */ +.experiment-tags li > span { + font-size: 10px; + background-color: #edede9; + padding: 5px; + border-radius: 10px; } -.outer-container { - padding: 20px; - border-radius: 4px; - border: 1px solid #ddd; +.experiment-tags button { + font-size: 9px; + padding: 5px 10px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s ease; + background-color: #8d99ae; + color: #000; } -.outer-container h2 { - font-size: 1.5em; - margin-bottom: 10px; +/* Create Experiment Component */ + +.info { + color: #2b2d42; + font-size: 10px; } -.outer-container ul { - list-style: none; - padding: 0; - margin: 0; +.outer-container { + color: #2b2d42; + display: flex; + background-color: var(--background-color); + flex-wrap: wrap; + gap: 20px; + padding: 10px; + border-radius: 10px; } -.outer-container li { - margin-bottom: 5px; - padding: 5px; - border-radius: 4px; - background-color: #f5f5f5; +.form-container > h1, .trial-container > h1 { + margin-bottom: 10px; } -.trial-container { +.form-container { + background-color: var(--form-background-color); + padding: 10px; + border-radius: 10px; + max-width: 300px; + width: 100%; +} + +.form-container > form { display: flex; flex-direction: column; - gap: 5px; - padding: 0 20px; - min-width: 300px; /* Ajustar a largura conforme necessário */ - border-radius: 8px; /* Cantos arredondados de 8px */ - background-color: #f5f5f5; + gap: 10px; } -.trail { - display:flex; - flex-direction: column; - justify-content: space-between; - padding: 5px; /* Espaçamento interno */ - border-radius: 8px; /* Cantos arredondados de 8px */ +.form-container > form hr { + margin: 10px 0; } -.trial-container .trail:nth-child(odd) { - background-color: lightgray; /* Set the background color for odd items */ +.form-container > form input { + font-size: 10px; /* Tamanho da fonte do input */ + padding: 10px; /* Espaçamento interno */ + border: 1px solid #ddd; /* Cor da borda */ + border-radius: 8px; /* Cantos arredondados */ + outline: none; /* Remover a borda de foco padrão */ + box-sizing: border-box; /* Incluir padding e border na largura e altura */ + transition: border-color 0.3s ease; /* Adiciona uma transição suave para a cor da borda */ } -.trial-container .trail:nth-child(even) { - background-color: #f0f0f0; /* Set the background color for odd items */ +.form-container > form input:focus { + border-color: #3498db; /* Cor da borda quando em foco */ } -.trial-container .trail:nth-child(even) .trail-row { - background-color: lightgray; /* Set the background color for odd items */ +.form-container > form button { + font-size: 9px; + padding: 5px 10px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s ease; + background-color: #edf2f4; + color: #000; } -.trial-container .trail:nth-child(odd) .trail-row { - background-color: #f0f0f0; /* Set the background color for odd items */ + +.form-container > form button:hover { + background-color: #2980b9; /* Nova cor de fundo ao passar o mouse */ } -.trail-row { - padding: 5px; +.trial-container { + background-color: var(--trial-background-color); + padding: 10px; + border-radius: 10px; + max-width: 300px; + width: 100%; +} + +.trial { + background-color: #edf2f4; + margin: 10px 0; + border-radius: 10px; + padding: 10px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.trial-header { + display: flex; + justify-content: space-between; + align-items: center; } -.trail-row > div { +.trial-header > div { display: flex; flex-direction: row; - gap: 5px; + gap: 10px; +} + +.trial-header button { + font-size: 9px; + padding: 5px 10px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s ease; + background-color: #8d99ae; + color: #000; } -.trail-row { - display:flex; +.factor-trial { + display: flex; flex-direction: row; justify-content: space-between; align-items: center; - border-radius: 8px; /* Cantos arredondados de 8px */ - margin-bottom: 10px; } -.factor-trail { - display:flex; - flex-direction: row; +.factor-trial button { + font-size: 9px; + padding: 5px 10px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s ease; + background-color: #8d99ae; + color: #000; +} + +/* MODAL STYLES */ +.modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + padding-top: 100px; /* Location of the box */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0, 0, 0); /* Fallback color */ + background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */ +} + +/* Modal Content */ +/* Default styles */ +.modal-content { + background-color: #fefefe; + margin: auto; + padding: 20px; + border: 1px solid #888; + width: 600px; + height: 600px; + border-radius: 10px; +} + +/* Responsive styles */ +@media (max-width: 768px) { + .modal { + padding: 3px; + } + + .modal-content { + width: 90%; /* Adjust the width for smaller screens */ + height: 90%; /* Allow height to adjust based on content */ + } +} + +.modal-header { + display: flex; justify-content: space-between; align-items: center; - border-radius: 8px; /* Cantos arredondados de 8px */ + padding-bottom: 10px; margin-bottom: 10px; - padding: 5px; /* Espaçamento interno */ + border-bottom: 1px solid #ddd; +} + +.modal-body { + overflow-y: scroll; + max-height: 90%; + width: 100%; + scroll-behavior: smooth; } -.trail .factor-trail:nth-child(odd) { - background-color: #f0f0f0;; /* Set the background color for odd items */ +.experiment-info { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; } -.trail .factor-trail:nth-child(even) { - background-color: lightgray; /* Set the background color for odd items */ +.experiment-info > span { + font-size: 20px; + margin: 3px; + border-radius: 10px; + background-color: #8d99ae; + color: #000; + min-width: 30px; + min-height: 30px; + display: flex; + justify-content: center; + align-items: center; } -.label-title { - font-family: "Roboto", sans-serif; /* Substitua pelo nome real da sua fonte */ - font-size: 24px; /* Tamanho da fonte do título */ - font-weight: bold; /* Negrito */ - color: #333; /* Cor do texto */ - margin-bottom: 20px; /* Espaçamento inferior para dar mais espaço */ - letter-spacing: 2px; /* Espaçamento entre as letras */ - line-height: 1.2; /* Altura da linha para melhor legibilidade */ - /* Adiciona bordas arredondadas às letras */ - border-radius: 10px; /* Valor de border-radius para ajustar a curvatura */ - display: inline-block; /* Para que o border-radius seja aplicado corretamente */ - background-color: #f0f0f0; /* Cor de fundo para cobrir os espaços vazios após arredondar */ - padding: 5px 10px; /* Adiciona algum preenchimento para espaçamento interno */ +.exp-info-trial { + background-color: #e0e1dd; + border-radius: 10px; + padding: 10px; + margin: 10px 0; } -.label-menor { - font-family: "Roboto", sans-serif; /* Substitua pelo nome real da sua fonte */ - font-size: 15px; /* Tamanho menor da fonte */ - color: #333; /* Cor do texto para diferenciar */ - margin-bottom: 13px; /* Espaçamento inferior menor */ - letter-spacing: 0.5px; /* Espaçamento entre as letras */ - line-height: 1.5; /* Altura da linha para melhor legibilidade */ +.exp-info-trial > h5 { + margin-bottom: 5px; } -.input-arredondado { - font-family: "Roboto", sans-serif; /* Substitua pelo nome real da sua fonte */ - font-size: 10px; /* Tamanho da fonte do input */ - padding: 10px; /* Espaçamento interno */ - border: 1px solid #ddd; /* Cor da borda */ - border-radius: 8px; /* Cantos arredondados */ - outline: none; /* Remover a borda de foco padrão */ - box-sizing: border-box; /* Incluir padding e border na largura e altura */ - transition: border-color 0.3s ease; /* Adiciona uma transição suave para a cor da borda */ +.exp-info-trial table, .exp-info-trial td,.exp-info-trial th { + border: 1px solid gray; + text-align: left; } -.input-arredondado:focus { - border-color: #3498db; /* Cor da borda quando em foco */ +.exp-info-trial table { + border-collapse: collapse; + width: 100%; } -.botao-responsivo { - font-family: "Roboto", sans-serif; - font-size: 9px; - padding: 5px 10px; - border: none; - border-radius: 8px; - cursor: pointer; - transition: background-color 0.3s ease; /* Adiciona uma transição suave para a cor de fundo */ +.exp-info-trial th, .exp-info-trial td { + padding: 5px; +} +/* The Close Button */ +.close { + font-size: 2rem; + padding: 2px 10px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s ease; + background-color: #8d99ae; + color: #000; +} - /* Adiciona estilos padrão */ - background-color: #3498db; - color: #fff; +.close:hover, +.close:focus { + background-color: #2980b9; + color: #fff; + text-decoration: none; + outline: none; } -/* Efeito quando o mouse passa por cima */ -.botao-responsivo:hover { - background-color: #2980b9; /* Nova cor de fundo ao passar o mouse */ +/* Custom scroll bar */ +.modal-body::-webkit-scrollbar, .classification-experiment > ul::-webkit-scrollbar { + width: 10px; + border-radius: 10px; } -.botao-responsivo-secundario { - font-family: "Roboto", sans-serif; - font-size: 9px; - padding: 5px 10px; - border: none; - border-radius: 8px; - cursor: pointer; - transition: background-color 0.3s ease; /* Adiciona uma transição suave para a cor de fundo */ +/* Track (the area the scrollbar sits on) */ +.modal-body::-webkit-scrollbar-track, .classification-experiment > ul::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} - /* Adiciona estilos padrão */ - background-color: #f0f0f0; - color: #333; +/* Handle (the draggable part of the scrollbar) */ +.modal-body::-webkit-scrollbar-thumb, .classification-experiment > ul::-webkit-scrollbar-thumb { + background: #888; + border-radius: 10px; } -/* Efeito quando o mouse passa por cima */ -.botao-responsivo-secundario:hover { - background-color: #ddd; /* Nova cor de fundo ao passar o mouse */ +/* Handle on hover */ +.modal-body::-webkit-scrollbar-thumb:hover, .classification-experiment > ul::-webkit-scrollbar-thumb:hover { + background: #555; } From b6237ce24458ac9d897264f0327304039497f002 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 17 Dec 2023 22:57:29 -0300 Subject: [PATCH 27/29] Adiciona e atualiza testes unitarios --- .../controllers/experiment_controller_spec.rb | 138 +++++++++++++++++- 1 file changed, 133 insertions(+), 5 deletions(-) diff --git a/spec/controllers/experiment_controller_spec.rb b/spec/controllers/experiment_controller_spec.rb index 0970e40b..a13da65f 100644 --- a/spec/controllers/experiment_controller_spec.rb +++ b/spec/controllers/experiment_controller_spec.rb @@ -5,6 +5,22 @@ it 'should create an experiment' do post :create, params: { experimentName: 'test', factors: { 'velocidade' => ['1', '2'], 'temperatura' => ['4', '5'] } } + + body = JSON.parse(response.body) + expect(body['experiment']['name']).to eq('test') + expect(body['experiment']['disabled']).to eq(false) + expect(body['trials'].length).to eq(4) + + body['trials'].each do |trial| + expect(trial['trial']['disabled']).to eq(false) + expect(trial['trial']['deleted']).to eq(false) + expect(trial['trial']['runs']).to eq(0) + expect(trial['trial']['experiment_id']).to eq(body['experiment']['id']) + end + + trial_names = body['trials'].map { |trial| trial['trial']['name'] } + expect(trial_names).to include('4-1', '4-2', '5-1', '5-2').or include('1-4', '1-5', '2-4', '2-5') + expect(response).to have_http_status(:ok) expect(JSON.parse(response.body)['message']).to eq('success') end @@ -16,6 +32,13 @@ expect(JSON.parse(response.body)['error']).to eq('Invalid params') end + it 'should not create an experiment if experimentName is empty' do + post :create, params: { experimentName: '', factors: { 'velocidade' => ['1', '2'], 'temperatura' => ['4', '5'] } } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') + end + it 'should not create an experiment if factors is nil' do post :create, params: { experimentName: 'test' } @@ -23,6 +46,20 @@ expect(JSON.parse(response.body)['error']).to eq('Invalid params') end + it 'should not create an experiment if factors is empty' do + post :create, params: { experimentName: 'test', factors: {} } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') + end + + it 'should not create an experiment if factors array is empty' do + post :create, params: { experimentName: 'test', factors: { 'velocidade' => [], 'temperatura' => [] } } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') + end + it 'should not create an experiment if factors is not a hash' do post :create, params: { experimentName: 'test', factors: 'velocidade' } @@ -42,7 +79,7 @@ # setup before(:each) do Experiment.create(id: 1, name: 'test') - Tag.create(id: 1, name: 'tag') + Tag.create(id: 1, name: 'tag', color: 'red') end it 'should add tag to experiment' do @@ -55,15 +92,15 @@ it 'should not add tag to experiment if experiment_id is nil' do post :add_tag, params: { tag_id: 1 } - expect(response).to have_http_status(:unprocessable_entity) - expect(JSON.parse(response.body)['error']).to eq('Invalid params') + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Experiment not found') end it 'should not add tag to experiment if tag_id is nil' do post :add_tag, params: { experiment_id: 1 } - expect(response).to have_http_status(:unprocessable_entity) - expect(JSON.parse(response.body)['error']).to eq('Invalid params') + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Tag not found') end it 'should not add tag to experiment if experiment does not exist' do @@ -89,4 +126,95 @@ end end + + context 'DELETE #remove_tag' do + + # setup + before(:each) do + Experiment.create(id: 1, name: 'test') + Tag.create(id: 1, name: 'tag', color: 'red') + ExperimentTag.create(experiment_id: 1, tag_id: 1) + Experiment.create(id: 2, name: 'test2') + Tag.create(id: 2, name: 'tag2', color: 'blue') + end + + it 'should remove tag from experiment' do + delete :remove_tag, params: { experiment_id: 1, tag_id: 1 } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['message']).to eq('success') + + expect(ExperimentTag.where(experiment_id: 1, tag_id: 1).any?).to eq(false) + end + + it 'should not remove tag from experiment if experiment_id is nil' do + delete :remove_tag, params: { tag_id: 1 } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Experiment not found') + end + + it 'should not remove tag from experiment if tag_id is nil' do + delete :remove_tag, params: { experiment_id: 1 } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Tag not found') + end + + it 'should not remove tag from experiment if experiment does not exist' do + delete :remove_tag, params: { experiment_id: 3, tag_id: 1 } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Experiment not found') + end + + it 'should not remove tag from experiment if tag does not exist' do + delete :remove_tag, params: { experiment_id: 1, tag_id: 3 } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Tag not found') + end + + it 'should not remove tag from experiment if experiment does not have tag' do + delete :remove_tag, params: { experiment_id: 2, tag_id: 2 } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Experiment does not have tag') + end + + it 'should not remove tag from experiment if experiment has tag but not the one being removed' do + delete :remove_tag, params: { experiment_id: 1, tag_id: 2 } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Experiment does not have tag') + end + end + + context 'GET #get_all' do + # setup + before(:each) do + Experiment.create(id: 1, name: 'test', disabled: false) + Factor.create(id: 1, name: 'velocidade', value: '1') + Trial.create(id: 1, name: '1', experiment_id: 1, disabled: false, deleted: false, runs: 0) + TrialFactor.create(id: 1, trial_id: 1, factor_id: 1) + Tag.create(id: 1, name: 'tag', color: 'red') + ExperimentTag.create(id: 1, experiment_id: 1, tag_id: 1) + end + + it 'should get all experiments' do + get :get_all + + expect(response).to have_http_status(:ok) + experiments = JSON.parse(response.body)['experiments'] + + expect(experiments.length).to eq(1) + experiment = experiments[0]['experiment'] + expect(experiment['id']).to eq(1) + + tags = experiments[0]['tags'] + expect(tags.length).to eq(1) + tag = tags[0] + expect(tag['id']).to eq(1) + end + end end From 82b2d5be93e4cb26c49e2a23c44feb33d5d4d221 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 17 Dec 2023 22:58:45 -0300 Subject: [PATCH 28/29] Adiciona pacotes e arquivos para criar metricas --- .document | 1 + .gitignore | 2 ++ Gemfile | 1 + spec/spec_helper.rb | 2 ++ 4 files changed, 6 insertions(+) create mode 100644 .document diff --git a/.document b/.document new file mode 100644 index 00000000..bb77f18a --- /dev/null +++ b/.document @@ -0,0 +1 @@ ++ app/controllers/experiment_controller.rb \ No newline at end of file diff --git a/.gitignore b/.gitignore index 288c5a2d..374ab4f4 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ !/app/assets/builds/.keep /node_modules +/coverage +/doc \ No newline at end of file diff --git a/Gemfile b/Gemfile index ac9a049c..3a4f9c50 100644 --- a/Gemfile +++ b/Gemfile @@ -45,6 +45,7 @@ gem "bootsnap", require: false # gem "image_processing", "~> 1.2" group :development, :test do + gem 'simplecov', require: false, group: :test gem 'rspec-rails' # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri mswin mswin64 mingw x64_mingw ] diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 327b58ea..93399d18 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,5 @@ +require 'simplecov' +SimpleCov.start # This file was generated by the `rails generate rspec:install` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # The generated `.rspec` file contains `--require spec_helper` which will cause From 31d42e4eb33bc4231f31aed2313651ffee30d1b9 Mon Sep 17 00:00:00 2001 From: Fernando de Alcantara <45339608+fernandodealcantara@users.noreply.github.com> Date: Sun, 17 Dec 2023 23:00:03 -0300 Subject: [PATCH 29/29] Update package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 437c20c7..94a92bb1 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dependencies": { "@hotwired/stimulus": "^3.2.2", "@hotwired/turbo-rails": "^7.3.0", + "@uidotdev/usehooks": "^2.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-simple-toasts": "^5.10.0",