Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions student_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .views import (
StudentAddRmView,
StudentCancelAttendanceView,
StudentClearanceView,
StudentConfirmAttendanceView,
StudentConsentView,
StudentGradeView,
Expand Down Expand Up @@ -32,6 +33,7 @@
path('', StudentHomeView.as_view(), name='student-app-home'),
path('onboarding/', StudentOnboardingWizardView.as_view(), name='student-app-onboarding'),
path('liberacao/', StudentConsentView.as_view(), name='student-app-consent'),
path('aguardando-liberacao/', StudentClearanceView.as_view(), name='student-app-clearance'),
path('grade/', StudentGradeView.as_view(), name='student-app-grade'),
path('aguardando-aprovacao/', StudentMembershipPendingView.as_view(), name='student-app-membership-pending'),
path('suspenso-financeiro/', StudentSuspendedFinancialView.as_view(), name='student-app-suspended-financial'),
Expand Down
3 changes: 2 additions & 1 deletion student_app/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
StudentSuspendedFinancialView,
StudentSwitchBoxView,
)
from .consent_views import StudentConsentView
from .consent_views import StudentClearanceView, StudentConsentView
from .onboarding_views import StudentOnboardingWizardView
from .public_workout_views import (
PublicWorkoutDetailView,
Expand Down Expand Up @@ -52,6 +52,7 @@
'PublicWorkoutServiceWorkerView',
'StudentAddRmView',
'StudentCancelAttendanceView',
'StudentClearanceView',
'StudentConfirmAttendanceView',
'StudentConsentView',
'StudentGradeView',
Expand Down
8 changes: 8 additions & 0 deletions student_app/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@ def dispatch(self, request, *args, **kwargs):
from student_app.workflows.consent_workflows import consent_is_pending
if consent_is_pending(identity=identity, box_root_slug=active_membership.box_root_slug):
return redirect('student-app-consent')
if (
getattr(settings, 'STUDENT_CONSENT_GATE_ENABLED', False)
and active_membership is not None
and active_membership.clearance_required
and active_membership.cleared_at is None
and request.path != reverse('student-app-clearance')
):
return redirect('student-app-clearance')
request.student_identity = identity
request.student_box_memberships = memberships
request.student_active_box_root_slug = active_membership.box_root_slug if active_membership is not None else ''
Expand Down
27 changes: 26 additions & 1 deletion student_app/views/consent_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from __future__ import annotations

from django.shortcuts import redirect
from django.views.generic import FormView
from django.views.generic import FormView, TemplateView

from student_identity.funnel_events import record_student_onboarding_event
from student_identity.models import StudentConsentDocument, StudentConsentDocumentKind
Expand Down Expand Up @@ -56,4 +56,29 @@
ip=request.META.get('REMOTE_ADDR', ''),
user_agent=request.META.get('HTTP_USER_AGENT', ''),
)
return redirect('student-app-home')

Check warning on line 59 in student_app/views/consent_views.py

View workflow job for this annotation

GitHub Actions / full-test-suite

Missing coverage

Missing coverage on line 59


class StudentClearanceView(StudentAnyMembershipMixin, TemplateView):
"""Parede de bloqueio do gate de entrada (Onda C).

Mostrada quando o membership do box ativo esta clearance_required e ainda nao
foi liberado. Nao e dead-end: instrui a levar o atestado ao box. A liberacao e
manual (Onda D). O gate (b) em dispatch pula esta rota (evita loop).
"""

template_name = 'student_app/clearance.html'

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
membership = next(
(
m for m in self.request.student_box_memberships
if m.box_root_slug == self.request.student_active_box_root_slug
),
None,
)
context['clearance_membership'] = membership
context['clearance_box'] = getattr(membership, 'box', None) if membership is not None else None
context.setdefault('student_shell_nav', 'home')
return self._attach_student_shell_context(context)
22 changes: 22 additions & 0 deletions templates/student_app/clearance.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% extends "student_app/layout.html" %}

{% block title %}Liberação pendente | OctoBox Aluno{% endblock %}

{% block body %}
<section class="student-card">
<p class="student-eyebrow">Quase lá</p>
<h1>Falta a liberação do seu box para você treinar.</h1>
<p class="student-copy">
Pela sua triagem de saúde, por segurança pedimos um atestado médico antes do primeiro treino.
Leve o atestado ao seu box{% if clearance_box %} <strong>{{ clearance_box.display_name }}</strong>{% endif %}
e a equipe libera seu acesso na hora.
</p>
<ul class="student-settings-list">
<li><strong>Box:</strong> {% if clearance_box %}{{ clearance_box.display_name }}{% else %}{{ student_active_box_root_slug }}{% endif %}</li>
</ul>
<p class="student-form-note">
Assim que o box confirmar o atestado, seu acesso abre automaticamente.
Se você já entregou e esta tela continua, fale com a equipe.
</p>
</section>
{% endblock %}
92 changes: 92 additions & 0 deletions tests/test_student_consent_wall.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""
Onda C do gate de entrada — parede de clearance + gate (b).

Cobre o que sustenta o verde da homologacao desta onda:
- aluno flagged e BARRADO na entrada (redirect para a parede)
- a propria parede renderiza (sem loop)
- aluno liberado (cleared_at) passa
- aluno sem flag nao ve a parede
- gate OFF e no-op
"""
from __future__ import annotations

from io import StringIO

from django.core.management import call_command
from django.test import Client, TestCase, override_settings
from django.urls import reverse

from shared_support.box_runtime import get_box_runtime_slug
from student_identity.infrastructure.session import build_student_session_value
from student_identity.models import (
StudentBoxMembership,
StudentBoxMembershipStatus,
StudentIdentity,
StudentIdentityProvider,
StudentIdentityStatus,
)
from students.models import Student

from student_app.workflows.consent_workflows import record_consent


def _seed():
call_command('seed_consent_documents', stdout=StringIO())


class ClearanceWallTests(TestCase):
def setUp(self):
_seed()
self.client = Client()
self.slug = get_box_runtime_slug()
self.student = Student.objects.create(
full_name='Atleta Wall', phone='5511666661111', email='wall@example.com',
)
self.identity = StudentIdentity.objects.create(
student_id=self.student.id, student_name=self.student.full_name,
box_root_slug=self.slug, primary_box_root_slug=self.slug,
provider=StudentIdentityProvider.GOOGLE, provider_subject='subject-wall',
email='wall@example.com', status=StudentIdentityStatus.ACTIVE,
)
self.membership = StudentBoxMembership.objects.create(
identity=self.identity, student_id=self.student.id, box_root_slug=self.slug,
status=StudentBoxMembershipStatus.ACTIVE,
)
self.client.cookies['octobox_student_session'] = build_student_session_value(
identity_id=self.identity.id, box_root_slug=self.slug,
)

def _flag(self):
record_consent(identity=self.identity, box=None, box_root_slug=self.slug, is_flagged=True)
self.membership.refresh_from_db()

@override_settings(STUDENT_CONSENT_GATE_ENABLED=True)
def test_flagged_member_is_redirected_to_clearance_wall(self):
self._flag()
response = self.client.get(reverse('student-app-home'))
self.assertRedirects(response, reverse('student-app-clearance'), fetch_redirect_response=False)

@override_settings(STUDENT_CONSENT_GATE_ENABLED=True)
def test_clearance_path_itself_renders(self):
self._flag()
response = self.client.get(reverse('student-app-clearance'))
self.assertEqual(response.status_code, 200)

@override_settings(STUDENT_CONSENT_GATE_ENABLED=True)
def test_cleared_member_passes(self):
self._flag()
self.membership.mark_cleared(by=None)
self.membership.save(update_fields=['cleared_at', 'cleared_by', 'updated_at'])
response = self.client.get(reverse('student-app-home'))
self.assertEqual(response.status_code, 200)

@override_settings(STUDENT_CONSENT_GATE_ENABLED=True)
def test_clear_member_not_redirected_to_wall(self):
record_consent(identity=self.identity, box=None, box_root_slug=self.slug, is_flagged=False)
response = self.client.get(reverse('student-app-home'))
self.assertEqual(response.status_code, 200)

def test_flag_off_does_not_show_wall(self):
self._flag()
response = self.client.get(reverse('student-app-home'))
self.assertNotEqual(response.get('Location', ''), reverse('student-app-clearance'))
Loading