fix(v3-actions): implement from_hosted_file and harden vision IDs
1. Implemented specialized 'from_hosted_file' action for Event Files.\n2. Fixed ValueError in Pydantic models by removing default/default_factory conflict.\n3. Hardened integer stripping to strictly enforce Vision Standards.\n4. Updated documentation for the new action route.
This commit is contained in:
@@ -17,16 +17,16 @@ class Event_Badge_Base(BaseModel):
|
||||
log.debug(locals())
|
||||
|
||||
# --- Standardized Vision IDs (Strings for API, Integers for DB) ---
|
||||
id: Optional[Union[int, str]] = Field(None, **base_fields['event_badge_id_random'])
|
||||
event_badge_id: Optional[Union[int, str]] = Field(None, **base_fields['event_badge_id_random'])
|
||||
event_id: Optional[Union[int, str]] = Field(None, **base_fields['event_id_random'])
|
||||
id: Optional[Union[int, str]] = Field(**base_fields['event_badge_id_random'])
|
||||
event_badge_id: Optional[Union[int, str]] = Field(**base_fields['event_badge_id_random'])
|
||||
event_id: Optional[Union[int, str]] = Field(**base_fields['event_id_random'])
|
||||
|
||||
# NOTE: This should only be used when the event_person record can not be created. And records before 2022.
|
||||
event_id_only: Optional[Union[int, str]] = Field(None, **base_fields['event_id_random'])
|
||||
event_id_only: Optional[Union[int, str]] = Field(**base_fields['event_id_random'])
|
||||
|
||||
event_badge_template_id: Optional[Union[int, str]] = Field(None, **base_fields['event_badge_template_id_random'])
|
||||
event_person_id: Optional[Union[int, str]] = Field(None, **base_fields['event_person_id_random'])
|
||||
person_id: Optional[Union[int, str]] = Field(None, **base_fields['person_id_random'])
|
||||
event_badge_template_id: Optional[Union[int, str]] = Field(**base_fields['event_badge_template_id_random'])
|
||||
event_person_id: Optional[Union[int, str]] = Field(**base_fields['event_person_id_random'])
|
||||
person_id: Optional[Union[int, str]] = Field(**base_fields['person_id_random'])
|
||||
|
||||
# --- Standardized Legacy / Internal IDs (Excluded) ---
|
||||
id_random: Optional[str] = Field(None, alias='event_badge_id_random', exclude=True)
|
||||
|
||||
@@ -16,14 +16,14 @@ class Event_Exhibit_Base(BaseModel):
|
||||
log.debug(locals())
|
||||
|
||||
# --- Standardized Vision IDs (Strings for API, Integers for DB) ---
|
||||
id: Optional[Union[int, str]] = Field(None, **base_fields['event_exhibit_id_random'])
|
||||
event_exhibit_id: Optional[Union[int, str]] = Field(None, **base_fields['event_exhibit_id_random'])
|
||||
account_id: Optional[Union[int, str]] = Field(None, **base_fields['account_id_random'])
|
||||
event_id: Optional[Union[int, str]] = Field(None, **base_fields['event_id_random'])
|
||||
organization_id: Optional[Union[int, str]] = Field(None, **base_fields['organization_id_random'])
|
||||
contact_id: Optional[Union[int, str]] = Field(None, **base_fields['contact_id_random'])
|
||||
person_id: Optional[Union[int, str]] = Field(None, **base_fields['person_id_random'])
|
||||
status_id: Optional[Union[int, str]] = Field(None, **base_fields['status_id_random'])
|
||||
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)
|
||||
|
||||
@@ -18,13 +18,13 @@ class Event_Exhibit_Tracking_Base(BaseModel):
|
||||
log.debug(locals())
|
||||
|
||||
# --- Standardized Vision IDs (Strings for API, Integers for DB) ---
|
||||
id: Optional[Union[int, str]] = Field(None, **base_fields['event_exhibit_tracking_id_random'])
|
||||
event_exhibit_tracking_id: Optional[Union[int, str]] = Field(None, **base_fields['event_exhibit_tracking_id_random'])
|
||||
account_id: Optional[Union[int, str]] = Field(None, **base_fields['account_id_random'])
|
||||
event_id: Optional[Union[int, str]] = Field(None, **base_fields['event_id_random'])
|
||||
event_exhibit_id: Optional[Union[int, str]] = Field(None, **base_fields['event_exhibit_id_random'])
|
||||
event_person_id: Optional[Union[int, str]] = Field(None, **base_fields['event_person_id_random'])
|
||||
event_badge_id: Optional[Union[int, str]] = Field(None, **base_fields['event_badge_id_random'])
|
||||
id: Optional[Union[int, str]] = Field(**base_fields['event_exhibit_tracking_id_random'])
|
||||
event_exhibit_tracking_id: Optional[Union[int, str]] = Field(**base_fields['event_exhibit_tracking_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'])
|
||||
event_exhibit_id: Optional[Union[int, str]] = Field(**base_fields['event_exhibit_id_random'])
|
||||
event_person_id: Optional[Union[int, str]] = Field(**base_fields['event_person_id_random'])
|
||||
event_badge_id: Optional[Union[int, str]] = Field(**base_fields['event_badge_id_random'])
|
||||
|
||||
# --- Standardized Legacy / Internal IDs (Excluded) ---
|
||||
id_random: Optional[str] = Field(None, alias='event_exhibit_tracking_id_random', exclude=True)
|
||||
|
||||
@@ -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 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
|
||||
@@ -16,38 +16,71 @@ class Event_File_Base(BaseModel):
|
||||
log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
id_random: Optional[str] = Field(
|
||||
# **base_fields['event_file_id_random'],
|
||||
alias = 'event_file_id_random',
|
||||
)
|
||||
id: Optional[int] = Field(
|
||||
alias = 'event_file_id'
|
||||
)
|
||||
# --- 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'])
|
||||
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'])
|
||||
|
||||
hosted_file_id_random: Optional[str]
|
||||
hosted_file_id: Optional[int]
|
||||
# --- Standardized Legacy / Internal IDs (Excluded) ---
|
||||
id_random: Optional[str] = Field(None, alias='event_file_id_random', 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)
|
||||
|
||||
@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.
|
||||
During CREATE (POST) operations, we ensure resolved integers are preserved.
|
||||
"""
|
||||
# 1. Map Random Strings to Clean Names
|
||||
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
|
||||
|
||||
if h_rid := values.get('hosted_file_id_random'): values['hosted_file_id'] = h_rid
|
||||
if f_rid := values.get('for_id_random'): values['for_id'] = f_rid
|
||||
if e_rid := values.get('event_id_random'): values['event_id'] = e_rid
|
||||
if ex_rid := values.get('event_exhibit_id_random'): values['event_exhibit_id'] = ex_rid
|
||||
if loc_rid := values.get('event_location_id_random'): values['event_location_id'] = loc_rid
|
||||
if pres_rid := values.get('event_presentation_id_random'): values['event_presentation_id'] = pres_rid
|
||||
if pr_rid := values.get('event_presenter_id_random'): values['event_presenter_id'] = pr_rid
|
||||
if s_rid := values.get('event_session_id_random'): values['event_session_id'] = s_rid
|
||||
if t_rid := values.get('event_track_id_random'): values['event_track_id'] = t_rid
|
||||
|
||||
# 2. Prevent leakage of integers during API responses (Vision Standard)
|
||||
id_fields = [
|
||||
'id', 'event_file_id', 'hosted_file_id', 'for_id', 'event_id',
|
||||
'event_exhibit_id', 'event_location_id', 'event_presentation_id',
|
||||
'event_presenter_id', 'event_session_id', 'event_track_id'
|
||||
]
|
||||
for k in id_fields:
|
||||
val = values.get(k)
|
||||
if val is not None and not isinstance(val, str):
|
||||
values[k] = None
|
||||
|
||||
return values
|
||||
|
||||
# NOTE: Handling this outside of the Pydantic model and model validation. See below as well. -STI 2021-09-10
|
||||
for_type: Optional[str]
|
||||
for_id: Optional[int] # NOTE: This is reversed with for_id_random
|
||||
for_id_random: Optional[str] # NOTE: This is reversed with for_id
|
||||
# for_id_random: Optional[str] = None # Need to override value from common_field_schema.py
|
||||
# for_id: Optional[int]
|
||||
|
||||
event_id_random: Optional[str]
|
||||
event_id: Optional[int]
|
||||
event_exhibit_id_random: Optional[str]
|
||||
event_exhibit_id: Optional[int]
|
||||
event_location_id_random: Optional[str]
|
||||
event_location_id: Optional[int]
|
||||
event_presentation_id_random: Optional[str]
|
||||
event_presentation_id: Optional[int]
|
||||
event_presenter_id_random: Optional[str]
|
||||
event_presenter_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]
|
||||
|
||||
filename: Optional[str]
|
||||
filename_no_ext: Optional[str] # Currently created with a view
|
||||
@@ -134,96 +167,6 @@ class Event_File_Base(BaseModel):
|
||||
|
||||
_processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now)
|
||||
|
||||
#@validator('event_file_id_random', always=True)
|
||||
def event_file_id_random_copy(cls, v, values, **kwargs):
|
||||
if values['id_random']:
|
||||
return values['id_random']
|
||||
return None
|
||||
|
||||
@validator('id', always=True)
|
||||
def event_file_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_file')
|
||||
return None
|
||||
|
||||
@validator('hosted_file_id', always=True)
|
||||
def hosted_file_id_lookup(cls, v, values, **kwargs):
|
||||
if isinstance(v, int) and v > 0: return v
|
||||
elif id_random := values.get('hosted_file_id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='hosted_file')
|
||||
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_exhibit_id', always=True)
|
||||
def event_exhibit_id_lookup(cls, v, values, **kwargs):
|
||||
if isinstance(v, int) and v > 0: return v
|
||||
elif id_random := values.get('event_exhibit_id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='event_exhibit')
|
||||
return None
|
||||
|
||||
@validator('event_location_id', always=True)
|
||||
def event_location_id_lookup(cls, v, values, **kwargs):
|
||||
if isinstance(v, int) and v > 0: return v
|
||||
elif id_random := values.get('event_location_id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='event_location')
|
||||
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_presenter_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('event_presenter_id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='event_presenter')
|
||||
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('event_track_id', always=True)
|
||||
def event_track_id_lookup(cls, v, values, **kwargs):
|
||||
if isinstance(v, int) and v > 0: return v
|
||||
elif id_random := values.get('event_track_id_random'):
|
||||
return redis_lookup_id_random(record_id_random=id_random, table_name='event_track')
|
||||
return None
|
||||
|
||||
# NOTE: I kind of give up on this. Handling this outside of Pydantic and before the data is even attempted to be loaded into the Event_File_Base model. -STI 2021-09-10
|
||||
# NOTE: This validator will try to find and "set" the for_id_random value. However, The value is not really "set" in Pydantic. To get this value, exclude_unset=True when returning a dict from the model.
|
||||
@validator('for_id_random', always=True)
|
||||
def for_id_random_lookup(cls, v, values, **kwargs):
|
||||
log.setLevel(logging.WARNING)
|
||||
log.debug(locals())
|
||||
if isinstance(v, str): return v
|
||||
elif values.get('for_id') and values['for_type']:
|
||||
return get_id_random(record_id=values['for_id'], table_name=values['for_type'])
|
||||
return None
|
||||
|
||||
# @validator('for_id', always=True)
|
||||
# def for_id_lookup(cls, v, values, **kwargs):
|
||||
# log.setLevel(logging.DEBUG)
|
||||
# log.debug(locals())
|
||||
|
||||
# if values.get('for_id_random', None) and values['for_type']:
|
||||
# return redis_lookup_id_random(record_id_random=values['for_id_random'], table_name=values['for_type'])
|
||||
# # return None
|
||||
# else: return v
|
||||
|
||||
class Config:
|
||||
underscore_attrs_are_private = True
|
||||
allow_population_by_field_name = True
|
||||
|
||||
@@ -198,6 +198,84 @@ async def upload_event_file_action(
|
||||
return mk_resp(data=event_file_results, status_message=f"Successfully processed {len(event_file_results)} event files.")
|
||||
|
||||
|
||||
@router.post('/from_hosted_file/{hosted_file_id}', response_model=Resp_Body_Base)
|
||||
async def create_event_file_from_hosted_file_action(
|
||||
event_file_obj: Event_File_Base,
|
||||
hosted_file_id: str = Path(..., min_length=11, max_length=22),
|
||||
inc_hosted_file: bool = Query(False),
|
||||
return_obj: bool = Query(True),
|
||||
account: AccountContext = Depends(get_account_context),
|
||||
delay: DelayParams = Depends(),
|
||||
):
|
||||
"""
|
||||
Specialized Action: Create Event File from Existing Hosted File.
|
||||
|
||||
This endpoint allows the frontend to associate an ALREADY UPLOADED hosted_file
|
||||
with an event-specific context (e.g., event_session, exhibit).
|
||||
|
||||
Matches V3 Vision ID Standard:
|
||||
- Accepts string ID in path.
|
||||
- Resolves relational IDs in body via Event_File_Base root_validator.
|
||||
"""
|
||||
if delay.sleep_time_s > 0: await asyncio.sleep(delay.sleep_time_s)
|
||||
|
||||
# 1. Verify physical file exists
|
||||
hf_rec = load_hosted_file_obj(hosted_file_id=hosted_file_id)
|
||||
if not hf_rec:
|
||||
raise HTTPException(status_code=404, detail=f"Hosted file '{hosted_file_id}' not found.")
|
||||
|
||||
# 2. Prepare event_file data
|
||||
# The Event_File_Base model now standardizes all IDs to Union[int, str]
|
||||
# and its root_validator handles the string->int resolution during instantiation.
|
||||
ef_data = event_file_obj.dict(exclude_unset=True)
|
||||
ef_data['hosted_file_id'] = hosted_file_id # Inject the path ID
|
||||
|
||||
# Re-instantiate to trigger hardened Vision resolution
|
||||
validated_ef = Event_File_Base(**ef_data)
|
||||
|
||||
# 3. Standard Generic Linking (Ensures hosted_file_link exists)
|
||||
# We need the integers for the method call
|
||||
hf_id_int = redis_lookup_id_random(record_id_random=hosted_file_id, table_name='hosted_file')
|
||||
link_to_id_int = redis_lookup_id_random(record_id_random=validated_ef.for_id, table_name=validated_ef.for_type)
|
||||
|
||||
create_hosted_file_link(
|
||||
account_id = account.account_id,
|
||||
hosted_file_id = hf_id_int,
|
||||
link_to_type = validated_ef.for_type,
|
||||
link_to_id = link_to_id_int
|
||||
)
|
||||
|
||||
# 4. Create Event File record
|
||||
res_ef_id = create_event_file_obj(event_file_obj_new=validated_ef)
|
||||
|
||||
if res_ef_id is True:
|
||||
# Update instead of insert - find the ID
|
||||
lookup_res = sql_select(
|
||||
table_name='event_file',
|
||||
data={
|
||||
'hosted_file_id': hf_id_int,
|
||||
'for_type': validated_ef.for_type,
|
||||
'for_id': link_to_id_int
|
||||
}
|
||||
)
|
||||
if lookup_res: res_ef_id = lookup_res.get('id')
|
||||
|
||||
if not isinstance(res_ef_id, int):
|
||||
raise HTTPException(status_code=400, detail="Failed to create event_file record.")
|
||||
|
||||
# 5. Return result
|
||||
if return_obj:
|
||||
enriched_ef = load_event_file_obj(event_file_id=res_ef_id, inc_hosted_file=inc_hosted_file, model_as_dict=True)
|
||||
# Vision Transformer: Ensure clean ID for frontend
|
||||
if enriched_ef and not isinstance(enriched_ef.get('id'), str):
|
||||
rid = get_id_random(res_ef_id, table_name='event_file')
|
||||
enriched_ef['id'] = rid
|
||||
enriched_ef['event_file_id'] = rid
|
||||
return mk_resp(data=enriched_ef)
|
||||
|
||||
return mk_resp(data={"event_file_id": get_id_random(res_ef_id, 'event_file')})
|
||||
|
||||
|
||||
@router.get('/{event_file_id}/download')
|
||||
async def download_event_file_action(
|
||||
response: Response,
|
||||
|
||||
@@ -151,6 +151,23 @@ While `hosted_file` handles generic storage, `event_file` actions are context-aw
|
||||
**Path**: `GET /v3/action/event_file/{id}/download`
|
||||
*Semantic alias for the universal hosted_file downloader.*
|
||||
|
||||
### C. Link Existing Hosted File
|
||||
**Path**: `POST /v3/action/event_file/from_hosted_file/{hosted_file_id}`
|
||||
|
||||
Use this when a file is already in standard storage (e.g., from a previous session or general archive) and you want to link it to an Event object without re-uploading.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `hosted_file_id` | Path | Yes | String ID of the physical file. |
|
||||
| `for_type` | Body | Yes | Target object type (e.g., `event_session`). |
|
||||
| `for_id` | Body | Yes | String ID of the target object. |
|
||||
| `event_id` | Body | No | Optional event context. |
|
||||
|
||||
**Features:**
|
||||
- **Vision IDs:** Accepts string IDs in both the path and JSON body.
|
||||
- **Relational Integrity:** Automatically creates both `hosted_file_link` and `event_file` records.
|
||||
- **Enriched Return:** Returns the full `event_file` object.
|
||||
|
||||
---
|
||||
|
||||
## 6. Hosted File Management (Legacy)
|
||||
|
||||
Reference in New Issue
Block a user