event: Zoom CSV import — use email as fallback external_id; populate address/phone fields
This commit is contained in:
@@ -16,9 +16,9 @@ from app.methods.event_person_methods import create_event_person_obj, create_upd
|
|||||||
# from app.methods.event_presenter_methods import create_update_event_presenter_obj_v4, get_event_presenter_rec_list, load_event_presenter_obj
|
# from app.methods.event_presenter_methods import create_update_event_presenter_obj_v4, get_event_presenter_rec_list, load_event_presenter_obj
|
||||||
from app.methods.hosted_file_methods import load_hosted_file_obj, save_file
|
from app.methods.hosted_file_methods import load_hosted_file_obj, save_file
|
||||||
|
|
||||||
from app.models.event_models import Event_Base
|
#from app.models.event_models import Event_Base
|
||||||
# from app.models.event_location_models import Event_Location_Base
|
# from app.models.event_location_models import Event_Location_Base
|
||||||
from app.models.event_person_models import Event_Person_Base
|
#from app.models.event_person_models import Event_Person_Base
|
||||||
# from app.models.event_presentation_models import Event_Presentation_Base
|
# from app.models.event_presentation_models import Event_Presentation_Base
|
||||||
# from app.models.event_presenter_models import Event_Presenter_Base
|
# from app.models.event_presenter_models import Event_Presenter_Base
|
||||||
# from app.models.event_session_models import Event_Session_Base
|
# from app.models.event_session_models import Event_Session_Base
|
||||||
@@ -472,4 +472,249 @@ async def event_id_badge_import(
|
|||||||
if return_detail:
|
if return_detail:
|
||||||
return mk_resp(data=event_badge_person_li, status_message=f'Importing badges from file. Found {len(person_li)} badges.', response=commons.response)
|
return mk_resp(data=event_badge_person_li, status_message=f'Importing badges from file. Found {len(person_li)} badges.', response=commons.response)
|
||||||
else:
|
else:
|
||||||
return mk_resp(data=event_badge_person_summary_li, status_message=f'Checked for badges from file. Found {len(event_badge_person_li)} badges.', response=commons.response)
|
return mk_resp(data=event_badge_person_summary_li, status_message=f'Checked for badges from file. Found {len(event_badge_person_li)} badges.', response=commons.response)
|
||||||
|
|
||||||
|
|
||||||
|
# ### BEGIN ### Zoom Events CSV Badge Import ### event_id_badge_import_zoom_csv() ###
|
||||||
|
# Accepts a Zoom Events registrant CSV export and upserts event_person records.
|
||||||
|
# Zoom CSV format: fixed columns (First name, Last name, Registrant email, Ticket name,
|
||||||
|
# Unique identifier, etc.) plus per-ticket-type custom fields using the pattern
|
||||||
|
# "FieldLabel_*_TicketTypeName". Delimiter is auto-detected (Zoom exports vary).
|
||||||
|
# Updated 2026-04-06
|
||||||
|
|
||||||
|
def _zoom_ticket_field(record: dict, field_prefix: str, ticket_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Extracts a per-ticket-type field value from a Zoom CSV row.
|
||||||
|
Tries the exact ticket match first, then falls back to the first non-empty value
|
||||||
|
across all variants of that field prefix.
|
||||||
|
"""
|
||||||
|
exact_key = f'{field_prefix}_*_{ticket_name}'
|
||||||
|
if val := str(record.get(exact_key, '')).strip():
|
||||||
|
return val
|
||||||
|
for key, val in record.items():
|
||||||
|
if key.startswith(f'{field_prefix}_*_') and str(val).strip():
|
||||||
|
return str(val).strip()
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/event/{event_id}/badge/import/zoom_csv', response_model=Resp_Body_Base)
|
||||||
|
async def event_id_badge_import_zoom_csv(
|
||||||
|
event_id: str = Path(min_length=11, max_length=22),
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
|
||||||
|
begin_at: int = 0,
|
||||||
|
end_at: int = 20000,
|
||||||
|
|
||||||
|
return_detail: bool = False,
|
||||||
|
|
||||||
|
commons: Common_Route_Params = Depends(common_route_params),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Import event badges from a Zoom Events registrant CSV export.
|
||||||
|
|
||||||
|
Zoom exports fixed columns (First name, Last name, Registrant email, Ticket name,
|
||||||
|
Unique identifier) plus per-ticket-type custom fields in the format
|
||||||
|
"FieldLabel_*_TicketTypeName". The 'Unique identifier' column is used as the
|
||||||
|
external_registration_id. Delimiter is auto-detected.
|
||||||
|
"""
|
||||||
|
log.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
account_id = commons.x_account_id
|
||||||
|
|
||||||
|
event_id_random = event_id
|
||||||
|
if event_id := redis_lookup_id_random(record_id_random=event_id, table_name='event'): pass
|
||||||
|
else: return mk_resp(data=None, status_code=404, response=commons.response)
|
||||||
|
|
||||||
|
link_to_type = 'event'
|
||||||
|
link_to_id = event_id
|
||||||
|
|
||||||
|
file_info = await save_file(
|
||||||
|
file=file,
|
||||||
|
account_id=account_id,
|
||||||
|
link_to_type=link_to_type,
|
||||||
|
link_to_id=link_to_id,
|
||||||
|
)
|
||||||
|
if file_info['saved']:
|
||||||
|
log.info('File saved')
|
||||||
|
else:
|
||||||
|
log.error('Something may have gone wrong while saving the uploaded file?')
|
||||||
|
return mk_resp(data=None, status_code=500, response=commons.response)
|
||||||
|
|
||||||
|
hosted_files_path = settings.FILES_PATH['hosted_files_root']
|
||||||
|
subdirectory_dest = os.path.join(hosted_files_path, file_info.get('subdirectory_path'))
|
||||||
|
hash_filename = file_info.get('hash_sha256') + '.file'
|
||||||
|
full_file_path = pathlib.Path(os.path.join(subdirectory_dest, hash_filename))
|
||||||
|
|
||||||
|
if not full_file_path.exists():
|
||||||
|
log.warning(f'Not found at full file path: {full_file_path}')
|
||||||
|
return mk_resp(data=None, status_code=500, response=commons.response)
|
||||||
|
|
||||||
|
# Zoom CSV layout: row 1 = "Report generated" metadata, row 2 = blank, row 3 = headers
|
||||||
|
# Delimiter is auto-detected (Zoom exports vary between comma and tab)
|
||||||
|
df = pandas.read_csv(
|
||||||
|
full_file_path,
|
||||||
|
sep=None,
|
||||||
|
engine='python',
|
||||||
|
skiprows=2,
|
||||||
|
na_filter=False,
|
||||||
|
dtype=str,
|
||||||
|
)
|
||||||
|
|
||||||
|
df_dict = df.to_dict(orient='records')
|
||||||
|
log.info(f'Zoom CSV total record count: {len(df_dict)}')
|
||||||
|
|
||||||
|
loop_count = 0
|
||||||
|
event_badge_person_li = []
|
||||||
|
event_badge_person_summary_li = []
|
||||||
|
|
||||||
|
log.setLevel(logging.DEBUG)
|
||||||
|
for record in df_dict:
|
||||||
|
log.info(f'Loop Count: {loop_count}')
|
||||||
|
loop_count += 1
|
||||||
|
if loop_count <= begin_at: continue
|
||||||
|
if loop_count > end_at: break
|
||||||
|
|
||||||
|
unique_id = str(record.get('Unique identifier', '')).strip()
|
||||||
|
email = str(record.get('Registrant email', '')).strip()
|
||||||
|
|
||||||
|
if unique_id:
|
||||||
|
external_id = unique_id
|
||||||
|
elif email:
|
||||||
|
log.info('Using email as external_id fallback for Zoom CSV row')
|
||||||
|
external_id = email
|
||||||
|
else:
|
||||||
|
log.warning('Row missing Unique identifier and email — skipping.')
|
||||||
|
continue
|
||||||
|
|
||||||
|
ticket_name = str(record.get('Ticket name', '')).strip()
|
||||||
|
given_name = str(record.get('First name', '')).strip()
|
||||||
|
family_name = str(record.get('Last name', '')).strip()
|
||||||
|
display_name = str(record.get('Display name', '')).strip()
|
||||||
|
|
||||||
|
# Per-ticket-type custom fields
|
||||||
|
organization = _zoom_ticket_field(record, 'Organization', ticket_name)
|
||||||
|
professional_title = _zoom_ticket_field(record, 'Job title', ticket_name)
|
||||||
|
phone = (_zoom_ticket_field(record, 'Phone', ticket_name)
|
||||||
|
or _zoom_ticket_field(record, 'Phone number', ticket_name))
|
||||||
|
address_line_1 = (_zoom_ticket_field(record, 'Address line 1', ticket_name)
|
||||||
|
or _zoom_ticket_field(record, 'Address', ticket_name))
|
||||||
|
address_line_2 = _zoom_ticket_field(record, 'Address line 2', ticket_name)
|
||||||
|
address_line_3 = _zoom_ticket_field(record, 'Address line 3', ticket_name)
|
||||||
|
city = _zoom_ticket_field(record, 'City', ticket_name)
|
||||||
|
state_province = _zoom_ticket_field(record, 'State/Province', ticket_name)
|
||||||
|
state_province_abb = _zoom_ticket_field(record, 'State/Province Abb', ticket_name)
|
||||||
|
postal_code = (_zoom_ticket_field(record, 'Postal code', ticket_name)
|
||||||
|
or _zoom_ticket_field(record, 'Zip code', ticket_name)
|
||||||
|
or _zoom_ticket_field(record, 'Zip/Postal Code', ticket_name))
|
||||||
|
country = _zoom_ticket_field(record, 'Country/Region', ticket_name)
|
||||||
|
country_alpha_2_code = _zoom_ticket_field(record, 'Country Alpha 2 Code', ticket_name)
|
||||||
|
country_subdivision_code = _zoom_ticket_field(record, 'Country Subdivision Code', ticket_name)
|
||||||
|
# location, full_address, location_long, location_short are computed by DB triggers
|
||||||
|
|
||||||
|
event_person_summary = {
|
||||||
|
'event_id': event_id,
|
||||||
|
'event_id_random': event_id_random,
|
||||||
|
'external_id': external_id,
|
||||||
|
'given_name': given_name,
|
||||||
|
'family_name': family_name,
|
||||||
|
'email': email,
|
||||||
|
}
|
||||||
|
|
||||||
|
event_person_data = {
|
||||||
|
'account_id': account_id,
|
||||||
|
'event_id': event_id,
|
||||||
|
'enable': True,
|
||||||
|
'external_id': external_id,
|
||||||
|
'external_registration_id': unique_id,
|
||||||
|
'event_person_profile': {
|
||||||
|
'enable': True,
|
||||||
|
'given_name': given_name,
|
||||||
|
'family_name': family_name,
|
||||||
|
'full_name': display_name or f'{given_name} {family_name}'.strip(),
|
||||||
|
'email': email,
|
||||||
|
'phone': phone,
|
||||||
|
'address_line_1': address_line_1,
|
||||||
|
'address_line_2': address_line_2,
|
||||||
|
'address_line_3': address_line_3,
|
||||||
|
'city': city,
|
||||||
|
'state_province': state_province,
|
||||||
|
'state_province_abb': state_province_abb,
|
||||||
|
'postal_code': postal_code,
|
||||||
|
'country': country,
|
||||||
|
'country_alpha_2_code': country_alpha_2_code,
|
||||||
|
'country_subdivision_code': country_subdivision_code,
|
||||||
|
'professional_title': professional_title,
|
||||||
|
'affiliations': organization,
|
||||||
|
},
|
||||||
|
'event_badge': {
|
||||||
|
'enable': True,
|
||||||
|
'external_id': external_id,
|
||||||
|
'external_registration_id': unique_id,
|
||||||
|
'given_name': given_name,
|
||||||
|
'family_name': family_name,
|
||||||
|
'full_name': display_name or f'{given_name} {family_name}'.strip(),
|
||||||
|
'email': email,
|
||||||
|
'phone': phone,
|
||||||
|
'address_line_1': address_line_1,
|
||||||
|
'address_line_2': address_line_2,
|
||||||
|
'address_line_3': address_line_3,
|
||||||
|
'city': city,
|
||||||
|
'state_province': state_province,
|
||||||
|
'state_province_abb': state_province_abb,
|
||||||
|
'postal_code': postal_code,
|
||||||
|
'country': country,
|
||||||
|
'country_alpha_2_code': country_alpha_2_code,
|
||||||
|
'country_subdivision_code': country_subdivision_code,
|
||||||
|
'professional_title': professional_title,
|
||||||
|
'affiliations': organization,
|
||||||
|
'badge_type': ticket_name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
event_badge_person_li.append(event_person_data)
|
||||||
|
event_badge_person_summary_li.append(event_person_summary)
|
||||||
|
|
||||||
|
sql_select_event_person = """
|
||||||
|
SELECT id AS event_person_id, id_random AS event_person_id_random,
|
||||||
|
external_id AS event_person_external_id,
|
||||||
|
event_badge_id AS event_badge_id,
|
||||||
|
event_person_profile_id AS event_person_profile_id
|
||||||
|
FROM `event_person`
|
||||||
|
WHERE event_person.event_id = :event_id
|
||||||
|
AND event_person.external_id = :external_id
|
||||||
|
"""
|
||||||
|
|
||||||
|
if event_person_result := sql_select(sql=sql_select_event_person, data=event_person_summary):
|
||||||
|
if isinstance(event_person_result, list):
|
||||||
|
log.error(f'Found more than one Event Person with external_id={external_id}. Count: {len(event_person_result)}')
|
||||||
|
else:
|
||||||
|
event_person_id = event_person_result.get('event_person_id')
|
||||||
|
event_badge_id = event_person_result.get('event_badge_id')
|
||||||
|
event_person_profile_id = event_person_result.get('event_person_profile_id')
|
||||||
|
log.info(f'Updating existing Event Person ID: {event_person_id}')
|
||||||
|
if create_update_event_person_obj_v4(
|
||||||
|
event_person_dict_obj=event_person_data,
|
||||||
|
event_person_id=event_person_id,
|
||||||
|
account_id=account_id,
|
||||||
|
event_id=event_id,
|
||||||
|
event_badge_id=event_badge_id,
|
||||||
|
event_person_profile_id=event_person_profile_id,
|
||||||
|
):
|
||||||
|
log.warning(f'Event Person updated. ID: {event_person_id}')
|
||||||
|
else:
|
||||||
|
log.warning(f'Event Person not updated. ID: {event_person_id}')
|
||||||
|
else:
|
||||||
|
log.info('No Event Person found. Creating new...')
|
||||||
|
if result_id := create_update_event_person_obj_v4(
|
||||||
|
event_person_dict_obj=event_person_data,
|
||||||
|
account_id=account_id,
|
||||||
|
event_id=event_id,
|
||||||
|
):
|
||||||
|
log.warning(f'Event Person created. ID: {result_id}')
|
||||||
|
else:
|
||||||
|
log.warning('Event Person not created.')
|
||||||
|
|
||||||
|
if return_detail:
|
||||||
|
return mk_resp(data=event_badge_person_li, status_message=f'Zoom CSV import complete. Processed {len(event_badge_person_li)} records.', response=commons.response)
|
||||||
|
else:
|
||||||
|
return mk_resp(data=event_badge_person_summary_li, status_message=f'Zoom CSV import complete. Processed {len(event_badge_person_summary_li)} records.', response=commons.response)
|
||||||
Reference in New Issue
Block a user