fix(importing): normalize non-breaking spaces in CSV datetime fields

Google Sheets embeds \xa0 (non-breaking space) in 12-hour time values
(e.g. "3:00\xa0PM") and when date/time columns are combined. This caused
MariaDB datetime INSERTs to fail with an OperationalError.

Adds _clean_datetime() which strips \xa0, normalizes whitespace, and
parses common import formats (M/D/YYYY H:MM AM/PM, etc.) into
YYYY-MM-DD HH:MM:SS before the DB write. Applied to all four datetime
fields: session and presentation start/end.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-15 10:15:13 -04:00
parent c8377a2b22
commit c64c3bc55a

View File

@@ -28,6 +28,21 @@ from app.models.response_models import Resp_Body_Base, mk_resp
router = APIRouter() router = APIRouter()
def _clean_datetime(value) -> str | None:
"""Normalize datetime strings from CSV imports (handles \xa0 from Excel, 12-hour format)."""
if not value:
return None
cleaned = str(value).replace('\xa0', ' ').strip()
if not cleaned:
return None
for fmt in ('%m/%d/%Y %I:%M %p', '%m/%d/%Y %H:%M', '%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M'):
try:
return datetime.datetime.strptime(cleaned, fmt).strftime('%Y-%m-%d %H:%M:%S')
except ValueError:
continue
return cleaned
# No longer needed? 2024-08-15 # No longer needed? 2024-08-15
# Based on the program import template the clients are given. # 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. # Ideally the import file should only contain records with new External IDs. Old records will be checked and only updated if needed.
@@ -332,7 +347,10 @@ router = APIRouter()
# ### BEGIN ### Event Importing ### event_importing_program_data() ### # ### BEGIN ### Event Importing ### event_importing_program_data() ###
# Based on the program import template the clients are given. # Based on the program import template the clients are given.
# Create and update locations, sessions, presentations, and presenters as needed. # Create and update locations, sessions, presentations, and presenters as needed.
# Updated 2024-03-25 # Careful with how date and time fields are combined
# This should work: =TEXT(G2,"M/D/YYYY")&" "&TEXT(H2,"H:MM AM/PM")
# Simply adding the fields (=D264+E264) sort of works. This produces non breaking spaces but clean up on import.
# Updated 2026-05-15
@router.post('/event/{event_id}/importing/program_data', response_model=Resp_Body_Base) @router.post('/event/{event_id}/importing/program_data', response_model=Resp_Body_Base)
async def event_importing_program_data( async def event_importing_program_data(
event_id: str = Path(min_length=11, max_length=22), event_id: str = Path(min_length=11, max_length=22),
@@ -656,13 +674,8 @@ async def event_importing_program_data(
if record.get('session_description'): if record.get('session_description'):
event_session_data['description'] = record.get('session_description', '').strip() event_session_data['description'] = record.get('session_description', '').strip()
event_session_data['start_datetime'] = record.get('session_start_datetime', '').strip() event_session_data['start_datetime'] = _clean_datetime(record.get('session_start_datetime'))
# event_session_start_datetime = record.get('event_session_start_date', '') + ' ' + record.get('event_session_start_time', '') event_session_data['end_datetime'] = _clean_datetime(record.get('session_end_datetime'))
# event_session_data['start_datetime'] = event_session_start_datetime
event_session_data['end_datetime'] = record.get('session_end_datetime', '').strip()
# event_session_end_datetime = record.get('event_session_end_date', '') + ' ' + record.get('event_session_end_time', '')
# event_session_data['end_datetime'] = event_session_end_datetime
event_session_data['sort'] = record.get('session_sort') event_session_data['sort'] = record.get('session_sort')
@@ -736,19 +749,11 @@ async def event_importing_program_data(
if record.get('presentation_description'): if record.get('presentation_description'):
event_presentation_data['description'] = record.get('presentation_description', '').strip() event_presentation_data['description'] = record.get('presentation_description', '').strip()
if record.get('presentation_start_datetime'): event_presentation_data['start_datetime'] = _clean_datetime(record.get('presentation_start_datetime'))
event_presentation_data['start_datetime'] = record.get('presentation_start_datetime', '').strip() data['presentation_start_datetime'] = event_presentation_data['start_datetime']
data['presentation_start_datetime'] = event_presentation_data['start_datetime']
else:
event_presentation_data['start_datetime'] = None
data['presentation_start_datetime'] = None
if record.get('presentation_end_datetime'): event_presentation_data['end_datetime'] = _clean_datetime(record.get('presentation_end_datetime'))
event_presentation_data['end_datetime'] = record.get('presentation_end_datetime', '').strip() data['presentation_end_datetime'] = event_presentation_data['end_datetime']
data['presentation_end_datetime'] = event_presentation_data['end_datetime']
else:
event_presentation_data['end_datetime'] = None
data['presentation_end_datetime'] = None
if record.get('presentation_abstract_code'): if record.get('presentation_abstract_code'):
event_presentation_data['abstract_code'] = record.get('presentation_abstract_code', '').strip() event_presentation_data['abstract_code'] = record.get('presentation_abstract_code', '').strip()