<%= 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.grouping.name %>
+ |
+
+
+ <%= t("activerecord.models.collection") %> |
+ <%= report.collection.name %> |
+
+
+ <% report.rounds.each do |round| %>
+
+
+ <%= 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") %> |
+
+ <% round.answers.each do |answer| %>
+
+ <%= 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 %> |
+
+ <% end %>
+ <% end %>
+
+
+
+
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