Appearance
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/quitModelo 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.
| Coluna | Tipo | Notas |
|---|---|---|
id | bigint | PK |
student_id | bigint | FK → Student |
scheduled_exams_model_id | bigint | FK → ScheduledExams::Model |
name | string | Ex.: "Trilha da #{exam.name}" |
done | boolean | Trilha concluída |
quit | boolean (default false) | Aluno abandonou a trilha |
latest | boolean | Marca a trilha mais recente do aluno |
discarded_at | datetime | Soft-delete (Discard::Model) |
created_at / updated_at | datetime |
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).
| Coluna | Tipo | Notas |
|---|---|---|
id | bigint | PK |
trail_exams_model_id | bigint | FK → TrailExams::Model |
questions_model_id | bigint | FK → Questions::Model |
number | integer | Ordem 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: :destroyTrailExams::AlternativeResponse — trail_exams_alternative_responses
Registra cada resposta do aluno a uma questão da trilha.
| Coluna | Tipo | Notas |
|---|---|---|
id | bigint | PK |
alternative | integer | Alternativa marcada (1–5 ↔ A–E) |
questions_model_id | bigint | FK → Questions::Model |
trail_exams_model_id | bigint | FK → TrailExams::Model |
trail_exams_question_id | bigint | FK → TrailExams::Questions |
student_id | bigint | FK → 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
endAssociaçõ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 >── StudentIntegraçã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::Modelreutilizadas 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 viaabilities_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 habilidadeTOTAL_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:
- Carrega a
ScheduledExams::Model. - Reagendamento: se
scheduled_finish_timefoi passado e difere dofinishatual, reagenda o job para o novo horário e encerra (trata o caso de o término ter sido alterado antes da execução). - Guard de duplicidade: retorna cedo se já existem
TrailExams::Modelpara essa atividade. - Análise de desempenho: para cada aluno com respostas, calcula o % de acerto por habilidade e ordena as habilidades da pior para a melhor.
- 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_quantityem[2, 4, 5]; excluindo as questões da prova original;ORDER BY RANDOM()), respeitando o teto de 10 questões. A habilidade precisa passar emvalid?(:learning_trail_activity_creation). - Criação: cria um
TrailExams::Modelpor aluno e osTrailExams::Questionscorrespondentes.
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 trilhae gera somente para os faltantes; encerra cedo se todos já possuem trilha. - Logging detalhado de início/saída/conclusão.
| Aspecto | GenerateTrailExamsJob | GenerateTrailExamsOnUpdateJob |
|---|---|---|
| Disparo | Agendamento da atividade | Alteração do finish após o agendamento |
| Alunos | Todos com respostas | Apenas os que ainda não têm trilha |
| Reagendamento | Sim (detecta mudança de finish) | Não |
| Guard de tempo | Não | Sim (sai se a prova não terminou) |
Onde os jobs são disparados
| Local | Gatilho | Job |
|---|---|---|
scheduled_exams/bulk_schedule_controller.rb | Múltipla marcação de atividades | GenerateTrailExamsJob |
scheduled_exams/models_controller.rb#create | Agendamento individual | GenerateTrailExamsJob |
scheduled_exams/models_controller.rb#update | finish alterado nos params | GenerateTrailExamsOnUpdateJob |
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
| Rota | Action | Descrição |
|---|---|---|
GET /learning_trail/index | index | Lista as trilhas do aluno (botões/missões), card do Duduq e atividades recentes |
GET /learning_trail/activity/:trail_exam_id | activity | Exibe uma questão (question_index, default 0) |
POST /learning_trail/activity/submit | submit_answer | Salva a resposta, mostra feedback, avança ou vai para resultados |
GET /learning_trail/results/:trail_exam_id | results | Resumo final (acertos / total / score) |
GET /learning_trail/result_review/:trail_exam_id | result_review | Revisão questão a questão após concluir |
POST /learning_trail/quit_exam/:trail_exam_id | quit_exam | Abandona a trilha (done: true, quit: true) |
POST /learning_trail/mark_onboarding_seen | mark_onboarding_seen | Marca o onboarding como visto |
Fluxo do aluno:
- Index → escolhe uma trilha disponível. Cada botão tem um estado:
open,end_soon,expired,blocked,doneouquit(calculado por semana, verapp/services/learning_trail.rb). - Activity → responde a questão (
question_index0, 1, 2, …). - Submit → cria
TrailExams::AlternativeResponse, compara comcorrect_alternative_number, mostra feedback (acerto/erro) e o botão "Próxima". - Última questão → marca
done: truee redireciona para results. - Result review (opcional) → revê cada resposta. Quit (opcional) → encerra a trilha.
LearningTrail::TeacherController
| Rota | Action | Descrição |
|---|---|---|
GET /learning_trail/teacher_index | index | Lista alunos com status das trilhas (abas todos/pendencias/concluidas, busca, filtro por sala) |
GET /learning_trail/student_index/:student_id | student_index | Dashboard de trilhas de um aluno específico |
GET /learning_trail/teacher/:student_id/exam_results/:exam_id | exam_results | Resultado detalhado de uma trilha concluída |
GET /learning_trail/teacher/:student_id/exam_results/:exam_id/review | result_review | Revisã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étodo | Rota | Action |
|---|---|---|
GET | /api/v3/learning_trail/student | index |
GET | /api/v3/learning_trail/student/exams/:trail_exam_id/activity | activity |
POST | /api/v3/learning_trail/student/exams/:trail_exam_id/submit_answer | submit_answer |
GET | /api/v3/learning_trail/student/exams/:trail_exam_id/results | results (só se done) |
GET | /api/v3/learning_trail/student/exams/:trail_exam_id/result_review | result_review (só se done) |
PATCH | /api/v3/learning_trail/student/exams/:trail_exam_id/finish | finish |
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étodo | Rota | Action |
|---|---|---|
GET | /api/v3/learning_trail/teacher | index (abas, contagem, lista de alunos) |
GET | /api/v3/learning_trail/teacher/students/:student_id | student_index |
GET | /api/v3/learning_trail/teacher/students/:student_id/exams/:exam_id/results | exam_results |
Serializers (v3/learning_trail)
app/serializers/v3/learning_trail/ — Panko serializers.
| Serializer | Atributos | Notas |
|---|---|---|
TrailExamSerializer | id, name, done, created_at, updated_at | name = "Trilha de Aprendizagem ##{id}" |
StudentSerializer | id, full_name, email, school_class_name | Delegam para user / school_class |
StudentListSerializer | id, full_name, school_class_name, pending_trail_exams_count, completed_trail_exams_count | Versão leve para listagem |
ExamResponseSerializer | question_number, question_title, user_answer, correct_answer, is_correct | user_answer/correct_answer convertidos para letra (A–E) |
Componentes de frontend (ViewComponents)
app/components/:
| Componente | Função |
|---|---|
LearningTrail::Component | Renderiza a trilha em onda (SVG), posiciona os botões/missões e o Duduq; faz scroll infinito |
TrailButton::Component | Botão de missão; cor/ícone conforme o estado (open, done, expired, blocked, etc.) e popovers de iniciar/revisar |
TrailForms::Component | Grade de alternativas (A–E) de uma questão; estados default/success/error e exibição opcional do gabarito |
TrailExamQuitModal::Component | Modal de confirmação para abandonar a trilha |
TrailStudentCard::Component | Card do aluno (avatar + badges de acertos/pendências) na visão do professor |
TrailCardLayout::Component | Layout de card com sombra colorida (esquemas gray/blue/red/green) |
TrailExamResults::Component | Lógica de montagem dos resultados (sem template) |
LearningTrailEmptyState::Component | Estados 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).
| Controller | Responsabilidade |
|---|---|
learning_trail_controller.js | Sincroniza altura/largura da sidebar e do header fixo de progresso |
learning_trail_tabs_controller.js | Abas (todos/pendencias/concluidas) na visão do professor, com Turbo + History API |
learning_trail_activity_controller.js | Habilita o botão "Enviar" quando uma alternativa é selecionada (escuta o evento alternativeSelected) |
trail_forms/controller.js | Seleção de alternativa; trava após enviar e dispara alternativeSelected |
trail_button/controller.js | Popovers de iniciar missão / revisar atividade |
trail_exam_quit_modal/controller.js | Abre/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 emopen/end_soon/expired/blocked/done/quitpor 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
| Data | Mudança |
|---|---|
| 2025-07-17 | Criação de trail_exams_models, trail_exams_questions, trail_exams_alternative_responses |
| 2025-07-31 | number em trail_exams_questions; trail_exams_question_id e student_id em alternative_responses |
| 2025-10-28 | Campo booleano not_finished |
| 2025-10-30 | Renomeia not_finished → quit |
| 2026-05-26 | FK de scheduled_exams_model passa a usar exclusão em cascata |