refactor: simplify response reconciliation
This commit is contained in:
@@ -787,16 +787,9 @@ def list_visible_scheduling_notifications(
|
|||||||
return visible
|
return visible
|
||||||
|
|
||||||
|
|
||||||
def refresh_participant_response_state(
|
def _participant_response_indexes(
|
||||||
session: Session,
|
participants: list[SchedulingParticipant],
|
||||||
*,
|
) -> tuple[dict[str, SchedulingParticipant], dict[str, SchedulingParticipant]]:
|
||||||
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_invitation_id = {
|
||||||
participant.poll_invitation_id: participant
|
participant.poll_invitation_id: participant
|
||||||
for participant in participants
|
for participant in participants
|
||||||
@@ -808,6 +801,97 @@ def refresh_participant_response_state(
|
|||||||
for identity in (participant.respondent_id, stored_identity):
|
for identity in (participant.respondent_id, stored_identity):
|
||||||
if isinstance(identity, str) and identity:
|
if isinstance(identity, str) and identity:
|
||||||
by_respondent_id[identity] = participant
|
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:
|
try:
|
||||||
responses = _poll_provider().list_responses(
|
responses = _poll_provider().list_responses(
|
||||||
session,
|
session,
|
||||||
@@ -826,60 +910,28 @@ def refresh_participant_response_state(
|
|||||||
matched_participant_ids: set[str] = set()
|
matched_participant_ids: set[str] = set()
|
||||||
unmatched_response_count = 0
|
unmatched_response_count = 0
|
||||||
for response in responses:
|
for response in responses:
|
||||||
participant = by_invitation_id.get(response.invitation_id)
|
participant = _response_participant(
|
||||||
if participant is None and response.respondent_id is not None:
|
response,
|
||||||
participant = by_respondent_id.get(response.respondent_id)
|
by_invitation_id=by_invitation_id,
|
||||||
# Poll deliberately ignores client-supplied invitation identifiers on
|
by_respondent_id=by_respondent_id,
|
||||||
# authenticated submissions. While reconciling for that same actor,
|
actor_ids=ids,
|
||||||
# map the server-authenticated respondent id to their unique Scheduling
|
actor_participants=actor_participants,
|
||||||
# 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]
|
|
||||||
if participant is None:
|
if participant is None:
|
||||||
unmatched_response_count += 1
|
unmatched_response_count += 1
|
||||||
continue
|
continue
|
||||||
matched_participant_ids.add(participant.id)
|
matched_participant_ids.add(participant.id)
|
||||||
if response.respondent_id:
|
changed = _apply_participant_response(
|
||||||
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,
|
session,
|
||||||
request=request,
|
request=request,
|
||||||
participant=participant,
|
participant=participant,
|
||||||
event_kind="scheduling.participant_responded",
|
response=response,
|
||||||
subject=f"Scheduling response: {request.title}",
|
) or changed
|
||||||
body_text=f"{participant.display_name or participant.email or 'A participant'} responded to {request.title}.",
|
if reset_missing and _reset_missing_participant_responses(
|
||||||
)
|
participants,
|
||||||
changed = True
|
matched_participant_ids=matched_participant_ids,
|
||||||
if reset_missing:
|
unmatched_response_count=unmatched_response_count,
|
||||||
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 = True
|
||||||
if changed:
|
if changed:
|
||||||
session.flush()
|
session.flush()
|
||||||
|
|||||||
Reference in New Issue
Block a user