R-016 — Desbloqueio de aulas para usuário em trial
TLDR: para quem está em trial, o campo
unlockedde uma aula depende só do que o trial libera.access_mode=allabre todas as aulas do curso;access_mode=customabre exatamente as aulas marcadas; nos dois casos a sequência é ignorada, e o resto fica bloqueado.
Given / When / Then
Dado um usuário cujo subscription_status não é "trial"
Quando ele consulta aulas
Então unlocked sai exatamente como antes — progresso, sequência e override de Lesson.unlocked
Dado um usuário em trial e um curso liberado com access_mode=all
Quando ele consulta as aulas desse curso
Então todas vêm unlocked: true, mesmo sem nenhuma conclusão anterior e mesmo as que têm Lesson.unlocked=False
Dado um usuário em trial e um curso liberado com access_mode=custom, com as aulas 1, 2 e 7 marcadas
Quando ele consulta as aulas desse curso
Então as aulas 1, 2 e 7 vêm unlocked: true mesmo sem nenhuma conclusão anterior, e as aulas 3, 4, 5 e 6 vêm unlocked: false
Dado um usuário em trial e uma aula com Lesson.unlocked=True que não foi marcada num curso custom
Quando ele consulta essa aula
Então ela vem unlocked: false — o trial vence o override
Dado um usuário em trial e um curso que não está em TrialCourse
Quando ele consulta as aulas desse curso
Então todas vêm unlocked: false, e o curso continua aparecendo nas listagens
Dado um usuário em trial sem nenhum TrialCourse configurado
Quando ele consulta qualquer aula
Então todas vêm unlocked: false
Dado um usuário em trial cujo subscription_status passa a enabled (conversão)
Quando ele consulta as aulas na requisição seguinte
Então ele já enxerga o fluxo normal do app, sem esperar a expiração do cache de autenticação
Constraints
- Quem responde se o usuário é trial é
User.is_trial, que comparasubscription_statuscomUser.SUBSCRIPTION_STATUS_TRIALsem diferenciar maiúsculas. A regra vive no model — serviços e serializers apenas consomem, nunca comparam a string por conta própria. access_modedecide quais aulas entram no conjunto liberado, não a semântica do desbloqueio:allentra com todas as aulas ativas do curso,customentra só com as marcadas emTrialCourseLesson. Nos dois modos, estar no conjunto significaunlocked: truedireto.- Os dois modos ignoram a sequência de propósito. Em
custom, uma aula marcada no meio de um módulo teria o pré-requisito fora do trial, portanto impossível de concluir — a aula liberada pelo admin ficaria inalcançável. Emall, o modo significa literalmente liberar o curso inteiro. resolve_trial_lesson_accessdevolve um únicosetde ids de aula, ouNone. Não há mais distinção entre conjuntos por modo de acesso.- A composição vive em
LessonSerializer.get_unlocked.Lesson.is_unlocked()não conhece trial e continua respondendo só por progresso/sequência — para usuário em trial ele não é chamado em nenhum caminho. - A resolução roda uma vez por request, guardada em
serializer.context['_trial_lesson_access']. None= usuário não é trial (sem restrição);set()vazio = é trial sem nada liberado (bloqueia tudo). Confundir os dois inverte o comportamento para toda a base de assinantes.- A decisão lê
subscription_statusdo objetoUserque a autenticação cacheia por 24h (user_authentication_{id}). O signalpost_saveemapps/accounts/signals.pyinvalida essa chave a cada save — sem isso, quem converte continuaria preso ao acesso de trial por até um dia. - Aulas e
TrialCoursesoft-deleted ou comis_active=Falseficam fora do conjunto. - Não há bloqueio em listagens: cursos, módulos e trilhas continuam sendo listados por completo. O curso expõe um
unlockedpróprio, com regra separada desta — ver R-017. SimpleLessonSerializer(continuar assistindo) não expõeunlockede não passa por esta regra.
Linked test
tests/trials/test_trial_content_service.py
tests/trials/test_lesson_unlock_gating.py