From 76be52f4114924201637052fa9a8fed8f1bd1bb6 Mon Sep 17 00:00:00 2001 From: Felipe Murakami <13895820+fhmurakami@users.noreply.github.com> Date: Thu, 19 Dec 2024 23:02:52 +0000 Subject: [PATCH 01/10] Create Report model --- app/models/collection.rb | 1 + app/models/report.rb | 24 +++++ app/models/round.rb | 4 + config/locales/pt-BR.yml | 4 + db/migrate/20241213171716_create_reports.rb | 11 +++ ...20241216155927_add_report_ref_to_rounds.rb | 5 ++ spec/factories/reports.rb | 7 ++ spec/factories/rounds.rb | 1 + spec/factories/user/participants.rb | 2 +- spec/models/report_spec.rb | 88 +++++++++++++++++++ 10 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 app/models/report.rb create mode 100644 db/migrate/20241213171716_create_reports.rb create mode 100644 db/migrate/20241216155927_add_report_ref_to_rounds.rb create mode 100644 spec/factories/reports.rb create mode 100644 spec/models/report_spec.rb diff --git a/app/models/collection.rb b/app/models/collection.rb index be2029d..3a15c71 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -2,6 +2,7 @@ class Collection < ApplicationRecord belongs_to :user_admin, class_name: "User::Admin", dependent: :destroy has_many :collection_equations, dependent: :destroy has_many :equations, through: :collection_equations + has_many :reports validates :name, :equations_quantity, presence: true end diff --git a/app/models/report.rb b/app/models/report.rb new file mode 100644 index 0000000..54c876d --- /dev/null +++ b/app/models/report.rb @@ -0,0 +1,24 @@ +class Report < ApplicationRecord + belongs_to :user_admin, class_name: "User::Admin", foreign_key: "user_admin_id" + belongs_to :collection + belongs_to :grouping + + has_many :participants, class_name: "User::Participant", + foreign_key: :user_participant_id, through: :grouping + has_many :rounds + + validate :rounds_length + + private + + def rounds_length + if rounds.length > grouping.participants.length + errors.add( + :base, + :too_many_rounds, + grouping: grouping.name, + count: grouping.participants.length + ) + end + end +end diff --git a/app/models/round.rb b/app/models/round.rb index e6fac6a..0da9aa6 100644 --- a/app/models/round.rb +++ b/app/models/round.rb @@ -1,5 +1,9 @@ class Round < ApplicationRecord belongs_to :collection belongs_to :participant, class_name: "User::Participant", foreign_key: "user_participant_id" + belongs_to :report + has_many :answers + + validates :user_participant_id, uniqueness: { scope: :collection_id } end diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 18f2f95..8cee54a 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -112,6 +112,7 @@ pt-BR: collection: "Etapa" equation: "Equação" grouping: "Grupo" + report: "Relatório" round: "Sessão" user/admin: "Admin" user/participant: "Participante" @@ -256,6 +257,9 @@ pt-BR: wrong_length: one: não possui o tamanho esperado (%{count} caracter) other: não possui o tamanho esperado (%{count} caracteres) + too_many_rounds: + one: "%{model} não pode ter mais que %{count} sessão, pois o Grupo %{grouping} possui apenas %{count} participante" + other: "%{model} não pode ter mais que %{count} sessões, pois o Grupo %{grouping} possui apenas %{count} participantes" template: body: "Por favor, verifique o(s) seguinte(s) campo(s):" header: diff --git a/db/migrate/20241213171716_create_reports.rb b/db/migrate/20241213171716_create_reports.rb new file mode 100644 index 0000000..335caef --- /dev/null +++ b/db/migrate/20241213171716_create_reports.rb @@ -0,0 +1,11 @@ +class CreateReports < ActiveRecord::Migration[7.2] + def change + create_table :reports do |t| + t.references :user_admin, null: false, foreign_key: true + t.references :collection, null: false, foreign_key: true + t.references :group, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20241216155927_add_report_ref_to_rounds.rb b/db/migrate/20241216155927_add_report_ref_to_rounds.rb new file mode 100644 index 0000000..ab31190 --- /dev/null +++ b/db/migrate/20241216155927_add_report_ref_to_rounds.rb @@ -0,0 +1,5 @@ +class AddReportRefToRounds < ActiveRecord::Migration[7.2] + def change + add_reference :rounds, :report, null: false, foreign_key: true + end +end diff --git a/spec/factories/reports.rb b/spec/factories/reports.rb new file mode 100644 index 0000000..a3f9166 --- /dev/null +++ b/spec/factories/reports.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :report do + user_admin + collection + grouping + end +end diff --git a/spec/factories/rounds.rb b/spec/factories/rounds.rb index 42b9a81..177a4ed 100644 --- a/spec/factories/rounds.rb +++ b/spec/factories/rounds.rb @@ -4,5 +4,6 @@ association :participant, factory: :user_participant started_at { "2024-12-06 14:00:00" } completed_at { "2024-12-06 14:02:50" } + report end end diff --git a/spec/factories/user/participants.rb b/spec/factories/user/participants.rb index 4a98c2d..f7248c7 100644 --- a/spec/factories/user/participants.rb +++ b/spec/factories/user/participants.rb @@ -3,7 +3,7 @@ first_name { FFaker::Name.first_name } last_name { FFaker::Name.last_name } birth_date { FFaker::Date.birthday(min_age: 5) } - grouping { build(:grouping) } + grouping user_admin diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb new file mode 100644 index 0000000..8471073 --- /dev/null +++ b/spec/models/report_spec.rb @@ -0,0 +1,88 @@ +require 'rails_helper' + +RSpec.describe Report, type: :model do + describe 'database columns' do + it { should have_db_column(:user_admin_id).of_type(:integer) } + it { should have_db_column(:collection_id).of_type(:integer) } + it { should have_db_column(:grouping_id).of_type(:integer) } + end + + describe 'model associations' do + it { should belong_to(:user_admin).class_name('User::Admin') } + it { should belong_to(:collection) } + it { should belong_to(:grouping) } + it { should have_many(:rounds) } + it do + should have_many(:participants) + .class_name('User::Participant') + .through(:grouping) + end + end + + describe 'model validations' do + let(:user_admin) { create(:user_admin) } + let(:collection) { create(:collection, user_admin: user_admin) } + let(:grouping) { create(:grouping, user_admin: user_admin) } + let(:participant) { + create( + :user_participant, + grouping: grouping, + user_admin: user_admin + ) + } + let(:report) { + create( + :report, + user_admin: user_admin, + collection: collection, + grouping: grouping + ) + } + + context 'when rounds count is less or equal to the number of participants in a grouping' do + it 'should be valid with no errors' do + report.rounds << create( + :round, + collection: collection, + participant: participant, + report: report + ) + + expect(report).to be_valid + expect(report.errors).to be_empty + end + end + + context 'when rounds count is greater than the number of participants in a grouping' do + it 'should not be valid' do + report.rounds << [ + create( + :round, + collection: collection, + participant: participant, + report: report + ), + create( + :round, + collection: collection, + participant: create(:user_participant, user_admin: user_admin), + report: report + ) + ] + + grouping.reload + + expect(report).not_to be_valid + expect(report.errors).not_to be_empty + expect(report.errors.full_messages).to include( + I18n.t( + 'errors.messages.too_many_rounds', + model: report.model_name.human, + grouping: grouping.name, + count: grouping.participants.length + ) + ) + end + end + end +end From 18704199b232b52bff2f05211d6c8237869b1b55 Mon Sep 17 00:00:00 2001 From: Felipe Murakami <13895820+fhmurakami@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:11:09 +0000 Subject: [PATCH 02/10] Add Report Controller and respective tests --- app/controllers/reports_controller.rb | 38 +++++++++++++ config/locales/pt-BR.yml | 38 +++++++------ config/routes.rb | 2 +- spec/rails_helper.rb | 3 ++ spec/requests/reports_spec.rb | 78 +++++++++++++++++++++++++++ spec/routing/reports_routing_spec.rb | 17 ++++++ 6 files changed, 160 insertions(+), 16 deletions(-) create mode 100644 app/controllers/reports_controller.rb create mode 100644 spec/requests/reports_spec.rb create mode 100644 spec/routing/reports_routing_spec.rb diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb new file mode 100644 index 0000000..0343403 --- /dev/null +++ b/app/controllers/reports_controller.rb @@ -0,0 +1,38 @@ +class ReportsController < ApplicationController + before_action :set_report, only: %i[ show destroy ] + + # GET /reports or /reports.json + def index + @reports = current_admin.reports + end + + # GET /reports/1 or /reports/1.json + def show + if @report.nil? + @reports = current_admin.reports + flash.now[:alert] = I18n.t("reports.errors.not_found") + render :index + end + end + + # DELETE /reports/1 or /reports/1.json + def destroy + @report.destroy! + + respond_to do |format| + format.html { redirect_to reports_path, status: :see_other, notice: I18n.t("reports.destroyed") } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_report + @report = Report.find_by_id(params[:id]) + end + + # Only allow a list of trusted parameters through. + def report_params + params.require(:report).permit(:user_admin_id, :collection_id, :grouping_id) + end +end diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 8cee54a..f7f0e91 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -15,9 +15,9 @@ pt-BR: register: "Cadastrar nova resposta" show: "Ver resposta" show_all: "Ver todas as respostas" - created: "A resposta foi criada com sucesso." - updated: "A resposta foi atualizada com sucesso." - destroyed: "A resposta foi excluída com sucesso." + created: "A resposta foi criada com sucesso!" + updated: "A resposta foi atualizada com sucesso!" + destroyed: "A resposta foi excluída com sucesso!" collections: back: "Voltar para etapas" delete: "Excluir etapa" @@ -27,9 +27,9 @@ pt-BR: register: "Cadastrar nova etapa" show: "Ver etapa" show_all: "Ver todas as etapas" - created: "A etapa foi criada com sucesso." - updated: "A etapa foi atualizada com sucesso." - destroyed: "A etapa foi excluída com sucesso." + created: "A etapa foi criada com sucesso!" + updated: "A etapa foi atualizada com sucesso!" + destroyed: "A etapa foi excluída com sucesso!" errors: equations_limit: one: "não pode ter mais do que %{count} equação" @@ -45,9 +45,9 @@ pt-BR: register: "Cadastrar equação" show: "Ver equação" show_all: "Ver todas as equações" - created: "A equação foi criada com sucesso." - updated: "A equação foi atualizada com sucesso." - destroyed: "A equação foi excluída com sucesso." + created: "A equação foi criada com sucesso!" + updated: "A equação foi atualizada com sucesso!" + destroyed: "A equação foi excluída com sucesso!" groupings: back: "Voltar para grupos" delete: "Excluir grupo" @@ -57,9 +57,9 @@ pt-BR: select: "Selecionar grupo" show: "Ver grupo" show_all: "Ver todos os grupos" - created: "O grupo foi criado com sucesso." - updated: "O grupo foi atualizado com sucesso." - destroyed: "O grupo foi excluído com sucesso." + created: "O grupo foi criado com sucesso!" + updated: "O grupo foi atualizado com sucesso!" + destroyed: "O grupo foi excluído com sucesso!" participants: back: "Voltar para participantes" delete: "Excluir participante" @@ -71,9 +71,17 @@ pt-BR: register: "Cadastrar participante" show: "Ver participante" show_all: "Ver todos os participantes" - created: "O participante foi criado com sucesso." - updated: "O participante foi atualizado com sucesso." - destroyed: "O participante foi excluído com sucesso." + created: "O participante foi criado com sucesso!" + updated: "O participante foi atualizado com sucesso!" + destroyed: "O participante foi excluído com sucesso!" + reports: + back: "Voltar para relatórios" + delete: "Excluir relatório" + show: "Ver relatório" + show_all: "Ver todos os relatórios" + destroyed: "O relatório foi excluído com sucesso!" + errors: + not_found: "O relatório não foi encontrado!" rounds: congrats: "Parabéns!" congrats_message: "Você respondeu todas as equações!" diff --git a/config/routes.rb b/config/routes.rb index caef95f..4641dca 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,7 +15,7 @@ resources :equations resources :groupings resources :participants, module: :user - resources :reports + resources :reports, only: [ :index, :show, :destroy ] resources :equations, only: [] do resources :collections, only: [] do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 382495f..dfb7a7a 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -42,6 +42,9 @@ RSpec.configure do |config| config.include Warden::Test::Helpers config.include FactoryBot::Syntax::Methods + config.include Devise::Test::IntegrationHelpers + config.include Devise::Test::IntegrationHelpers, type: :feature + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_paths = [ Rails.root.join("spec/fixtures") diff --git a/spec/requests/reports_spec.rb b/spec/requests/reports_spec.rb new file mode 100644 index 0000000..7ac3ce3 --- /dev/null +++ b/spec/requests/reports_spec.rb @@ -0,0 +1,78 @@ +require 'rails_helper' + +# This spec was generated by rspec-rails when you ran the scaffold generator. +# It demonstrates how one might use RSpec to test the controller code that +# was generated by Rails when you ran the scaffold generator. +# +# It assumes that the implementation code is generated by the rails scaffold +# generator. If you are using any extension libraries to generate different +# controller code, this generated spec may or may not pass. +# +# It only uses APIs available in rails and/or rspec-rails. There are a number +# of tools you can use to make these specs even more expressive, but we're +# sticking to rails and rspec-rails APIs to keep things simple and stable. + +RSpec.describe "/reports", type: :request do + # This should return the minimal set of attributes required to create a valid + # Report. As you add validations to Report, be sure to + # adjust the attributes here as well. + let(:user_admin) { create(:user_admin) } + let(:valid_attributes) { + { + user_admin: user_admin, + collection: create(:collection), + grouping: create(:grouping) + } + } + + let(:invalid_attributes) { + { + user_admin: nil, + collection: nil, + grouping: nil + } + } + + describe "GET /index" do + it "renders a successful response" do + sign_in user_admin + Report.create! valid_attributes + get reports_url + expect(response).to be_successful + end + end + + describe "GET /show" do + context "with valid parameters" do + it "renders a successful response" do + report = Report.create! valid_attributes + get report_url(report, locale: I18n.locale) + expect(response).to be_successful + end + end + + context "with invalid parameters" do + it "redirects to index page" do + sign_in user_admin + invalid_report_id = -1 + get report_url(invalid_report_id, locale: I18n.locale) + expect(response.body).to match(I18n.t("reports.errors.not_found")) + end + end + end + + describe "DELETE /destroy" do + it "destroys the requested report" do + report = Report.create! valid_attributes + expect { + delete report_url(report, locale: I18n.locale) + }.to change(Report, :count).by(-1) + end + + it "redirects to the reports list" do + report = Report.create! valid_attributes + delete report_url(report, locale: I18n.locale) + expect(response).to redirect_to(reports_url) + end + end +end diff --git a/spec/routing/reports_routing_spec.rb b/spec/routing/reports_routing_spec.rb new file mode 100644 index 0000000..803a787 --- /dev/null +++ b/spec/routing/reports_routing_spec.rb @@ -0,0 +1,17 @@ +require "rails_helper" + +RSpec.describe ReportsController, type: :routing do + describe "routing" do + it "routes to #index" do + expect(get: "/reports").to route_to("reports#index") + end + + it "routes to #show" do + expect(get: "/reports/1").to route_to("reports#show", id: "1") + end + + it "routes to #destroy" do + expect(delete: "/reports/1").to route_to("reports#destroy", id: "1") + end + end +end From e3cd4a5f0294a9c2d1df9954ba8f7deba93e1d4a Mon Sep 17 00:00:00 2001 From: Felipe Murakami <13895820+fhmurakami@users.noreply.github.com> Date: Sun, 22 Dec 2024 00:13:36 -0300 Subject: [PATCH 03/10] Change Report reference column on table Rounds to allow null values (#17) --- ...0241221023018_change_round_column_report_to_allow_null.rb | 5 +++++ db/schema.rb | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20241221023018_change_round_column_report_to_allow_null.rb diff --git a/db/migrate/20241221023018_change_round_column_report_to_allow_null.rb b/db/migrate/20241221023018_change_round_column_report_to_allow_null.rb new file mode 100644 index 0000000..37e3193 --- /dev/null +++ b/db/migrate/20241221023018_change_round_column_report_to_allow_null.rb @@ -0,0 +1,5 @@ +class ChangeRoundColumnReportToAllowNull < ActiveRecord::Migration[7.2] + def change + change_column_null :rounds, :report_id, true + end +end diff --git a/db/schema.rb b/db/schema.rb index deabf6a..0f6ab62 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.2].define(version: 2024_12_18_195846) do +ActiveRecord::Schema[7.2].define(version: 2024_12_21_023018) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -87,7 +87,7 @@ t.integer "round_time" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.bigint "report_id", null: false + t.bigint "report_id" t.index ["collection_id"], name: "index_rounds_on_collection_id" t.index ["current_equation_id"], name: "index_rounds_on_current_equation_id" t.index ["report_id"], name: "index_rounds_on_report_id" From dd9e9d1ae911cee4aa322e93e77a939aad8b5954 Mon Sep 17 00:00:00 2001 From: Felipe Murakami <13895820+fhmurakami@users.noreply.github.com> Date: Wed, 18 Dec 2024 19:26:59 -0300 Subject: [PATCH 04/10] Update ci.yml Commenting jobs: - `scan_js`: not sure if I'll need this. - `test`: Will be uncommented as soon as I finish adding the missing tests --- .github/workflows/ci.yml | 116 +++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 038861c..8e62e4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,21 +22,21 @@ jobs: - name: Scan for common Rails security vulnerabilities using static analysis run: bin/brakeman --no-pager - scan_js: - runs-on: ubuntu-latest + # scan_js: + # runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 + # steps: + # - name: Checkout code + # uses: actions/checkout@v4 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: .ruby-version - bundler-cache: true + # - name: Set up Ruby + # uses: ruby/setup-ruby@v1 + # with: + # ruby-version: .ruby-version + # bundler-cache: true - - name: Scan for security vulnerabilities in JavaScript dependencies - run: bin/importmap audit + # - name: Scan for security vulnerabilities in JavaScript dependencies + # run: bin/importmap audit lint: runs-on: ubuntu-latest @@ -53,49 +53,49 @@ jobs: - name: Lint code for consistent style run: bin/rubocop -f github - test: - runs-on: ubuntu-latest - - services: - postgres: - image: postgres - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - ports: - - 5432:5432 - options: --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3 - - # redis: - # image: redis - # ports: - # - 6379:6379 - # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 - - steps: - - name: Install packages - run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libjemalloc2 libvips postgresql-client - - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: .ruby-version - bundler-cache: true - - - name: Run tests - env: - RAILS_ENV: test - DATABASE_URL: postgres://postgres:postgres@localhost:5432 - # REDIS_URL: redis://localhost:6379/0 - run: bin/rails db:test:prepare test test:system - - - name: Keep screenshots from failed system tests - uses: actions/upload-artifact@v4 - if: failure() - with: - name: screenshots - path: ${{ github.workspace }}/tmp/screenshots - if-no-files-found: ignore + # test: + # runs-on: ubuntu-latest + + # services: + # postgres: + # image: postgres + # env: + # POSTGRES_USER: postgres + # POSTGRES_PASSWORD: postgres + # ports: + # - 5432:5432 + # options: --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3 + + # # redis: + # # image: redis + # # ports: + # # - 6379:6379 + # # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + + # steps: + # - name: Install packages + # run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libjemalloc2 libvips postgresql-client + + # - name: Checkout code + # uses: actions/checkout@v4 + + # - name: Set up Ruby + # uses: ruby/setup-ruby@v1 + # with: + # ruby-version: .ruby-version + # bundler-cache: true + + # - name: Run tests + # env: + # RAILS_ENV: test + # DATABASE_URL: postgres://postgres:postgres@localhost:5432 + # # REDIS_URL: redis://localhost:6379/0 + # run: bin/rails db:test:prepare test test:system + + # - name: Keep screenshots from failed system tests + # uses: actions/upload-artifact@v4 + # if: failure() + # with: + # name: screenshots + # path: ${{ github.workspace }}/tmp/screenshots + # if-no-files-found: ignore From 865861962914084dd695bdec20a30e383ea79a23 Mon Sep 17 00:00:00 2001 From: Felipe Murakami <13895820+fhmurakami@users.noreply.github.com> Date: Sat, 28 Dec 2024 17:45:30 -0300 Subject: [PATCH 05/10] Refactor and Add Rounds Controller (#19) * Add RoundsController * Add views tests for round * Refactor Finish service * Add specs for Round::StartService * Add specs for Round::NextEquationService * Add specs for Round::SubmitAnswerService --- Gemfile | 1 + Gemfile.lock | 5 + app/controllers/collections_controller.rb | 51 ------ app/controllers/home_controller.rb | 8 - app/controllers/rounds_controller.rb | 77 ++++++++ app/models/answer.rb | 3 +- app/services/round/finish_service.rb | 31 ++-- app/services/round/next_equation_service.rb | 2 +- app/services/round/start_service.rb | 5 +- app/services/round/submit_answer_service.rb | 2 +- .../collections/_unknown_position_a.html.erb | 9 +- .../collections/_unknown_position_b.html.erb | 5 +- .../collections/_unknown_position_c.html.erb | 5 +- app/views/home/index.html.erb | 2 +- app/views/{home => rounds}/_form.html.erb | 2 +- .../finish.html.erb} | 0 .../new.html.erb} | 2 +- .../start.html.erb} | 2 +- config/locales/pt-BR.yml | 2 +- config/routes.rb | 17 +- spec/factories/rounds.rb | 19 ++ spec/rails_helper.rb | 2 + spec/requests/rounds_spec.rb | 172 ++++++++++++++++++ spec/services/round/finish_service_spec.rb | 54 ++++++ .../round/next_equation_service_spec.rb | 51 ++++++ spec/services/round/start_service_spec.rb | 40 ++++ .../round/submit_answer_service_spec.rb | 103 +++++++++++ spec/views/rounds/_form.html.erb_spec.rb | 39 ++++ spec/views/rounds/finish.html.erb_spec.rb | 21 +++ spec/views/rounds/new.html.erb_spec.rb | 21 +++ spec/views/rounds/start.html.erb_spec.rb | 58 ++++++ 31 files changed, 717 insertions(+), 94 deletions(-) create mode 100644 app/controllers/rounds_controller.rb rename app/views/{home => rounds}/_form.html.erb (91%) rename app/views/{collections/finish_round.html.erb => rounds/finish.html.erb} (100%) rename app/views/{home/new_round.html.erb => rounds/new.html.erb} (80%) rename app/views/{collections/start_round.html.erb => rounds/start.html.erb} (52%) create mode 100644 spec/requests/rounds_spec.rb create mode 100644 spec/services/round/finish_service_spec.rb create mode 100644 spec/services/round/next_equation_service_spec.rb create mode 100644 spec/services/round/start_service_spec.rb create mode 100644 spec/services/round/submit_answer_service_spec.rb create mode 100644 spec/views/rounds/_form.html.erb_spec.rb create mode 100644 spec/views/rounds/finish.html.erb_spec.rb create mode 100644 spec/views/rounds/new.html.erb_spec.rb create mode 100644 spec/views/rounds/start.html.erb_spec.rb diff --git a/Gemfile b/Gemfile index c8d8a37..354a7ba 100644 --- a/Gemfile +++ b/Gemfile @@ -72,4 +72,5 @@ group :test do gem "selenium-webdriver" gem "simplecov", require: false gem "shoulda-matchers", "~> 6.0" + gem "rails-controller-testing" end diff --git a/Gemfile.lock b/Gemfile.lock index 7b431de..ec63603 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -213,6 +213,10 @@ GEM activesupport (= 7.2.2) bundler (>= 1.15.0) railties (= 7.2.2) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -358,6 +362,7 @@ DEPENDENCIES pry-byebug puma (>= 5.0) rails (~> 7.2.2) + rails-controller-testing rspec-rails (~> 7.0.0) rubocop-rails-omakase selenium-webdriver diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index d49dc9c..c905208 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -2,12 +2,7 @@ class CollectionsController < ApplicationController before_action :find_equation, only: :remove_equation before_action :set_collection, only: %i[ show edit update destroy remove_equation - start_round submit_answer finish_round ] - before_action :set_participant, only: %i[ - start_round submit_answer finish_round - ] - before_action :set_round, only: %i[ submit_answer finish_round ] # GET /collections or /collections.json def index @@ -87,39 +82,6 @@ def remove_equation redirect_to @collection end - def start_round - # Create service with the minimal required information - @current_round = Round::StartService.call(@collection, @participant) - - # Get the first equation - @current_equation = Round::NextEquationService.call(@current_round) - - if @current_equation.nil? - redirect_to action: :finish_round, - collection_id: @collection.id, participant_id: @participant.id - else - render :start_round - end - end - - def submit_answer - Round::SubmitAnswerService.call(@current_round, params[:answer_value]) - - # Get next equation or finalize the round - @current_equation = Round::NextEquationService.call(@current_round) - if @current_equation.nil? - redirect_to action: :finish_round, - collection_id: @collection.id, participant_id: @participant.id - else - render :start_round - end - end - - def finish_round - # @current_round = Round.find_by(collection: @collection, participant: @participant, completed_at: nil) - Round::FinishService.call(@current_round) - end - private # Use callbacks to share common setup or constraints between actions. def set_collection @@ -135,19 +97,6 @@ def find_equation @equation ||= Equation.find(params[:equation_id]) end - def set_round - @current_round ||= Round.find_by( - collection: @collection, - participant: @participant, - completed_at: nil - ) - end - - # Finds and returns the participant associated with the given participant_id parameter. - def set_participant - @participant ||= current_admin.participants.find(params[:participant_id]) - end - # Only allow a list of trusted parameters through. def collection_params params.require(:collection).permit(:name, :equations_quantity, :user_admin_id) diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 7c57d4b..95f2992 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,12 +1,4 @@ class HomeController < ApplicationController def index end - - def new_round - end - - private - def home_params - params.require(:home).permit(:collection_id, :participant_id,) - end end diff --git a/app/controllers/rounds_controller.rb b/app/controllers/rounds_controller.rb new file mode 100644 index 0000000..117b8fe --- /dev/null +++ b/app/controllers/rounds_controller.rb @@ -0,0 +1,77 @@ +class RoundsController < ApplicationController + before_action :set_collection, only: %i[ + start submit_answer finish + ] + before_action :set_participant, only: %i[ + start submit_answer finish + ] + before_action :set_round, only: %i[ submit_answer finish ] + + def new + end + + def start + # Create service with the minimal required information + @current_round = Round::StartService.call(@collection, @participant) + + # Get the first equation + @current_equation = Round::NextEquationService.call(@current_round) + + if @current_equation.nil? + redirect_to action: :finish, + collection_id: @collection.id, + participant_id: @participant.id, + round_id: @current_round.id + else + render :start + end + end + + def submit_answer + Round::SubmitAnswerService.call(@current_round, params[:answer_value]) + + # Get next equation or finalize the round + @current_equation = Round::NextEquationService.call(@current_round) + if @current_equation.nil? + redirect_to action: :finish, + collection_id: @collection.id, + participant_id: @participant.id, + round_id: @current_round.id + else + render :start + end + end + + def finish + Round::FinishService.call( + round: @current_round, + collection: @collection, + participant: @participant, + admin: current_admin + ) + end + + private + + def set_collection + @collection ||= Collection.find(params[:collection_id]) + end + + # Finds and returns the participant associated with the given participant_id parameter. + def set_participant + @participant ||= current_admin.participants.find(params[:participant_id]) + end + + def set_round + @current_round ||= Round.find_by( + collection: @collection, + participant: @participant, + completed_at: nil + ) + end + + # Only allow a list of trusted parameters through. + def round_params + params.require(:round).permit(:answer_value, :round_id, :collection_id, :participant_id) + end +end diff --git a/app/models/answer.rb b/app/models/answer.rb index 8d4c23d..c258289 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -6,7 +6,8 @@ class Answer < ApplicationRecord has_one :collection, through: :collection_equation has_one :equation, through: :collection_equation - validates_presence_of :answer_value, :correct_answer, :time_spent, on: :create + validates_presence_of :answer_value, :time_spent, on: :create + validates_inclusion_of :correct_answer, in: [ true, false ], on: :create validates :answer_value, numericality: { only_integer: true }, on: :create validates_uniqueness_of :answer_value, scope: [ :user_participant_id, :round_id, :collection_equation_id diff --git a/app/services/round/finish_service.rb b/app/services/round/finish_service.rb index a15ba22..e69977f 100644 --- a/app/services/round/finish_service.rb +++ b/app/services/round/finish_service.rb @@ -1,11 +1,12 @@ class Round::FinishService - def initialize(current_round) - @current_round = current_round - @collection = @current_round.collection - @participant = @current_round.participant + def initialize(round, collection, participant, admin) + @current_round = round + @collection = collection + @participant = participant + @user_admin = admin end - def self.call(current_round) - new(current_round).call + def self.call(round:, collection:, participant:, admin:) + new(round, collection, participant, admin).call end def call @@ -26,23 +27,19 @@ def collection_completed? end def finalize_round - @current_round.update!( - completed_at: Time.current - ) + @completed_at = Time.current - calculate_round_time - Report.new( - user_admin: current_admin, - collection: @collection, - grouping: @participant.grouping + @current_round.update!( + completed_at: @completed_at, + round_time: calculate_round_time ) end def calculate_round_time # Logic to calculate time spent on the entire round start_time = @current_round.started_at.to_f * 1000 - end_time = Time.current.to_f * 1000 - @current_round.round_time = (end_time - start_time).to_i - @current_round.save + end_time = @completed_at.to_f * 1000 + + (end_time - start_time).to_i end end diff --git a/app/services/round/next_equation_service.rb b/app/services/round/next_equation_service.rb index dc11f0a..03795df 100644 --- a/app/services/round/next_equation_service.rb +++ b/app/services/round/next_equation_service.rb @@ -31,7 +31,7 @@ def next_equation # Returns an array of equations that have not been answered by the current participant yet. def unanswered_equations - @unanswered_equations ||= @collection.equations.select do |equation| + @unanswered_equations = @collection.equations.select do |equation| !equation.answers.exists?(participant: @participant, round: @current_round) end end diff --git a/app/services/round/start_service.rb b/app/services/round/start_service.rb index 1aa5d56..2c07e85 100644 --- a/app/services/round/start_service.rb +++ b/app/services/round/start_service.rb @@ -18,10 +18,11 @@ def call def start_round # Find or Create a participant collection round record - @round = Round.find_or_create_by!( + @round = Round.find_or_create_by( collection: @collection, participant: @participant, - completed_at: nil + completed_at: nil, + report: nil ) if @round.started_at.nil? diff --git a/app/services/round/submit_answer_service.rb b/app/services/round/submit_answer_service.rb index 188677a..13651b8 100644 --- a/app/services/round/submit_answer_service.rb +++ b/app/services/round/submit_answer_service.rb @@ -5,7 +5,7 @@ def initialize(current_round, answer_value) @collection = @current_round.collection @participant = @current_round.participant @equation_started_at = @current_round.equation_started_at - @current_equation = Equation.find(@current_round.current_equation_id) + @current_equation = Equation.find_by_id(@current_round.current_equation_id) end def self.call(current_round, answer_value) diff --git a/app/views/collections/_unknown_position_a.html.erb b/app/views/collections/_unknown_position_a.html.erb index 1d17159..0c89c76 100644 --- a/app/views/collections/_unknown_position_a.html.erb +++ b/app/views/collections/_unknown_position_a.html.erb @@ -1,4 +1,7 @@ - <%= form_with url: submit_answer_path, method: :post do |form| %> + <%= form_with( + url: submit_answer_round_path(@current_round, locale: I18n.locale), + method: :post + ) do |form| %>
<%= @@ -29,6 +32,10 @@ <%= form.label :participant_id %> <%= form.text_field :participant_id, value: @participant.id %>
+
<%= form.submit t("helpers.submit.ok"), class: "btn green" %> diff --git a/app/views/collections/_unknown_position_b.html.erb b/app/views/collections/_unknown_position_b.html.erb index 93a5426..d2826ac 100644 --- a/app/views/collections/_unknown_position_b.html.erb +++ b/app/views/collections/_unknown_position_b.html.erb @@ -1,4 +1,7 @@ - <%= form_with url: submit_answer_path, method: :post do |form| %> + <%= form_with( + url: submit_answer_round_path(@current_round, locale: I18n.locale), + method: :post + ) do |form| %>
<%= diff --git a/app/views/collections/_unknown_position_c.html.erb b/app/views/collections/_unknown_position_c.html.erb index 63ce5b5..63fde49 100644 --- a/app/views/collections/_unknown_position_c.html.erb +++ b/app/views/collections/_unknown_position_c.html.erb @@ -1,4 +1,7 @@ - <%= form_with url: submit_answer_path, method: :post do |form| %> + <%= form_with( + url: submit_answer_round_path(@current_round, locale: I18n.locale), + method: :post + ) do |form| %>
<%= diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb index 02db6ff..20545f5 100644 --- a/app/views/home/index.html.erb +++ b/app/views/home/index.html.erb @@ -10,7 +10,7 @@
- <%= link_to t("home.new_round"), new_round_path, class: "btn btn-large blue" %> + <%= link_to t("rounds.new"), new_round_path, class: "btn btn-large blue" %> <%#= link_to t("reports.show_all"), reports_path, class: "btn btn-large raspberry" %>
diff --git a/app/views/home/_form.html.erb b/app/views/rounds/_form.html.erb similarity index 91% rename from app/views/home/_form.html.erb rename to app/views/rounds/_form.html.erb index 4bf2a14..b849b3e 100644 --- a/app/views/home/_form.html.erb +++ b/app/views/rounds/_form.html.erb @@ -1,4 +1,4 @@ - <%= form_with url: start_round_path, method: :get do |form| %> + <%= form_with url: start_rounds_path, method: :post do |form| %>
<%= form.label :participant_id, t("activerecord.models.user/participant") %> <%= diff --git a/app/views/collections/finish_round.html.erb b/app/views/rounds/finish.html.erb similarity index 100% rename from app/views/collections/finish_round.html.erb rename to app/views/rounds/finish.html.erb diff --git a/app/views/home/new_round.html.erb b/app/views/rounds/new.html.erb similarity index 80% rename from app/views/home/new_round.html.erb rename to app/views/rounds/new.html.erb index 7add5aa..6f8ea0f 100644 --- a/app/views/home/new_round.html.erb +++ b/app/views/rounds/new.html.erb @@ -1,7 +1,7 @@
<%= render "shared/header" %> -

<%= t "home.new_round" %>

+

<%= t "rounds.new" %>

<%= render "form" %>
diff --git a/app/views/collections/start_round.html.erb b/app/views/rounds/start.html.erb similarity index 52% rename from app/views/collections/start_round.html.erb rename to app/views/rounds/start.html.erb index bd402bf..172f6ed 100644 --- a/app/views/collections/start_round.html.erb +++ b/app/views/rounds/start.html.erb @@ -1,7 +1,7 @@
- <%= render "unknown_position_#{@current_equation.unknown_position.downcase}" %> + <%= render "collections/unknown_position_#{@current_equation.unknown_position.downcase}" %>
\ No newline at end of file diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index f7f0e91..942c717 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -2,7 +2,6 @@ pt-BR: home: homepage: "Home" - new_round: "Nova sessão" title: "S.A.M." subtitle: "Software de Avaliação Matemática" back: "Voltar para página inicial" @@ -83,6 +82,7 @@ pt-BR: errors: not_found: "O relatório não foi encontrado!" rounds: + new: "Nova sessão" congrats: "Parabéns!" congrats_message: "Você respondeu todas as equações!" activerecord: diff --git a/config/routes.rb b/config/routes.rb index 4641dca..1eb7641 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,10 +3,6 @@ scope "(:locale)", locale: /pt-BR|en/ do # Defines the root path route ("/") root "home#index" - get "new_round", to: "home#new_round" - get "collections/start_round", to: "collections#start_round", as: "start_round" - post "collections/submit_answer", to: "collections#submit_answer", as: "submit_answer" - get "collections/finish_round", to: "collections#finish_round", as: "finish_round" # Devise routes for User::Admin authentication devise_for :admins, class_name: "User::Admin" @@ -28,10 +24,21 @@ resources :participants, module: :user, only: [] do resources :groupings, only: [] do member do - patch "remove", to: "/groupings#remove_participant", as: :remove + patch "remove", to: "groupings#remove_participant", as: :remove end end end + + resources :rounds, param: :round_id, only: [ :new ] do + collection do + post "start" + end + + member do + post "submit_answer" + get "finish" + end + end end # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. diff --git a/spec/factories/rounds.rb b/spec/factories/rounds.rb index 177a4ed..786645c 100644 --- a/spec/factories/rounds.rb +++ b/spec/factories/rounds.rb @@ -5,5 +5,24 @@ started_at { "2024-12-06 14:00:00" } completed_at { "2024-12-06 14:02:50" } report + + trait :unfinished do + started_at { Time.current } + completed_at { nil } + end + + trait :uncompleted do + started_at { Time.current } + completed_at { nil } + collection { + create( + :collection, + equations: [ + create(:equation), + create(:equation) + ] + ) + } + end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index dfb7a7a..865f666 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -44,6 +44,8 @@ config.include FactoryBot::Syntax::Methods config.include Devise::Test::IntegrationHelpers config.include Devise::Test::IntegrationHelpers, type: :feature + config.include Devise::Test::ControllerHelpers, type: :controller + config.include Devise::Test::ControllerHelpers, type: :view # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_paths = [ diff --git a/spec/requests/rounds_spec.rb b/spec/requests/rounds_spec.rb new file mode 100644 index 0000000..70f3b8d --- /dev/null +++ b/spec/requests/rounds_spec.rb @@ -0,0 +1,172 @@ +require 'rails_helper' + +RSpec.describe "Rounds", type: :request do + describe "GET /start" do + let(:current_admin) { create(:user_admin) } + + before(:each) do + sign_in current_admin + end + + it "renders a successful response" do + participant = create(:user_participant, user_admin: current_admin) + collection = create(:collection, user_admin: current_admin) + create(:equation, collections: [ collection ], user_admin: current_admin) + + post start_rounds_path, params: { + collection_id: collection.id, + participant_id: participant.id + } + + expect(response).to be_successful + end + + it "redirects to finish if no equation is found" do + participant = create(:user_participant, user_admin: current_admin) + collection = create(:collection, user_admin: current_admin) + round = create(:round, collection: collection, participant: participant) + + allow(Round::StartService).to receive(:call).and_return(round) + allow(Round::NextEquationService).to receive(:call).and_return(nil) + + post start_rounds_path, params: { + collection_id: collection.id, + participant_id: participant.id, + round_id: round.id + } + + expect(response).to redirect_to( + action: :finish, + collection_id: collection.id, + participant_id: participant.id, + round_id: round.id + ) + end + + it "renders the start template if an equation is found" do + participant = create(:user_participant, user_admin: current_admin) + collection = create(:collection, user_admin: current_admin) + round = create(:round, collection: collection, participant: participant) + equation = create(:equation, collections: [ collection ], user_admin: current_admin) + + allow(Round::StartService).to receive(:call).and_return(round) + allow(Round::NextEquationService).to receive(:call).and_return(equation) + + post start_rounds_path, params: { + collection_id: collection.id, + participant_id: participant.id, + round_id: round.id + } + + expect(response).to render_template(:start) + end + end + + describe "POST /submit_answer" do + let(:current_admin) { create(:user_admin) } + + before(:each) do + sign_in current_admin + end + + it "calls the submit answer service" do + participant = create(:user_participant, user_admin: current_admin) + collection = create(:collection, user_admin: current_admin) + equation = create(:equation, collections: [ collection ], user_admin: current_admin) + round = create( + :round, + collection: collection, + current_equation_id: equation.id, + participant: participant + ) + + allow(Round).to receive(:find_by).and_return(round) + + expect(Round::SubmitAnswerService).to receive(:call).with(round, '42') + + post submit_answer_round_url( + locale: I18n.locale, + round_id: round.id, + collection_id: collection.id, + participant_id: participant.id, + params: { answer_value: '42' } + ) + end + + it "redirects to finish if no next equation is found" do + participant = create(:user_participant, user_admin: current_admin) + collection = create(:collection, user_admin: current_admin) + round = create( + :round, + collection: collection, + participant: participant + ) + + allow(Round).to receive(:find_by).and_return(round) + allow(Round::NextEquationService).to receive(:call).and_return(nil) + allow(Round::SubmitAnswerService).to receive(:call).with(round, '42') + + + post submit_answer_round_url( + locale: I18n.locale, + round_id: round.id, + collection_id: collection.id, + participant_id: participant.id, + params: { answer_value: '42' } + ) + + expect(response).to redirect_to( + action: :finish, + collection_id: collection.id, + participant_id: participant.id, + round_id: round.id + ) + end + + it "renders the start template if a next equation is found" do + answer_value = '42' + participant = create(:user_participant, user_admin: current_admin) + collection = create(:collection, user_admin: current_admin) + round = create(:round, collection: collection, participant: participant) + equation = create(:equation, collections: [ collection ], user_admin: current_admin) + + allow(Round).to receive(:find_by).and_return(round) + allow(Round::NextEquationService).to receive(:call).and_return(equation) + allow(Round::SubmitAnswerService).to receive(:call).with(round, answer_value) + + post submit_answer_round_url( + locale: I18n.locale, + round_id: round.id, + collection_id: collection.id, + participant_id: participant.id, + params: { answer_value: answer_value } + ) + + expect(response).to render_template(:start) + end + end + + describe "GET /finish" do + let(:current_admin) { create(:user_admin) } + + before(:each) do + sign_in current_admin + end + it "renders the finish template" do + participant = create(:user_participant, user_admin: current_admin) + collection = create(:collection, user_admin: current_admin) + round = create(:round, collection: collection, participant: participant) + + allow(Round::FinishService).to receive(:call).and_return(true) + + get finish_round_url( + locale: I18n.locale, + round_id: round.id, + collection_id: collection.id, + participant_id: participant.id + ) + + expect(response).to render_template(:finish) + end + end +end diff --git a/spec/services/round/finish_service_spec.rb b/spec/services/round/finish_service_spec.rb new file mode 100644 index 0000000..ea527fe --- /dev/null +++ b/spec/services/round/finish_service_spec.rb @@ -0,0 +1,54 @@ +require "rails_helper" + +RSpec.describe Round::FinishService, type: :service do + let(:current_round) { create(:round, :unfinished) } + let(:current_admin) { create(:user_admin) } + let(:collection) { create(:collection, user_admin: current_admin) } + let(:participant) { create(:user_participant, user_admin: current_admin) } + let(:completed_at) { Time.current } + let(:finish_service) do + described_class.new(current_round, collection, participant, current_admin) + end + + describe "#call" do + context "when collection is completed" do + before do + allow(finish_service).to receive(:collection_completed?).and_return(true) + end + + it "finalizes the round" do + expect(finish_service).to receive(:finalize_round) + finish_service.call + end + + it "updates completed_at field for current_round" do + allow(Time).to receive(:current) + .and_return(completed_at) + + finish_service.call + + expect(current_round.completed_at).to be_within(0.1.seconds).of(completed_at) + end + + it "calculates the time spent to finish the round" do + end_time = current_round.started_at + 5.minutes + allow(Time).to receive(:current).and_return(end_time) + + finish_service.call + + expect(current_round.round_time).to eq 5.minutes.in_milliseconds + end + end + + context "when collection is not completed" do + before do + allow(finish_service).to receive(:collection_completed?).and_return(false) + end + + it "does not finalize the round" do + expect(finish_service).not_to receive(:finalize_round) + finish_service.call + end + end + end +end diff --git a/spec/services/round/next_equation_service_spec.rb b/spec/services/round/next_equation_service_spec.rb new file mode 100644 index 0000000..36cb324 --- /dev/null +++ b/spec/services/round/next_equation_service_spec.rb @@ -0,0 +1,51 @@ +require "rails_helper" + +RSpec.describe Round::NextEquationService, type: :service do + let(:participant) { create(:user_participant) } + let(:collection) { create(:collection) } + let(:current_round) { create(:round, collection: collection, participant: participant) } + let(:service) { described_class.new(current_round) } + + describe ".call" do + it "initializes the service and calls #call" do + expect_any_instance_of(described_class).to receive(:call) + described_class.call(current_round) + end + end + + describe "#call" do + context "when there are unanswered equations" do + let!(:equation) { create(:equation, collections: [ collection ]) } + + before do + allow(service).to receive(:unanswered_equations).and_return([ equation ]) + end + + it "returns the next equation" do + current_equation = service.call + expect(current_equation).to be_an Equation + expect(current_equation).to eq(equation) + end + + it "sets the start time for the current equation" do + service.call + expect(current_round.equation_started_at).to be_within(1.second).of(Time.current) + end + + it "updates the current equation id for the round" do + service.call + expect(current_round.current_equation_id).to eq(equation.id) + end + end + + context "when there are no unanswered equations" do + before do + allow(service).to receive(:unanswered_equations).and_return([]) + end + + it "returns nil" do + expect(service.call).to be_nil + end + end + end +end diff --git a/spec/services/round/start_service_spec.rb b/spec/services/round/start_service_spec.rb new file mode 100644 index 0000000..302d3f7 --- /dev/null +++ b/spec/services/round/start_service_spec.rb @@ -0,0 +1,40 @@ +require "rails_helper" + +RSpec.describe Round::StartService, type: :service do + let(:collection) { create(:collection) } + let(:participant) { create(:user_participant) } + let(:start_service) do + described_class.new(collection, participant) + end + + describe "#call" do + it "calls the start round method" do + expect(start_service).to receive(:start_round) + start_service.call + end + + context "when the round is started for the first time" do + it "creates and returns the round" do + round = start_service.call + + expect(round).to be_a Round + expect(Round.count).to eq 1 + expect(round).to be_persisted + expect(round.started_at).to be_present + expect(round.collection).to eq collection + expect(round.participant).to eq participant + end + end + + context "when the round already exists" do + it "returns the existing round" do + first_round = start_service.call + + round = start_service.call + + expect(round).to eq first_round + expect(Round.count).to eq 1 + end + end + end +end diff --git a/spec/services/round/submit_answer_service_spec.rb b/spec/services/round/submit_answer_service_spec.rb new file mode 100644 index 0000000..94970dc --- /dev/null +++ b/spec/services/round/submit_answer_service_spec.rb @@ -0,0 +1,103 @@ +require 'rails_helper' + +RSpec.describe Round::SubmitAnswerService, type: :service do + let(:current_equation) { create(:equation) } + let(:current_round) { create(:round, current_equation_id: current_equation.id) } + let(:answer_value) { 42 } + let(:service) { described_class.new(current_round, answer_value) } + + describe '#initialize' do + it 'initializes with current_round and answer_value' do + expect(service.instance_variable_get(:@current_round)).to eq(current_round) + expect(service.instance_variable_get(:@answer_value)).to eq(answer_value) + end + end + + describe ".call" do + context "when calling the class method" do + it "calls the instance method" do + service = double + allow(described_class).to receive(:new).and_return(service) + allow(service).to receive(:call) + + described_class.call(current_round, answer_value) + + expect(described_class).to have_received(:new).with(current_round, answer_value) + expect(service).to have_received(:call) + end + end + end + + describe '#call' do + context 'when the answer is submitted' do + it 'calls the submit_answer method' do + allow(service).to receive(:submit_answer) + service.call + expect(service).to have_received(:submit_answer) + end + end + + context 'when there is no current equation' do + it 'raises an error' do + current_round.current_equation_id = nil + + expect { service.call }.to raise_error(StandardError, I18n.t("errors.messages.no_current_equation")) + end + end + + context 'when there is a current equation' do + it 'creates an answer' do + correct_answer = true + collection = create(:collection, equations: [ current_equation ]) + + current_round.collection = collection + allow(service).to receive(:correct_answer?).and_return(correct_answer) + allow(service).to receive(:calculate_time_spent).and_return(10) + + expect { + service.call + }.to change(Answer, :count).by(1) + end + + context 'when the answer is correct' do + it 'creates an answer with correct_answer true' do + correct_answer = true + collection = create(:collection, equations: [ current_equation ]) + + current_round.collection = collection + allow(service).to receive(:correct_answer?).and_return(correct_answer) + allow(service).to receive(:calculate_time_spent).and_return(10) + + answer = service.call + + expect(answer.correct_answer).to be true + end + end + + context 'when the answer is incorrect' do + it 'creates an answer with correct_answer false' do + correct_answer = false + collection = create(:collection, equations: [ current_equation ]) + + current_round.collection = collection + allow(service).to receive(:correct_answer?).and_return(correct_answer) + allow(service).to receive(:calculate_time_spent).and_return(10) + + answer = service.call + + expect(answer.correct_answer).to be false + end + end + + it 'calculates the time spent to solve the equation' do + collection = create(:collection, equations: [ current_equation ]) + current_round.collection = collection + current_round.equation_started_at = 1.minute.ago + + answer = service.call + + expect(answer.time_spent).to be_within(100).of(60000) + end + end + end +end diff --git a/spec/views/rounds/_form.html.erb_spec.rb b/spec/views/rounds/_form.html.erb_spec.rb new file mode 100644 index 0000000..9d404ed --- /dev/null +++ b/spec/views/rounds/_form.html.erb_spec.rb @@ -0,0 +1,39 @@ +require "rails_helper" + +RSpec.describe "rounds/_form.html.erb", type: :view do + let(:current_admin) { create(:user_admin) } + before(:each) do + assign(:round, Round.new( + participant: create(:user_participant, first_name: "John", last_name: "Doe", user_admin: current_admin), + collection: create(:collection, name: "Collection", equations_quantity: 2, user_admin: current_admin) + )) + end + + it "renders new round form" do + sign_in current_admin + stub_template("shared/_header.html.erb" => "This content") + render + + expect(rendered).to have_selector("form") + expect(rendered).to have_selector( + "label", text: I18n.t("activerecord.models.user/participant") + ) + expect(rendered).to have_selector("select", text: "John Doe") + expect(rendered).to have_selector( + "label", text: I18n.t("activerecord.models.collection") + ) + expect(rendered).to have_selector("select", text: "Collection") + expect(rendered).to have_selector("div.actions.mt-1") + expect(rendered).to have_selector( + :button, + name: "commit", + class: "btn green", + value: I18n.t("helpers.submit.start_round") # The error is in the text + ) + expect(rendered).to have_selector( + "a", + text: I18n.t("helpers.submit.cancel"), + class: "btn crimson" + ) + end +end diff --git a/spec/views/rounds/finish.html.erb_spec.rb b/spec/views/rounds/finish.html.erb_spec.rb new file mode 100644 index 0000000..1beec65 --- /dev/null +++ b/spec/views/rounds/finish.html.erb_spec.rb @@ -0,0 +1,21 @@ +require "rails_helper" + +RSpec.describe "rounds/finish", type: :view do + let(:current_admin) { create(:user_admin) } + before(:each) do + assign(:round, Round.new( + participant: create(:user_participant, first_name: "John", last_name: "Doe", user_admin: current_admin), + collection: create(:collection, name: "Collection", equations_quantity: 2, user_admin: current_admin) + )) + end + + it "renders new round form partial" do + sign_in current_admin + stub_template("shared/_header.html.erb" => "This content") + + render + + expect(rendered).to have_selector("h1", text: I18n.t("rounds.congrats")) + expect(rendered).to have_selector("p", text: I18n.t("rounds.congrats_message")) + end +end diff --git a/spec/views/rounds/new.html.erb_spec.rb b/spec/views/rounds/new.html.erb_spec.rb new file mode 100644 index 0000000..c149b69 --- /dev/null +++ b/spec/views/rounds/new.html.erb_spec.rb @@ -0,0 +1,21 @@ +require "rails_helper" + +RSpec.describe "rounds/new", type: :view do + let(:current_admin) { create(:user_admin) } + before(:each) do + assign(:round, Round.new( + participant: create(:user_participant, first_name: "John", last_name: "Doe", user_admin: current_admin), + collection: create(:collection, name: "Collection", equations_quantity: 2, user_admin: current_admin) + )) + end + + it "renders new round form partial" do + sign_in current_admin + stub_template("shared/_header.html.erb" => "This content") + + render + + expect(rendered).to have_rendered("rounds/_form") + expect(rendered).to have_selector("h2", text: I18n.t("rounds.new")) + end +end diff --git a/spec/views/rounds/start.html.erb_spec.rb b/spec/views/rounds/start.html.erb_spec.rb new file mode 100644 index 0000000..55b73ff --- /dev/null +++ b/spec/views/rounds/start.html.erb_spec.rb @@ -0,0 +1,58 @@ +require "rails_helper" + +RSpec.describe "rounds/start", type: :view do + let(:current_admin) { create(:user_admin) } + let(:collection) { create(:collection, user_admin: current_admin) } + let(:participant) { create(:user_participant, user_admin: current_admin) } + let(:current_round) { create(:round, :uncompleted, collection: collection, participant: participant) } + before(:each) do + assign(:current_round, current_round) + assign(:collection, collection) + assign(:participant, participant) + end + + it "renders partial collections/unknown_position_a" do + assign(:current_equation, create( + :equation, + unknown_position: "a", + user_admin: current_admin + )) + + sign_in current_admin + stub_template("shared/_header.html.erb" => "This content") + + render + + expect(rendered).to have_rendered("collections/_unknown_position_a") + end + + it "renders partial collections/unknown_position_b" do + assign(:current_equation, create( + :equation, + unknown_position: "b", + user_admin: current_admin + )) + + sign_in current_admin + stub_template("shared/_header.html.erb" => "This content") + + render + + expect(rendered).to have_rendered("collections/_unknown_position_b") + end + + it "renders partial collections/unknown_position_a" do + assign(:current_equation, create( + :equation, + unknown_position: "c", + user_admin: current_admin + )) + + sign_in current_admin + stub_template("shared/_header.html.erb" => "This content") + + render + + expect(rendered).to have_rendered("collections/_unknown_position_c") + end +end From 9c7f33567838b7d56c41c2dbfb9e4fad034a2dcb Mon Sep 17 00:00:00 2001 From: Felipe Murakami <13895820+fhmurakami@users.noreply.github.com> Date: Thu, 19 Dec 2024 23:02:52 +0000 Subject: [PATCH 06/10] Create Report model From 5a662fe8307cb554c87f0dcace571d67dbac9bb8 Mon Sep 17 00:00:00 2001 From: Felipe Murakami <13895820+fhmurakami@users.noreply.github.com> Date: Sun, 29 Dec 2024 01:21:54 +0000 Subject: [PATCH 07/10] Move round length validation to Round model --- app/models/report.rb | 15 ------- app/models/round.rb | 26 ++++++++++++- spec/models/report_spec.rb | 67 ------------------------------- spec/models/round_spec.rb | 80 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 83 deletions(-) diff --git a/app/models/report.rb b/app/models/report.rb index 54c876d..cde4bcd 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -6,19 +6,4 @@ class Report < ApplicationRecord has_many :participants, class_name: "User::Participant", foreign_key: :user_participant_id, through: :grouping has_many :rounds - - validate :rounds_length - - private - - def rounds_length - if rounds.length > grouping.participants.length - errors.add( - :base, - :too_many_rounds, - grouping: grouping.name, - count: grouping.participants.length - ) - end - end end diff --git a/app/models/round.rb b/app/models/round.rb index 0da9aa6..0f7bd68 100644 --- a/app/models/round.rb +++ b/app/models/round.rb @@ -1,9 +1,33 @@ class Round < ApplicationRecord belongs_to :collection belongs_to :participant, class_name: "User::Participant", foreign_key: "user_participant_id" - belongs_to :report + belongs_to :report, optional: true has_many :answers validates :user_participant_id, uniqueness: { scope: :collection_id } + validate :rounds_length + + private + + def rounds_length + report = Report.find_by(collection: collection) + grouping = report&.grouping + + if report && grouping + rounds = report&.rounds + participants = grouping&.participants + + if rounds && participants + return true if rounds.length <= participants.length + + errors.add( + :base, + :too_many_rounds, + grouping: grouping.name, + count: grouping.participants.length + ) + end + end + end end diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index 8471073..9093d6e 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -18,71 +18,4 @@ .through(:grouping) end end - - describe 'model validations' do - let(:user_admin) { create(:user_admin) } - let(:collection) { create(:collection, user_admin: user_admin) } - let(:grouping) { create(:grouping, user_admin: user_admin) } - let(:participant) { - create( - :user_participant, - grouping: grouping, - user_admin: user_admin - ) - } - let(:report) { - create( - :report, - user_admin: user_admin, - collection: collection, - grouping: grouping - ) - } - - context 'when rounds count is less or equal to the number of participants in a grouping' do - it 'should be valid with no errors' do - report.rounds << create( - :round, - collection: collection, - participant: participant, - report: report - ) - - expect(report).to be_valid - expect(report.errors).to be_empty - end - end - - context 'when rounds count is greater than the number of participants in a grouping' do - it 'should not be valid' do - report.rounds << [ - create( - :round, - collection: collection, - participant: participant, - report: report - ), - create( - :round, - collection: collection, - participant: create(:user_participant, user_admin: user_admin), - report: report - ) - ] - - grouping.reload - - expect(report).not_to be_valid - expect(report.errors).not_to be_empty - expect(report.errors.full_messages).to include( - I18n.t( - 'errors.messages.too_many_rounds', - model: report.model_name.human, - grouping: grouping.name, - count: grouping.participants.length - ) - ) - end - end - end end diff --git a/spec/models/round_spec.rb b/spec/models/round_spec.rb index bbf9cc2..8b1b6f8 100644 --- a/spec/models/round_spec.rb +++ b/spec/models/round_spec.rb @@ -14,6 +14,86 @@ describe 'model associations' do it { should belong_to(:collection) } it { should belong_to(:participant).class_name('User::Participant') } + it { should belong_to(:report).optional } it { should have_many(:answers) } end + + + describe 'model validations' do + let(:user_admin) { create(:user_admin) } + let(:collection) { create(:collection, user_admin: user_admin) } + let(:grouping) { create(:grouping, user_admin: user_admin) } + let(:participant) { + create( + :user_participant, + grouping: grouping, + user_admin: user_admin + ) + } + let(:report) { + create( + :report, + user_admin: user_admin, + collection: collection, + grouping: grouping + ) + } + + context "when report is nil" do + it "should be valid" do + round = create(:round, report: nil) + + expect(round).to be_valid + expect(round.errors).to be_empty + end + end + + context 'when rounds count is less or equal to the number of participants in a grouping' do + it 'should be valid with no errors' do + round = create( + :round, + collection: collection, + participant: participant, + report: report + ) + + expect(round).to be_valid + expect(round.errors).to be_empty + end + end + + context 'when rounds count is greater than the number of participants in a grouping' do + it 'should not be valid' do + grouping.reload + + report.rounds << [ + create( + :round, + collection: collection, + participant: participant, + report: report + ) + ] + + round = create( + :round, + collection: collection, + participant: create(:user_participant, user_admin: user_admin), + report: report + ) + + + expect(round).not_to be_valid + expect(round.errors).not_to be_empty + expect(round.errors.full_messages).to include( + I18n.t( + 'errors.messages.too_many_rounds', + model: round.model_name.human, + grouping: grouping.name, + count: grouping.participants.length + ) + ) + end + end + end end From d657d04a987654f3d63d82829f059ed00cb99e01 Mon Sep 17 00:00:00 2001 From: Felipe Murakami <13895820+fhmurakami@users.noreply.github.com> Date: Sun, 29 Dec 2024 02:11:57 +0000 Subject: [PATCH 08/10] Add report views and tests --- app/assets/stylesheets/main.css | 17 +++++-- app/views/reports/_report.html.erb | 54 +++++++++++++++++++++ app/views/reports/_report.json.jbuilder | 2 + app/views/reports/index.html.erb | 18 +++++++ app/views/reports/index.json.jbuilder | 1 + app/views/reports/show.html.erb | 12 +++++ app/views/reports/show.json.jbuilder | 1 + app/views/shared/_header.html.erb | 2 +- config/routes.rb | 2 +- spec/views/reports/_report.html.erb_spec.rb | 40 +++++++++++++++ spec/views/reports/index.html.erb_spec.rb | 34 +++++++++++++ spec/views/reports/show.html.erb_spec.rb | 27 +++++++++++ 12 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 app/views/reports/_report.html.erb create mode 100644 app/views/reports/_report.json.jbuilder create mode 100644 app/views/reports/index.html.erb create mode 100644 app/views/reports/index.json.jbuilder create mode 100644 app/views/reports/show.html.erb create mode 100644 app/views/reports/show.json.jbuilder create mode 100644 spec/views/reports/_report.html.erb_spec.rb create mode 100644 spec/views/reports/index.html.erb_spec.rb create mode 100644 spec/views/reports/show.html.erb_spec.rb diff --git a/app/assets/stylesheets/main.css b/app/assets/stylesheets/main.css index 84ea2e1..4f679f5 100644 --- a/app/assets/stylesheets/main.css +++ b/app/assets/stylesheets/main.css @@ -112,6 +112,10 @@ li { margin-top: 1rem; } +.pr-9 { + padding-right: 9rem; +} + .text-center { text-align: center; } @@ -248,7 +252,8 @@ nav div.user { .buttons .collections, .buttons .equations, .buttons .groupings, -.buttons .participants { +.buttons .participants, +.buttons .reports { width: 100%; gap: 2.5vmin; } @@ -326,7 +331,8 @@ nav div.user { .collections, .equations, .groupings, -.participants { +.participants, +.reports { width: 90%; display: flex; flex-direction: column; @@ -363,7 +369,8 @@ nav div.user { #answers, #collections, -#groupings { +#groupings, +#reports { width: 100%; display: flex; flex-direction: column; @@ -427,7 +434,8 @@ nav div.user { .collections a, .equations a, .groupings a, -.participants a { +.participants a, +.reports a { text-align: center; font-size: 1.5rem; margin: 2vmin auto; @@ -525,6 +533,7 @@ a.forgot-password:hover { } .btn { + /* font-size: 1.2rem; */ cursor: pointer; width: 100%; text-align: center; diff --git a/app/views/reports/_report.html.erb b/app/views/reports/_report.html.erb new file mode 100644 index 0000000..03947bc --- /dev/null +++ b/app/views/reports/_report.html.erb @@ -0,0 +1,54 @@ +
+ + + + + + + + + + + <% report.rounds.each do |round| %> + + + + + + + + + + + + + + + + + + + + <% round.answers.each do |answer| %> + + + + + + + + + + + <% end %> + <% end %> + + +
+ <%= report.grouping.name %> +
<%= t("activerecord.models.collection") %><%= report.collection.name %>
<%= t("activerecord.models.user/participant") %><%= round.participant.full_name %>
+ <%= t("activerecord.models.equation").pluralize %> + <%= t("activerecord.attributes.equation.position_a") %><%= t("activerecord.attributes.equation.operator") %><%= t("activerecord.attributes.equation.position_b") %><%= t("activerecord.attributes.equation.position_c") %><%= t("activerecord.attributes.equation.unknown_position") %> + <%= t("activerecord.models.answer").pluralize %> + <%= t("activerecord.attributes.answer.answer_value") %><%= t("activerecord.attributes.answer.correct_answer") %><%= t("activerecord.attributes.answer.time") %>
<%= answer.equation.position_a %><%= answer.equation.operator %><%= answer.equation.position_b %><%= answer.equation.position_c %><%= answer.equation.unknown_position %><%= answer.answer_value %><%= answer.correct_answer ? "✔️" : "❌" %><%= answer.formatted_time %>
+
diff --git a/app/views/reports/_report.json.jbuilder b/app/views/reports/_report.json.jbuilder new file mode 100644 index 0000000..4e5c131 --- /dev/null +++ b/app/views/reports/_report.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! report, :id, :user_admin_id, :user_participant_id, :collection_id, :grouping_id, :created_at, :updated_at +json.url report_url(report, format: :json) diff --git a/app/views/reports/index.html.erb b/app/views/reports/index.html.erb new file mode 100644 index 0000000..6096e1c --- /dev/null +++ b/app/views/reports/index.html.erb @@ -0,0 +1,18 @@ +<% content_for :title, t("activerecord.models.report").pluralize %> +
+
+ <%= render "shared/header" %> +

<%= t("activerecord.models.report").pluralize %>

+
+
+ <% @reports.each do |report| %> +
+ <%= render report %> + <%= link_to t("reports.show"), report %> +
+ <% end %> +
+ <%= link_to t("home.back"), root_path, class: "btn mustard btn-text-black" %> +
+
+
diff --git a/app/views/reports/index.json.jbuilder b/app/views/reports/index.json.jbuilder new file mode 100644 index 0000000..31811f6 --- /dev/null +++ b/app/views/reports/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @reports, partial: "reports/report", as: :report diff --git a/app/views/reports/show.html.erb b/app/views/reports/show.html.erb new file mode 100644 index 0000000..0975380 --- /dev/null +++ b/app/views/reports/show.html.erb @@ -0,0 +1,12 @@ +
+
+ <%= render "shared/header" %> +
+ <%= render @report %> +
+ <%= link_to t("reports.back"), reports_path %> + <%= button_to t("reports.delete"), @report, class: "btn crimson", method: :delete, data: { turbo_method: :delete } %> +
+
+
+
diff --git a/app/views/reports/show.json.jbuilder b/app/views/reports/show.json.jbuilder new file mode 100644 index 0000000..b5a5508 --- /dev/null +++ b/app/views/reports/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "reports/report", report: @report diff --git a/app/views/shared/_header.html.erb b/app/views/shared/_header.html.erb index 784f74a..9643956 100644 --- a/app/views/shared/_header.html.erb +++ b/app/views/shared/_header.html.erb @@ -7,7 +7,7 @@ <%= link_to t("activerecord.models.grouping").pluralize, groupings_path, class: "nav-item" %> <%= link_to t("activerecord.models.collection").pluralize, collections_path, class: "nav-item" %> <%= link_to t("activerecord.models.equation").pluralize, equations_path, class: "nav-item" %> - <%#= link_to t("activerecord.models.answer").pluralize, answers_path, class: "nav-item" %> + <%= link_to t("activerecord.models.report").pluralize, reports_path, class: "nav-item" %> <% end %>
diff --git a/config/routes.rb b/config/routes.rb index 1eb7641..c6ed3b3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,6 @@ # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html Rails.application.routes.draw do - scope "(:locale)", locale: /pt-BR|en/ do + scope "(:locale)", locale: /pt-BR|en/, defaults: { locale: "pt-BR" } do # Defines the root path route ("/") root "home#index" diff --git a/spec/views/reports/_report.html.erb_spec.rb b/spec/views/reports/_report.html.erb_spec.rb new file mode 100644 index 0000000..b57eed4 --- /dev/null +++ b/spec/views/reports/_report.html.erb_spec.rb @@ -0,0 +1,40 @@ +require 'rails_helper' + +RSpec.describe "reports/_report", type: :view do + let(:current_admin) { create(:user_admin) } + it "renders a report table" do + report = create(:report) + assign(:report, report) + + render(locals: { report: report }) + + expect(rendered).to have_selector("table") + expect(rendered).to have_selector("th > strong", text: report.grouping.name) + expect(rendered).to have_selector("th", text: report.collection.name) + + report.rounds.each do |round| + expect(rendered).to have_selector("th", text: round.participant.full_name) + expect(rendered).to have_selector("th > strong", text: I18n.t("activerecord.models.equation").pluralize) + expect(rendered).to have_selector("th", text: I18n.t("activerecord.attributes.equation.position_a")) + expect(rendered).to have_selector("th", text: I18n.t("activerecord.attributes.equation.operator")) + expect(rendered).to have_selector("th", text: I18n.t("activerecord.attributes.equation.position_b")) + expect(rendered).to have_selector("th", text: I18n.t("activerecord.attributes.equation.position_c")) + expect(rendered).to have_selector("th", text: I18n.t("activerecord.attributes.equation.unknown_position")) + expect(rendered).to have_selector("th > strong", text: I18n.t("activerecord.models.answer").pluralize) + expect(rendered).to have_selector("th", text: I18n.t("activerecord.attributes.answer.answer_value")) + expect(rendered).to have_selector("th", text: I18n.t("activerecord.attributes.answer.correct_answer")) + expect(rendered).to have_selector("th", text: I18n.t("activerecord.attributes.answer.time")) + + round.answers.each do |answer| + expect(rendered).to have_selector("td", text: answer.equation.position_a) + expect(rendered).to have_selector("td", text: answer.equation.operator) + expect(rendered).to have_selector("td", text: answer.equation.position_b) + expect(rendered).to have_selector("td", text: answer.equation.position_c) + expect(rendered).to have_selector("td", text: answer.equation.unknown_position) + expect(rendered).to have_selector("td", text: answer.answer_value) + expect(rendered).to have_selector("td", text: answer.correct_answer ? "✔️" : "❌") + expect(rendered).to have_selector("td", text: answer.formatted_time) + end + end + end +end diff --git a/spec/views/reports/index.html.erb_spec.rb b/spec/views/reports/index.html.erb_spec.rb new file mode 100644 index 0000000..cae0c5f --- /dev/null +++ b/spec/views/reports/index.html.erb_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +RSpec.describe "reports/index", type: :view do + let(:current_admin) { create(:user_admin) } + + before(:each) do + assign(:reports, [ + Report.create!( + user_admin: current_admin, + collection: create(:collection), + grouping: create(:grouping) + ), + Report.create!( + user_admin: current_admin, + collection: create(:collection), + grouping: create(:grouping) + ) + ]) + end + + it "renders a list of reports" do + sign_in current_admin + stub_template("shared/_header.html.erb" => "This content") + + render + + expect(rendered).to have_selector("main", class: "background") + expect(rendered).to have_selector("section", class: "blur") + expect(rendered).to have_rendered("shared/_header") + expect(rendered).to have_selector("h2", text: I18n.t("activerecord.models.report").pluralize) + expect(rendered).to have_rendered("reports/_report") + expect(rendered).to have_link(I18n.t("reports.show"), href: report_path(Report.last)) + end +end diff --git a/spec/views/reports/show.html.erb_spec.rb b/spec/views/reports/show.html.erb_spec.rb new file mode 100644 index 0000000..fe4eb95 --- /dev/null +++ b/spec/views/reports/show.html.erb_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +RSpec.describe "reports/show", type: :view do + let(:current_admin) { create(:user_admin) } + + before(:each) do + assign(:report, Report.create!( + user_admin: current_admin, + collection: create(:collection), + grouping: create(:grouping) + )) + end + + it "renders partial reports/report" do + sign_in current_admin + stub_template("shared/_header.html.erb" => "This content") + + render + + expect(rendered).to have_selector("main", class: "background") + expect(rendered).to have_selector("section", class: "blur") + expect(rendered).to have_rendered("shared/_header") + expect(rendered).to have_rendered("reports/_report") + expect(rendered).to have_link(I18n.t("reports.back"), href: reports_path) + expect(rendered).to have_selector("button", text: I18n.t("reports.delete"), class: "btn crimson") + end +end From 88089a378ea42e67c4bb9ff112825fac8b695f7f Mon Sep 17 00:00:00 2001 From: Felipe Murakami <13895820+fhmurakami@users.noreply.github.com> Date: Sun, 29 Dec 2024 02:23:00 +0000 Subject: [PATCH 09/10] Add a service to create Reports --- app/services/report/create_service.rb | 23 +++++++++ app/services/round/finish_service.rb | 9 +++- spec/services/report/create_service_spec.rb | 54 +++++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 app/services/report/create_service.rb create mode 100644 spec/services/report/create_service_spec.rb diff --git a/app/services/report/create_service.rb b/app/services/report/create_service.rb new file mode 100644 index 0000000..e0f31ad --- /dev/null +++ b/app/services/report/create_service.rb @@ -0,0 +1,23 @@ +class Report::CreateService + def initialize(user_admin, collection, grouping) + @user_admin = user_admin + @collection = collection + @grouping = grouping + end + + def self.call(user_admin:, collection:, grouping:) + new(user_admin, collection, grouping).call + end + + def call + create_report + end + + def create_report + @report = Report.find_or_create_by( + user_admin: @user_admin, + collection: @collection, + grouping: @grouping + ) + end +end diff --git a/app/services/round/finish_service.rb b/app/services/round/finish_service.rb index e69977f..8525e31 100644 --- a/app/services/round/finish_service.rb +++ b/app/services/round/finish_service.rb @@ -29,9 +29,16 @@ def collection_completed? def finalize_round @completed_at = Time.current + report = Report::CreateService.call( + user_admin: @user_admin, + collection: @collection, + grouping: @participant.grouping, + ) + @current_round.update!( completed_at: @completed_at, - round_time: calculate_round_time + round_time: calculate_round_time, + report: report ) end diff --git a/spec/services/report/create_service_spec.rb b/spec/services/report/create_service_spec.rb new file mode 100644 index 0000000..c1c8e9b --- /dev/null +++ b/spec/services/report/create_service_spec.rb @@ -0,0 +1,54 @@ +require "rails_helper" + +RSpec.describe Report::CreateService, type: :service do + describe ".call" do + let(:user_admin) { create(:user_admin) } + let(:collection) { create(:collection) } + let(:grouping) { create(:grouping) } + + it "calls the instance method #call" do + service_instance = instance_double(Report::CreateService) + allow(Report::CreateService).to receive(:new).and_return(service_instance) + allow(service_instance).to receive(:call) + + Report::CreateService.call(user_admin: user_admin, collection: collection, grouping: grouping) + + expect(Report::CreateService).to have_received(:new).with(user_admin, collection, grouping) + expect(service_instance).to have_received(:call) + end + end + + describe "#initialize" do + subject(:service) { described_class.new(user_admin, collection, grouping) } + let(:user_admin) { create(:user_admin) } + let(:collection) { create(:collection) } + let(:grouping) { create(:grouping) } + + it "initializes with user_admin, collection and grouping" do + expect(service.instance_variable_get(:@user_admin)).to eq(user_admin) + expect(service.instance_variable_get(:@collection)).to eq(collection) + expect(service.instance_variable_get(:@grouping)).to eq(grouping) + end + end + + describe "#call" do + subject(:service) { described_class.new(user_admin, collection, grouping) } + let(:user_admin) { create(:user_admin) } + let(:collection) { create(:collection) } + let(:grouping) { create(:grouping) } + + context "when report doesn't exist" do + it "creates a report" do + expect { service.call }.to change(Report, :count).by(1) + end + end + + context "when report already exists" do + let!(:report) { create(:report, user_admin: user_admin, collection: collection, grouping: grouping) } + + it "doesn't create a report" do + expect { service.call }.not_to change(Report, :count) + end + end + end +end From 1972762c694661711f0031ad205dcb21ceb3457f Mon Sep 17 00:00:00 2001 From: Felipe Murakami <13895820+fhmurakami@users.noreply.github.com> Date: Sun, 29 Dec 2024 02:47:59 +0000 Subject: [PATCH 10/10] Add locale to routing/reports specs --- spec/routing/reports_routing_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/routing/reports_routing_spec.rb b/spec/routing/reports_routing_spec.rb index 803a787..760dbc6 100644 --- a/spec/routing/reports_routing_spec.rb +++ b/spec/routing/reports_routing_spec.rb @@ -3,15 +3,15 @@ RSpec.describe ReportsController, type: :routing do describe "routing" do it "routes to #index" do - expect(get: "/reports").to route_to("reports#index") + expect(get: "/reports").to route_to("reports#index", locale: I18n.locale.to_s) end it "routes to #show" do - expect(get: "/reports/1").to route_to("reports#show", id: "1") + expect(get: "/reports/1").to route_to("reports#show", id: "1", locale: I18n.locale.to_s) end it "routes to #destroy" do - expect(delete: "/reports/1").to route_to("reports#destroy", id: "1") + expect(delete: "/reports/1").to route_to("reports#destroy", id: "1", locale: I18n.locale.to_s) end end end