Files
OSIT-AE-API-FastAPI/app/models/event_badge_models.py
Scott Idem 0f4b4d2f51 feat: Implement V3 ID Vision and fields_to_exclude_from_db across core models
This commit refactors numerous Pydantic models to align with the V3 ID Vision standard, ensuring that primary and foreign key fields are represented as clean string IDs in the API. It also introduces and populates the  ClassVar in each model to prevent view-only fields and linked objects from being inadvertently written to the database during PATCH/POST operations.

Specifically, this includes:
- Adding  to exclude view-derived or joined fields such as , , nested objects (e.g., , ), and convenience fields (e.g., ).
- Adjusting root validators to correctly map string IDs and strip internal integer IDs for API responses.
- Resolving a KeyError by adding  to .

These changes are crucial for maintaining data integrity and consistency with the V3 API architecture.
2026-02-24 16:21:27 -05:00

347 lines
16 KiB
Python

import datetime, pytz
from typing import Dict, List, Optional, Set, Union, ClassVar
from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator, root_validator
from app.db_sql import redis_lookup_id_random
from app.lib_general import log, logging
from app.models.common_field_schema import base_fields, default_num_bytes
from app.models.event_badge_template_models import Event_Badge_Template_Base
from app.models.order_models import Order_Base
# ### BEGIN ### API Event Badge Models ### Event_Badge_Base() ###
class Event_Badge_Base(BaseModel):
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
# --- Standardized Vision IDs (Strings for API, Integers for DB) ---
id: Optional[Union[int, str]] = Field(**base_fields['event_badge_id_random'])
event_badge_id: Optional[Union[int, str]] = Field(**base_fields['event_badge_id_random'])
account_id: Optional[Union[int, str]] = Field(None, **base_fields['account_id_random'])
event_id: Optional[Union[int, str]] = Field(**base_fields['event_id_random'])
# NOTE: This should only be used when the event_person record can not be created. And records before 2022.
event_id_only: Optional[Union[int, str]] = Field(**base_fields['event_id_random'])
event_badge_template_id: Optional[Union[int, str]] = Field(**base_fields['event_badge_template_id_random'])
event_person_id: Optional[Union[int, str]] = Field(**base_fields['event_person_id_random'])
person_id: Optional[Union[int, str]] = Field(**base_fields['person_id_random'])
# --- Standardized Legacy / Internal IDs (Excluded) ---
id_random: Optional[str] = Field(None, alias='event_badge_id_random', exclude=True)
account_id_random: Optional[str] = Field(None, exclude=True)
event_id_random: Optional[str] = Field(None, exclude=True)
event_id_random_only: Optional[str] = Field(None, exclude=True)
event_badge_template_id_random: Optional[str] = Field(None, exclude=True)
event_person_id_random: Optional[str] = Field(None, exclude=True)
person_id_random: Optional[str] = Field(None, exclude=True)
@root_validator(pre=True)
def map_v3_ids(cls, values):
"""
Vision Transformer:
Map DB keys to clean API keys and strip internal integers during READ operations.
During CREATE (POST) operations, we ensure resolved integers are preserved.
"""
# 1. Map Random Strings to Clean Names
rid = values.get('id_random') or values.get('event_badge_id_random')
if rid and isinstance(rid, str):
values['id'] = rid
values['event_badge_id'] = rid
if a_rid := values.get('account_id_random'): values['account_id'] = a_rid
if e_rid := values.get('event_id_random'): values['event_id'] = e_rid
if eo_rid := values.get('event_id_random_only'): values['event_id_only'] = eo_rid
if et_rid := values.get('event_badge_template_id_random'): values['event_badge_template_id'] = et_rid
if ep_rid := values.get('event_person_id_random'): values['event_person_id'] = ep_rid
if p_rid := values.get('person_id_random'): values['person_id'] = p_rid
# 2. Prevent leakage of integers during API responses (Vision Standard)
for k in ['id', 'event_badge_id', 'account_id', 'event_id', 'event_id_only', 'event_badge_template_id', 'event_person_id', 'person_id']:
val = values.get(k)
if val is not None and not isinstance(val, str):
values[k] = None
return values
external_id: Optional[str] # Generated internally or externally. Needs to be stable. It should not change.
external_event_id: Optional[str] # Event ID generated by external system. Needs to be stable. It should not change.
external_registration_id: Optional[str] # Registration ID generated by external system (should be stable and not change)
external_reg_id: Optional[str] # NOTE: Deprecated; Move to external_registration_id. Registration ID generated by external system (should be stable and not change)
external_person_id: Optional[str] # Person ID generated by external system (should be stable and not change)
external_sys_id: Optional[str] # NOTE: Deprecated; Move to external_person_id. Person ID generated by external system (should be stable and not change)
pronouns: Optional[str] # Preferred pronouns
pronouns_override: Optional[str] # Override pronouns
informal_name: Optional[str]
title_names: Optional[str] # Title for generation, official position, or professional or academic qualification, other honorific, or other name prefix
given_name: Optional[str]
middle_name: Optional[str]
family_name: Optional[str]
designations: Optional[str] # Temporary or long-term designations related to family, relationships, person differentiation (Junior/Senior), location, social status, professional qualifications, legal status, or other name suffix (degrees and credentials)
professional_title: Optional[str] # Professional title
display_professional_title: Optional[str] # NOTE: Deprecated! Phasing out! Use *full_name_override* instead.
professional_title_override: Optional[str] # Override professional title
# title: Optional[str] # NOTE: Phasing out! Use *professional_title* instead.
# BEGIN # Auto created name variations
full_name: Optional[str] # title_names given_name middle_name family_name designations
full_name_override: Optional[str] # # Override full_name; Actual name shown on badge and other "public" areas
affiliations: Optional[str] # One or more affiliations with organizations, companies, and other groups
affiliations_override: Optional[str] # Override affiliations
email: Optional[str]
email_override: Optional[str]
phone: Optional[str]
phone_override: Optional[str]
address_line_1: Optional[str]
address_line_2: Optional[str]
address_line_3: Optional[str]
city: Optional[str]
county: Optional[str] # NOTE: This is for a county within a state or province
country_subdivision_code: Optional[str]
state_province: Optional[str]
state_province_abb: Optional[str]
postal_code: Optional[str]
country_alpha_2_code: Optional[str]
country: Optional[str]
# full_address: Optional[str]
location: Optional[str] # Actual location name shown on badge and other "public" areas
location_override: Optional[str] # Override location
location_short: Optional[str] # Auto generated short version
location_long: Optional[str] # Auto generated long version
# This is updated using SQL triggers and a SQL function
# Combines informal, given, middle, family, email
query_str: Optional[str]
# NOTE: More badge fields need to be added here once things are cleaned up
badge_type_code_override: Optional[str]
badge_type_override: Optional[str]
badge_type_code: Optional[str]
badge_type: Optional[str]
member_type_code: Optional[str]
member_type: Optional[str]
member_status: Optional[str]
registration_type_code: Optional[str]
registration_type: Optional[str]
other_1: Optional[str]
other_2: Optional[str]
ticket_0_code: Optional[str]
ticket_1_code: Optional[str]
ticket_2_code: Optional[str]
ticket_3_code: Optional[str]
ticket_4_code: Optional[str]
ticket_5_code: Optional[str]
ticket_6_code: Optional[str]
ticket_7_code: Optional[str]
ticket_8_code: Optional[str]
ticket_9_code: Optional[str]
ticket_10_code: Optional[str]
agree_to_tc: Optional[bool] # Agree to terms and conditions
allow_tracking: Optional[bool] # Allow tracking for lead retrieval and other marketing
print_first_datetime: Optional[datetime.datetime] = None
print_last_datetime: Optional[datetime.datetime] = None
print_count: Optional[int]
# full_name_font_size: Optional[str] # Not currently used 2023-01-25
# professional_title_font_size: Optional[str] # Not currently used 2023-01-25
# affiliations_font_size: Optional[str] # Not currently used 2023-01-25
# location_font_size: Optional[str] # Not currently used 2023-01-25
# css: Optional[str] # Not currently used 2023-01-25
cfg_json: Optional[Union[Json, None]] # Store per badge config options like font size; Not currently used 2024-06-11
data_json: Optional[Union[Json, None]] # For key value data. Careful with overwriting existing fields! Not currently used 2024-06-11
default_qry_str: Optional[str] # Default query string used for searching and filtering badges. Updated using SQL triggers and a SQL function
hide: Optional[bool]
priority: Optional[bool]
sort: Optional[int]
group: Optional[str]
enable: Optional[bool]
notes: Optional[str]
created_on: Optional[datetime.datetime] = None
updated_on: Optional[datetime.datetime] = None
# Including other related objects
order: Optional[Union[Order_Base, None]]
ticket_list: Optional[list]
event_badge_template: Optional[Union[Event_Badge_Template_Base, None]]
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
# Fields that are part of the model (for reading) but should not be saved to the DB table
fields_to_exclude_from_db: ClassVar[list] = [
'account_id', 'order', 'ticket_list', 'event_badge_template'
]
class Config:
underscore_attrs_are_private = True
allow_population_by_field_name = True
fields = base_fields
class Event_Badge_Basic_Base(BaseModel):
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
# --- Standardized Vision IDs (Strings for API, Integers for DB) ---
id: Optional[Union[int, str]] = Field(None, **base_fields['event_badge_id_random'])
event_badge_id: Optional[Union[int, str]] = Field(None, **base_fields['event_badge_id_random'])
account_id: Optional[Union[int, str]] = Field(None, **base_fields['account_id_random'])
event_badge_template_id: Optional[Union[int, str]] = Field(None, **base_fields['event_badge_template_id_random'])
event_person_id: Optional[Union[int, str]] = Field(None, **base_fields['event_person_id_random'])
# --- Standardized Legacy / Internal IDs (Excluded) ---
id_random: Optional[str] = Field(None, alias='event_badge_id_random', exclude=True)
account_id_random: Optional[str] = Field(None, exclude=True)
event_badge_template_id_random: Optional[str] = Field(None, exclude=True)
event_person_id_random: Optional[str] = Field(None, exclude=True)
@root_validator(pre=True)
def map_v3_ids(cls, values):
"""
Vision Transformer:
Map DB keys to clean API keys and strip internal integers during READ operations.
During CREATE (POST) operations, we ensure resolved integers are preserved.
"""
# 1. Map Random Strings to Clean Names
rid = values.get('id_random') or values.get('event_badge_id_random')
if rid and isinstance(rid, str):
values['id'] = rid
values['event_badge_id'] = rid
if a_rid := values.get('account_id_random'): values['account_id'] = a_rid
if et_rid := values.get('event_badge_template_id_random'): values['event_badge_template_id'] = et_rid
if ep_rid := values.get('event_person_id_random'): values['event_person_id'] = ep_rid
# 2. Prevent "Collision Population" or leakage of integers during API responses
for k in ['id', 'event_badge_id', 'account_id', 'event_badge_template_id', 'event_person_id']:
val = values.get(k)
if val is not None and not isinstance(val, str):
if values.get(f'{k}_random') or (k=='id' and values.get('id_random')):
del values[k]
return values
external_id: Optional[str] # Generated internally or externally. Needs to be stable. It should not change.
# external_sys_id: Optional[str] # Person ID generated by external system (should be stable and not change)
# external_reg_id: Optional[str] # Registration ID generated by external system (should be stable and not change)
pronouns: Optional[str] # Preferred pronouns
pronouns_override: Optional[str] # Preferred pronouns
informal_name: Optional[str]
title_names: Optional[str] # Title for generation, official position, or professional or academic qualification, other honorific, or other name prefix
given_name: Optional[str]
middle_name: Optional[str]
family_name: Optional[str]
designations: Optional[str] # Temporary or long-term designations related to family, relationships, person differentiation (Junior/Senior), location, social status, professional qualifications, legal status, or other name suffix
professional_title: Optional[str] # Professional title
professional_title_override: Optional[str] # Override professional title
# BEGIN # Auto created name variations
full_name: Optional[str] # title_names given_name middle_name family_name designations
full_name_override: Optional[str] # Override full_name; Actual name shown on badge and other "public" areas
affiliations: Optional[str] # One or more affiliations with organizations, companies, and other groups
affiliations_override: Optional[str] # Override affiliations
email: Optional[str]
email_override: Optional[str]
phone: Optional[str]
phone_override: Optional[str]
# address_line_1: Optional[str]
# address_line_2: Optional[str]
# address_line_3: Optional[str]
# city: Optional[str]
# county: Optional[str] # NOTE: This is for a county within a state or province
# country_subdivision_code: Optional[str]
# state_province: Optional[str]
# state_province_abb: Optional[str]
# postal_code: Optional[str]
country_alpha_2_code: Optional[str]
country: Optional[str]
# full_address: Optional[str]
location: Optional[str] # Actual location name shown on badge and other "public" areas
location_override: Optional[str] # Override location
# location_short: Optional[str] # Auto generated short version
# location_long: Optional[str] # Auto generated long version
# NOTE: More badge fields need to be added here once things are cleaned up
# badge_type_code: Optional[str]
# badge_type: Optional[str]
# member_type_code: Optional[str]
# member_type: Optional[str]
# registration_type_code: Optional[str]
# registration_type: Optional[str]
# other_1: Optional[str]
# other_2: Optional[str]
allow_tracking: Optional[bool] # Allow tracking for lead retrieval and other marketing
# agree_to_tc: Optional[bool] # Agree to terms and conditions
print_first_datetime: Optional[datetime.datetime] = None
print_last_datetime: Optional[datetime.datetime] = None
print_count: Optional[int]
hide: Optional[bool]
priority: Optional[bool]
sort: Optional[int]
group: Optional[str]
enable: Optional[bool]
notes: Optional[str]
created_on: Optional[datetime.datetime] = None
updated_on: Optional[datetime.datetime] = None
# Including other related objects
# order: Optional[Union[Order_Base, None]]
# ticket_list: Optional[list]
event_badge_template: Optional[Union[Event_Badge_Template_Base, None]]
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
# Fields that are part of the model (for reading) but should not be saved to the DB table
fields_to_exclude_from_db: ClassVar[list] = [
'account_id', 'event_badge_template'
]
class Config:
underscore_attrs_are_private = True
allow_population_by_field_name = True
fields = base_fields