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.
205 lines
8.9 KiB
Python
205 lines
8.9 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.account_models import Account_Base
|
|
from app.models.contact_models import Contact_Base
|
|
from app.models.journal_models import Journal_Base
|
|
from app.models.membership_group_models import Membership_Group_Base
|
|
from app.models.membership_person_models import Membership_Person_Base
|
|
from app.models.membership_type_models import Membership_Type_Base
|
|
# from app.models.order_models import Order_Base
|
|
from app.models.organization_models import Organization_Base
|
|
from app.models.user_models import User_Base
|
|
|
|
|
|
# ### BEGIN ### API Person Models ### Person_Base() ###
|
|
class Person_Base(BaseModel):
|
|
log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
|
log.debug(locals())
|
|
|
|
# --- Standardized Vision IDs (Strings) ---
|
|
id: Optional[str] = Field(None, **base_fields['person_id_random'])
|
|
person_id: Optional[str] = Field(None, **base_fields['person_id_random'])
|
|
account_id: Optional[str] = Field(None, **base_fields['account_id_random'])
|
|
contact_id: Optional[str] = Field(None, **base_fields['contact_id_random'])
|
|
organization_id: Optional[str] = Field(None, **base_fields['organization_id_random'])
|
|
user_id: Optional[str] = Field(None, **base_fields['user_id_random'])
|
|
membership_person_id: Optional[str] = Field(None, **base_fields['membership_person_id_random'])
|
|
|
|
# pronouns: Optional[str] # MISSING in physical table
|
|
# informal_name: Optional[str] # MISSING in physical table
|
|
|
|
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
|
|
|
|
# preferred_display_name: Optional[str] # MISSING in physical table
|
|
|
|
# BEGIN # Auto created name variations
|
|
first_last_name: Optional[str] # With SQL view?
|
|
first_middle_last_name: Optional[str] # With SQL view?
|
|
last_first_name: Optional[str] # With SQL view?
|
|
last_first_middle_name: Optional[str] # With SQL view?
|
|
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 person
|
|
informal_full_name: Optional[str] # informal_name family_name May be auto created from informal, given, family, etc if not given
|
|
professional_full_name: Optional[str] # title_names given_name middle_name family_name designations professional_title
|
|
# END # Auto created name variations
|
|
|
|
affiliations: Optional[str] # One or more affiliations with organizations, companies, and other groups
|
|
|
|
primary_email: Optional[str]
|
|
|
|
tagline: Optional[Union[None, str]]
|
|
|
|
birth_date: Optional[datetime.date]
|
|
lu_gender_id: Optional[int]
|
|
lu_gender_name: Optional[str]
|
|
|
|
email_allowed: Optional[bool]
|
|
paper_mail_allowed: Optional[bool]
|
|
|
|
source_code: Optional[str]
|
|
|
|
external_id: Optional[str] # Generated internally or externally. Needs to be stable. It should not change.
|
|
external_sys_id: Optional[str] # Generated by external system (should be stable and not change)
|
|
|
|
stripe_customer_id: Optional[str]
|
|
|
|
allow_auth_key: Optional[bool]
|
|
auth_key: Optional[str] # Intended for one time use; more complex
|
|
passcode: Optional[str] # For basic quick access; 8 characters or more; can change as needed
|
|
|
|
status: Optional[str]
|
|
# status_id: Optional[int] # From a lookup
|
|
# status_name: Optional[str] # Status name from the lookup
|
|
|
|
hide: Optional[bool]
|
|
priority: Optional[bool]
|
|
sort: Optional[int]
|
|
group: Optional[str]
|
|
enable: Optional[bool]
|
|
|
|
notes: Optional[str]
|
|
created_on: Optional[datetime.datetime] = None
|
|
updated_on: Optional[datetime.datetime] = None
|
|
|
|
test_field: str = 'abcd 1234'
|
|
|
|
# Including convenience data
|
|
# This is only for convenience. Probably going to keep unless it causes a problem.
|
|
email: Optional[str]
|
|
cc_email: Optional[str]
|
|
# Maybe add timezone in the future?
|
|
|
|
username: Optional[str]
|
|
user_name: Optional[str]
|
|
user_email: Optional[str]
|
|
user_allow_auth_key: Optional[bool]
|
|
user_super: Optional[bool]
|
|
user_manager: Optional[bool]
|
|
user_administrator: Optional[bool]
|
|
user_public: Optional[bool]
|
|
|
|
# Including JSON data
|
|
data_json: Optional[Union[Json, None]]
|
|
other_json: Optional[Union[Json, None]]
|
|
meta_json: Optional[Union[Json, None]]
|
|
|
|
# Including other related objects
|
|
# archive_list: Optional[list] # Archive_Base()
|
|
event_list: Optional[list] # Event_Base() # Priority l1
|
|
hosted_file_list: Optional[list] # Hosted_File_Base() # Priority l2
|
|
journal_list: Optional[list[Journal_Base]] # Journal_Base() # Priority l3
|
|
contact: Optional[Union[Contact_Base, None]]
|
|
membership_person: Optional[Membership_Person_Base]
|
|
|
|
membership_group_list: Optional[list[Membership_Group_Base]] # Membership_Group_Base() list of member group options
|
|
membership_type_list: Optional[list[Membership_Type_Base]] # Membership_Type_Base() list of member type options
|
|
# membership_person_type_list: Optional[list] # Membership_Person_Type_Base() list of member type person records... for now this should be only one at most.
|
|
# membership_person_group_list: Optional[list] # Membership_Person_Group_Base() list of member group person records
|
|
|
|
orders_info: Optional[dict] # closed_count, etc
|
|
order_list: Optional[list] # Order_Base() # Priority l2
|
|
order_cart: Optional[dict] # Order_Cart_Base() # Priority l2
|
|
order_cart_v3: Optional[dict] # Order_Base()
|
|
|
|
organization: Optional[Union[Organization_Base, None]]
|
|
post_list: Optional[list] # Post_Base() # Priority l1
|
|
# from app.models.user_models import User_Base
|
|
user: Optional[Union[User_Base, None]]
|
|
|
|
_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('person_id_random'):
|
|
values['id'] = rid
|
|
values['person_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 o_rid := values.get('organization_id_random'):
|
|
values['organization_id'] = o_rid
|
|
if u_rid := values.get('user_id_random'):
|
|
values['user_id'] = u_rid
|
|
if mp_rid := values.get('membership_person_id_random'):
|
|
values['membership_person_id'] = mp_rid
|
|
|
|
# 2. Prevent "Collision Population"
|
|
for k in ['id', 'account_id', 'contact_id', 'organization_id', 'user_id', 'membership_person_id']:
|
|
if k in values and not isinstance(values[k], str):
|
|
del values[k]
|
|
|
|
return values
|
|
|
|
@validator('given_name', always=True)
|
|
def given_name_validator(cls, v):
|
|
if v is None:
|
|
return ""
|
|
return v
|
|
|
|
@validator('allow_auth_key', always=True)
|
|
def allow_auth_key_validator(cls, v):
|
|
if v is None:
|
|
return True
|
|
return v
|
|
|
|
# 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] = [
|
|
'membership_person_id', 'first_last_name', 'first_middle_last_name',
|
|
'last_first_name', 'last_first_middle_name', 'informal_full_name',
|
|
'professional_full_name', 'lu_gender_name', 'email', 'cc_email',
|
|
'username', 'user_name', 'user_email', 'user_allow_auth_key',
|
|
'user_super', 'user_manager', 'user_administrator', 'user_public',
|
|
'event_list', 'hosted_file_list', 'journal_list', 'contact',
|
|
'membership_person', 'membership_group_list', 'membership_type_list',
|
|
'orders_info', 'order_list', 'order_cart', 'order_cart_v3',
|
|
'organization', 'post_list', 'user'
|
|
]
|
|
|
|
class Config:
|
|
underscore_attrs_are_private = True
|
|
allow_population_by_field_name = False
|
|
fields = base_fields
|
|
# ### END ### API Person Models ### Person_Base() ###
|