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.
This commit is contained in:
Scott Idem
2026-02-24 16:21:27 -05:00
parent 9d89d4c8e4
commit 0f4b4d2f51
22 changed files with 611 additions and 585 deletions

View File

@@ -1,7 +1,7 @@
import datetime, pytz
from typing import Dict, List, Optional, Set, Union
from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator
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
@@ -17,28 +17,23 @@ class Event_Person_Profile_Base(BaseModel):
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
id_random: Optional[str] = Field(
**base_fields['event_person_profile_id_random'],
alias = 'event_person_profile_id_random',
)
id: Optional[int] = Field(
alias = 'event_person_profile_id'
)
# --- Standardized Vision IDs (Strings) ---
id: Optional[str] = Field(None, **base_fields['event_person_profile_id_random'])
event_person_profile_id: Optional[str] = Field(None, **base_fields['event_person_profile_id_random'])
account_id_random: Optional[str]
account_id: Optional[int]
account_id: Optional[str] = Field(None, **base_fields['account_id_random'])
contact_id: Optional[str] = Field(None, **base_fields['contact_id_random'])
event_id: Optional[str] = Field(None, **base_fields['event_id_random'])
event_person_id: Optional[str] = Field(None, **base_fields['event_person_id_random'])
organization_id: Optional[str] = Field(None, **base_fields['organization_id_random'])
contact_id_random: Optional[str]
contact_id: Optional[int]
event_id_random: Optional[str] # Only in view
event_id: Optional[int] # Only in view
event_person_id_random: Optional[str] # Only in view
event_person_id: Optional[int] # Only in view
organization_id_random: Optional[str]
organization_id: Optional[int]
# --- Standardized Legacy / Internal IDs (Excluded) ---
id_random: Optional[str] = Field(None, alias='event_person_profile_id_random', exclude=True)
account_id_random: Optional[str] = Field(None, exclude=True)
contact_id_random: Optional[str] = Field(None, exclude=True)
event_id_random: Optional[str] = Field(None, exclude=True)
event_person_id_random: Optional[str] = Field(None, exclude=True)
organization_id_random: Optional[str] = Field(None, exclude=True)
pronouns: Optional[str] # Preferred pronouns
informal_name: Optional[str]
@@ -65,21 +60,18 @@ class Event_Person_Profile_Base(BaseModel):
email: Optional[str]
website_url: Optional[str]
thumbnail_hosted_file_id: Optional[int]
thumbnail_hosted_file_id_random: Optional[str]
thumbnail_hosted_file_id: Optional[str] = Field(None, **base_fields['hosted_file_id_random'])
thumbnail_path: Optional[str]
thumbnail_bg_color: Optional[str]
# photo_path: Optional[str]
# photo_bg_color: Optional[str]
picture_hosted_file_id: Optional[int]
picture_hosted_file_id_random: Optional[str]
picture_hosted_file_id: Optional[str] = Field(None, **base_fields['hosted_file_id_random'])
picture_path: Optional[str]
picture_bg_color: Optional[str]
about_hosted_file_id: Optional[int]
about_hosted_file_id_random: Optional[str]
about_hosted_file_id: Optional[str] = Field(None, **base_fields['hosted_file_id_random'])
about_path: Optional[str]
email_allowed: Optional[bool]
@@ -110,66 +102,37 @@ class Event_Person_Profile_Base(BaseModel):
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
#@validator('event_person_profile_id_random', always=True)
def event_person_profile_id_random_copy(cls, v, values, **kwargs):
if values['id_random']:
return values['id_random']
return None
@root_validator(pre=True)
def map_v3_ids(cls, values):
"""
Vision Transformer:
Map DB keys to clean API keys and strip internal integers.
"""
# 1. Map Random Strings to Clean Names
if rid := values.get('id_random') or values.get('event_person_profile_id_random'):
values['id'] = rid
values['event_person_profile_id'] = rid
if a_rid := values.get('account_id_random'): values['account_id'] = a_rid
if c_rid := values.get('contact_id_random'): values['contact_id'] = c_rid
if e_rid := values.get('event_id_random'): values['event_id'] = e_rid
if ep_rid := values.get('event_person_id_random'): values['event_person_id'] = ep_rid
if o_rid := values.get('organization_id_random'): values['organization_id'] = o_rid
# 2. Prevent "Collision Population"
for k in ['id', 'event_person_profile_id', 'account_id', 'contact_id', 'event_id', 'event_person_id', 'organization_id']:
if k in values and not isinstance(values[k], str) and values[k] is not None:
del values[k]
return values
@validator('id', always=True)
def event_person_profile_id_lookup(cls, v, values, **kwargs):
if isinstance(v, int) and v > 0: return v
elif id_random := values.get('id_random'):
return redis_lookup_id_random(record_id_random=id_random, table_name='event_person_profile')
return None
@validator('account_id', always=True)
def account_id_lookup(cls, v, values, **kwargs):
if isinstance(v, int) and v > 0: return v
elif id_random := values.get('account_id_random'):
return redis_lookup_id_random(record_id_random=id_random, table_name='account')
return None
@validator('contact_id', always=True)
def contact_id_lookup(cls, v, values, **kwargs):
if isinstance(v, int) and v > 0: return v
elif id_random := values.get('contact_id_random'):
return redis_lookup_id_random(record_id_random=id_random, table_name='contact')
return None
@validator('organization_id', always=True)
def organization_id_lookup(cls, v, values, **kwargs):
if isinstance(v, int) and v > 0: return v
elif id_random := values.get('organization_id_random'):
return redis_lookup_id_random(record_id_random=id_random, table_name='organization')
return None
@validator('thumbnail_hosted_file_id', always=True)
def thumbnail_hosted_file_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values.get('thumbnail_hosted_file_id_random', None):
return redis_lookup_id_random(record_id_random=values['thumbnail_hosted_file_id_random'], table_name='thumbnail_hosted_file')
return None
@validator('picture_hosted_file_id', always=True)
def picture_hosted_file_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values.get('picture_hosted_file_id_random', None):
return redis_lookup_id_random(record_id_random=values['picture_hosted_file_id_random'], table_name='picture_hosted_file')
return None
@validator('about_hosted_file_id', always=True)
def about_hosted_file_id_lookup(cls, v, values, **kwargs):
log.setLevel(logging.WARNING)
log.debug(locals())
if values.get('about_hosted_file_id_random', None):
return redis_lookup_id_random(record_id_random=values['about_hosted_file_id_random'], table_name='about_hosted_file')
return None
# 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_id', 'event_person_id',
'full_name', 'full_name_override', 'display_name',
'thumbnail_path', 'picture_path', 'about_path',
'contact', 'event_cfg', 'organization'
]
class Config:
underscore_attrs_are_private = True