Modo “Todas as aulas” do trial libera todas as aulas direto
TLDR: quando um curso é liberado no trial com
access_mode=all, todas as aulas desse curso passam a virunlocked: truede imediato, sem depender de progresso ou da sequência do curso.
Contexto
Hoje o access_mode de um TrialCourse decide como o conjunto de aulas é montado, mas os dois modos têm semânticas diferentes de desbloqueio (R-016):
custom— as aulas marcadas emTrialCourseLessonabrem direto, ignorando a sequência.all— as aulas do curso continuam sujeitas à regra normal do app (Lesson.is_unlocked()): primeira aula aberta, resto conforme o progresso.
A composição vive em LessonSerializer.get_unlocked (apps/courses/serializers.py:87-97), que manda o caso all de volta para obj.is_unlocked(...).
O comportamento esperado do modo “Todas as aulas” é literal: liberar o curso inteiro para o usuário em trial. A partir desta mudança, all passa a se comportar como custom no ponto que importa — a aula abre porque o trial liberou, não porque o usuário avançou na sequência.
Objetivos
- Fazer com que toda aula de um curso liberado com
access_mode=allretorneunlocked: truepara o usuário em trial, independentemente de progresso, sequência ou do valor deLesson.unlocked. - Manter intacto o comportamento de quem não está em trial: continua passando por
Lesson.is_unlocked()normalmente. - Manter intacto o bloqueio do que está fora do trial: aula de curso
customnão marcada, ou de curso fora deTrialCourse, continuaunlocked: false. - Manter o custo de uma resolução por request, sem N+1.
Fora de escopo
- Não muda
Lesson.is_unlocked()— o model continua sem conhecer trial. - Não muda o
unlockeddo curso (R-017), que tem regra própria. - Não muda o admin de
Trialnem o fallback desave_relatedque forçaaccess_mode=allquando o modocustomé salvo sem nenhuma aula marcada. - Não mexe em filtragem de listagens,
Trial.features, expiração de trial ouSimpleLessonSerializer. - Não cria migration — nenhum modelo é alterado.
Regra nova
não é trial -> unlocked = regra_atual (Lesson.is_unlocked)
curso access_mode=all -> unlocked = True (todas as aulas ativas do curso)
curso access_mode=custom -> unlocked = True (apenas aulas marcadas em TrialCourseLesson)
fora do trial -> unlocked = False
| Situação | Antes | Depois |
|---|---|---|
| Usuário não é trial | regra atual | regra atual (inalterado) |
Curso all, primeira aula |
true |
true |
Curso all, aula 3 sem nenhuma conclusão anterior |
false |
true |
Curso custom, aula marcada |
true |
true (inalterado) |
Curso custom, aula não marcada |
false |
false (inalterado) |
Curso fora de TrialCourse |
false |
false (inalterado) |
Trial sem nenhum TrialCourse |
tudo false |
tudo false (inalterado) |
O único quadrante que muda é o do access_mode=all.
Decisão de implementação
Com essa mudança, all_access e custom_access passam a significar exatamente a mesma coisa — “aula liberada pelo trial, abre direto”. Manter dois conjuntos com comportamento idêntico deixa no código uma distinção que não existe mais.
Escolha (recomendada): colapsar resolve_trial_lesson_access para devolver um único set de ids (ou None para não-trial), unindo os dois modos na própria consulta. TrialLessonAccess é removido.
Alternativa considerada e descartada: manter o namedtuple e apenas unir os dois campos no serializer (obj.id in access.all_access or obj.id in access.custom_access). Diff menor, mas preserva uma estrutura que sugere uma diferença semântica inexistente.
Mudanças
apps/trials/services/trial_content.py
- Remover o namedtuple
TrialLessonAccess. resolve_trial_lesson_access(user)passa a devolverset[int] | None:Nonequando o usuário não é trial — mantém o sinal de “sem restrição”. Essa distinção entreNonee conjunto vazio continua sendo o ponto crítico da regra e não pode ser perdida.- Caso contrário, a união dos ids de aulas liberadas: aulas ativas de cursos
access_mode=allmais as aulas marcadas emTrialCourseLessonde cursosaccess_mode=custom.
_lesson_ids_from_all_access_coursese_lesson_ids_from_custom_access_coursesseguem existindo como as duas consultas que alimentam a união (continuam ignorando soft-deleted eis_active=False).resolve_trial_course_accessnão muda.
apps/courses/serializers.py
LessonSerializer.get_unlocked passa a:
```python def get_unlocked(self, obj): request = self.context.get(‘request’) user = getattr(request, ‘user’, None) if not user: return False
access = self._get_trial_lesson_access(user)
if access is None:
return obj.is_unlocked(user.id, progress=self._get_cached_progress(obj))
return obj.id in access ```
_get_trial_lesson_access continua guardando o resultado em self.context['_trial_lesson_access'], resolvendo uma vez por request. Consequência: para usuário em trial, is_unlocked() e o progresso não são mais consultados em nenhum caminho — a contagem de queries cai.
Testes
tests/trials/test_lesson_unlock_gating.py:
test_all_mode_opens_the_same_lessons_a_subscriber_would_see— deixa de valer; substituir por um teste que afirma que todas as aulas do cursoallvêm abertas sem nenhum progresso.test_all_mode_opens_the_next_lesson_after_completing_the_previous_one— deixa de fazer sentido (não depende mais de conclusão); remover.- Adicionar: curso
allcuja aula temLesson.unlocked=Falsee nenhuma anterior concluída →true. - Preservar sem alteração: não-trial mantém a regra atual;
custommarcada abre;customnão marcada bloqueia; curso fora do trial bloqueia; trial sem configuração bloqueia tudo.
tests/trials/test_trial_content_service.py:
- Adaptar as asserções que hoje leem
access.all_access/access.custom_accesspara osetúnico. - Manter os casos de borda: não-trial devolve
None; trial semTrialCoursedevolveset()vazio; aulas inativas e soft-deleted ficam de fora; trial com um curso de cada modo devolve a união dos dois.
tests/courses/test_models_and_services.py não muda — testa Lesson.is_unlocked() puro, que não é alterado.
Como verificar
make deps.up && python manage.py seed.- No admin, editar um
Triale liberar um curso com “Todas as aulas”. - Autenticar como usuário desse trial e chamar
GET /v1/courses/<id>/modules: todas as aulas do curso voltamunlocked: true, sem nenhum progresso registrado — inclusive as do meio e do fim dos módulos. - Liberar outro curso com “Aulas específicas” marcando só uma aula: apenas ela volta
unlocked: true; as demais,false. - Consultar um curso que não está no trial: todas as aulas voltam
unlocked: false. - Autenticar como usuário não trial e repetir: nenhuma diferença em relação a hoje (primeira aula aberta, resto conforme progresso).
make testpassa.
Documentação
Atualizar .project/docs/rules/trials/trial_lesson_unlock.md (R-016):
- Trocar o Given/When/Then do
access_mode=all— passa de “segue a regra normal do app” para “todas as aulas do curso vêmunlocked: true”. - Atualizar a constraint sobre
access_mode: os dois modos agora abrem direto; o que muda entre eles é apenas quais aulas entram no conjunto (curso inteiro vs. seleção manual). - Atualizar a constraint sobre a composição: para usuário em trial,
Lesson.is_unlocked()não é mais chamado em nenhum caso. - Ajustar a menção a
TrialLessonAccesspara osetúnico. - Bumpar
updatedpara 2026-08-24.
A spec 20260818114553_trial_lesson_gating.md permanece como registro histórico; não é reescrita.