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.
230 lines
10 KiB
Python
230 lines
10 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 get_id_random, redis_lookup_id_random
|
|
# from app.lib_general import log, logging
|
|
from app.log import log, logging, logger_reset
|
|
|
|
from app.models.common_field_schema import base_fields, default_num_bytes
|
|
from app.models.hosted_file_models import Hosted_File_Base
|
|
|
|
|
|
# ### BEGIN ### API Event File Models ### Event_File_Base() ###
|
|
class Event_File_Base(BaseModel):
|
|
log.setLevel(logging.INFO) # 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_file_id_random'])
|
|
event_file_id: Optional[Union[int, str]] = Field(**base_fields['event_file_id_random'])
|
|
account_id: Optional[Union[int, str]] = Field(None, **base_fields['account_id_random'])
|
|
hosted_file_id: Optional[Union[int, str]] = Field(**base_fields['hosted_file_id_random'])
|
|
|
|
# Generic Relational target
|
|
for_id: Optional[Union[int, str]] = Field(**base_fields['obj_id_random'])
|
|
|
|
event_id: Optional[Union[int, str]] = Field(**base_fields['event_id_random'])
|
|
event_exhibit_id: Optional[Union[int, str]] = Field(**base_fields['event_exhibit_id_random'])
|
|
event_location_id: Optional[Union[int, str]] = Field(**base_fields['event_location_id_random'])
|
|
event_presentation_id: Optional[Union[int, str]] = Field(**base_fields['event_presentation_id_random'])
|
|
event_presenter_id: Optional[Union[int, str]] = Field(**base_fields['event_presenter_id_random'])
|
|
event_session_id: Optional[Union[int, str]] = Field(**base_fields['event_session_id_random'])
|
|
event_track_id: Optional[Union[int, str]] = Field(**base_fields['event_track_id_random'])
|
|
|
|
# --- Standardized Legacy / Internal IDs (Excluded) ---
|
|
id_random: Optional[str] = Field(None, alias='event_file_id_random', exclude=True)
|
|
account_id_random: Optional[str] = Field(None, exclude=True)
|
|
hosted_file_id_random: Optional[str] = Field(None, exclude=True)
|
|
for_id_random: Optional[str] = Field(None, exclude=True)
|
|
event_id_random: Optional[str] = Field(None, exclude=True)
|
|
event_exhibit_id_random: Optional[str] = Field(None, exclude=True)
|
|
event_location_id_random: Optional[str] = Field(None, exclude=True)
|
|
event_presentation_id_random: Optional[str] = Field(None, exclude=True)
|
|
event_presenter_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)
|
|
|
|
# Internal flag to signal the model to load nested hosted_file
|
|
inc_hosted_file: Optional[bool] = Field(False, 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.
|
|
"""
|
|
# 1. Map Primary Object ID
|
|
rid = values.get('id_random') or values.get('event_file_id_random')
|
|
if rid and isinstance(rid, str):
|
|
values['id'] = rid
|
|
values['event_file_id'] = rid
|
|
|
|
# 2. Map & Resolve Relational IDs
|
|
# (Field Name, Table Name)
|
|
id_map = [
|
|
('account_id', 'account'),
|
|
('hosted_file_id', 'hosted_file'),
|
|
('event_id', 'event'),
|
|
('event_exhibit_id', 'event_exhibit'),
|
|
('event_location_id', 'event_location'),
|
|
('event_presentation_id', 'event_presentation'),
|
|
('event_presenter_id', 'event_presenter'),
|
|
('event_session_id', 'event_session'),
|
|
('event_track_id', 'event_track'),
|
|
]
|
|
|
|
# 2a. Handle specific relational fields
|
|
for field, table in id_map:
|
|
# Check for existing random string version
|
|
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
|
|
|
|
# 2b. Handle Polymorphic for_id
|
|
if f_rid := values.get('for_id_random'):
|
|
values['for_id'] = f_rid
|
|
elif values.get('for_id') and isinstance(values.get('for_id'), int) and values.get('for_type'):
|
|
# Resolve based on the for_type
|
|
resolved_for_rid = get_id_random(values['for_id'], values['for_type'])
|
|
if resolved_for_rid:
|
|
values['for_id'] = resolved_for_rid
|
|
values['for_id_random'] = resolved_for_rid
|
|
|
|
# 3. Final Vision Enforcement: Strip internal integers
|
|
id_fields = [f for f, t in id_map] + ['id', 'event_file_id', 'for_id']
|
|
for k in id_fields:
|
|
val = values.get(k)
|
|
if val is not None and not isinstance(val, str):
|
|
values[k] = None
|
|
|
|
# 4. Conditionally load nested 'hosted_file' object
|
|
if values.get('inc_hosted_file') and values.get('hosted_file_id'):
|
|
from app.methods.hosted_file_methods import load_hosted_file_obj
|
|
if hosted_file_obj := load_hosted_file_obj(hosted_file_id=values['hosted_file_id']):
|
|
values['hosted_file'] = hosted_file_obj
|
|
|
|
# Clean up internal inc_hosted_file flag after processing
|
|
if 'inc_hosted_file' in values:
|
|
del values['inc_hosted_file']
|
|
|
|
return values
|
|
|
|
|
|
for_type: Optional[str]
|
|
|
|
filename: Optional[str]
|
|
filename_no_ext: Optional[str] # Currently created with a view
|
|
filename_w_ext: Optional[str] # Currently created with a view
|
|
extension: Optional[str]
|
|
title: Optional[str]
|
|
description: Optional[str]
|
|
|
|
lu_file_purpose_id: Optional[int] = Field(None, exclude=True)
|
|
file_purpose: Optional[str]
|
|
|
|
# New internal use fields to help with logistics and planning 2022-09-15
|
|
internal_use: Optional[bool] # Will hide from moderators, presenters, non support people, etc
|
|
|
|
#### NOTE ***** NOTE
|
|
open_in_os: Optional[str]
|
|
public_use: Optional[bool] = False
|
|
public: Optional[bool]
|
|
approve: Optional[bool]
|
|
#### open_in_os needs to be set
|
|
#### Are other important fields missing????
|
|
publish_optout: Optional[bool]
|
|
#### NOTE ^^^^^ NOTE
|
|
|
|
enable: Optional[bool]
|
|
enable_from: Optional[datetime.datetime] = None
|
|
enable_to: Optional[datetime.datetime] = None
|
|
|
|
hide: Optional[bool]
|
|
priority: Optional[bool]
|
|
sort: Optional[int]
|
|
group: Optional[str] # Same or similar as file_purpose?
|
|
|
|
notes: Optional[str]
|
|
created_on: Optional[datetime.datetime] = None
|
|
updated_on: Optional[datetime.datetime] = None
|
|
|
|
# Including convenience data for Hosted Files (top-level properties)
|
|
# These fields provide direct access to frequently needed properties from the associated
|
|
# hosted file, effectively flattening some aspects of the nested 'hosted_file' object.
|
|
#
|
|
# IMPORTANT: These fields are designed to be populated directly from the SQL View
|
|
# (e.g., `v_event_file_simple`) via JOINs. They should **NOT** have Pydantic `alias`
|
|
# definitions here if the view provides them with matching names (e.g., `hosted_file_hash_sha256`).
|
|
# Pydantic's default mapping will handle them directly from the incoming data dictionary
|
|
# (the `sql_result` in `api_crud_v3.py`).
|
|
# The `root_validator` does **NOT** populate these top-level fields; its role is
|
|
# solely to conditionally load the *nested* `hosted_file` object.
|
|
hosted_file_hash_sha256: Optional[str]
|
|
hosted_file_subdirectory_path: Optional[str]
|
|
hosted_file_content_type: Optional[str]
|
|
hosted_file_size: Optional[str]
|
|
|
|
lu_event_file_purpose_name: Optional[str] = Field(
|
|
alias = 'file_purpose_name'
|
|
)
|
|
|
|
event_name: Optional[str]
|
|
event_code: 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_presenter_code: Optional[str]
|
|
event_presenter_given_name: Optional[str]
|
|
event_presenter_family_name: Optional[str]
|
|
event_presenter_full_name: Optional[str]
|
|
event_presenter_email: Optional[str]
|
|
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]
|
|
|
|
# Including other related objects
|
|
hosted_file: Optional[Union[Hosted_File_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', 'filename_no_ext', 'filename_w_ext',
|
|
'hosted_file_hash_sha256', 'hosted_file_subdirectory_path',
|
|
'hosted_file_content_type', 'hosted_file_size',
|
|
'event_name', 'event_code', '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_presenter_code', 'event_presenter_given_name', 'event_presenter_family_name',
|
|
'event_presenter_full_name', 'event_presenter_email', 'event_session_code',
|
|
'event_session_type_code', 'event_session_name', 'event_session_start_datetime',
|
|
'event_session_end_datetime', 'event_track_code', 'event_track_name',
|
|
'hosted_file'
|
|
]
|
|
|
|
class Config:
|
|
underscore_attrs_are_private = True
|
|
allow_population_by_field_name = False
|
|
fields = base_fields
|
|
# ### END ### API Event File Models ### Event_File_Base() ###
|