Files
OSIT-AE-API-FastAPI/app/models/event_presenter_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

360 lines
17 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_abstract_models import Event_Abstract_Base
from app.models.event_cfg_models import Event_Cfg_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_session_models import Event_Session_Base
# ### BEGIN ### API Event Presenter Models ### Event_Presenter_Base() ###
class Event_Presenter_Base(BaseModel):
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
# --- 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: 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]
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
title_names: Optional[str] # Title for generation, official position, or professional or academic qualification, other honorific, or other name prefix
# prefix: Optional[str] # NOTE: Phasing out! Use *title_names* instead.
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
suffix: Optional[str] # NOTE: Phasing out! Use *designations* instead.
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]
passcode: Optional[str]
cfg_json: Optional[Union[Json, None]] # Store per presenter config options like theme, language, etc
data_json: Optional[Union[Json, None]] # For key value data. Careful with overwriting existing fields!
file_count: Optional[int] # File count for the presenter
event_file_id_li_json: Optional[Union[Json, None]] # List of file IDs (actually id_random)
# General catchall for agreement or consent
agree: Optional[bool]
# Comments from the presenter. This is for internal use only.
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
group: Optional[str]
notes: Optional[str]
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]
event_track_code: Optional[str]
event_track_name: Optional[str]
person_external_id: Optional[str]
person_external_sys_id: Optional[str]
person_given_name: Optional[str]
person_family_name: Optional[str]
person_professional_title: Optional[str]
person_full_name: Optional[str]
person_affiliations: Optional[str]
person_primary_email: Optional[str]
person_passcode: Optional[str]
# Including other related objects
# event: Optional[Event_Base]
# event_abstract: Optional[Event_Abstract_Base]
event_abstract: Optional[dict]
event_abstract_list: Optional[list] # Optional[Event_Abstract_Base] Is more than one abstract allowed per presenter?
event_cfg: Optional[Event_Cfg_Base]
# event_device_list: Optional[list] # Optional[Event_Device_Base]
event_file_list: Optional[list] # Optional[Event_File_Base]
# event_location: Optional[Event_Location_Base]
event_person: Optional[Event_Person_Base]
if __name__ == '__main__':
from app.models.event_presentation_models import Event_Presentation_Base
event_presentation: Optional[Event_Presentation_Base]
else:
event_presentation: Optional[dict]
# event_presentation: Optional[dict]
# event_session: Optional[Event_Session_Base]
event_session: Optional[dict]
# event_track: Optional[Event_Track_Base]
# person: Optional[Person_Base] # This is under event_person
# user: Optional[User_Base] # This is under event_person
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
@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
# 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 = False
fields = base_fields
# ### END ### API Event Presenter Models ### Event_Presenter_Base() ###
# ### BEGIN ### API Event Presenter Models ### Event_Presenter_Base() ###
class Event_Presenter_Out_Base(BaseModel):
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
log.debug(locals())
# --- 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: 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]
pronouns: Optional[str] # Preferred pronouns
informal_name: Optional[str] # Informal or nick name they commonly go by
title_names: Optional[str] # Title for generation, official position, or professional or academic qualification, other honorific, or other name prefix
# prefix: Optional[str] # NOTE: Phasing out! Use *title_names* instead.
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
# suffix: Optional[str] # NOTE: Phasing out! Use *designations* instead.
professional_title: Optional[str] # 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 for presenter
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]
# 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]
# 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
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
file_count: Optional[int]
# General catchall for agreement or consent
agree: Optional[bool]
# Comments from the presenter. This is for internal use only.
comments: Optional[str]
enable: Optional[bool]
hide: Optional[bool]
priority: Optional[bool]
sort: Optional[int] # The presenter number if given
group: Optional[str]
notes: Optional[str]
created_on: Optional[datetime.datetime] = None
updated_on: Optional[datetime.datetime] = None
person_external_id: Optional[str]
person_external_sys_id: Optional[str]
person_given_name: Optional[str]
person_family_name: Optional[str]
person_professional_title: Optional[str]
person_full_name: Optional[str]
person_affiliations: Optional[str]
person_primary_email: Optional[str]
person_passcode: Optional[str]
# Including other related objects
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
@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 = False
fields = base_fields
# ### END ### API Event Presenter Models ### Event_Presenter_Base() ###