Skip to content

Trilha de Aprendizado (Learning Trail)

A Trilha de Aprendizado é um sistema de revisão adaptativa implementado em 2025. Após um aluno responder uma atividade agendada (ScheduledExams::Model), o sistema analisa seu desempenho por habilidade e gera automaticamente uma trilha de questões focada nos pontos fracos do aluno.

A trilha é apresentada de forma gamificada (com o mascote Duduq, missões e uma trilha em formato de onda) e está disponível tanto na web quanto via API v3 para o app mobile.

Feature flag: todo o sistema é controlado pela flag de munícipio :learning_trail (require_feature_flag_for_user_county(:learning_trail) / learning_trail_enabled?(user)). Sem a flag habilitada para o munícipio do usuário, as trilhas não são geradas nem exibidas.

Visão geral do fluxo

1. Professor agenda uma atividade (ScheduledExams::Model)
   → se learning_trail_enabled?, agenda GenerateTrailExamsJob.perform_at(exam.finish, ...)

2. Alunos respondem a atividade normalmente

3. No horário de término (finish) o job roda:
   → analisa desempenho por habilidade (pior → melhor)
   → seleciona questões novas focadas nas piores habilidades
   → cria TrailExams::Model + TrailExams::Questions por aluno

4. Aluno acessa a trilha (web ou app):
   activity → submit_answer → results / result_review → finish/quit

Modelo de dados

Todos os modelos usam o prefixo de tabela trail_exams_ (via table_name_prefix do módulo TrailExams). Arquivos em app/models/trail_exams/.

TrailExams::Model — trail_exams_models

Representa uma trilha de um aluno, gerada a partir de uma atividade agendada.

ColunaTipoNotas
idbigintPK
student_idbigintFK → Student
scheduled_exams_model_idbigintFK → ScheduledExams::Model
namestringEx.: "Trilha da #{exam.name}"
donebooleanTrilha concluída
quitboolean (default false)Aluno abandonou a trilha
latestbooleanMarca a trilha mais recente do aluno
discarded_atdatetimeSoft-delete (Discard::Model)
created_at / updated_atdatetime
ruby
belongs_to :student
belongs_to :scheduled_exams_model, class_name: "ScheduledExams::Model",
           inverse_of: :trail_exams_models, dependent: :destroy
has_many   :trail_exams_questions, dependent: :destroy

include Discard::Model
scope :with_questions, -> { joins(:trail_exams_questions).distinct }

TrailExams::Questions — trail_exams_questions

Tabela de junção (ordenada) entre uma trilha e as questões selecionadas (Questions::Model).

ColunaTipoNotas
idbigintPK
trail_exams_model_idbigintFK → TrailExams::Model
questions_model_idbigintFK → Questions::Model
numberintegerOrdem da questão na trilha
ruby
belongs_to :trail_exams_model, class_name: "TrailExams::Model"
belongs_to :questions_model,   class_name: "Questions::Model"
has_many   :trail_exams_alternative_responses, dependent: :destroy

TrailExams::AlternativeResponse — trail_exams_alternative_responses

Registra cada resposta do aluno a uma questão da trilha.

ColunaTipoNotas
idbigintPK
alternativeintegerAlternativa marcada (1–5 ↔ A–E)
questions_model_idbigintFK → Questions::Model
trail_exams_model_idbigintFK → TrailExams::Model
trail_exams_question_idbigintFK → TrailExams::Questions
student_idbigintFK → Student
ruby
validates :alternative, presence: true, inclusion: { in: 1..5 }

def alternative_as_char
  ("A".ord + alternative - 1).chr   # 1 → "A", 2 → "B", ...
end

def correct?
  questions_model.correct_alternative_number == alternative
end

Associações no Student

ruby
has_many :trail_exams                                   # todas
has_many :pending_trail_exams,   -> { where(done: false) }
has_many :completed_trail_exams, -> { where(done: true) }

SchoolClass expõe pending_trail_exams_count / completed_trail_exams_count (via alunos) para os painéis de professor.

Diagrama de relacionamentos

Student ──< TrailExams::Model >── ScheduledExams::Model

                  └──< TrailExams::Questions >── Questions::Model

                              └──< TrailExams::AlternativeResponse >── Student

Integração com o sistema de atividades existente

  • A trilha nasce de uma ScheduledExams::Model (atividade agendada). Ao deletar a atividade agendada, as trilhas associadas são removidas em cascata.
  • As questões da trilha são Questions::Model reutilizadas do banco de questões — nunca as mesmas questões da prova original (são explicitamente excluídas na geração).
  • Questions::Model#used_in_activities? considera tanto provas (exams_questions) quanto trilhas (trail_exams_questions).
  • A priorização usa o sistema de Habilidades (Ability), associadas às questões via abilities_questions_models. Veja Habilidade.

Jobs de geração

Os jobs ficam em app/sidekiq/ e herdam de Sidekiq::Worker (fila padrão). Ambos selecionam questões com os mesmos limites:

  • QUESTIONS_PER_ABILITY_LIMIT = 2 — no máximo 2 questões por habilidade
  • TOTAL_QUESTIONS_LIMIT = 10 — no máximo 10 questões por trilha

GenerateTrailExamsJob

app/sidekiq/generate_trail_exams_job.rb

ruby
GenerateTrailExamsJob.perform_at(scheduled_exam.finish, scheduled_exam.id, scheduled_exam.finish.to_s)

Argumentos: scheduled_exam_id, scheduled_finish_time (opcional, string).

Passos:

  1. Carrega a ScheduledExams::Model.
  2. Reagendamento: se scheduled_finish_time foi passado e difere do finish atual, reagenda o job para o novo horário e encerra (trata o caso de o término ter sido alterado antes da execução).
  3. Guard de duplicidade: retorna cedo se já existem TrailExams::Model para essa atividade.
  4. Análise de desempenho: para cada aluno com respostas, calcula o % de acerto por habilidade e ordena as habilidades da pior para a melhor.
  5. Seleção de questões (select_trail_exam_questions): começando pelas piores habilidades, busca até 2 questões válidas por habilidade (com conteúdo, alternativas e gabarito definido; alternative_quantity em [2, 4, 5]; excluindo as questões da prova original; ORDER BY RANDOM()), respeitando o teto de 10 questões. A habilidade precisa passar em valid?(:learning_trail_activity_creation).
  6. Criação: cria um TrailExams::Model por aluno e os TrailExams::Questions correspondentes.

GenerateTrailExamsOnUpdateJob

app/sidekiq/generate_trail_exams_on_update_job.rb

ruby
GenerateTrailExamsOnUpdateJob.perform_at(scheduled_exam.finish, scheduled_exam.id)

Versão incremental, disparada quando o horário de término de uma atividade é alterado depois que ela já havia sido configurada. Diferenças em relação ao job principal:

  • Guard de tempo: se scheduled_exam.finish >= Time.current, encerra (a prova ainda não terminou).
  • Apenas alunos sem trilha: calcula alunos com respostas − alunos que já têm trilha e gera somente para os faltantes; encerra cedo se todos já possuem trilha.
  • Logging detalhado de início/saída/conclusão.
AspectoGenerateTrailExamsJobGenerateTrailExamsOnUpdateJob
DisparoAgendamento da atividadeAlteração do finish após o agendamento
AlunosTodos com respostasApenas os que ainda não têm trilha
ReagendamentoSim (detecta mudança de finish)Não
Guard de tempoNãoSim (sai se a prova não terminou)

Onde os jobs são disparados

LocalGatilhoJob
scheduled_exams/bulk_schedule_controller.rbMúltipla marcação de atividadesGenerateTrailExamsJob
scheduled_exams/models_controller.rb#createAgendamento individualGenerateTrailExamsJob
scheduled_exams/models_controller.rb#updatefinish alterado nos paramsGenerateTrailExamsOnUpdateJob

Todos os gatilhos são condicionados a learning_trail_enabled?(current_user).

Controllers e fluxo do estudante (Web)

Rotas no namespace learning_trail (config/routes.rb), controllers em app/controllers/learning_trail/. Layout logged_user_layout; before_action: require_login + feature flag.

LearningTrail::StudentController

RotaActionDescrição
GET /learning_trail/indexindexLista as trilhas do aluno (botões/missões), card do Duduq e atividades recentes
GET /learning_trail/activity/:trail_exam_idactivityExibe uma questão (question_index, default 0)
POST /learning_trail/activity/submitsubmit_answerSalva a resposta, mostra feedback, avança ou vai para resultados
GET /learning_trail/results/:trail_exam_idresultsResumo final (acertos / total / score)
GET /learning_trail/result_review/:trail_exam_idresult_reviewRevisão questão a questão após concluir
POST /learning_trail/quit_exam/:trail_exam_idquit_examAbandona a trilha (done: true, quit: true)
POST /learning_trail/mark_onboarding_seenmark_onboarding_seenMarca o onboarding como visto

Fluxo do aluno:

  1. Index → escolhe uma trilha disponível. Cada botão tem um estado: open, end_soon, expired, blocked, done ou quit (calculado por semana, ver app/services/learning_trail.rb).
  2. Activity → responde a questão (question_index 0, 1, 2, …).
  3. Submit → cria TrailExams::AlternativeResponse, compara com correct_alternative_number, mostra feedback (acerto/erro) e o botão "Próxima".
  4. Última questão → marca done: true e redireciona para results.
  5. Result review (opcional) → revê cada resposta. Quit (opcional) → encerra a trilha.

LearningTrail::TeacherController

RotaActionDescrição
GET /learning_trail/teacher_indexindexLista alunos com status das trilhas (abas todos/pendencias/concluidas, busca, filtro por sala)
GET /learning_trail/student_index/:student_idstudent_indexDashboard de trilhas de um aluno específico
GET /learning_trail/teacher/:student_id/exam_results/:exam_idexam_resultsResultado detalhado de uma trilha concluída
GET /learning_trail/teacher/:student_id/exam_results/:exam_id/reviewresult_reviewRevisão das respostas do aluno

API v3 (App mobile)

Namespace api/v3/learning_trail (config/routes.rb). Controllers em app/controllers/api/v3/learning_trail/. Respostas em JSON (Panko serializers). before_action: feature flag, require_student, set_trail_exam e authorize_trail_exam_access! (garante que o aluno é dono da trilha).

Endpoints do aluno

MétodoRotaAction
GET/api/v3/learning_trail/studentindex
GET/api/v3/learning_trail/student/exams/:trail_exam_id/activityactivity
POST/api/v3/learning_trail/student/exams/:trail_exam_id/submit_answersubmit_answer
GET/api/v3/learning_trail/student/exams/:trail_exam_id/resultsresults (só se done)
GET/api/v3/learning_trail/student/exams/:trail_exam_id/result_reviewresult_review (só se done)
PATCH/api/v3/learning_trail/student/exams/:trail_exam_id/finishfinish

submit_answer recebe { question_index, selected_alternative_id } e retorna correção + informação da próxima questão. finish encerra a trilha (done) sem exigir todas as questões.

Endpoints do professor

MétodoRotaAction
GET/api/v3/learning_trail/teacherindex (abas, contagem, lista de alunos)
GET/api/v3/learning_trail/teacher/students/:student_idstudent_index
GET/api/v3/learning_trail/teacher/students/:student_id/exams/:exam_id/resultsexam_results

Serializers (v3/learning_trail)

app/serializers/v3/learning_trail/ — Panko serializers.

SerializerAtributosNotas
TrailExamSerializerid, name, done, created_at, updated_atname = "Trilha de Aprendizagem ##{id}"
StudentSerializerid, full_name, email, school_class_nameDelegam para user / school_class
StudentListSerializerid, full_name, school_class_name, pending_trail_exams_count, completed_trail_exams_countVersão leve para listagem
ExamResponseSerializerquestion_number, question_title, user_answer, correct_answer, is_correctuser_answer/correct_answer convertidos para letra (A–E)

Componentes de frontend (ViewComponents)

app/components/:

ComponenteFunção
LearningTrail::ComponentRenderiza a trilha em onda (SVG), posiciona os botões/missões e o Duduq; faz scroll infinito
TrailButton::ComponentBotão de missão; cor/ícone conforme o estado (open, done, expired, blocked, etc.) e popovers de iniciar/revisar
TrailForms::ComponentGrade de alternativas (A–E) de uma questão; estados default/success/error e exibição opcional do gabarito
TrailExamQuitModal::ComponentModal de confirmação para abandonar a trilha
TrailStudentCard::ComponentCard do aluno (avatar + badges de acertos/pendências) na visão do professor
TrailCardLayout::ComponentLayout de card com sombra colorida (esquemas gray/blue/red/green)
TrailExamResults::ComponentLógica de montagem dos resultados (sem template)
LearningTrailEmptyState::ComponentEstados vazios (aluno sem trilha, professor sem alunos/atividades, busca sem resultado)

Controllers Stimulus

Globais em app/javascript/controllers/; alguns são scoped ao componente (.../controller.js).

ControllerResponsabilidade
learning_trail_controller.jsSincroniza altura/largura da sidebar e do header fixo de progresso
learning_trail_tabs_controller.jsAbas (todos/pendencias/concluidas) na visão do professor, com Turbo + History API
learning_trail_activity_controller.jsHabilita o botão "Enviar" quando uma alternativa é selecionada (escuta o evento alternativeSelected)
trail_forms/controller.jsSeleção de alternativa; trava após enviar e dispara alternativeSelected
trail_button/controller.jsPopovers de iniciar missão / revisar atividade
trail_exam_quit_modal/controller.jsAbre/fecha o modal de abandono e bloqueia o scroll de fundo

Service de apresentação

app/services/learning_trail.rb concentra a lógica de UI (separada da geração):

  • generate_trail_buttons — dados dos botões/missões para o aluno.
  • determine_trail_exam_type — classifica a trilha em open / end_soon / expired / blocked / done / quit por semana (semana 0 = aberta, semana 1 = termina em breve, semana 2+ = expirada).
  • generate_duduq_card / determine_duduq_type — mensagens e humor do mascote Duduq.
  • generate_activities_data — progresso agregado por matéria.

Linha do tempo das migrations

DataMudança
2025-07-17Criação de trail_exams_models, trail_exams_questions, trail_exams_alternative_responses
2025-07-31number em trail_exams_questions; trail_exams_question_id e student_id em alternative_responses
2025-10-28Campo booleano not_finished
2025-10-30Renomeia not_finishedquit
2026-05-26FK de scheduled_exams_model passa a usar exclusão em cascata