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

156 lines
6.6 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_exhibit_tracking_models import Event_Exhibit_Tracking_Base
# ### BEGIN ### API Event Exhibit Models ### Event_Exhibit_Base() ###
class Event_Exhibit_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_exhibit_id_random'])
event_exhibit_id: Optional[Union[int, str]] = Field(**base_fields['event_exhibit_id_random'])
account_id: Optional[Union[int, str]] = Field(**base_fields['account_id_random'])
event_id: Optional[Union[int, str]] = Field(**base_fields['event_id_random'])
organization_id: Optional[Union[int, str]] = Field(**base_fields['organization_id_random'])
contact_id: Optional[Union[int, str]] = Field(**base_fields['contact_id_random'])
person_id: Optional[Union[int, str]] = Field(**base_fields['person_id_random'])
status_id: Optional[Union[int, str]] = Field(**base_fields['status_id_random'])
# --- Standardized Legacy / Internal IDs (Excluded) ---
id_random: Optional[str] = Field(None, alias='event_exhibit_id_random', exclude=True)
account_id_random: Optional[str] = Field(None, exclude=True)
event_id_random: Optional[str] = Field(None, exclude=True)
organization_id_random: Optional[str] = Field(None, exclude=True)
contact_id_random: Optional[str] = Field(None, exclude=True)
person_id_random: Optional[str] = Field(None, exclude=True)
status_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.
Falls back to Redis/DB lookups if random string IDs are missing from the view.
"""
from app.db_sql import get_id_random
# 1. Map Primary Object ID
rid = values.get('id_random') or values.get('event_exhibit_id_random')
if rid and isinstance(rid, str):
values['id'] = rid
values['event_exhibit_id'] = rid
elif values.get('id') and isinstance(values.get('id'), int):
# Fallback for primary ID
resolved_rid = get_id_random(values['id'], 'event_exhibit')
if resolved_rid:
values['id'] = resolved_rid
values['event_exhibit_id'] = resolved_rid
values['id_random'] = resolved_rid
# 2. Map & Resolve Relational IDs
id_map = [
('account_id', 'account'),
('event_id', 'event'),
('organization_id', 'organization'),
('contact_id', 'contact'),
('person_id', 'person'),
('status_id', 'status'),
]
for field, table in id_map:
r_val = values.get(f'{field}_random')
if r_val and isinstance(r_val, str):
values[field] = r_val
elif values.get(field) and isinstance(values[field], int):
# Fallback: Resolve from Redis/DB if missing from view result
resolved_rid = get_id_random(values[field], table)
if resolved_rid:
values[field] = resolved_rid
values[f'{field}_random'] = resolved_rid
# 3. Final Vision Enforcement: Strip internal integers
for k in ['id', 'event_exhibit_id', 'account_id', 'event_id', 'organization_id', 'contact_id', 'person_id', 'status_id']:
val = values.get(k)
if val is not None and not isinstance(val, str):
values[k] = None
return values
code: Optional[str] # The assigned booth number or ID
staff_passcode: Optional[str]
name: Optional[str]
tagline: Optional[str]
description: Optional[str]
logo_path: Optional[str]
thumbnail_path: Optional[str]
banner_image_path: Optional[str]
banner_bg_color: Optional[str]
background_image_path: Optional[str]
background_bg_color: Optional[str]
embed_html: Optional[str]
staff_limit: Optional[int]
hosted_file_limit: Optional[int]
leads_api_access: Optional[bool] # NOTE: API access and use
# Json type NOTE: All keys and values must be double quoted!
# {"field_name": "The field value"}
leads_custom_questions_json: Optional[Json] # NOTE: For now just using string instead of Json Pydantic data type. When getting the dict version for SQL this should be a string.
leads_device_sm_qty: Optional[int] # NOTE: Cell phone sized devices rented by exhibitor. Should this be a separate linked table (event_device)?
leads_device_lg_qty: Optional[int] # NOTE: Tablet (8 or 9 inch) sized devices rented by exhibitor. Should this be a separate linked table (event_device)?
data_json: Optional[Union[Json, None]]
license_max: Optional[int]
license_li_json: Optional[Union[Json, None]]
cfg_json: Optional[Union[Json, None]]
enable_organization_name_change: Optional[bool]
enable_name_change: Optional[bool]
enable_banner_image: Optional[bool]
enable_background_image: Optional[bool]
enable_embedded_content: Optional[bool]
enable_staff_photo: Optional[bool]
# access_key: Optional[str] # Maybe use in the future?
enable: Optional[bool]
# enable_from: Optional[datetime.datetime] = None # Maybe use in the future?
# enable_to: Optional[datetime.datetime] = None # Maybe use in the future?
hide: Optional[bool]
priority: Optional[bool]
sort: Optional[int]
group: Optional[str]
notes: Optional[str]
created_on: Optional[datetime.datetime] = None
updated_on: Optional[datetime.datetime] = None
# Including other related objects
event_exhibit_tracking_list: Optional[list[Event_Exhibit_Tracking_Base]]
_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] = [
'event_exhibit_tracking_list'
]
class Config:
underscore_attrs_are_private = True
allow_population_by_field_name = True
fields = base_fields
# ### END ### API Event Exhibit Models ### Event_Exhibit_Base() ###