Schedule

Times are UTC unless marked local. Server time now: {{ utc(summary.now) }}. {% if dry_run %}Dry run: messages are rendered and logged, not delivered.{% endif %} Channels: {{ channel_types|join(', ') if channel_types else 'none (no messages)' }}. Contact details are never shown here.

Totals

Enrolled{{ summary.participants|length }}
{% for k in ('answered', 'expired', 'missed', 'open', 'upcoming') %}
Sessions {{ k }}{{ summary.sessions|selectattr('status', 'equalto', k)|list|length }}
{% endfor %} {% for k in ('pending', 'sent', 'dry_run', 'failed', 'skipped') %}
Messages {{ k|replace('_', ' ') }}{{ summary.counts.get(k, 0) }}
{% endfor %}

Participants and compliance

{% if not summary.participants %}

Nobody has enrolled yet. Participants enrol when they finish the regular study flow.

{% else %}
{% for p in summary.participants %} {% endfor %}
ParticipantEnrolledTime zoneContact AnsweredExpiredMissedComplianceMedian latencyNext session (local)Actions
{{ p.participant }} {{ utc(p.enrolled_at) }} {{ p.timezone }} {% if p.withdrawn %}stopped{% elif not p.consent %}no consent{% else %}{{ p.channels|join(', ') or 'none' }}{% if p.contact.email %} · email held{% endif %}{% if p.contact.phone %} · phone held{% endif %}{% endif %} {{ p.counts.answered }} {{ p.counts.expired }} {{ p.counts.missed }} {% if p.compliance is not none %}{{ (p.compliance * 100)|round|int }}% of {{ p.closed }}{% else %}none closed yet{% endif %} {% if p.median_latency is not none %}{{ span(p.median_latency) }}{% else %}no answers yet{% endif %} {% if p.next %}{{ p.next.title or p.next.name }}: {{ local(p.next.opens_at, p.timezone) }}{% else %}none{% endif %} {% if p.contact.email or p.contact.phone %}
{% endif %}
{% endif %}
{% macro notes_table(rows, id, empty) %} {% if not rows %}

{{ empty }}

{% else %}
{% for n in rows %} {% endfor %}
DueSentParticipantSessionRuleChannelStatusDetail
{{ utc(n.due_at) }} {{ utc(n.sent_at) if n.sent_at else '' }} {{ n.participant }} {{ n.session_id }} {{ n.rule }} {{ n.channel }} {{ n.status|replace('_', ' ') }} {{ n.detail or '' }}{% if n.preview %}
Text
{{ n.preview }}
{% endif %}
{% endif %} {% endmacro %}

Failed messages

{{ notes_table(summary.failed, 'schedule-failed', 'No failed messages.') }}

Upcoming messages

{{ notes_table(summary.upcoming, 'schedule-upcoming', 'No messages waiting.') }}

Sent messages (latest 200)

{{ notes_table(summary.sent, 'schedule-sent', 'Nothing sent yet.') }}

Skipped messages (latest 100)

{{ notes_table(summary.skipped, 'schedule-skipped', 'Nothing skipped.') }}
{% if has_delphi %}

Delphi rounds

{% if summary.delphi_stop %}

Stopped: {{ summary.delphi_stop }}.

{% endif %}
{% for r in summary.rounds %} {% endfor %}
RoundOpensClosesStatusAnswered
{{ r.round }}{{ utc(r.opens_at) }}{{ utc(r.closed_at or r.closes_at) }} {{ r.status }}{% if r.stop_reason %} ({{ r.stop_reason }}){% endif %} {{ summary.sessions|selectattr('id', 'equalto', 'round:' ~ r.round)|selectattr('status', 'equalto', 'answered')|list|length }}
{% if summary.stats %}

Item statistics

{% for st in summary.stats %} {% for k in ('median', 'q1', 'q3', 'iqr', 'median_change') %}{% endfor %} {% endfor %}
RoundItemnMedianQ1Q3IQRMedian changeChanged shareSettled
{{ st.round }}{{ st.item }}{{ st.n }}{{ '%.2f'|format(st[k]) if st[k] is not none else '' }}{{ '%.0f%%'|format(st.changed_share * 100) if st.changed_share is not none else '' }} {{ 'yes' if st.settled else 'no' }}
{% endif %}
{% endif %}

All sessions

{% if not summary.sessions %}

No sessions yet.

{% else %}
{% for s in summary.sessions[:500] %} {% endfor %}
ParticipantSessionKindOpens (UTC)Opens (local)Closes (UTC)Status
{{ s.participant }}{{ s.id }}{{ s.kind }} {{ utc(s.opens_at) }}{{ local(s.opens_at, tzs.get(s.participant, 'UTC')) }} {{ utc(s.closes_at) }}{{ s.status }}
{% if summary.sessions|length > 500 %}

Showing the first 500 of {{ summary.sessions|length }}; the schedule export has them all.

{% endif %} {% endif %}