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
43 changes: 36 additions & 7 deletions attendanceToTestout.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import traceback
import os
from typing import Optional

import helpers.neon as neon
from helpers.api import apiCall
Expand Down Expand Up @@ -55,6 +56,29 @@
}


def _get_attendee_email(attendee: dict) -> Optional[str]:
for key in ("email", "email1", "emailAddress"):
if attendee.get(key):
return attendee.get(key)
return None


def _get_neon_id_by_email(email: str) -> Optional[int]:
if not email:
return None
results = neon.get_acct_by_email(email)
if not results:
logging.warning("No Neon account found for attendee email %s", email)
return None
if len(results) > 1:
logging.warning("Multiple Neon accounts found for attendee email %s; using first match", email)
try:
return int(results[0].get("Account ID"))
except (TypeError, ValueError):
logging.warning("Invalid Neon account ID for attendee email %s", email)
return None


def toolTestingUpdate(className: str, neonId: int, inputDate: str):
date = datetime.datetime.strftime(
datetime.datetime.strptime(inputDate, "%Y-%m-%d"), "%m/%d/%Y"
Expand Down Expand Up @@ -141,13 +165,18 @@ def main():
registrants = neon.getEventRegistrants(eventId)["eventRegistrations"]
if type(registrants) is not type(None):
for registrant in registrants:
attended = registrant["tickets"][0]["attendees"][0][
"markedAttended"
]
if attended == True:
toolTestingUpdate(
eventName, registrant["registrantAccountId"], eventDate
)
for ticket in registrant.get("tickets", []):
for attendee in ticket.get("attendees", []):
if attendee.get("markedAttended") == True:
attendee_email = _get_attendee_email(attendee)
neon_id = _get_neon_id_by_email(attendee_email)
if neon_id:
toolTestingUpdate(eventName, neon_id, eventDate)
else:
logging.info(
"Skipping attendee without valid Neon account for email %s",
attendee_email,
)
else:
logging.info("Event Search contained no results")
except TypeError:
Expand Down
35 changes: 23 additions & 12 deletions tests/neon_mocker.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@


import random
import string
from typing import List, Dict, Any, Optional
from neonUtil import N_baseURL
import neonUtil
Expand Down Expand Up @@ -188,11 +187,20 @@ def __init__(
self.start_time = start_time
self.end_time = end_time
self.capacity = capacity
self._registrants: List[tuple] = [] # List of (NeonUserMock, status, marked_attended)
self._registrants: List[tuple] = [] # List of (NeonUserMock, status, marked_attended, attendees)

def add_registrant(self, account: 'NeonUserMock', status: str = "SUCCEEDED", marked_attended: bool = False) -> 'NeonEventMock':
"""Add a registrant to this event."""
self._registrants.append((account, status, marked_attended))
def add_registrant(
self,
account: 'NeonUserMock',
status: str = "SUCCEEDED",
marked_attended: bool = False,
attendees: Optional[List[dict]] = None,
) -> 'NeonEventMock':
"""Add a registrant to this event.

attendees can be a list of dicts with attendee fields to model multiple attendees.
"""
self._registrants.append((account, status, marked_attended, attendees))
return self

def search_result(self) -> Dict[str, Any]:
Expand Down Expand Up @@ -220,16 +228,19 @@ def mock(self, requests_mock):
"""
event_registrations = []
account_mocks = []
for account, status, marked_attended in self._registrants:
for account, status, marked_attended, attendees in self._registrants:
if attendees is None:
attendees = [{
"firstName": account.firstName,
"lastName": account.lastName,
"email": account.email,
"registrationStatus": status,
"markedAttended": marked_attended
}]
event_registrations.append({
"registrantAccountId": account.account_id,
"tickets": [{
"attendees": [{
"firstName": account.firstName,
"lastName": account.lastName,
"registrationStatus": status,
"markedAttended": marked_attended
}]
"attendees": attendees
}]
})
account.mock(requests_mock)
Expand Down
81 changes: 81 additions & 0 deletions tests/test_attendanceToTestout.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ def test_main_processes_attended_event(requests_mock):

search_mock, [(registrants_mock, account_mocks)] = NeonEventMock.mock_events(requests_mock, [event])

# Mock the account search by attendee email
account_search_mock = requests_mock.post(
f'{N_baseURL}/accounts/search',
json={"searchResults": [student.search_result()], "pagination": {"totalPages": 1, "currentPage": 0}}
)

# Mock the PATCH to update the account with the new field
patch_mock = requests_mock.patch(
f'{N_baseURL}/accounts/{student.account_id}',
Expand All @@ -30,6 +36,7 @@ def test_main_processes_attended_event(requests_mock):
assert search_mock.called, "Event search API should be called"
assert registrants_mock.called, "Event registrants API should be called"
assert account_mocks[0].called, "Account info API should be called"
assert account_search_mock.called, "Account search API should be called"
assert patch_mock.called, "Account PATCH API should be called to update custom field"

# Verify the PATCH request contains the correct custom field ID
Expand All @@ -46,6 +53,12 @@ def test_main_skips_already_marked_accounts(requests_mock):

search_mock, [(registrants_mock, account_mocks)] = NeonEventMock.mock_events(requests_mock, [event])

# Mock the account search by attendee email
account_search_mock = requests_mock.post(
f'{N_baseURL}/accounts/search',
json={"searchResults": [student.search_result()], "pagination": {"totalPages": 1, "currentPage": 0}}
)

# Mock PATCH but it should NOT be called
patch_mock = requests_mock.patch(
f'{N_baseURL}/accounts/{student.account_id}',
Expand All @@ -59,11 +72,79 @@ def test_main_skips_already_marked_accounts(requests_mock):
assert search_mock.called, "Event search API should be called"
assert registrants_mock.called, "Event registrants API should be called"
assert account_mocks[0].called, "Account info API should be called"
assert account_search_mock.called, "Account search API should be called"

# Verify PATCH was NOT called since account already has the field
assert not patch_mock.called, "PATCH should not be called when field already exists"


def test_main_handles_multiple_attendees_by_email(requests_mock):
"""Test that main() updates attendance for each attended attendee using their email"""
registrant = NeonUserMock(firstName="Reg", lastName="Owner")
attendee_1 = NeonUserMock(firstName="Ann", lastName="Attended")
attendee_2 = NeonUserMock(firstName="Skip", lastName="Absent")

attendees = [
{
"firstName": attendee_1.firstName,
"lastName": attendee_1.lastName,
"email": attendee_1.email,
"registrationStatus": "SUCCEEDED",
"markedAttended": True,
},
{
"firstName": attendee_2.firstName,
"lastName": attendee_2.lastName,
"email": attendee_2.email,
"registrationStatus": "SUCCEEDED",
"markedAttended": False,
},
]

event = NeonEventMock(event_name="Woodshop Safety")\
.add_registrant(registrant, attendees=attendees)

search_mock, [(registrants_mock, account_mocks)] = NeonEventMock.mock_events(requests_mock, [event])
attendee_1.mock(requests_mock)

def _matches_email(email):
def _matcher(request):
payload = request.json()
return payload["searchFields"][0]["value"] == email
return _matcher

account_search_mock_1 = requests_mock.post(
f'{N_baseURL}/accounts/search',
json={"searchResults": [attendee_1.search_result()], "pagination": {"totalPages": 1, "currentPage": 0}},
additional_matcher=_matches_email(attendee_1.email),
)
account_search_mock_2 = requests_mock.post(
f'{N_baseURL}/accounts/search',
json={"searchResults": [attendee_2.search_result()], "pagination": {"totalPages": 1, "currentPage": 0}},
additional_matcher=_matches_email(attendee_2.email),
)

patch_mock_1 = requests_mock.patch(
f'{N_baseURL}/accounts/{attendee_1.account_id}',
status_code=200
)
patch_mock_2 = requests_mock.patch(
f'{N_baseURL}/accounts/{attendee_2.account_id}',
status_code=200
)

import attendanceToTestout
attendanceToTestout.main()

assert search_mock.called, "Event search API should be called"
assert registrants_mock.called, "Event registrants API should be called"
assert account_mocks[0].called, "Registrant account API should be called"
assert account_search_mock_1.called, "Account search API should be called for attendee 1"
assert not account_search_mock_2.called, "Account search API should not be called for non-attended attendee"
assert patch_mock_1.called, "Attended attendee should be updated"
assert not patch_mock_2.called, "Non-attended attendee should not be updated"


def test_main_handles_empty_search_results(requests_mock):
"""Test that main() handles empty event search results gracefully"""
search_mock, _ = NeonEventMock.mock_events(requests_mock, [])
Expand Down