Skip to content
Merged
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
Binary file modified Resources/JITAI_Task_Overview.docx
Binary file not shown.
Binary file modified Resources/REACT_Architecture.docx
Binary file not shown.
7 changes: 5 additions & 2 deletions backend/app/distress.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,19 @@ def raise_baseline_flag(user, signals):

def active_distress_flag(user, now=None):
now = now or timezone.now()
return (
candidates = (
DistressFlag.objects
.filter(user=user)
.filter(
Q(source='baseline', contact_documented_at__isnull=True)
| Q(source='momentary', expires_at__gt=now)
)
.order_by('source', '-raised_at')
.first()
)
for flag in candidates:
if flag.is_active(now):
return flag
return None


def resource_card():
Expand Down
5 changes: 4 additions & 1 deletion backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,10 @@ class Meta:
def is_active(self, now=None):
now = now or timezone.now()
if self.source == 'baseline':
return self.contact_documented_at is None
from dashboard.data.config import BASELINE_ALERT_ONLY_SIGNALS
suppresses = not self.signals or any(
code not in BASELINE_ALERT_ONLY_SIGNALS for code in self.signals)
return suppresses and self.contact_documented_at is None
return self.expires_at is not None and now < self.expires_at

def __str__(self):
Expand Down
21 changes: 21 additions & 0 deletions backend/app/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5423,6 +5423,27 @@ def test_a_baseline_flag_does_not_expire_with_time(self):
raised_at=self.T - timedelta(days=400))
self.assertIsNotNone(active_distress_flag(self.user, now=self.T))

def test_alert_only_baseline_signals_do_not_suppress(self):
from app.distress import active_distress_flag
for signals in (['audit_c'], ['problem_gambling'], ['audit_c', 'problem_gambling']):
flag = DistressFlag.objects.create(user=self.user, source='baseline', signals=signals)
self.assertFalse(flag.is_active(self.T))
self.assertIsNone(active_distress_flag(self.user, now=self.T))

def test_a_baseline_flag_with_any_suppressing_signal_still_suppresses(self):
from app.distress import active_distress_flag
for signals in (['audit_c', 'scoff'], ['phq9_self_harm'], ['food_insecurity'], []):
DistressFlag.objects.all().delete()
flag = DistressFlag.objects.create(user=self.user, source='baseline', signals=signals)
self.assertTrue(flag.is_active(self.T))
self.assertEqual(active_distress_flag(self.user, now=self.T), flag)

def test_an_alert_only_flag_does_not_hide_a_suppressing_one(self):
from app.distress import active_distress_flag
DistressFlag.objects.create(user=self.user, source='baseline', signals=['audit_c'])
suppressing = DistressFlag.objects.create(user=self.user, source='baseline', signals=['scoff'])
self.assertEqual(active_distress_flag(self.user, now=self.T), suppressing)

def test_a_momentary_flag_with_no_expiry_is_inactive(self):
from app.distress import active_distress_flag
self._momentary(expires_at=None)
Expand Down
8 changes: 8 additions & 0 deletions dashboard/data/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
__all__ = [
'ARM_RANDOMIZATION_P_DEFAULT',
'ARM_RANDOMIZATION_P_ENV',
'BASELINE_ALERT_ONLY_SIGNALS',
'BENCHMARKS',
'CHECKIN_REMINDER_DELAY_MINUTES',
'DAILY_PROMPT_CAP',
Expand Down Expand Up @@ -93,6 +94,13 @@ def _int_list_from_env(name):

DISTRESS_MOMENTARY_PAUSE_HOURS = 24

# Baseline signals that are a note in the record and a conversation at the next
# visit, nothing more. They never suppress randomization (Dr. Chang, 2026-10-05):
# suppressing every alcohol or gambling screen-positive would remove the
# participants the study is about. PHQ-9, SCOFF and the Hunger Vital Sign still
# suppress.
BASELINE_ALERT_ONLY_SIGNALS = ('audit_c', 'problem_gambling')

# Momentary distress override cutoffs, confirmed by Dr. Chang 2026-09-22: exact
# scale-endpoint values, not thresholds. Separate from ROUTING_TRIGGER_RULES in
# app/ema_catalog.py:174-230 (coping-message topic selection only) -- do not
Expand Down
Loading