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
1 change: 1 addition & 0 deletions static/css/student_app/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ PONTOS CRITICOS:
@import url("./primitives/progress-strip.css");
@import url("./primitives/compact-state.css");
@import url("./forms.css");
@import url("./screens/consent.css");
105 changes: 105 additions & 0 deletions static/css/student_app/screens/consent.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
ARQUIVO: tela do gate de entrada (consentimento + PAR-Q) do app do aluno.

POR QUE ELE EXISTE:
- mantem o waiver rolavel e a triagem Sim/Nao legiveis e tapaveis no mobile,
sem "role ate o fim pra habilitar" e reusando os tokens canonicos do tema.

PONTOS CRITICOS:
- seletores escopados em .student-consent para vencer a regra generica de label
do forms.css sem !important.
- alvo de toque >= 44px; contraste valido em light e dark.
*/

.student-consent {
display: grid;
gap: 16px;
}

.student-consent .student-consent-doc__title {
margin: 0 0 8px;
font-size: 1rem;
}

.student-consent .student-consent-doc__body {
max-height: 220px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 14px 16px;
border-radius: 16px;
border: 1px solid color-mix(in srgb, var(--theme-border-soft) 86%, transparent);
background: color-mix(in srgb, var(--theme-surface-strong) 92%, transparent);
color: var(--theme-text-secondary);
font-size: 0.92rem;
line-height: 1.6;
}

.student-consent .student-consent-accept {
display: flex;
align-items: flex-start;
gap: 10px;
font-weight: 700;
color: var(--theme-text-primary);
}

.student-consent .student-consent-accept input {
width: auto;
min-height: 0;
margin-top: 3px;
}

.student-consent .student-consent-parq {
display: grid;
gap: 14px;
margin: 0;
padding: 16px;
border: 1px solid color-mix(in srgb, var(--theme-border-soft) 80%, transparent);
border-radius: 18px;
}

.student-consent .student-consent-parq legend {
padding: 0 8px;
font-weight: 800;
color: var(--theme-text-primary);
}

.student-consent .student-consent-question {
display: grid;
gap: 8px;
}

.student-consent .student-consent-question__label {
font-weight: 600;
color: var(--theme-text-primary);
}

.student-consent .student-consent-question__options ul {
list-style: none;
display: flex;
gap: 8px;
margin: 0;
padding: 0;
}

.student-consent .student-consent-question__options li {
flex: 1;
}

.student-consent .student-consent-question__options label {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 44px;
padding: 8px 12px;
border-radius: 14px;
border: 1px solid color-mix(in srgb, var(--theme-border-soft) 82%, transparent);
background: color-mix(in srgb, var(--theme-surface-strong) 90%, transparent);
font-weight: 700;
cursor: pointer;
}

.student-consent .student-consent-question__options input {
width: auto;
min-height: 0;
}
26 changes: 26 additions & 0 deletions student_app/consent_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
ARQUIVO: conteudo estruturado do PAR-Q para a tela de consentimento (Onda B).

POR QUE ELE EXISTE:
- as 7 perguntas canonicas que dirigem os toggles Sim/Nao da triagem.
- o texto de registro (legal/clinico) vive no StudentConsentDocument (seed);
esta lista dirige a UI.

PONTOS CRITICOS:
- manter em sincronia com o seed do PAR-Q (PENDENTE_REVISAO_CLINICA) ate a revisao
clinica humana (gate B2 do plano).
- responder "sim" a qualquer pergunta sinaliza risco (flagged).
"""
from __future__ import annotations

PARQ_QUESTIONS = [
('q1', 'Algum medico ja disse que voce tem um problema de coracao e que so deveria fazer atividade fisica supervisionada?'),
('q2', 'Voce sente dor no peito quando faz atividade fisica?'),
('q3', 'No ultimo mes, voce sentiu dor no peito sem estar fazendo atividade fisica?'),
('q4', 'Voce perde o equilibrio por tontura ou ja perdeu a consciencia?'),
('q5', 'Voce tem algum problema osseo ou articular que pode piorar com atividade fisica?'),
('q6', 'Voce toma algum remedio para pressao ou para o coracao?'),
('q7', 'Voce sabe de qualquer outra razao para nao fazer atividade fisica?'),
]

PARQ_QUESTION_KEYS = [key for key, _ in PARQ_QUESTIONS]
34 changes: 34 additions & 0 deletions student_app/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)
from shared_support.phone_numbers import normalize_phone_number
from student_identity.models import StudentIdentity, StudentIdentityStatus
from student_app.consent_content import PARQ_QUESTIONS
from student_app.models import StudentExerciseMax
from students.models import Student

Expand Down Expand Up @@ -233,3 +234,36 @@ def __init__(self, *args, **kwargs):

def clean_email(self):
return (self.cleaned_data.get('email') or '').strip().lower()


class StudentConsentForm(forms.Form):
"""Gate de entrada: aceite do termo + triagem PAR-Q (Onda B).

Botao sempre habilitado; validacao no submit (waiver obrigatorio). PAR-Q em
toggles Sim/Nao com default 'nao' (estado seguro). `is_flagged` = qualquer 'sim'.
"""

waiver_accepted = forms.BooleanField(
required=True,
label='Li e aceito o termo de responsabilidade.',
error_messages={'required': 'Marque que voce leu e aceita o termo para continuar.'},
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for key, label in PARQ_QUESTIONS:
self.fields[key] = forms.ChoiceField(
label=label,
choices=(('nao', 'Não'), ('sim', 'Sim')),
initial='nao',
widget=forms.RadioSelect,
required=True,
)

@property
def parq_fields(self):
return [self[key] for key, _ in PARQ_QUESTIONS]

@property
def is_flagged(self) -> bool:
return any(self.cleaned_data.get(key) == 'sim' for key, _ in PARQ_QUESTIONS)
2 changes: 2 additions & 0 deletions student_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
StudentAddRmView,
StudentCancelAttendanceView,
StudentConfirmAttendanceView,
StudentConsentView,
StudentGradeView,
StudentHomeView,
StudentManifestView,
Expand All @@ -30,6 +31,7 @@
path('auth/', include('student_identity.urls')),
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('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
2 changes: 2 additions & 0 deletions student_app/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
StudentSuspendedFinancialView,
StudentSwitchBoxView,
)
from .consent_views import StudentConsentView
from .onboarding_views import StudentOnboardingWizardView
from .public_workout_views import (
PublicWorkoutDetailView,
Expand Down Expand Up @@ -52,6 +53,7 @@
'StudentAddRmView',
'StudentCancelAttendanceView',
'StudentConfirmAttendanceView',
'StudentConsentView',
'StudentGradeView',
'StudentHomeView',
'StudentInviteEntryView',
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 @@ -152,9 +152,17 @@
for membership in runtime_memberships
if membership.status == StudentBoxMembershipStatus.SUSPENDED_FINANCIAL
]
if runtime_memberships and len(suspended_memberships) == len(runtime_memberships):
return redirect('student-app-suspended-financial')
return redirect('student-app-no-active-box')

Check warning on line 157 in student_app/views/base.py

View workflow job for this annotation

GitHub Actions / full-test-suite

Missing coverage

Missing coverage on lines 155-157
if (
getattr(settings, 'STUDENT_CONSENT_GATE_ENABLED', False)
and active_membership is not None
and request.path != reverse('student-app-consent')
):
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')
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
59 changes: 59 additions & 0 deletions student_app/views/consent_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
ARQUIVO: gate de entrada — tela de consentimento (Onda B).

POR QUE ELE EXISTE:
- corredor unico de waiver + PAR-Q pos-auth para os 3 corredores de onboarding.

PONTOS CRITICOS:
- usa StudentAnyMembershipMixin (resolve identidade sem exigir membership ativo).
- o gate em StudentIdentityRequiredMixin.dispatch PULA esta rota (evita loop).
- a decisao clear/flagged e a gravacao moram no workflow, nao aqui.
"""
from __future__ import annotations

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

from student_identity.funnel_events import record_student_onboarding_event
from student_identity.models import StudentConsentDocument, StudentConsentDocumentKind
from student_app.forms import StudentConsentForm
from student_app.workflows.consent_workflows import ENTRY_GATE_JOURNEY, record_consent
from .base import StudentAnyMembershipMixin


class StudentConsentView(StudentAnyMembershipMixin, FormView):
template_name = 'student_app/consent.html'
form_class = StudentConsentForm

def get(self, request, *args, **kwargs):
record_student_onboarding_event(
journey=ENTRY_GATE_JOURNEY,
event='consent_step_viewed',
target_label=request.student_identity.student_name,
description='Tela de consentimento exibida.',
metadata={
'box_root_slug': request.student_active_box_root_slug,
'identity_id': request.student_identity.id,
},
)
return super().get(request, *args, **kwargs)

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['waiver_document'] = StudentConsentDocument.objects.filter(
kind=StudentConsentDocumentKind.WAIVER, is_active=True,
).first()
context.setdefault('student_shell_nav', 'home')
return self._attach_student_shell_context(context)

def form_valid(self, form):
request = self.request
record_consent(
identity=request.student_identity,
box=getattr(request.student_identity, 'box', None),
box_root_slug=request.student_active_box_root_slug,
is_flagged=form.is_flagged,
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 lines 50-59
Loading
Loading