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:
@@ -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
|
||||
@@ -19,45 +19,32 @@ class Event_Presenter_Base(BaseModel):
|
||||
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
id_random: Optional[str] = Field(
|
||||
# **base_fields['event_presenter_id_random'],
|
||||
alias = 'event_presenter_id_random',
|
||||
)
|
||||
id: Optional[int] = Field(
|
||||
alias = 'event_presenter_id'
|
||||
)
|
||||
# --- Standardized Vision IDs (Strings) ---
|
||||
id: Optional[str] = Field(None, **base_fields['event_presenter_id_random'])
|
||||
event_presenter_id: Optional[str] = Field(None, **base_fields['event_presenter_id_random'])
|
||||
|
||||
account_id_random: Optional[str]
|
||||
account_id: Optional[int]
|
||||
account_id: Optional[str] = Field(None, **base_fields['account_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'])
|
||||
event_presentation_id: Optional[str] = Field(None, **base_fields['event_presentation_id_random'])
|
||||
event_session_id: Optional[str] = Field(None, **base_fields['event_session_id_random'])
|
||||
event_track_id: Optional[str] = Field(None, **base_fields['event_track_id_random'])
|
||||
person_id: Optional[str] = Field(None, **base_fields['person_id_random'])
|
||||
|
||||
# --- Standardized Legacy / Internal IDs (Excluded) ---
|
||||
id_random: Optional[str] = Field(None, alias='event_presenter_id_random', exclude=True)
|
||||
account_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)
|
||||
event_presentation_id_random: Optional[str] = Field(None, exclude=True)
|
||||
event_session_id_random: Optional[str] = Field(None, exclude=True)
|
||||
event_track_id_random: Optional[str] = Field(None, exclude=True)
|
||||
person_id_random: Optional[str] = Field(None, exclude=True)
|
||||
|
||||
external_id: Optional[str]
|
||||
|
||||
code: Optional[str]
|
||||
|
||||
event_id_random: Optional[str]
|
||||
event_id: Optional[int]
|
||||
|
||||
# event_abstract_id_random: Optional[str]
|
||||
# event_abstract_id: Optional[int]
|
||||
|
||||
event_location_id_random: Optional[str]
|
||||
event_location_id: Optional[int]
|
||||
|
||||
event_person_id_random: Optional[str]
|
||||
event_person_id: Optional[int]
|
||||
|
||||
event_presentation_id_random: Optional[str]
|
||||
event_presentation_id: Optional[int]
|
||||
|
||||
event_session_id_random: Optional[str]
|
||||
event_session_id: Optional[int]
|
||||
|
||||
event_track_id_random: Optional[str]
|
||||
event_track_id: Optional[int]
|
||||
|
||||
person_id_random: Optional[str]
|
||||
person_id: Optional[int]
|
||||
|
||||
for_type: Optional[str]
|
||||
for_id: Optional[int]
|
||||
|
||||
@@ -193,58 +180,52 @@ class Event_Presenter_Base(BaseModel):
|
||||
|
||||
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
|
||||
|
||||
@validator('id', always=True)
|
||||
def event_presenter_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_presenter')
|
||||
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_presenter_id_random'):
|
||||
values['id'] = rid
|
||||
values['event_presenter_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 ep_rid := values.get('event_person_id_random'): values['event_person_id'] = ep_rid
|
||||
if epr_rid := values.get('event_presentation_id_random'): values['event_presentation_id'] = epr_rid
|
||||
if es_rid := values.get('event_session_id_random'): values['event_session_id'] = es_rid
|
||||
if et_rid := values.get('event_track_id_random'): values['event_track_id'] = et_rid
|
||||
if p_rid := values.get('person_id_random'): values['person_id'] = p_rid
|
||||
|
||||
# 2. Prevent "Collision Population"
|
||||
for k in ['id', 'event_presenter_id', 'account_id', 'event_id', 'event_person_id', 'event_presentation_id', 'event_session_id', 'event_track_id', 'person_id']:
|
||||
if k in values and not isinstance(values[k], str) and values[k] is not None:
|
||||
del values[k]
|
||||
|
||||
return values
|
||||
|
||||
@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('event_id', always=True)
|
||||
def event_id_lookup(cls, v, values, **kwargs):
|
||||
if isinstance(v, int) and v > 0: return v
|
||||
elif id_random := values.get('event_id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='event')
|
||||
return None
|
||||
|
||||
@validator('event_person_id', always=True)
|
||||
def event_person_id_lookup(cls, v, values, **kwargs):
|
||||
if isinstance(v, int) and v > 0: return v
|
||||
elif id_random := values.get('event_person_id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='event_person')
|
||||
return None
|
||||
|
||||
@validator('event_presentation_id', always=True)
|
||||
def event_presentation_id_lookup(cls, v, values, **kwargs):
|
||||
if isinstance(v, int) and v > 0: return v
|
||||
elif id_random := values.get('event_presentation_id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='event_presentation')
|
||||
return None
|
||||
|
||||
@validator('event_session_id', always=True)
|
||||
def event_session_id_lookup(cls, v, values, **kwargs):
|
||||
if isinstance(v, int) and v > 0: return v
|
||||
elif id_random := values.get('event_session_id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='event_session')
|
||||
return None
|
||||
|
||||
@validator('person_id', always=True)
|
||||
def person_id_lookup(cls, v, values, **kwargs):
|
||||
if isinstance(v, int) and v > 0: return v
|
||||
elif id_random := values.get('person_id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='person')
|
||||
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', 'file_count', 'event_file_id_li_json',
|
||||
'event_name', 'event_start_datetime', 'event_end_datetime',
|
||||
'event_location_code', 'event_location_name',
|
||||
'event_presentation_code', 'event_presentation_type_code', 'event_presentation_name',
|
||||
'event_presentation_start_datetime', 'event_presentation_end_datetime',
|
||||
'event_session_code', 'event_session_type_code', 'event_session_name',
|
||||
'event_session_start_datetime', 'event_session_end_datetime',
|
||||
'event_track_code', 'event_track_name',
|
||||
'person_external_id', 'person_external_sys_id', 'person_given_name',
|
||||
'person_family_name', 'person_professional_title', 'person_full_name',
|
||||
'person_affiliations', 'person_primary_email', 'person_passcode',
|
||||
'event_abstract', 'event_abstract_list', 'event_cfg', 'event_file_list',
|
||||
'event_person', 'event_presentation', 'event_session'
|
||||
]
|
||||
|
||||
class Config:
|
||||
underscore_attrs_are_private = True
|
||||
allow_population_by_field_name = True
|
||||
allow_population_by_field_name = False
|
||||
fields = base_fields
|
||||
# ### END ### API Event Presenter Models ### Event_Presenter_Base() ###
|
||||
|
||||
@@ -255,48 +236,28 @@ class Event_Presenter_Out_Base(BaseModel):
|
||||
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
id_random: Optional[str] = Field(
|
||||
**base_fields['event_presenter_id_random'],
|
||||
alias = 'event_presenter_id_random',
|
||||
)
|
||||
id: Optional[int] = Field(
|
||||
alias = 'event_presenter_id'
|
||||
)
|
||||
# --- Standardized Vision IDs (Strings) ---
|
||||
id: Optional[str] = Field(None, **base_fields['event_presenter_id_random'])
|
||||
event_presenter_id: Optional[str] = Field(None, **base_fields['event_presenter_id_random'])
|
||||
|
||||
account_id_random: Optional[str]
|
||||
account_id: Optional[int]
|
||||
account_id: Optional[str] = Field(None, **base_fields['account_id_random'])
|
||||
event_id: Optional[str] = Field(None, **base_fields['event_id_random'])
|
||||
person_id: Optional[str] = Field(None, **base_fields['person_id_random'])
|
||||
event_presentation_id: Optional[str] = Field(None, **base_fields['event_presentation_id_random'])
|
||||
event_session_id: Optional[str] = Field(None, **base_fields['event_session_id_random'])
|
||||
|
||||
# --- Standardized Legacy / Internal IDs (Excluded) ---
|
||||
id_random: Optional[str] = Field(None, alias='event_presenter_id_random', exclude=True)
|
||||
account_id_random: Optional[str] = Field(None, exclude=True)
|
||||
event_id_random: Optional[str] = Field(None, exclude=True)
|
||||
person_id_random: Optional[str] = Field(None, exclude=True)
|
||||
event_presentation_id_random: Optional[str] = Field(None, exclude=True)
|
||||
event_session_id_random: Optional[str] = Field(None, exclude=True)
|
||||
|
||||
external_id: Optional[str]
|
||||
|
||||
code: Optional[str]
|
||||
|
||||
event_id_random: Optional[str]
|
||||
event_id: Optional[int]
|
||||
|
||||
# event_abstract_id_random: Optional[str]
|
||||
# event_abstract_id: Optional[int]
|
||||
|
||||
# event_location_id_random: Optional[str]
|
||||
# event_location_id: Optional[int]
|
||||
|
||||
# event_person_id_random: Optional[str]
|
||||
# event_person_id: Optional[int]
|
||||
|
||||
event_presentation_id_random: Optional[str]
|
||||
event_presentation_id: Optional[int]
|
||||
|
||||
event_session_id_random: Optional[str]
|
||||
event_session_id: Optional[int]
|
||||
|
||||
# event_track_id_random: Optional[str]
|
||||
# event_track_id: Optional[int]
|
||||
|
||||
person_id_random: Optional[str]
|
||||
person_id: Optional[int]
|
||||
|
||||
# for_type: Optional[str]
|
||||
# for_id: Optional[int]
|
||||
|
||||
pronouns: Optional[str] # Preferred pronouns
|
||||
informal_name: Optional[str] # Informal or nick name they commonly go by
|
||||
|
||||
@@ -311,38 +272,24 @@ class Event_Presenter_Out_Base(BaseModel):
|
||||
professional_title: Optional[str] # Professional title
|
||||
# title: Optional[str] # NOTE: Phasing out! Use *professional_title* instead.
|
||||
|
||||
# display_name: Optional[str] # NOTE: This will be changed to full_name_override to match event_badge, event_person_profile, and person
|
||||
|
||||
# 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 for presenter
|
||||
|
||||
# degree: Optional[str] # NOTE: Phasing out! Use *designations* instead.
|
||||
# degrees: Optional[str] # NOTE: Phasing out! Use *designations* instead.
|
||||
# credentials: Optional[str] # NOTE: Phasing out! Use *designations* instead.
|
||||
|
||||
affiliations: Optional[str] # One or more affiliations with organizations, companies, and other groups
|
||||
# affiliation: Optional[str] # NOTE: Phasing out! Use *affiliations* instead.
|
||||
|
||||
email: Optional[str]
|
||||
website_url: Optional[str]
|
||||
|
||||
# phone_li_json: Optional[Union[Json, None]]
|
||||
|
||||
# For social media in a JSON object format. The Aether standard field names should be used. Examples: url, url_text, icon, etc.
|
||||
social_li_json: Optional[Union[Json, None]]
|
||||
|
||||
tagline: Optional[str]
|
||||
biography: Optional[str]
|
||||
|
||||
# picture_path: Optional[str] # Start using image_li_json instead
|
||||
# picture_bg_color: Optional[str]
|
||||
|
||||
# For image files only in a JSON object format. The Aether standard field names should be used. Examples: url, url_text, alt_text, width, height, size (in bytes), etc.
|
||||
image_li_json: Optional[Union[Json, None]] # "headshot" is probably the most common
|
||||
# media_li_json: Optional[Union[Json, None]]
|
||||
|
||||
# role: Optional[str]
|
||||
|
||||
data_json: Optional[Union[Json, None]] # For key value data. Careful with overwriting existing fields!
|
||||
cfg_json: Optional[Union[Json, None]] # Store per presenter config options like theme, language, etc
|
||||
@@ -356,13 +303,8 @@ class Event_Presenter_Out_Base(BaseModel):
|
||||
comments: Optional[str]
|
||||
|
||||
enable: Optional[bool]
|
||||
# enable_from: Optional[datetime.datetime] = None
|
||||
# enable_to: Optional[datetime.datetime] = None
|
||||
|
||||
hide: Optional[bool]
|
||||
# public: Optional[bool]
|
||||
# public_hide: Optional[bool]
|
||||
# hide_event_launcher: Optional[bool]
|
||||
|
||||
priority: Optional[bool]
|
||||
sort: Optional[int] # The presenter number if given
|
||||
@@ -372,24 +314,6 @@ class Event_Presenter_Out_Base(BaseModel):
|
||||
created_on: Optional[datetime.datetime] = None
|
||||
updated_on: Optional[datetime.datetime] = None
|
||||
|
||||
# Including convenience data
|
||||
# This is only for convenience. Probably going to keep unless it causes a problem.
|
||||
# event_name: Optional[str]
|
||||
# event_start_datetime: Optional[datetime.datetime]
|
||||
# event_end_datetime: Optional[datetime.datetime]
|
||||
# event_location_code: Optional[str]
|
||||
# event_location_name: Optional[str]
|
||||
# event_presentation_code: Optional[str]
|
||||
# event_presentation_type_code: Optional[str]
|
||||
# event_presentation_name: Optional[str]
|
||||
# event_presentation_start_datetime: Optional[datetime.datetime]
|
||||
# event_presentation_end_datetime: Optional[datetime.datetime]
|
||||
# event_session_code: Optional[str]
|
||||
# event_session_type_code: Optional[str]
|
||||
# event_session_name: Optional[str]
|
||||
# event_session_start_datetime: Optional[datetime.datetime]
|
||||
# event_session_end_datetime: Optional[datetime.datetime]
|
||||
|
||||
person_external_id: Optional[str]
|
||||
person_external_sys_id: Optional[str]
|
||||
person_given_name: Optional[str]
|
||||
@@ -404,50 +328,32 @@ class Event_Presenter_Out_Base(BaseModel):
|
||||
|
||||
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
|
||||
|
||||
@validator('id', always=True)
|
||||
def event_presenter_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_presenter')
|
||||
return None
|
||||
|
||||
@validator('event_id', always=True)
|
||||
def event_id_lookup(cls, v, values, **kwargs):
|
||||
if isinstance(v, int) and v > 0: return v
|
||||
elif id_random := values.get('event_id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='event')
|
||||
return None
|
||||
|
||||
# @validator('event_person_id', always=True)
|
||||
# def event_person_id_lookup(cls, v, values, **kwargs):
|
||||
# if isinstance(v, int) and v > 0: return v
|
||||
# elif id_random := values.get('event_person_id_random'):
|
||||
# return redis_lookup_id_random(record_id_random=id_random, table_name='event_person')
|
||||
# return None
|
||||
|
||||
@validator('event_presentation_id', always=True)
|
||||
def event_presentation_id_lookup(cls, v, values, **kwargs):
|
||||
if isinstance(v, int) and v > 0: return v
|
||||
elif id_random := values.get('event_presentation_id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='event_presentation')
|
||||
return None
|
||||
|
||||
@validator('event_session_id', always=True)
|
||||
def event_session_id_lookup(cls, v, values, **kwargs):
|
||||
if isinstance(v, int) and v > 0: return v
|
||||
elif id_random := values.get('event_session_id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='event_session')
|
||||
return None
|
||||
|
||||
@validator('person_id', always=True)
|
||||
def person_id_lookup(cls, v, values, **kwargs):
|
||||
if isinstance(v, int) and v > 0: return v
|
||||
elif id_random := values.get('person_id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='person')
|
||||
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_presenter_id_random'):
|
||||
values['id'] = rid
|
||||
values['event_presenter_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 epr_rid := values.get('event_presentation_id_random'): values['event_presentation_id'] = epr_rid
|
||||
if es_rid := values.get('event_session_id_random'): values['event_session_id'] = es_rid
|
||||
if p_rid := values.get('person_id_random'): values['person_id'] = p_rid
|
||||
|
||||
# 2. Prevent "Collision Population"
|
||||
for k in ['id', 'event_presenter_id', 'account_id', 'event_id', 'event_presentation_id', 'event_session_id', 'person_id']:
|
||||
if k in values and not isinstance(values[k], str) and values[k] is not None:
|
||||
del values[k]
|
||||
|
||||
return values
|
||||
|
||||
class Config:
|
||||
underscore_attrs_are_private = True
|
||||
allow_population_by_field_name = True
|
||||
allow_population_by_field_name = False
|
||||
fields = base_fields
|
||||
# ### END ### API Event Presenter Models ### Event_Presenter_Base() ###
|
||||
|
||||
Reference in New Issue
Block a user