refactor: simplify response reconciliation
This commit is contained in:
@@ -787,16 +787,9 @@ def list_visible_scheduling_notifications(
|
||||
return visible
|
||||
|
||||
|
||||
def refresh_participant_response_state(
|
||||
session: Session,
|
||||
*,
|
||||
request: SchedulingRequest,
|
||||
actor_ids: tuple[str, ...] = (),
|
||||
reset_missing: bool = False,
|
||||
) -> None:
|
||||
if request.poll_id is None:
|
||||
return
|
||||
participants = _active_participants(request)
|
||||
def _participant_response_indexes(
|
||||
participants: list[SchedulingParticipant],
|
||||
) -> tuple[dict[str, SchedulingParticipant], dict[str, SchedulingParticipant]]:
|
||||
by_invitation_id = {
|
||||
participant.poll_invitation_id: participant
|
||||
for participant in participants
|
||||
@@ -808,6 +801,97 @@ def refresh_participant_response_state(
|
||||
for identity in (participant.respondent_id, stored_identity):
|
||||
if isinstance(identity, str) and identity:
|
||||
by_respondent_id[identity] = participant
|
||||
return by_invitation_id, by_respondent_id
|
||||
|
||||
|
||||
def _response_participant(
|
||||
response: PollResponseRef,
|
||||
*,
|
||||
by_invitation_id: dict[str, SchedulingParticipant],
|
||||
by_respondent_id: dict[str, SchedulingParticipant],
|
||||
actor_ids: set[str],
|
||||
actor_participants: list[SchedulingParticipant],
|
||||
) -> SchedulingParticipant | None:
|
||||
participant = by_invitation_id.get(response.invitation_id)
|
||||
if participant is None and response.respondent_id is not None:
|
||||
participant = by_respondent_id.get(response.respondent_id)
|
||||
# Poll deliberately ignores client-supplied invitation identifiers on
|
||||
# authenticated submissions. While reconciling for that same actor, map
|
||||
# the server-authenticated respondent to their unique Scheduling row.
|
||||
if participant is None and response.respondent_id in actor_ids and len(actor_participants) == 1:
|
||||
return actor_participants[0]
|
||||
return participant
|
||||
|
||||
|
||||
def _apply_participant_response(
|
||||
session: Session,
|
||||
*,
|
||||
request: SchedulingRequest,
|
||||
participant: SchedulingParticipant,
|
||||
response: PollResponseRef,
|
||||
) -> bool:
|
||||
changed = False
|
||||
if response.respondent_id:
|
||||
metadata = participant.metadata_ or {}
|
||||
if metadata.get("poll_response_respondent_id") != response.respondent_id:
|
||||
participant.metadata_ = {
|
||||
**metadata,
|
||||
"poll_response_respondent_id": response.respondent_id,
|
||||
}
|
||||
changed = True
|
||||
if participant.status == "responded":
|
||||
return changed
|
||||
participant.status = "responded"
|
||||
participant.responded_at = response_datetime(response.submitted_at)
|
||||
_emit_scheduling_center_notification(
|
||||
session,
|
||||
request=request,
|
||||
participant=participant,
|
||||
event_kind="scheduling.participant_responded",
|
||||
subject=f"Scheduling response: {request.title}",
|
||||
body_text=f"{participant.display_name or participant.email or 'A participant'} responded to {request.title}.",
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def _reset_missing_participant_responses(
|
||||
participants: list[SchedulingParticipant],
|
||||
*,
|
||||
matched_participant_ids: set[str],
|
||||
unmatched_response_count: int,
|
||||
) -> bool:
|
||||
changed = False
|
||||
for participant in participants:
|
||||
if participant.status != "responded" or participant.id in matched_participant_ids:
|
||||
continue
|
||||
stored_identity = (participant.metadata_ or {}).get("poll_response_respondent_id")
|
||||
if not isinstance(stored_identity, str) and unmatched_response_count:
|
||||
# Preserve an older projection while an active response exists
|
||||
# that cannot yet be mapped safely to a participant.
|
||||
continue
|
||||
participant.status = "invited" if participant.poll_invitation_id else "draft"
|
||||
participant.responded_at = None
|
||||
if isinstance(stored_identity, str):
|
||||
participant.metadata_ = {
|
||||
key: value
|
||||
for key, value in (participant.metadata_ or {}).items()
|
||||
if key != "poll_response_respondent_id"
|
||||
}
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
|
||||
def refresh_participant_response_state(
|
||||
session: Session,
|
||||
*,
|
||||
request: SchedulingRequest,
|
||||
actor_ids: tuple[str, ...] = (),
|
||||
reset_missing: bool = False,
|
||||
) -> None:
|
||||
if request.poll_id is None:
|
||||
return
|
||||
participants = _active_participants(request)
|
||||
by_invitation_id, by_respondent_id = _participant_response_indexes(participants)
|
||||
try:
|
||||
responses = _poll_provider().list_responses(
|
||||
session,
|
||||
@@ -826,61 +910,29 @@ def refresh_participant_response_state(
|
||||
matched_participant_ids: set[str] = set()
|
||||
unmatched_response_count = 0
|
||||
for response in responses:
|
||||
participant = by_invitation_id.get(response.invitation_id)
|
||||
if participant is None and response.respondent_id is not None:
|
||||
participant = by_respondent_id.get(response.respondent_id)
|
||||
# Poll deliberately ignores client-supplied invitation identifiers on
|
||||
# authenticated submissions. While reconciling for that same actor,
|
||||
# map the server-authenticated respondent id to their unique Scheduling
|
||||
# participant row (which may have been invited by email only).
|
||||
if (
|
||||
participant is None
|
||||
and response.respondent_id in ids
|
||||
and len(actor_participants) == 1
|
||||
):
|
||||
participant = actor_participants[0]
|
||||
participant = _response_participant(
|
||||
response,
|
||||
by_invitation_id=by_invitation_id,
|
||||
by_respondent_id=by_respondent_id,
|
||||
actor_ids=ids,
|
||||
actor_participants=actor_participants,
|
||||
)
|
||||
if participant is None:
|
||||
unmatched_response_count += 1
|
||||
continue
|
||||
matched_participant_ids.add(participant.id)
|
||||
if response.respondent_id:
|
||||
metadata = participant.metadata_ or {}
|
||||
if metadata.get("poll_response_respondent_id") != response.respondent_id:
|
||||
participant.metadata_ = {
|
||||
**metadata,
|
||||
"poll_response_respondent_id": response.respondent_id,
|
||||
}
|
||||
changed = True
|
||||
if participant.status != "responded":
|
||||
participant.status = "responded"
|
||||
participant.responded_at = response_datetime(response.submitted_at)
|
||||
_emit_scheduling_center_notification(
|
||||
session,
|
||||
request=request,
|
||||
participant=participant,
|
||||
event_kind="scheduling.participant_responded",
|
||||
subject=f"Scheduling response: {request.title}",
|
||||
body_text=f"{participant.display_name or participant.email or 'A participant'} responded to {request.title}.",
|
||||
)
|
||||
changed = True
|
||||
if reset_missing:
|
||||
for participant in participants:
|
||||
if participant.status != "responded" or participant.id in matched_participant_ids:
|
||||
continue
|
||||
stored_identity = (participant.metadata_ or {}).get("poll_response_respondent_id")
|
||||
if not isinstance(stored_identity, str) and unmatched_response_count:
|
||||
# Do not reset an older projection while an active response
|
||||
# exists that cannot yet be mapped safely to a participant.
|
||||
continue
|
||||
participant.status = "invited" if participant.poll_invitation_id else "draft"
|
||||
participant.responded_at = None
|
||||
if isinstance(stored_identity, str):
|
||||
participant.metadata_ = {
|
||||
key: value
|
||||
for key, value in (participant.metadata_ or {}).items()
|
||||
if key != "poll_response_respondent_id"
|
||||
}
|
||||
changed = True
|
||||
changed = _apply_participant_response(
|
||||
session,
|
||||
request=request,
|
||||
participant=participant,
|
||||
response=response,
|
||||
) or changed
|
||||
if reset_missing and _reset_missing_participant_responses(
|
||||
participants,
|
||||
matched_participant_ids=matched_participant_ids,
|
||||
unmatched_response_count=unmatched_response_count,
|
||||
):
|
||||
changed = True
|
||||
if changed:
|
||||
session.flush()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user