Files
OSIT-AE-API-FastAPI/app/routers/event_badge_importing.py
Scott Idem 9962176c74 fix(event_badge): don't overwrite enable on re-import
Remove enable from event_person_data (and sub-dicts) before calling
create_update_event_person_obj_v4 on the update path in all three import
endpoints. enable=True is preserved for initial record creation only,
so manually disabled (blacklisted) records survive subsequent imports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:36:08 -04:00

1049 lines
48 KiB
Python

import datetime, json, os, pathlib, pytz, secrets, shutil, time
import pandas, xlrd # qrcode
from fastapi import APIRouter, Body, Depends, File, Header, HTTPException, Path, Query, Response, status, UploadFile
from pydantic import BaseModel, EmailStr, Field
from typing import Dict, List, Optional, Set, Union
from app.lib_general import log, logging, secure_hash_string, common_route_params, Common_Route_Params
from app.config import settings
from app.db_sql import sql_insert, sql_update, sql_insert_or_update, sql_select, sql_delete, redis_lookup_id_random
from app.routers.api_crud import delete_obj_template, get_obj_template, get_obj_li_template, patch_obj_template, post_obj_template
from app.methods.event_person_methods import create_event_person_obj, create_update_event_person_obj_v4, get_event_person_rec_list, load_event_person_obj, update_event_person_obj, update_event_person_obj_v3
# from app.methods.event_session_methods import create_update_event_session_obj_v4, get_event_session_rec_list, load_event_session_obj, update_event_session_obj
# from app.methods.event_presentation_methods import create_update_event_presentation_obj_v4, get_event_presentation_rec_list, load_event_presentation_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.models.event_models import Event_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_presentation_models import Event_Presentation_Base
# from app.models.event_presenter_models import Event_Presenter_Base
# from app.models.event_session_models import Event_Session_Base
from app.models.response_models import Resp_Body_Base, mk_resp
router = APIRouter()
# Based on the program import template the clients are given.
# Ideally the import file should only contain records with new External IDs. Old records will be checked and only updated if needed.
# Updated 2021-10-19
# ### BEGIN ### Event Importing ### event_importing_program_data() ###
# Based on the program import template the clients are given.
# Create and update locations, sessions, presentations, and presenters as needed.
# Updated 2023-01-12
@router.post('/event/{event_id}/badge/import', response_model=Resp_Body_Base)
async def event_id_badge_import(
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),
):
log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
allow_inserts = True
allow_updates = True
account_id = commons.x_account_id
# event_location_id = None
# event_session_id = None
# event_presentation_id = None
# event_presenter_id = None
# Processing Config Options:
# How should the external_id generation and matching be done?
# external_sys_id, external_reg_id and given and family name
# prefix external_id with related events (registration setups) from external system
# external_id from: external_sys_id, external_reg_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,
# account_id_random = account_id_random,
link_to_type = link_to_type,
link_to_id = link_to_id,
# link_to_id_random = link_to_id_random,
# check_allowed_extension = check_allowed_extension,
)
if file_info['saved']:
log.info('File saved')
log.debug(file_info)
else:
log.error('Something may have gone wrong while saving the uploaded file?')
return mk_resp(data=None, status_code=500, response=commons.response)
# log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
hosted_files_path = settings.FILES_PATH['hosted_files_root']
# hosted_files_path = '/home/scott/tmp/hosted_files_dev/'
log.info(f'Hosted Files Path: {hosted_files_path}')
log.debug(shutil.disk_usage(hosted_files_path))
# full_file_path = 'admin/temp/import_event_program_external_id.csv'
subdirectory_dest = os.path.join(hosted_files_path, file_info.get('subdirectory_path'))
log.debug(subdirectory_dest)
hash_filename = file_info.get('hash_sha256')+'.file'
full_file_path = pathlib.Path( os.path.join(subdirectory_dest, hash_filename) ) # NOTE: Must use pathlib.Path to use .exists()
log.debug(full_file_path)
if full_file_path.exists():
log.info(f'Full File Path: {full_file_path}')
else:
log.warning(f'Not found at full File Path: {full_file_path}')
return mk_resp(data=None, status_code=500, response=commons.response)
# return mk_resp(data=file_info, response=commons.response)
df = pandas.read_csv(
full_file_path,
na_filter=False,
dtype={
'external_id': str, 'External ID': str, # Must be unique per person; Generate if needed
'external_event_id': str, 'External Event ID': str, # Usually same for all registrants
'external_registration_id': str, 'External Registration ID': str, # Must be unique per registration (may be more than one person; usually guests)
'external_person_id': str, 'External Person ID': str, # Must be unique per person
# 'external_sys_id': str, 'External Sys ID': str, # Must be unique per person
'event_badge_template_id': int, 'Event Badge Template ID': int, # An actual ID number
# 'event_location_external_id': str, 'event_location_code': str, 'event_location_sort': int,
# 'event_presentation_external_id': str, 'event_presentation_code': str, 'event_presentation_sort': int,
# 'event_presenter_external_id': str, 'event_presenter_code': str, 'event_presenter_number': int, 'event_presenter_designations': str, 'event_presenter_sort': int,
# 'event_session_external_id': str, 'event_session_code': str, 'event_session_sort': int,
# 'location_external_id': str, 'location_code': str, 'location_sort': int,
# 'presentation_external_id': str, 'presentation_code': str, 'presentation_sort': int,
# 'presenter_external_id': str, 'presenter_code': str, 'presenter_number': int, 'presenter_designations': str, 'presenter_sort': int,
# 'session_external_id': str, 'session_code': str, 'session_sort': int,
# 'source_id': str, 'Source ID': str,
'pronouns': str, 'Pronouns': str, # For badge rendering
'informal_name': str, 'Informal Name': str, # For badge rendering
'title_names': str, 'Title Names': str, # For badge rendering
'given_name': str, 'Given Name': str, # For badge rendering
'middle_name': str, 'Middle Name': str, # For badge rendering
'family_name': str, 'Family Name': str, # For badge rendering
'designations': str, 'Designations': str, # For badge rendering
'professional_title': str, 'Professional Title': str, # For badge rendering
'professional_title_override': str, 'display_professional_title': str, 'Display Professional Title': str, # For badge rendering override
'full_name': str, 'Full Name': str,
'full_name_override': str, 'display_name': str, 'Display Name': str, 'Full Name Override': str, # For badge rendering override
'affiliations': str, 'Affiliations': str, # For badge rendering override
'affiliations_override': str, 'display_affiliations': str, 'Display Affiliations': str, # For badge rendering override
'email': str, 'Email Address': str,
'phone': str, 'Phone': str,
'address_line_1': str, 'Address Line 1': str,
'address_line_2': str, 'Address Line 2': str,
'address_line_3': str, 'Address Line 3': str,
'city': str, 'City': str,
'country_subdivision_code': str, 'Country Subdivision Code': str,
'state_province': str, 'State Province': str,
'state_province_abb': str, 'State Province Abb': str,
'postal_code': str, 'Postal Code': str,
'country_alpha_2_code': str, 'Country Alpha 2 Code': str,
'country': str, 'Country': str,
'full_address': str, 'Full Address': str,
'location': str, 'Location': str, # For badge rendering
'location_override': str, 'display_location': str, 'Display Location': str, # For badge rendering override
'badge_type_code': str, 'Badge Type Code': str, # Must be mapped to a badge template ID
'badge_type_code_override': str, 'Badge Type Code Override': str,
'badge_type': str, 'Badge Type': str,
'badge_type_override': str, 'Badge Type Override': str,
'member_type_code': str, 'Member Type Code': str,
'member_type': str, 'Member Type': str,
'registration_type_code': str, 'Registration Type Code': str,
'registration_type': str, 'Registration Type': str,
'other_1': str, 'Other 1': str,
'other_2': str, 'Other 2': str,
'ticket_1_code': str, 'Ticket 1 Code': str,
'ticket_2_code': str, 'Ticket 2 Code': str,
'ticket_3_code': str, 'Ticket 3 Code': str,
'ticket_4_code': str, 'Ticket 4 Code': str,
'ticket_5_code': str, 'Ticket 5 Code': str,
'ticket_6_code': str, 'Ticket 6 Code': str,
'ticket_7_code': str, 'Ticket 7 Code': str,
'ticket_8_code': str, 'Ticket 8 Code': str,
'allow_tracking': str, 'Allow Tracking': str,
'agree_to_tc': str, 'Agree to TC': str,
}
)
df.rename(columns={
'External ID': 'external_id',
'External Event ID': 'external_event_id',
'External Registration ID': 'external_registration_id',
'External Person ID': 'external_person_id',
'External Sys ID': 'external_person_id',
'Event Badge Template ID': 'event_badge_template_id',
'Pronouns': 'pronouns',
'Informal_name': 'informal_name',
'Title Names': 'title_names',
'Given Name': 'given_name',
'Middle Name': 'middle_name',
'Family Name': 'family_name',
'Designations': 'designations',
'Professional Title': 'professional_title',
'Full Name': 'full_name',
'Display Name': 'full_name_override',
'Full Name Override': 'full_name_override',
'Affiliations': 'affiliations',
# 'Title': 'presenter_title_names',
# 'Prefix': 'presenter_title_names',
# 'Nickname': 'presenter_informal_name',
# 'given_name (first)': 'presenter_given_name',
# 'First Name': 'presenter_given_name',
# 'family_name (last)': 'presenter_family_name',
# 'Last Name': 'presenter_family_name',
# 'Suffix': 'presenter_designations',
'Email': 'email',
'Email Address': 'email',
'CC Email Address': 'cc_email',
'Phone': 'phone',
'Location': 'location',
'Badge Type Code': 'badge_type_code',
'Badge Type': 'badge_type',
'Member Type Code': 'member_type_code',
'Member Type': 'member_type',
'Registration Type Code': 'registration_type_code',
'Registration Type': 'registration_type',
'Other 1': 'other_1',
'Other 2': 'other_2',
'Ticket 1 Code': 'ticket_1_code',
'Ticket 2 Code': 'ticket_2_code',
'Ticket 3 Code': 'ticket_3_code',
'Ticket 4 Code': 'ticket_4_code',
'Ticket 5 Code': 'ticket_5_code',
'Ticket 6 Code': 'ticket_6_code',
'Ticket 7 Code': 'ticket_7_code',
'Ticket 8 Code': 'ticket_8_code',
'Allow Tracking': 'allow_tracking',
'Agree to TC': 'agree_to_tc',
# 'location_title': 'location_name',
# 'Location Code': 'location_code',
# 'Location Name': 'location_name',
# 'session_location': 'location_name',
# 'Session Location': 'location_name',
# 'session_title': 'session_name',
# 'Session Code': 'session_code',
# 'Session Name': 'session_name',
# 'presentation_title': 'presentation_name',
# 'Presentation Code': 'presentation_code',
# 'Presentation Name': 'presentation_name',
# 'Presenter Code': 'code',
# 'Presenter Number': 'number', # for sorting
# 'Presenter Name': 'name',
},
inplace = True)
# log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(df)
df_dict = df.to_dict(orient='records')
log.info(f'Total record count: {len(df_dict)}')
loop_count = 0
event_badge_person_li = []
event_badge_person_summary_li = []
log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
for record in df_dict:
# ### Figure out the external IDs
log.info(f'Loop Count: {loop_count}')
loop_count = loop_count + 1
if loop_count <= begin_at: continue
if loop_count > end_at: break
if external_id := record.get('external_id'):
log.info('Using given external_id for external_id')
elif record.get('external_registration_id') and record.get('external_person_id'):
log.info('Using external_registration_id and external_person_id for external_id')
external_id = str(record.get('external_registration_id'))+':'+str(record.get('external_person_id'))
elif record.get('external_event_id') and record.get('external_person_id'):
log.info('Using external_event_id and external_person_id for external_id')
external_id = str(record.get('external_event_id'))+':'+str(record.get('external_person_id'))
elif record.get('external_person_id'):
log.info('Using only external_person_id for external_id')
external_id = str(record.get('external_person_id'))
elif record.get('external_registration_id'):
log.info('Using only external_registration_id for external_id')
external_id = str(record.get('external_registration_id'))
else:
log.warning('No external ID was found or could safely be generated.')
break
log.debug(f'Event Badge External ID: {external_id}')
event_person_summary = {}
event_person_summary['event_id'] = event_id
event_person_summary['event_id_random'] = event_id_random
event_person_summary['external_id'] = external_id
event_person_summary['given_name'] = record.get('given_name')
event_person_summary['family_name'] = record.get('family_name')
event_person_summary['email'] = record.get('email')
event_person_data = {}
event_person_data['account_id'] = account_id # Is this needed?
event_person_data['event_id'] = event_id
event_person_data['enable'] = True
event_person_data['external_id'] = external_id
event_person_data['external_event_id'] = record.get('external_event_id')
event_person_data['external_registration_id'] = record.get('external_registration_id')
# event_person_data['external_reg_id'] = record.get('external_registration_id') # Deprecated
event_person_data['external_person_id'] = record.get('external_person_id')
# event_person_data['external_sys_id'] = record.get('external_person_id') # Deprecated
event_person_data['event_badge'] = {}
event_person_data['event_person_profile'] = {}
# event_person_data['event_registration'] = {} # Not currently used
event_person_data['event_person_profile']['enable'] = True
event_person_data['event_person_profile']['pronouns'] = record.get('pronouns')
event_person_data['event_person_profile']['informal_name'] = record.get('informal_name')
event_person_data['event_person_profile']['title_names'] = record.get('title_names')
event_person_data['event_person_profile']['given_name'] = record.get('given_name')
event_person_data['event_person_profile']['family_name'] = record.get('family_name')
event_person_data['event_person_profile']['designations'] = record.get('designations')
event_person_data['event_person_profile']['professional_title'] = record.get('professional_title')
event_person_data['event_person_profile']['full_name'] = record.get('full_name')
event_person_data['event_person_profile']['affiliations'] = record.get('affiliations')
event_person_data['event_person_profile']['email'] = record.get('email')
event_person_data['event_person_profile']['phone'] = record.get('phone')
event_person_data['event_person_profile']['location'] = record.get('location')
event_person_data['event_badge']['external_id'] = external_id
event_person_data['event_badge']['external_event_id'] = record.get('external_event_id')
event_person_data['event_badge']['external_registration_id'] = record.get('external_registration_id')
# event_person_data['event_badge']['external_reg_id'] = record.get('external_registration_id') # Deprecated
event_person_data['event_badge']['external_person_id'] = record.get('external_person_id')
# event_person_data['event_badge']['external_sys_id'] = record.get('external_person_id') # Deprecated
event_person_data['event_badge']['enable'] = True
event_person_data['event_badge']['pronouns'] = record.get('pronouns')
event_person_data['event_badge']['informal_name'] = record.get('informal_name')
event_person_data['event_badge']['title_names'] = record.get('title_names')
event_person_data['event_badge']['given_name'] = record.get('given_name')
event_person_data['event_badge']['family_name'] = record.get('family_name')
event_person_data['event_badge']['designations'] = record.get('designations')
event_person_data['event_badge']['professional_title'] = record.get('professional_title')
event_person_data['event_badge']['full_name'] = record.get('full_name')
event_person_data['event_badge']['affiliations'] = record.get('affiliations')
event_person_data['event_badge']['email'] = record.get('email')
event_person_data['event_badge']['phone'] = record.get('phone')
event_person_data['event_badge']['location'] = record.get('location')
event_person_data['event_badge']['event_badge_template_id'] = 9 # record.get('event_badge_template_id')
event_person_data['event_badge']['badge_type_code'] = record.get('badge_type_code')
event_person_data['event_badge']['badge_type'] = record.get('badge_type')
# email = None
# city = None
# country_subdivision_code = None
# state_province = None
# state_province_abb = None
# country_alpha_2_code = None
# country = None
# city = record.get('city')
# state_province = record.get('state_province')
# country = record.get('country')
# country_alpha_2_code = record.get('country_alpha_2_code')
# if location := record.get('location'):
# else:
# location = f'{city} {state_province} {country_alpha_2_code}'
event_badge_person_li.append(event_person_data)
event_badge_person_summary_li.append(event_person_summary)
# event_person_summary_li.append(event_person_summary_data)
loop_count = loop_count + 1
sql_select_event_person = f"""
SELECT id AS event_person_id, id_random AS event_person_id_random, external_id AS event_person_external_id, external_event_id AS event_person_external_event_id, external_registration_id AS event_person_external_registration_id, external_person_id AS event_person_external_person_id, event_badge_id AS event_badge_id, event_person_profile_id AS event_person_profile_id, event_registration_id AS event_registration_id
FROM `event_person` AS `event_person`
WHERE event_person.event_id = :event_id
AND event_person.external_id = :external_id
/*LIMIT 1*/;
"""
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 the same External ID. Count: {len(event_person_result)}')
# return False
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'Found Event Person. Updating existing... Event Person ID: {event_person_id}')
event_person_data.pop('enable', None)
event_person_data.get('event_badge', {}).pop('enable', None)
event_person_data.get('event_person_profile', {}).pop('enable', None)
if create_event_person_obj_result := 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,
# create_sub_obj = create_sub_obj,
# fail_any = fail_any,
# return_outline = False,
):
event_person_id = create_event_person_obj_result
log.warning(f'Event Person updated. Event Person ID: {event_person_id}')
else:
log.warning(f'Event Person not updated. Event Person ID: {event_person_id}')
log.debug(event_badge_obj_in_result)
# return False
else:
log.info('No Event Person found. Creating new...')
if create_event_person_obj_result := create_update_event_person_obj_v4(
event_person_dict_obj = event_person_data,
account_id = account_id,
event_id = event_id,
# create_sub_obj = create_sub_obj,
# fail_any = fail_any,
# return_outline = False,
):
event_person_id = create_event_person_obj_result
log.warning(f'Event Person created. Event Person ID: {event_person_id}')
else:
log.warning(f'Event Person not created.')
log.debug(event_badge_obj_in_result)
# return False
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)
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)
# ### 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
# Notes specific to Axonius 2026
# SELECT id, badge_type, badge_type_code
# FROM event_badge
# WHERE badge_type = 'In-Person Attendee';
# UPDATE event_badge
# SET badge_type_code = 'attendee'
# WHERE badge_type = 'In-Person Attendee';
# SELECT id, badge_type, badge_type_code
# FROM event_badge
# WHERE badge_type = 'Adapt26 Sponsor';
# UPDATE event_badge
# SET badge_type_code = 'sponsor'
# WHERE badge_type = 'Adapt26 Sponsor';
def _split_full_name(full_name: str) -> tuple:
"""Split 'First Last' on last space into (given_name, family_name)."""
parts = full_name.strip().rsplit(' ', 1)
if len(parts) == 2:
return parts[0], parts[1]
return full_name.strip(), ''
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
# Force use of Registrant email as the external_id for Zoom CSV imports.
# Many Zoom exports (for this group) have a useless "Unique identifier"
# column that contains "N/A" for every row — rely on email instead.
email = str(record.get('Registrant email', '')).strip()
if not email:
log.warning('Row missing registrant email — skipping.')
continue
external_id = email
# Sanitize the Unique identifier value and only use it as the
# external_registration_id if it appears meaningful. Treat common
# placeholders like 'N/A'/'NA'/'UNKNOWN' as missing.
unique_id_raw = str(record.get('Unique identifier', '')).strip()
if unique_id_raw and unique_id_raw.upper() not in ('N/A', 'NA', 'UNKNOWN'):
external_registration_id = unique_id_raw
else:
external_registration_id = None
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,
}
# TEMPORARY: Axonius-specific mapping for certain ticket / badge labels
# to internal `badge_type_code` values. Remove after the event (~2 weeks).
normalized_ticket = ticket_name.strip().lower()
badge_type_code = None
if 'sponsor' in normalized_ticket:
badge_type_code = 'sponsor'
elif 'attend' in normalized_ticket or 'attendee' in normalized_ticket:
badge_type_code = 'attendee'
if badge_type_code:
log.info(f"Axonius mapping applied: '{ticket_name}' -> '{badge_type_code}'")
# Parse marketing consent column (if present) and map to badge fields.
# Expected values: "Opt-in" => agree_to_tc=True, allow_tracking=True
# "Opt-out" => agree_to_tc=False, allow_tracking=False
# "N/A" => None/NULL
marketing_raw = None
for _k in ('Agree to receive marketing communication?', 'Agree to receive marketing communication', 'Agree to TC', 'agree_to_tc'):
if _k in record and str(record.get(_k)).strip() != '':
marketing_raw = str(record.get(_k)).strip()
break
agree_to_tc_val = None
allow_tracking_val = None
if marketing_raw is not None:
m = marketing_raw.strip()
m_low = m.lower()
if m_low in ('n/a', 'na'):
agree_to_tc_val = None
allow_tracking_val = None
elif m_low in ('opt-in', 'optin', 'opt in'):
agree_to_tc_val = True
allow_tracking_val = True
elif m_low in ('opt-out', 'optout', 'opt out'):
agree_to_tc_val = False
allow_tracking_val = False
else:
if m_low in ('yes', 'y', 'true', '1'):
agree_to_tc_val = True
allow_tracking_val = True
elif m_low in ('no', 'n', 'false', '0'):
agree_to_tc_val = False
allow_tracking_val = False
else:
agree_to_tc_val = None
allow_tracking_val = None
# Need to deal with this special field/column for Axonius
# "Agree to receive marketing communication?"
event_person_data = {
'account_id': account_id,
'event_id': event_id,
'enable': True,
'external_id': external_id,
'external_registration_id': external_registration_id,
'event_person_profile': {
'event_id': event_id,
'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': {
# 'event_id': event_id,
'enable': True,
'external_id': external_id,
'external_registration_id': external_registration_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,
# TEMPORARY: Axonius export does not include a badge template id.
# Default to the Axonius group's badge template `RKYp2HcQm9o (21)`.
# This is a temporary hardcode — remove or replace when mapping is provided.
'event_badge_template_id': 21,
'event_badge_template_id_random': 'RKYp2HcQm9o',
'badge_type': ticket_name,
'badge_type_code': badge_type_code,
'agree_to_tc': agree_to_tc_val,
'allow_tracking': allow_tracking_val,
},
}
# Look up existing event_person by event_id + external_id (should be 0 or 1).
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
/*LIMIT 2*/;
"""
event_person_result = sql_select(sql=sql_select_event_person, data=event_person_summary)
if event_person_result:
# If multiple rows are returned that's an integrity problem — log it and
# use the first row for the update to avoid creating duplicates.
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)}')
event_person_result = event_person_result[0]
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'Found Event Person. Updating existing... Event Person ID: {event_person_id}')
event_person_data.pop('enable', None)
event_person_data.get('event_badge', {}).pop('enable', None)
event_person_data.get('event_person_profile', {}).pop('enable', None)
updated_id = 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,
)
if updated_id:
log.warning(f'Event Person updated. ID: {updated_id}')
else:
log.warning(f'Event Person not updated. ID: {event_person_id}')
else:
log.info('No Event Person found. Creating new...')
result_id = create_update_event_person_obj_v4(
event_person_dict_obj=event_person_data,
account_id=account_id,
event_id=event_id,
)
if result_id:
log.warning(f'Event Person created. ID: {result_id}')
else:
log.warning('Event Person not created.')
# Record the processed input for response summary after DB ops.
event_badge_person_li.append(event_person_data)
event_badge_person_summary_li.append(event_person_summary)
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)
# ### BEGIN ### Splash (Cvent) XLSX Badge Import ### event_id_badge_import_splash_xlsx() ###
# Accepts a Splash (Cvent) registrant XLSX export and inserts/updates event_person records.
# Splash exports fixed columns: Full Name, Email, Time of RSVP, Status, plus custom
# fields prefixed with "Custom: ". Email is used as external_id. Full Name is split
# on the last space into given_name/family_name and also stored directly as full_name.
# Updated 2026-06-02
@router.post('/event/{event_id}/badge/import/splash_xlsx', response_model=Resp_Body_Base)
async def event_id_badge_import_splash_xlsx(
event_id: str = Path(min_length=11, max_length=22),
file: UploadFile = File(...),
begin_at: int = 0,
end_at: int = 20000,
import_status_filter: str = 'Attending', # set to '' to import all statuses
return_detail: bool = False,
commons: Common_Route_Params = Depends(common_route_params),
):
"""
Import event badges from a Splash (Cvent) registrant XLSX export.
Splash exports fixed columns (Full Name, Email, Time of RSVP, Status) plus
custom fields prefixed with "Custom: ". Email is used as external_id.
Full Name is split on the last space into given_name/family_name and also
stored directly as full_name. Pass import_status_filter='' to import all
statuses (default is 'Attending').
"""
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)
df = pandas.read_excel(full_file_path, dtype=str, na_filter=False)
df_dict = df.to_dict(orient='records')
log.info(f'Splash XLSX total record count: {len(df_dict)}')
loop_count = 0
skipped_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
# Status filter — skip rows that don't match when a filter is set.
if import_status_filter:
status = str(record.get('Status', '')).strip()
if status != import_status_filter:
log.info(f'Skipping row with status "{status}" (filter: "{import_status_filter}")')
skipped_count += 1
continue
email = str(record.get('Email', '')).strip()
if not email:
log.warning('Row missing Email — skipping.')
skipped_count += 1
continue
external_id = email
full_name = str(record.get('Full Name', '')).strip()
given_name, family_name = _split_full_name(full_name)
professional_title = str(record.get('Custom: Job Title', '')).strip()
organization = str(record.get('Custom: Company Name', '')).strip()
country = str(record.get('Custom: Country', '')).strip()
state_province = str(record.get('Custom: State', '')).strip()
dietary_restrictions = str(record.get('Custom: Please note any dietary restrictions or preferences.', '')).strip()
# "Custom: Opt-In" → agree_to_tc / allow_tracking
opt_in_raw = str(record.get('Custom: Opt-In', '')).strip().lower()
if opt_in_raw in ('yes', 'y', 'true', '1', 'opt-in', 'opt_in'):
agree_to_tc_val = True
allow_tracking_val = True
elif opt_in_raw in ('no', 'n', 'false', '0', 'opt-out', 'opt_out'):
agree_to_tc_val = False
allow_tracking_val = False
else:
agree_to_tc_val = None
allow_tracking_val = None
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,
'event_person_profile': {
'event_id': event_id,
'enable': True,
'given_name': given_name,
'family_name': family_name,
'full_name': full_name,
'email': email,
'professional_title': professional_title,
'affiliations': organization,
'country': country,
'state_province': state_province,
},
'event_badge': {
'enable': True,
'external_id': external_id,
'given_name': given_name,
'family_name': family_name,
'full_name': full_name,
'email': email,
'professional_title': professional_title,
'affiliations': organization,
'country': country,
'state_province': state_province,
'other_1': dietary_restrictions,
# TEMPORARY: Axonius DC event badge template mu_7SRuJYum (23).
'event_badge_template_id': 23,
'event_badge_template_id_random': 'mu_7SRuJYum',
'badge_type_code': 'attendee',
'agree_to_tc': agree_to_tc_val,
'allow_tracking': allow_tracking_val,
},
}
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
/*LIMIT 2*/;
"""
event_person_result = sql_select(sql=sql_select_event_person, data=event_person_summary)
if event_person_result:
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)}')
event_person_result = event_person_result[0]
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'Found Event Person. Updating existing... Event Person ID: {event_person_id}')
event_person_data.pop('enable', None)
event_person_data.get('event_badge', {}).pop('enable', None)
event_person_data.get('event_person_profile', {}).pop('enable', None)
updated_id = 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,
)
if updated_id:
log.warning(f'Event Person updated. ID: {updated_id}')
else:
log.warning(f'Event Person not updated. ID: {event_person_id}')
else:
log.info('No Event Person found. Creating new...')
result_id = create_update_event_person_obj_v4(
event_person_dict_obj=event_person_data,
account_id=account_id,
event_id=event_id,
)
if result_id:
log.warning(f'Event Person created. ID: {result_id}')
else:
log.warning('Event Person not created.')
event_badge_person_li.append(event_person_data)
event_badge_person_summary_li.append(event_person_summary)
processed = len(event_badge_person_li)
if return_detail:
return mk_resp(data=event_badge_person_li, status_message=f'Splash XLSX import complete. Processed {processed} records, skipped {skipped_count}.', response=commons.response)
else:
return mk_resp(data=event_badge_person_summary_li, status_message=f'Splash XLSX import complete. Processed {processed} records, skipped {skipped_count}.', response=commons.response)