Compare commits
21 Commits
1f9cbb0a1f
...
c837d465ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c837d465ca | ||
|
|
2659047d24 | ||
|
|
18374f855f | ||
|
|
e5acefe8f6 | ||
|
|
082163b5df | ||
|
|
e35fdb4f67 | ||
|
|
02a2be7275 | ||
|
|
eba3456b7b | ||
|
|
987b552157 | ||
|
|
7ad158883a | ||
|
|
2b608d7a1a | ||
|
|
535fc9f2b5 | ||
|
|
8e9fb88e5a | ||
|
|
42eaa6676e | ||
|
|
b5c50fd116 | ||
|
|
2a1f270db6 | ||
|
|
ebc5db96da | ||
|
|
153c2ce6dd | ||
|
|
9faf22d841 | ||
|
|
293f447a1c | ||
|
|
4629e1ec63 |
@@ -1,5 +1,5 @@
|
||||
|
||||
# Aether API v3.x (FastAPI)
|
||||
# Aether API v3.00.xx (FastAPI)
|
||||
|
||||
The **Aether API** is a high-performance, multi-tenant backend for the Aether Platform, built on Python **FastAPI**. It powers both legacy and modern (V3/V4) applications, and is now fully containerized for robust, scalable deployment.
|
||||
|
||||
|
||||
@@ -199,7 +199,11 @@ def sql_search_qry_part(
|
||||
if hasattr(item, 'field'):
|
||||
clause, item_data = process_filter(item)
|
||||
node_clauses.append(clause); data.update(item_data)
|
||||
else: node_clauses.append(f"({process_node(item, current_depth + 1)})")
|
||||
else:
|
||||
# Recurse into nested SearchQuery; only append if non-empty
|
||||
sub_clause = process_node(item, current_depth + 1)
|
||||
if sub_clause:
|
||||
node_clauses.append(f"({sub_clause})")
|
||||
if node_clauses:
|
||||
joiner = ' AND ' if 'and' in filter_attr else ' OR '
|
||||
clauses.append(f"({joiner.join(node_clauses)})")
|
||||
@@ -261,6 +265,18 @@ def sql_search_qry_part(
|
||||
except Exception as e:
|
||||
log.warning(f"Failed to resolve random ID for field {target_field}: {e}")
|
||||
|
||||
# site_domain: 'access_key' is a virtual field.
|
||||
# site_access_key (site-level) takes priority; fall back to site_domain_access_key
|
||||
# when site_access_key is not set (NULL or empty).
|
||||
if target_field == 'access_key' and table_name and 'site_domain' in table_name:
|
||||
sql_op = operator_map.get(f.op.lower())
|
||||
if not sql_op: raise HTTPException(status_code=400, detail=f"Unsupported operator: {f.op}")
|
||||
p1, p2 = get_param_name(), get_param_name()
|
||||
return (
|
||||
f"(site_access_key {sql_op} :{p1} OR "
|
||||
f"((site_access_key IS NULL OR site_access_key = '') AND site_domain_access_key {sql_op} :{p2}))"
|
||||
), {p1: f.value, p2: f.value}
|
||||
|
||||
if searchable_fields is not None and target_field not in searchable_fields:
|
||||
# Fallback check for original field just in case
|
||||
if f.field not in searchable_fields:
|
||||
|
||||
@@ -96,7 +96,7 @@ app = FastAPI(
|
||||
# debug = True,
|
||||
title = 'Aether API',
|
||||
description = 'One Sky IT\'s Aether API v3.0 using FastAPI.',
|
||||
version = '3.00.03',
|
||||
version = '3.00.10',
|
||||
operationsSorter = 'method',
|
||||
lifespan = lifespan,
|
||||
)
|
||||
|
||||
@@ -322,10 +322,9 @@ def create_update_event_badge_obj_v4(
|
||||
elif event_person_id := event_badge_obj.event_person_id: pass
|
||||
|
||||
if event_badge_id:
|
||||
if event_badge_dict_up_result := sql_update(data=event_badge_dict, table_name='event_badge', rm_id_random=True): pass
|
||||
else:
|
||||
log.warning(f'Event Badge not updated. Event Badge ID: {event_badge_id}')
|
||||
log.debug(event_badge_dict_up_result)
|
||||
event_badge_dict_up_result = sql_update(data=event_badge_dict, table_name='event_badge', record_id=event_badge_id, rm_id_random=True)
|
||||
if event_badge_dict_up_result is False:
|
||||
log.warning(f'Event Badge update failed (DB error). Event Badge ID: {event_badge_id}')
|
||||
return False
|
||||
log.debug(event_badge_dict_up_result)
|
||||
else:
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
from __future__ import annotations
|
||||
import datetime
|
||||
|
||||
from typing import Dict, List, Optional, Set, Union
|
||||
from pydantic import BaseModel, EmailStr, Field, PrivateAttr, ValidationError, validator
|
||||
|
||||
from app.db_sql import redis_lookup_id_random, sql_insert, sql_select, sql_update
|
||||
from app.lib_general import log, logging
|
||||
|
||||
from app.methods.event_methods import load_event_obj
|
||||
|
||||
|
||||
# ### BEGIN ### API Event Methods ### load_event_obj_list() ###
|
||||
def load_event_obj_list(
|
||||
account_id: int|str,
|
||||
limit: int = 1000,
|
||||
model_as_dict: bool = False,
|
||||
enabled: str = 'enabled', # enabled, disabled, all
|
||||
inc_contact_1: bool = False,
|
||||
inc_contact_2: bool = False,
|
||||
inc_contact_3: bool = False,
|
||||
inc_event_abstract_list: bool = False,
|
||||
inc_event_badge_list: bool = False,
|
||||
inc_event_cfg: bool = False,
|
||||
inc_event_device_list: bool = False,
|
||||
inc_event_exhibit_list: bool = False,
|
||||
inc_event_file_list: bool = False,
|
||||
inc_event_location: bool = False, # For event_session child object
|
||||
inc_event_location_list: bool = False,
|
||||
inc_event_person_list: bool = False,
|
||||
inc_event_presentation_list: bool = False,
|
||||
inc_event_presenter_cat: bool = False, # For event_session child object
|
||||
inc_event_presenter_list: bool = False,
|
||||
inc_event_registration_cfg: bool = False,
|
||||
inc_event_registration_list: bool = False,
|
||||
inc_event_session_list: bool = False,
|
||||
inc_event_track: bool = False, # For event_session child object
|
||||
inc_event_track_list: bool = False,
|
||||
inc_location_address: bool = False,
|
||||
inc_poc_event_person: bool = False,
|
||||
inc_person: bool = False,
|
||||
inc_user: bool = False,
|
||||
) -> list|bool:
|
||||
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
if account_id := redis_lookup_id_random(record_id_random=account_id, table_name='account'): pass
|
||||
else: return False
|
||||
|
||||
data = {}
|
||||
data['account_id'] = account_id
|
||||
|
||||
if enabled in ['enabled', 'disabled', 'all']:
|
||||
if enabled == 'enabled':
|
||||
data['enable'] = True
|
||||
sql_enabled = f'AND `tbl`.enable = :enable'
|
||||
elif enabled == 'disabled':
|
||||
data['enable'] = False
|
||||
sql_enabled = f'AND `tbl`.enable = :enable'
|
||||
elif enabled == 'all':
|
||||
sql_enabled = ''
|
||||
# else: tbl_obj['account'] = None
|
||||
|
||||
if limit:
|
||||
data['limit'] = limit
|
||||
sql_limit = f'LIMIT :limit'
|
||||
else:
|
||||
sql_limit = ''
|
||||
|
||||
sql = f"""
|
||||
SELECT `tbl`.id AS 'event_id', `tbl`.id_random AS 'event_id_random'
|
||||
FROM `event` AS `tbl`
|
||||
WHERE `tbl`.account_id = :account_id
|
||||
{sql_enabled}
|
||||
ORDER BY `tbl`.created_on DESC, `tbl`.updated_on DESC
|
||||
{sql_limit};
|
||||
"""
|
||||
|
||||
if event_rec_li_result := sql_select(data=data, sql=sql, as_list=True):
|
||||
log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(event_rec_li_result)
|
||||
event_result_li = []
|
||||
for event_rec in event_rec_li_result:
|
||||
event_id = event_rec.get('event_id', None)
|
||||
if event_result := load_event_obj(
|
||||
event_id = event_id,
|
||||
limit = limit,
|
||||
model_as_dict = model_as_dict,
|
||||
enabled = enabled,
|
||||
# inc_location_address = inc_address,
|
||||
# inc_contact_1 = inc_contact,
|
||||
# inc_contact_2 = inc_contact,
|
||||
# inc_contact_3 = inc_contact,
|
||||
# inc_event_abstract_list = inc_event_abstract_list,
|
||||
# inc_event_badge_list = inc_event_badge_list,
|
||||
# inc_event_device_list = inc_event_device_list,
|
||||
inc_event_exhibit_list = inc_event_exhibit_list,
|
||||
inc_event_file_list = inc_event_file_list,
|
||||
inc_event_location_list = inc_event_location_list,
|
||||
inc_event_person_list = inc_event_person_list,
|
||||
inc_event_presentation_list = inc_event_presentation_list,
|
||||
inc_event_presenter_list = inc_event_presenter_list,
|
||||
inc_event_registration_list = inc_event_registration_list,
|
||||
inc_event_session_list = inc_event_session_list,
|
||||
inc_event_track_list = inc_event_track_list,
|
||||
# inc_person = inc_person,
|
||||
# inc_user = inc_user,
|
||||
):
|
||||
log.debug(event_result)
|
||||
event_result_li.append(event_result)
|
||||
else:
|
||||
log.debug(event_result)
|
||||
event_result_li.append(None)
|
||||
log.debug(event_result_li)
|
||||
else:
|
||||
log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(event_rec_li_result)
|
||||
event_result_li = []
|
||||
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
|
||||
return event_result_li
|
||||
# ### END ### API Event Methods ### load_event_obj_list() ###
|
||||
@@ -3,7 +3,7 @@ import datetime
|
||||
from typing import Dict, List, Optional, Set, Union
|
||||
from pydantic import BaseModel, EmailStr, Field, PrivateAttr, ValidationError, validator
|
||||
|
||||
from app.db_sql import get_account_id_w_for_type_id, redis_lookup_id_random, sql_insert, sql_select, sql_update
|
||||
from app.db_sql import get_account_id_w_for_type_id, redis_lookup_id_random, sql_insert, sql_select, sql_update, get_id_random
|
||||
from app.lib_general import log, logging, logger_reset
|
||||
|
||||
# from app.methods.event_abstract_methods import load_event_abstract_obj
|
||||
@@ -355,7 +355,7 @@ def create_update_event_person_obj_v4(
|
||||
fail_any: bool = False, # Fail if any thing goes wrong for sub objects
|
||||
return_outline: bool = False,
|
||||
) -> int|bool:
|
||||
log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
# ### SECTION ### Secondary data validation
|
||||
@@ -420,7 +420,19 @@ def create_update_event_person_obj_v4(
|
||||
if account_id:
|
||||
event_person_dict['account_id'] = account_id
|
||||
if event_id:
|
||||
event_person_dict['event_id'] = event_id
|
||||
# The model expects random-string IDs (eg. id_random). If we have an
|
||||
# integer internal ID, convert it to the random string form so the
|
||||
# Pydantic root_validator preserves it. This ensures `event_id` is
|
||||
# present when inserting a new `event_person` record.
|
||||
if isinstance(event_id, int):
|
||||
if idr := get_id_random(record_id=event_id, table_name='event'):
|
||||
event_person_dict['event_id_random'] = idr
|
||||
else:
|
||||
# Fallback: set the integer (will likely be removed by the model),
|
||||
# but allow downstream logic to attempt insertion.
|
||||
event_person_dict['event_id'] = event_id
|
||||
else:
|
||||
event_person_dict['event_id'] = event_id
|
||||
try:
|
||||
event_person_obj = Event_Person_Base(**event_person_dict)
|
||||
except ValidationError as e:
|
||||
@@ -434,7 +446,16 @@ def create_update_event_person_obj_v4(
|
||||
if account_id:
|
||||
event_person_obj.account_id = account_id
|
||||
if event_id:
|
||||
event_person_obj.event_id = event_id
|
||||
# If an integer internal ID was provided, convert to the random ID
|
||||
# string form for the Pydantic object so it is preserved when
|
||||
# serializing to the DB insert/update payload.
|
||||
if isinstance(event_id, int):
|
||||
if idr := get_id_random(record_id=event_id, table_name='event'):
|
||||
event_person_obj.event_id = idr
|
||||
else:
|
||||
event_person_obj.event_id = event_id
|
||||
else:
|
||||
event_person_obj.event_id = event_id
|
||||
log.debug(event_person_obj)
|
||||
|
||||
event_person_dict = event_person_obj.dict(by_alias=False, exclude_defaults=False, exclude_unset=True, exclude={'event_badge', 'event_person_profile', 'event_registration', 'created_on', 'updated_on', 'external_id_old'})
|
||||
@@ -453,11 +474,11 @@ def create_update_event_person_obj_v4(
|
||||
event_person_profile_id = event_person_obj.event_person_profile_id
|
||||
|
||||
if event_person_id:
|
||||
if event_person_dict_up_result := sql_update(data=event_person_dict, table_name='event_person', rm_id_random=True): pass
|
||||
else:
|
||||
log.warning(f'Event Person not updated. Event Person ID: {event_person_id}')
|
||||
log.debug(event_person_dict_up_result)
|
||||
event_person_dict_up_result = sql_update(data=event_person_dict, table_name='event_person', record_id=event_person_id, rm_id_random=True)
|
||||
if event_person_dict_up_result is False:
|
||||
log.warning(f'Event Person update failed (DB error). Event Person ID: {event_person_id}')
|
||||
return False
|
||||
# None means 0 rows affected (record unchanged) — not an error, continue to sub-objects
|
||||
log.debug(event_person_dict_up_result)
|
||||
else:
|
||||
if event_person_dict_in_result := sql_insert(data=event_person_dict, table_name='event_person', rm_id_random=True, id_random_length=None): pass
|
||||
|
||||
@@ -154,11 +154,12 @@ def create_update_event_person_profile_obj_v4(
|
||||
contact_id = event_person_profile_obj.contact_id
|
||||
|
||||
if event_person_profile_id:
|
||||
if event_person_profile_dict_up_result := sql_update(data=event_person_profile_dict, table_name='event_person_profile', rm_id_random=True): pass
|
||||
else:
|
||||
log.warning(f'Event Person Profile not updated. Event Person Profile ID: {event_person_profile_id}')
|
||||
event_person_profile_dict_up_result = sql_update(data=event_person_profile_dict, table_name='event_person_profile', record_id=event_person_profile_id, rm_id_random=True)
|
||||
if event_person_profile_dict_up_result is False:
|
||||
log.warning(f'Event Person Profile update failed (DB error). Event Person Profile ID: {event_person_profile_id}')
|
||||
log.debug(event_person_profile_dict_up_result)
|
||||
return False
|
||||
# None means 0 rows affected (record unchanged) — not an error
|
||||
log.debug(event_person_profile_dict_up_result)
|
||||
else:
|
||||
if event_person_profile_dict_in_result := sql_insert(data=event_person_profile_dict, table_name='event_person_profile', rm_id_random=True, id_random_length=8): pass
|
||||
|
||||
@@ -429,9 +429,9 @@ def create_update_event_presentation_obj_v4(
|
||||
event_presentation_dict = event_presentation_obj.dict(by_alias=False, exclude_defaults=False, exclude_unset=True, exclude={'event_presenter', 'event_presenter_list', 'created_on', 'updated_on'})
|
||||
|
||||
if event_presentation_id:
|
||||
if event_presentation_dict_up_result := sql_update(data=event_presentation_dict, table_name='event_presentation', rm_id_random=True): pass
|
||||
else:
|
||||
log.warning(f'Event Presentation not updated. Event Presentation ID: {event_presentation_id}')
|
||||
event_presentation_dict_up_result = sql_update(data=event_presentation_dict, table_name='event_presentation', record_id=event_presentation_id, rm_id_random=True)
|
||||
if event_presentation_dict_up_result is False:
|
||||
log.warning(f'Event Presentation update failed (DB error). Event Presentation ID: {event_presentation_id}')
|
||||
log.debug(event_presentation_dict_up_result)
|
||||
return False
|
||||
log.debug(event_presentation_dict_up_result)
|
||||
|
||||
@@ -404,9 +404,9 @@ def create_update_event_presenter_obj_v4(
|
||||
event_presenter_dict = event_presenter_obj.dict(by_alias=False, exclude_defaults=False, exclude_unset=True, exclude={'created_on', 'updated_on'})
|
||||
|
||||
if event_presenter_id:
|
||||
if event_presenter_dict_up_result := sql_update(data=event_presenter_dict, table_name='event_presenter', rm_id_random=True): pass
|
||||
else:
|
||||
log.warning(f'Event Presenter not updated. Event Presenter ID: {event_presenter_id}')
|
||||
event_presenter_dict_up_result = sql_update(data=event_presenter_dict, table_name='event_presenter', record_id=event_presenter_id, rm_id_random=True)
|
||||
if event_presenter_dict_up_result is False:
|
||||
log.warning(f'Event Presenter update failed (DB error). Event Presenter ID: {event_presenter_id}')
|
||||
log.debug(event_presenter_dict_up_result)
|
||||
return False
|
||||
log.debug(event_presenter_dict_up_result)
|
||||
|
||||
@@ -147,8 +147,9 @@ def get_site_domain_rec_list(
|
||||
# ### BEGIN ### API Site Domain Methods ### lookup_site_domain_fqdn() ###
|
||||
def lookup_site_domain_fqdn(
|
||||
fqdn: str,
|
||||
# TODO: Accept access_key as an argument for validation (str|None)
|
||||
# access_key: Optional[str] = None,
|
||||
# Accept access_key as an argument for validation (str|None)
|
||||
access_key: Optional[str] = None,
|
||||
referrer: Optional[str] = None,
|
||||
enabled: str = 'enabled', # enabled, disabled, all
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
@@ -158,22 +159,37 @@ def lookup_site_domain_fqdn(
|
||||
|
||||
data = {}
|
||||
data['fqdn'] = fqdn
|
||||
# TODO: If access_key is provided, add it to the data dict for SQL parameterization
|
||||
# if access_key is not None:
|
||||
# data['access_key'] = access_key
|
||||
# If access_key is provided, add it to the data dict for SQL parameterization
|
||||
data['domain_access_key'] = access_key
|
||||
if referrer:
|
||||
data['required_referrer'] = referrer
|
||||
|
||||
sql_enabled, data['enable'] = sql_enable_part(table_name='site_domain', enabled=enabled) # Reasonably safe return str and bool
|
||||
sql_limit = sql_limit_offset_part(limit=limit, offset=offset) # Reasonably safe return str
|
||||
|
||||
# TODO: Add access_key to WHERE clause if provided, e.g.:
|
||||
# WHERE site_domain.fqdn = :fqdn AND (:access_key IS NULL OR site_domain.access_key = :access_key)
|
||||
# Build access key / referrer SQL similar to router.lookup_fqdn behavior
|
||||
if access_key and referrer:
|
||||
sql_access_key_referrer = """
|
||||
AND site_domain.domain_access_key = :domain_access_key
|
||||
AND site_domain.required_referrer = :required_referrer
|
||||
"""
|
||||
elif access_key:
|
||||
sql_access_key_referrer = """
|
||||
AND site_domain.domain_access_key = :domain_access_key
|
||||
AND (site_domain.required_referrer IS NULL OR site_domain.required_referrer = '')
|
||||
"""
|
||||
else:
|
||||
sql_access_key_referrer = """
|
||||
AND (site_domain.domain_access_key IS NULL OR site_domain.domain_access_key = '')
|
||||
AND (site_domain.required_referrer IS NULL OR site_domain.required_referrer = '')
|
||||
"""
|
||||
|
||||
sql = f"""
|
||||
SELECT `site_domain`.id AS 'site_domain_id', `site_domain`.id_random AS 'site_domain_id_random'
|
||||
FROM `v_site_domain` AS site_domain
|
||||
WHERE
|
||||
site_domain.fqdn = :fqdn
|
||||
-- TODO: Add access_key check here for stricter validation
|
||||
-- AND (:access_key IS NULL OR site_domain.access_key = :access_key)
|
||||
{sql_access_key_referrer}
|
||||
{sql_enabled}
|
||||
ORDER BY `site_domain`.fqdn ASC, `site_domain`.access_key ASC, `site_domain`.required_referrer ASC, `site_domain`.created_on DESC, `site_domain`.updated_on DESC
|
||||
{sql_limit};
|
||||
|
||||
@@ -37,6 +37,7 @@ class Event_Badge_Template_Base(BaseModel):
|
||||
header_background: Optional[str]
|
||||
|
||||
secondary_header_path: Optional[str] # Path to image file for back of badge and other sections
|
||||
background_image_path: Optional[str]
|
||||
|
||||
footer_path: Optional[str] # Path to image file
|
||||
footer_title: Optional[str]
|
||||
@@ -73,7 +74,8 @@ class Event_Badge_Template_Base(BaseModel):
|
||||
script_src: Optional[str]
|
||||
passcode: Optional[str]
|
||||
|
||||
other_json: Optional[str]
|
||||
other_json: Optional[Json]
|
||||
cfg_json: Optional[Json]
|
||||
|
||||
enable: Optional[bool]
|
||||
hide: Optional[bool]
|
||||
@@ -96,18 +98,18 @@ class Event_Badge_Template_Base(BaseModel):
|
||||
if rid := values.get('id_random') or values.get('event_badge_template_id_random'):
|
||||
values['id'] = rid
|
||||
values['event_badge_template_id'] = rid
|
||||
|
||||
|
||||
if e_rid := values.get('event_id_random'):
|
||||
values['event_id'] = e_rid
|
||||
|
||||
if a_rid := values.get('account_id_random'):
|
||||
values['account_id'] = a_rid
|
||||
|
||||
|
||||
# 2. Prevent "Collision Population" (ensure no integers leak into the clean string fields)
|
||||
for k in ['id', 'event_badge_template_id', 'event_id', 'account_id']:
|
||||
if k in values and not isinstance(values[k], str) and values[k] is not None:
|
||||
del values[k]
|
||||
|
||||
|
||||
return values
|
||||
|
||||
class Config:
|
||||
|
||||
@@ -59,7 +59,7 @@ class Event_Base(BaseModel):
|
||||
if rid and isinstance(rid, str):
|
||||
values['id'] = rid
|
||||
values['event_id'] = rid
|
||||
|
||||
|
||||
if a_rid := values.get('account_id_random'): values['account_id'] = a_rid
|
||||
if pep_rid := values.get('poc_event_person_id_random'): values['poc_event_person_id'] = pep_rid
|
||||
if pp_rid := values.get('poc_person_id_random'): values['poc_person_id'] = pp_rid
|
||||
@@ -68,7 +68,7 @@ class Event_Base(BaseModel):
|
||||
if c1_rid := values.get('contact_1_id_random'): values['contact_1_id'] = c1_rid
|
||||
if c2_rid := values.get('contact_2_id_random'): values['contact_2_id'] = c2_rid
|
||||
if c3_rid := values.get('contact_3_id_random'): values['contact_3_id'] = c3_rid
|
||||
|
||||
|
||||
# 2. Prevent "Collision Population" or leakage of integers during API responses
|
||||
# WE MUST NOT DELETE these if they are already integers during a POST operation
|
||||
# as they have been resolved by sanitize_payload.
|
||||
@@ -77,7 +77,7 @@ class Event_Base(BaseModel):
|
||||
if val is not None and not isinstance(val, str):
|
||||
if values.get(f'{k}_random') or (k=='id' and values.get('id_random')):
|
||||
del values[k]
|
||||
|
||||
|
||||
return values
|
||||
|
||||
code: Optional[str] = Field(
|
||||
@@ -171,6 +171,7 @@ class Event_Base(BaseModel):
|
||||
|
||||
cfg_json: Optional[Union[Json, None]] # Store per event config options; Not currently used 2024-06-11
|
||||
data_json: Optional[Union[Json, None]] # For key value data. Careful with overwriting existing fields! Not currently used 2024-06-11
|
||||
default_qry_str: Optional[str] # Default query string used for searching and filtering events. Updated using SQL triggers and a SQL function
|
||||
|
||||
enable: Optional[bool] # Also in Event_Cfg_Base model
|
||||
enable_from: Optional[datetime.datetime] = None
|
||||
@@ -288,7 +289,7 @@ class Event_Meeting_Flat_Base(BaseModel):
|
||||
if rid and isinstance(rid, str):
|
||||
values['id'] = rid
|
||||
values['event_id'] = rid
|
||||
|
||||
|
||||
if a_rid := values.get('account_id_random'): values['account_id'] = a_rid
|
||||
if pep_rid := values.get('poc_event_person_id_random'): values['poc_event_person_id'] = pep_rid
|
||||
if pp_rid := values.get('poc_person_id_random'): values['poc_person_id'] = pp_rid
|
||||
@@ -297,14 +298,14 @@ class Event_Meeting_Flat_Base(BaseModel):
|
||||
if c1_rid := values.get('contact_1_id_random'): values['contact_1_id'] = c1_rid
|
||||
if c2_rid := values.get('contact_2_id_random'): values['contact_2_id'] = c2_rid
|
||||
if c3_rid := values.get('contact_3_id_random'): values['contact_3_id'] = c3_rid
|
||||
|
||||
|
||||
# 2. Prevent "Collision Population" or leakage of integers during API responses
|
||||
for k in ['id', 'event_id', 'account_id', 'poc_event_person_id', 'poc_person_id', 'user_id', 'address_location_id', 'contact_1_id', 'contact_2_id', 'contact_3_id']:
|
||||
val = values.get(k)
|
||||
if val is not None and not isinstance(val, str):
|
||||
if values.get(f'{k}_random') or (k=='id' and values.get('id_random')):
|
||||
del values[k]
|
||||
|
||||
|
||||
return values
|
||||
|
||||
code: Optional[str] = Field(
|
||||
@@ -396,6 +397,7 @@ class Event_Meeting_Flat_Base(BaseModel):
|
||||
|
||||
cfg_json: Optional[Union[Json, None]] # Store per event config options; Not currently used 2024-06-11
|
||||
data_json: Optional[Union[Json, None]] # For key value data. Careful with overwriting existing fields! Not currently used 2024-06-11
|
||||
default_qry_str: Optional[str] # Default query string used for searching and filtering events. Updated using SQL triggers and a SQL function
|
||||
|
||||
enable: Optional[bool] # Also in Event_Cfg_Base model
|
||||
enable_from: Optional[datetime.datetime] = None
|
||||
@@ -413,7 +415,7 @@ class Event_Meeting_Flat_Base(BaseModel):
|
||||
# --- IDAA Recovery Meetings: Convenience Data (Flat) ---
|
||||
# These fields are primarily for the flat "Meeting" view used by the IDAA mobile/web apps.
|
||||
# Note: We prioritize string IDs (id_random) for all external API consumers.
|
||||
|
||||
|
||||
address_id_random: Optional[str] = Field(None, **base_fields['address_id_random'])
|
||||
address_name: Optional[str]
|
||||
address_line_1: Optional[str]
|
||||
|
||||
@@ -125,6 +125,7 @@ class Event_Presenter_Base(BaseModel):
|
||||
notes: Optional[str]
|
||||
created_on: Optional[datetime.datetime] = None
|
||||
updated_on: Optional[datetime.datetime] = None
|
||||
default_qry_str: Optional[str] # Default query string used for searching and filtering presenters. Updated using SQL triggers and a SQL function
|
||||
|
||||
# Including convenience data
|
||||
# This is only for convenience. Probably going to keep unless it causes a problem.
|
||||
@@ -190,7 +191,7 @@ class Event_Presenter_Base(BaseModel):
|
||||
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
|
||||
@@ -198,12 +199,12 @@ class Event_Presenter_Base(BaseModel):
|
||||
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
|
||||
@@ -313,6 +314,7 @@ class Event_Presenter_Out_Base(BaseModel):
|
||||
notes: Optional[str]
|
||||
created_on: Optional[datetime.datetime] = None
|
||||
updated_on: Optional[datetime.datetime] = None
|
||||
default_qry_str: Optional[str] # Default query string used for searching and filtering presenters. Updated using SQL triggers and a SQL function
|
||||
|
||||
person_external_id: Optional[str]
|
||||
person_external_sys_id: Optional[str]
|
||||
@@ -338,18 +340,18 @@ class Event_Presenter_Out_Base(BaseModel):
|
||||
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:
|
||||
|
||||
@@ -138,6 +138,7 @@ class Event_Session_Base(BaseModel):
|
||||
notes: Optional[str]
|
||||
created_on: Optional[datetime.datetime] = None
|
||||
updated_on: Optional[datetime.datetime] = None
|
||||
default_qry_str: Optional[str] # Default query string used for searching and filtering sessions. Updated using SQL triggers and a SQL function
|
||||
|
||||
# Including convenience data
|
||||
# This is only for convenience. Probably going to keep unless it causes a problem.
|
||||
@@ -193,7 +194,7 @@ class Event_Session_Base(BaseModel):
|
||||
if rid := values.get('id_random') or values.get('event_session_id_random'):
|
||||
values['id'] = rid
|
||||
values['event_session_id'] = rid
|
||||
|
||||
|
||||
if a_rid := values.get('account_id_random'):
|
||||
values['account_id'] = a_rid
|
||||
if e_rid := values.get('event_id_random'):
|
||||
@@ -206,24 +207,24 @@ class Event_Session_Base(BaseModel):
|
||||
values['poc_event_person_id'] = pep_rid
|
||||
if pp_rid := values.get('poc_person_id_random'):
|
||||
values['poc_person_id'] = pp_rid
|
||||
|
||||
|
||||
# 2. Prevent "Collision Population"
|
||||
for k in ['id', 'event_session_id', 'account_id', 'event_id', 'event_location_id', 'event_track_id', 'poc_event_person_id', 'poc_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', 'internal_use_count', 'event_file_id_li_json', 'file_count_all',
|
||||
'event_name', 'event_start_datetime', 'event_end_datetime',
|
||||
'event_name', 'event_start_datetime', 'event_end_datetime',
|
||||
'event_location_name', 'event_track_name',
|
||||
'event_abstract_list', 'event_badge_list', 'event_device_list',
|
||||
'event_file_list', 'event_file_internal_use_list', 'event_location',
|
||||
'event_location_list', 'event_person_list', 'event_presenter_cat',
|
||||
'event_presentation_list', 'event_presenter_list', 'event_track',
|
||||
'event_abstract_list', 'event_badge_list', 'event_device_list',
|
||||
'event_file_list', 'event_file_internal_use_list', 'event_location',
|
||||
'event_location_list', 'event_person_list', 'event_presenter_cat',
|
||||
'event_presentation_list', 'event_presenter_list', 'event_track',
|
||||
'poc_event_person', 'poc_person',
|
||||
'poc_person_external_id', 'poc_person_given_name', 'poc_person_family_name',
|
||||
'poc_person_full_name', 'poc_person_primary_email', 'poc_person_passcode'
|
||||
|
||||
@@ -73,7 +73,7 @@ class Journal_Entry_Base(BaseModel):
|
||||
|
||||
parent_id: Optional[str] = Field(None, **base_fields['journal_entry_id_random'])
|
||||
# parent_id_random: Optional[str]
|
||||
|
||||
|
||||
related_entry_id_random: Optional[List[str]]
|
||||
related_entry_id_li: Optional[List[int]] = Field(None, exclude=True)
|
||||
|
||||
@@ -102,6 +102,7 @@ class Journal_Entry_Base(BaseModel):
|
||||
notes: Optional[str]
|
||||
created_on: Optional[datetime.datetime] = None
|
||||
updated_on: Optional[datetime.datetime] = None
|
||||
default_qry_str: Optional[str] = None # Default query string used for searching and filtering journal entries
|
||||
|
||||
# Including other related objects
|
||||
# This is only for convenience. Probably going to keep unless it causes a problem.
|
||||
@@ -119,21 +120,21 @@ class Journal_Entry_Base(BaseModel):
|
||||
if rid := values.get('id_random') or values.get('journal_entry_id_random'):
|
||||
values['id'] = rid
|
||||
values['journal_entry_id'] = rid
|
||||
|
||||
|
||||
if j_rid := values.get('journal_id_random'):
|
||||
values['journal_id'] = j_rid
|
||||
|
||||
|
||||
if a_rid := values.get('account_id_random'):
|
||||
values['account_id'] = a_rid
|
||||
|
||||
|
||||
if p_rid := values.get('parent_id_random'):
|
||||
values['parent_id'] = p_rid
|
||||
|
||||
|
||||
# 2. Prevent "Collision Population"
|
||||
for k in ['id', 'journal_entry_id', 'journal_id', 'account_id', 'parent_id']:
|
||||
if k in values and not isinstance(values[k], str):
|
||||
del values[k]
|
||||
|
||||
|
||||
return values
|
||||
|
||||
# Fields that are part of the model (for reading) but should not be saved to the DB table
|
||||
|
||||
@@ -124,7 +124,7 @@ cms_obj_li = {
|
||||
'searchable_fields': [
|
||||
'id', 'account_id', 'site_id',
|
||||
'id_random', 'account_id_random', 'site_id_random',
|
||||
'fqdn', 'access_key', 'site_access_key',
|
||||
'fqdn', 'access_key', 'site_access_key', 'site_domain_access_key',
|
||||
'enable', 'created_on', 'updated_on'
|
||||
],
|
||||
},
|
||||
|
||||
@@ -168,7 +168,9 @@ async def get_api_temp_token(
|
||||
|
||||
# --- Jitsi Token ---
|
||||
|
||||
# NOTE: This is still actively used by IDAA for their video conferences using self hosted Jitsi. Thi is actually live. We do need to change the app secret once things have stabilized.
|
||||
# NOTE: Actively used by IDAA for video conferences on self-hosted Jitsi (jitsi.dgrzone.com).
|
||||
# JWT_APP_ID and JWT_APP_SECRET must match the values in the Jitsi server .env file.
|
||||
# TODO: Rotate JWT_APP_SECRET — update it here AND in /mnt/nfs_dgr_storage/env/dgr_zone_jitsi/.env (JWT_APP_SECRET) then restart prosody + jicofo.
|
||||
|
||||
JWT_APP_ID = "my_jitsi_app_id"
|
||||
JWT_APP_SECRET = "my_jitsi_app_secret-9876543210"
|
||||
@@ -187,14 +189,12 @@ class JitsiTokenRequest(BaseModel):
|
||||
@router.post("/jitsi_token")
|
||||
async def create_jitsi_jwt(request_data: JitsiTokenRequest = Body(...)):
|
||||
log.setLevel(logging.INFO)
|
||||
if not request_data.is_moderator:
|
||||
raise HTTPException(status_code=403, detail="JWT generation is only permitted for moderators.")
|
||||
|
||||
try:
|
||||
payload = {
|
||||
"aud": JWT_APP_ID, "iss": JWT_APP_ID, "sub": JITSI_DOMAIN,
|
||||
"room": request_data.room,
|
||||
"exp": int(time.time()) + 3600,
|
||||
"exp": int(time.time()) + 7200, # 2 hour expiry
|
||||
"config": request_data.config or {},
|
||||
"context": {
|
||||
"user": {
|
||||
|
||||
@@ -61,16 +61,16 @@ async def get_obj_schema(
|
||||
):
|
||||
"""
|
||||
Dynamic Schema Introspection.
|
||||
|
||||
|
||||
Allows the frontend (e.g., Svelte/React apps) to retrieve the structure of an object type on the fly.
|
||||
Returns:
|
||||
- Database column definitions (types, defaults, nullability).
|
||||
- Pydantic model field definitions (validation rules, aliases).
|
||||
|
||||
|
||||
This enables dynamic form generation without hardcoding schemas in the frontend.
|
||||
"""
|
||||
schema_info = get_object_schema_info(obj_type, view, variant)
|
||||
|
||||
|
||||
if "error" in schema_info:
|
||||
status_code = 400 if "not found" in schema_info["error"] else 500
|
||||
return mk_resp(data=False, status_code=status_code, response=response, status_message=schema_info["error"])
|
||||
@@ -87,7 +87,7 @@ async def validate_obj_payload(
|
||||
):
|
||||
"""
|
||||
Dry-Run Payload Validation.
|
||||
|
||||
|
||||
Verifies that a payload is valid according to the Pydantic model
|
||||
without performing any database operations.
|
||||
"""
|
||||
@@ -118,7 +118,7 @@ async def get_obj(
|
||||
):
|
||||
"""
|
||||
Retrieve a Single Object.
|
||||
|
||||
|
||||
1. Resolves the public `id_random` (string) to the internal `id` (integer).
|
||||
2. Performs a SQL SELECT.
|
||||
3. Enforces Multi-Tenant access checks.
|
||||
@@ -149,10 +149,10 @@ async def get_obj(
|
||||
if account.auth_method == 'guest' or (account.account_id is None and not account.super):
|
||||
reason = account.auth_error or "Account context required."
|
||||
return mk_resp(data=False, status_code=403, response=response, status_message=reason)
|
||||
|
||||
|
||||
if not check_account_access(sql_result, account, obj_name):
|
||||
return mk_resp(data=False, status_code=403, response=response, status_message="Access denied. Record belongs to another account.")
|
||||
|
||||
|
||||
# Pass inc_hosted_file to the Pydantic model if applicable
|
||||
if obj_name == 'event_file' and inc_hosted_file:
|
||||
sql_result['inc_hosted_file'] = True
|
||||
@@ -181,7 +181,7 @@ async def get_obj_li(
|
||||
):
|
||||
"""
|
||||
List Objects (Pagination & Filtering).
|
||||
|
||||
|
||||
Supports:
|
||||
- Standard filtering (enabled/hidden).
|
||||
- Advanced filtering via JSON Payload (`jp`) param (Search, Fulltext, AND/OR queries).
|
||||
@@ -199,7 +199,7 @@ async def get_obj_li(
|
||||
and_like_dict_obj = None
|
||||
or_like_dict_obj = None
|
||||
and_in_dict_li_obj = None
|
||||
|
||||
|
||||
jp_obj = safe_json_loads(urllib.parse.unquote(jp)) if jp else None
|
||||
if jp_obj:
|
||||
if jp_obj.get('qry'): qry_dict_li = jp_obj['qry']
|
||||
@@ -213,7 +213,7 @@ async def get_obj_li(
|
||||
obj_name = obj_type_l1
|
||||
if obj_name not in obj_type_kv_li:
|
||||
return mk_resp(data=False, status_code=400, response=response, status_message=f"Object type '{obj_name}' not found.")
|
||||
|
||||
|
||||
obj_cfg = obj_type_kv_li[obj_name]
|
||||
|
||||
if obj_name == 'site' and not (for_obj_type == 'account' and for_obj_id):
|
||||
@@ -232,7 +232,7 @@ async def get_obj_li(
|
||||
|
||||
order_by_li = filter_order_by(order_by_li, base_name, table_name)
|
||||
status_filter = get_supported_filters(base_name, status_filter)
|
||||
|
||||
|
||||
if not obj_cfg.get('public_read', False):
|
||||
and_qry_dict_obj = apply_forced_account_filter(and_qry_dict_obj, account, base_name, obj_name, table_name=table_name)
|
||||
|
||||
@@ -278,10 +278,10 @@ async def get_obj_li(
|
||||
if sql_result is False:
|
||||
# Standardized rich error bubbling
|
||||
db_err = format_db_error(get_last_sql_error())
|
||||
|
||||
|
||||
# If it's a schema error (like Unknown Column), it's a 400 Bad Request
|
||||
status_code = 400 if db_err.category == "database_schema" else 500
|
||||
|
||||
|
||||
return mk_resp(data=False, status_code=status_code, response=response, status_message="Listing failed due to database error.", details=db_err.dict())
|
||||
|
||||
if sql_result:
|
||||
@@ -308,7 +308,7 @@ async def search_obj_li(
|
||||
):
|
||||
"""
|
||||
Search Objects (POST).
|
||||
|
||||
|
||||
Advanced search endpoint using `SearchQuery` body.
|
||||
- Security: Guests can access specific objects (e.g., site_domain) if permitted.
|
||||
- Filtering: Supports dynamic AND/OR filters built from the frontend.
|
||||
@@ -343,6 +343,31 @@ async def search_obj_li(
|
||||
status_filter = get_supported_filters(base_name, status_filter)
|
||||
searchable_fields = obj_cfg.get('searchable_fields')
|
||||
|
||||
# site_domain access-key enforcement:
|
||||
# - site_access_key (site-level) takes priority; site_domain_access_key used as fallback.
|
||||
# - A domain is public only if site_domain_access_key is NULL/empty (and site_access_key is also unset).
|
||||
# - Falsy access_key values (empty string, None) are stripped — treated as "no key".
|
||||
# - When a key IS provided, lib_sql_search handles the SQL expansion (see process_filter).
|
||||
if obj_name == 'site_domain':
|
||||
# Sanity check: drop access_key filters with falsy values
|
||||
if search_query.and_filters:
|
||||
search_query.and_filters = [
|
||||
f for f in search_query.and_filters
|
||||
if not (isinstance(f, SearchFilter) and f.field == 'access_key' and not f.value)
|
||||
]
|
||||
key_fields = {'access_key', 'site_access_key', 'site_domain_access_key'}
|
||||
has_key_filter = any(
|
||||
isinstance(f, SearchFilter) and f.field in key_fields
|
||||
for f in (search_query.and_filters or [])
|
||||
)
|
||||
if not has_key_filter:
|
||||
if search_query.and_filters is None:
|
||||
search_query.and_filters = []
|
||||
for col in ('site_access_key', 'site_domain_access_key'):
|
||||
search_query.and_filters.append(SearchQuery.parse_obj({
|
||||
'or': [{'field': col, 'op': 'is_null'}, {'field': col, 'op': 'eq', 'value': ''}]
|
||||
}))
|
||||
|
||||
if for_obj_type == 'account' and for_obj_id:
|
||||
if not account.super and for_obj_id != account.account_id_random:
|
||||
return mk_resp(data=False, status_code=403, response=response, status_message="Access denied to requested account.")
|
||||
@@ -388,10 +413,10 @@ async def search_obj_li(
|
||||
if sql_result is False:
|
||||
# Standardized rich error bubbling
|
||||
db_err = format_db_error(get_last_sql_error())
|
||||
|
||||
|
||||
# If it's a schema error (like Unknown Column), it's a 400 Bad Request
|
||||
status_code = 400 if db_err.category == "database_schema" else 500
|
||||
|
||||
|
||||
return mk_resp(data=False, status_code=status_code, response=response, status_message="Search failed due to database error.", details=db_err.dict())
|
||||
|
||||
if sql_result:
|
||||
@@ -414,7 +439,7 @@ async def post_obj(
|
||||
):
|
||||
"""
|
||||
Create Object.
|
||||
|
||||
|
||||
1. Injects `account_id` for ownership.
|
||||
2. **Sanitizes Payload**: Resolves `*_id_random` -> `*_id`, removes virtual fields, and view-only fields.
|
||||
- If `x-ae-ignore-extra-fields: true` header is provided, unknown fields are stripped.
|
||||
@@ -496,7 +521,7 @@ async def patch_obj(
|
||||
):
|
||||
"""
|
||||
Update Object (Partial).
|
||||
|
||||
|
||||
1. Resolves ID and checks access permissions.
|
||||
2. **Sanitizes Payload**: Resolves `*_id_random` -> `*_id`, removes virtual fields, and view-only fields.
|
||||
- If `x-ae-ignore-extra-fields: true` header is provided, unknown fields are stripped.
|
||||
@@ -557,7 +582,7 @@ async def delete_obj(
|
||||
):
|
||||
"""
|
||||
Delete Object.
|
||||
|
||||
|
||||
Supports:
|
||||
- Soft Delete: `method='hide'` or `method='disable'`.
|
||||
- Hard Delete: `method='delete'`.
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import datetime
|
||||
from fastapi import APIRouter, Body, Depends, Header, HTTPException, Path, Query, Response, status
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from typing import Dict, List, Optional, Set, Union
|
||||
|
||||
from app.lib_general import log, logging, common_route_params, Common_Route_Params
|
||||
from app.config import settings
|
||||
from app.db_sql import sql_insert, sql_update, sql_insert_or_update, sql_select, sql_delete, get_id_random, redis_lookup_id_random
|
||||
|
||||
from app.routers.api_crud import delete_obj_template, get_obj_template, get_obj_li_template, patch_obj_template, post_obj_template
|
||||
|
||||
from app.methods.event_presenter_methods import get_event_presenter_url_list
|
||||
|
||||
from app.models.response_models import Resp_Body_Base, mk_resp
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ### BEGIN ### API Event Reports ### event_id_rpt_presenter_links() ###
|
||||
# Updated 2022-04-12
|
||||
@router.get('/event/{event_id}/rpt_presenter_links', response_model=Resp_Body_Base)
|
||||
async def event_id_rpt_presenter_links(
|
||||
event_id: str = Path(min_length=11, max_length=22),
|
||||
|
||||
commons: Common_Route_Params = Depends(common_route_params),
|
||||
):
|
||||
log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
if event_id := redis_lookup_id_random(record_id_random=event_id, table_name='event'): pass
|
||||
else: return mk_resp(data=None, status_code=404, response=commons.response)
|
||||
|
||||
|
||||
if order_line_rec_list_result := get_order_line_rec_list(
|
||||
for_obj_type = obj_type,
|
||||
for_obj_id = obj_id,
|
||||
from_datetime = from_datetime,
|
||||
to_datetime = to_datetime,
|
||||
product_for_type = prod_type,
|
||||
status = status,
|
||||
full_detail = full_detail,
|
||||
# enabled = enabled,
|
||||
limit = limit,
|
||||
):
|
||||
order_line_result_list = []
|
||||
data_dict_list_for_export = []
|
||||
for order_line_rec in order_line_rec_list_result:
|
||||
if not full_detail:
|
||||
if load_order_line_result := load_order_obj_line(
|
||||
order_line_id = order_line_rec.get('order_line_id', None),
|
||||
by_alias = by_alias,
|
||||
exclude_unset = exclude_unset,
|
||||
# model_as_dict = model_as_dict,
|
||||
):
|
||||
order_line_result_list.append(load_order_line_result)
|
||||
else:
|
||||
order_line_result_list.append(None)
|
||||
else: # Uses a different view: v_order_line_full_detail
|
||||
if load_order_line_result := load_order_obj_line_full_detail(
|
||||
order_line_rec = order_line_rec,
|
||||
by_alias = by_alias,
|
||||
exclude_unset = exclude_unset,
|
||||
model_as_dict = False,
|
||||
):
|
||||
if create_export:
|
||||
data_dict = load_order_line_result.dict(by_alias=by_alias, exclude_unset=exclude_unset)
|
||||
data_dict_list_for_export.append(data_dict)
|
||||
order_line_result_list.append(load_order_line_result)
|
||||
else:
|
||||
order_line_result_list.append(None)
|
||||
response_data = order_line_result_list
|
||||
elif isinstance(order_line_rec_list_result, list) or order_line_rec_list_result is None: # Empty list or None
|
||||
log.info('No results')
|
||||
return mk_resp(data=None, status_code=404, response=response) # Not Found
|
||||
else:
|
||||
log.warning('Likely bad request')
|
||||
return mk_resp(data=False, status_code=400, response=response) # Bad Request
|
||||
|
||||
if create_export:
|
||||
# column_name_li = ['order_id_random', 'order_line_id_random', '', 'product_name', 'quantity', 'amount', 'dollar_amount', 'person_email']
|
||||
|
||||
# column_name_li = ['order_line_id_random', 'order_id_random', 'product_id_random', 'product_type', 'product_name', 'product_unit_price', 'product_recurring', 'curr_product_id_random', 'curr_product_type', 'curr_product_type_name', 'curr_product_name', 'name', 'quantity', 'amount', 'dollar_amount', 'recurring', 'message', 'person_id_random', 'person_given_name', 'person_family_name', 'person_full_name', 'person_full_name_override', 'person_contact_email', 'person_contact_cc_email', 'person_contact_phone_mobile', 'person_contact_phone_home', 'person_contact_phone_office', 'person_contact_phone_land', 'person_contact_phone_fax', 'person_contact_phone_other', 'person_contact_address_name', 'person_contact_address_organization_name', 'person_contact_address_line_1', 'person_contact_address_line_2', 'person_contact_address_line_3', 'person_contact_address_city', 'person_contact_address_country_subdivision_code', 'person_contact_address_state_province', 'person_contact_address_postal_code', 'person_contact_address_country_alpha_2_code', 'person_contact_address_country_name', 'person_contact_address_country', 'order_status', 'order_created_on', 'order_updated_on', 'created_on', 'updated_on']
|
||||
|
||||
column_name_li = [
|
||||
'event_presenter_id_random',
|
||||
'event_id_random',
|
||||
'events_session_id_random',
|
||||
'events_presentation_id_random',
|
||||
'event_presenter_given_name',
|
||||
'event_presenter_family_name',
|
||||
'event_presenter_email',
|
||||
'event_presenter_created_on', 'event_presenter_updated_on'
|
||||
]
|
||||
|
||||
|
||||
# column_name_li = []
|
||||
datetime_format='%Y-%m-%d_%H%M'
|
||||
|
||||
# current_datetime = datetime.datetime.now() # Servers timezone (Eastern)
|
||||
current_datetime_utc = datetime.datetime.utcnow()
|
||||
current_datetime_utc = current_datetime_utc.strftime(datetime_format)
|
||||
filename = f'order_line_list_{current_datetime_utc}'
|
||||
if result := create_export_file(data_dict_list=data_dict_list_for_export, column_name_li=column_name_li, subdir_path='order_line', filename=filename, export_type='Excel'):
|
||||
tmp_file_path = result
|
||||
else:
|
||||
log.error('Something went wrong while creating or saving the export file')
|
||||
tmp_file_path = result
|
||||
else: tmp_file_path = None
|
||||
|
||||
return mk_resp(data=response_data, tmp_file_path=tmp_file_path, response=response)
|
||||
# ### END ### API Event Reports ### get_obj_id_order_line_list() ###
|
||||
@@ -1,480 +0,0 @@
|
||||
import datetime
|
||||
from fastapi import APIRouter, Body, Depends, Header, HTTPException, Path, Query, Response, status
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from typing import Dict, List, Optional, Set, Union
|
||||
|
||||
from app.lib_general import *
|
||||
from app.config import settings
|
||||
from app.db_sql import *
|
||||
|
||||
from app.routers.api_crud import delete_obj_template, get_obj_template, get_obj_li_template, patch_obj_template, post_obj_template
|
||||
|
||||
from app.methods.order_methods import create_order_obj, update_order_obj, get_order_rec_list, load_order_obj, save_order_obj
|
||||
from app.methods.order_line_methods import create_order_obj_line, update_order_obj_line, load_order_obj_line
|
||||
|
||||
from app.models.response_models import Resp_Body_Base, mk_resp
|
||||
from app.models.order_models_v3 import Order_Base
|
||||
from app.models.order_line_models_v3 import Order_Line_Base
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ### BEGIN ### API Order Routers ### post_order_obj() ###
|
||||
# Updated 2022-01-18
|
||||
@router.post('/v3/order', response_model=Resp_Body_Base)
|
||||
@router.post('/v3/person/{person_id}/order', response_model=Resp_Body_Base)
|
||||
async def post_order_obj(
|
||||
order_obj: Order_Base,
|
||||
person_id: str = Path(min_length=11, max_length=22),
|
||||
|
||||
inc_address: bool = False,
|
||||
inc_contact: bool = False,
|
||||
inc_order_line_list: bool = True,
|
||||
inc_person: bool = False,
|
||||
return_obj: bool = True,
|
||||
|
||||
commons: Common_Route_Params = Depends(common_route_params),
|
||||
):
|
||||
log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
# ### SECTION ### Secondary data validation
|
||||
if person_id := redis_lookup_id_random(record_id_random=person_id, table_name='person'): pass
|
||||
# elif person_id is None: pass
|
||||
else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The person ID was invalid or not found.')
|
||||
|
||||
# ### SECTION ### Process data
|
||||
if order_id := create_order_obj(
|
||||
account_id = commons.x_account_id,
|
||||
person_id = person_id,
|
||||
order_dict_obj = order_obj,
|
||||
): pass
|
||||
else:
|
||||
return mk_resp(data=False, status_code=400, response=commons.response, status_message='Something failed while processing the data.') # Bad Request
|
||||
|
||||
# ### SECTION ### Return successful results
|
||||
if return_obj:
|
||||
if load_order_obj_result := load_order_obj(
|
||||
order_id = order_id,
|
||||
inc_address = inc_address,
|
||||
inc_contact = inc_contact,
|
||||
inc_order_line_list = inc_order_line_list,
|
||||
inc_person = inc_person,
|
||||
).dict(by_alias=commons.by_alias, exclude_unset=commons.exclude_unset):
|
||||
log.info('Loading successful. Returning result')
|
||||
log.debug(load_order_obj_result)
|
||||
return mk_resp(data=load_order_obj_result, response=commons.response)
|
||||
elif isinstance(load_order_obj_result, list) or load_order_obj_result is None: # Empty list or None
|
||||
return mk_resp(data=None, status_code=404, response=commons.response) # Not Found
|
||||
else:
|
||||
return mk_resp(data=False, status_code=400, response=commons.response) # Bad Request
|
||||
else:
|
||||
order_id_random = get_id_random(record_id=order_id, table_name='order')
|
||||
data = {}
|
||||
data['order_id'] = order_id
|
||||
data['order_id_random'] = order_id_random
|
||||
return mk_resp(data=data, response=commons.response)
|
||||
# ### END ### API Order Routers ### post_order_obj() ###
|
||||
|
||||
|
||||
# ### BEGIN ### API Order Routers ### patch_order_obj() ###
|
||||
# Updated 2022-01-18
|
||||
@router.patch('/v3/order/{order_id}', response_model=Resp_Body_Base)
|
||||
# @router.patch('/v3/person/{person_id}/order/{order_id}', response_model=Resp_Body_Base)
|
||||
async def patch_order_obj(
|
||||
order_obj: Order_Base,
|
||||
order_id: str = Path(min_length=11, max_length=22),
|
||||
# person_id: str = Query(None, min_length=11, max_length=22),
|
||||
|
||||
inc_address: bool = False,
|
||||
inc_contact: bool = False,
|
||||
inc_order_line_list: bool = True,
|
||||
inc_person: bool = False,
|
||||
return_obj: Optional[bool] = True,
|
||||
|
||||
commons: Common_Route_Params = Depends(common_route_params),
|
||||
):
|
||||
log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
# ### SECTION ### Secondary data validation
|
||||
order_id_random = order_id # This is used later for the response data
|
||||
# person_id_random = person_id # This is used later for the response data
|
||||
|
||||
if order_id := redis_lookup_id_random(record_id_random=order_id, table_name='order'): pass
|
||||
else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The order ID was invalid or not found.')
|
||||
|
||||
# if person_id := redis_lookup_id_random(record_id_random=person_id, table_name='person'): pass
|
||||
# elif person_id is None: pass
|
||||
# else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The person ID was invalid or not found.')
|
||||
|
||||
# ### SECTION ### Process data
|
||||
if update_order_obj_result := update_order_obj(
|
||||
order_id = order_id,
|
||||
order_dict_obj = order_obj,
|
||||
# person_id = person_id,
|
||||
): pass
|
||||
else:
|
||||
return mk_resp(data=False, status_code=400, response=commons.response, status_message='Something failed while processing the data.') # Bad Request
|
||||
|
||||
# ### SECTION ### Return successful results
|
||||
if return_obj:
|
||||
if load_order_obj_result := load_order_obj(
|
||||
order_id = order_id,
|
||||
inc_address = inc_address,
|
||||
inc_contact = inc_contact,
|
||||
inc_order_line_list = inc_order_line_list,
|
||||
inc_person = inc_person,
|
||||
).dict(by_alias=commons.by_alias, exclude_unset=commons.exclude_unset):
|
||||
log.info('Loading successful. Returning result')
|
||||
log.debug(load_order_obj_result)
|
||||
return mk_resp(data=load_order_obj_result, response=commons.response)
|
||||
elif isinstance(load_order_obj_result, list) or load_order_obj_result is None: # Empty list or None
|
||||
return mk_resp(data=None, status_code=404, response=commons.response) # Not Found
|
||||
else:
|
||||
return mk_resp(data=False, status_code=400, response=commons.response) # Bad Request
|
||||
else:
|
||||
data = {}
|
||||
data['order_id'] = order_id
|
||||
data['order_id_random'] = order_id_random
|
||||
return mk_resp(data=data, response=commons.response)
|
||||
# ### END ### API Order Routers ### patch_order_obj() ###
|
||||
|
||||
|
||||
# ### BEGIN ### API Order Routers ### patch_order_obj_add_line() ###
|
||||
# Updated 2022-01-18
|
||||
@router.patch('/v3/order/{order_id}/line/add', response_model=Resp_Body_Base)
|
||||
async def patch_order_obj_add_line(
|
||||
order_line_obj: Order_Line_Base,
|
||||
order_id: str = Path(min_length=11, max_length=22),
|
||||
|
||||
# inc_order: bool = False,
|
||||
inc_order_line_list: bool = True,
|
||||
return_obj: Optional[bool] = True,
|
||||
|
||||
commons: Common_Route_Params = Depends(common_route_params),
|
||||
):
|
||||
log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
# ### SECTION ### Secondary data validation
|
||||
order_id_random = order_id # This is used later for the response data
|
||||
|
||||
if order_id := redis_lookup_id_random(record_id_random=order_id, table_name='order'): pass
|
||||
else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The order ID was invalid or not found.')
|
||||
|
||||
# ### SECTION ### Process data
|
||||
if order_line_id := add_order_obj_line(
|
||||
order_id = order_id,
|
||||
order_line_dict_obj = order_line_obj,
|
||||
): pass
|
||||
else:
|
||||
return mk_resp(data=False, status_code=400, response=commons.response, status_message='Something failed while processing the data.') # Bad Request
|
||||
|
||||
# ### SECTION ### Return successful results
|
||||
if return_obj:
|
||||
if load_order_obj_result := load_order_obj(
|
||||
order_id = order_id,
|
||||
# inc_address = inc_address,
|
||||
# inc_contact = inc_contact,
|
||||
inc_order_line_list = inc_order_line_list,
|
||||
# inc_person = inc_person,
|
||||
).dict(by_alias=commons.by_alias, exclude_unset=commons.exclude_unset):
|
||||
log.info('Loading successful. Returning result')
|
||||
log.debug(load_order_obj_result)
|
||||
return mk_resp(data=load_order_obj_result, response=commons.response)
|
||||
elif isinstance(load_order_obj_result, list) or load_order_obj_result is None: # Empty list or None
|
||||
return mk_resp(data=None, status_code=404, response=commons.response) # Not Found
|
||||
else:
|
||||
return mk_resp(data=False, status_code=400, response=commons.response) # Bad Request
|
||||
else:
|
||||
order_line_id = order_line_add_result
|
||||
order_line_id_random = get_id_random(record_id=order_line_id, table_name='order_line')
|
||||
data = {}
|
||||
data['order_id'] = order_id
|
||||
data['order_id_random'] = order_id_random
|
||||
data['order_line_id'] = order_line_id
|
||||
data['order_line_id_random'] = order_line_id_random
|
||||
return mk_resp(data=data, response=commons.response)
|
||||
# ### END ### API Order Routers ### patch_order_obj_add_line() ###
|
||||
|
||||
|
||||
# ### BEGIN ### API Order Routers ### patch_order_obj_update_line() ###
|
||||
# Updated 2022-01-18
|
||||
@router.patch('/v3/order/{order_id}/line/{order_line_id}/update', response_model=Resp_Body_Base)
|
||||
async def patch_order_obj_update_line(
|
||||
order_obj: Order_Line_Base,
|
||||
order_id: str = Path(min_length=11, max_length=22),
|
||||
order_line_id: str = Path(min_length=11, max_length=22),
|
||||
|
||||
# inc_order: bool = False,
|
||||
inc_order_line_list: bool = True,
|
||||
return_obj: Optional[bool] = True,
|
||||
|
||||
commons: Common_Route_Params = Depends(common_route_params),
|
||||
):
|
||||
log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
# ### SECTION ### Secondary data validation
|
||||
order_id_random = order_id # This is used later for the response data
|
||||
order_line_id_random = order_line_id # This is used later for the response data
|
||||
|
||||
if order_id := redis_lookup_id_random(record_id_random=order_id, table_name='order'): pass
|
||||
else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The order ID was invalid or not found.')
|
||||
|
||||
if order_line_id := redis_lookup_id_random(record_id_random=order_line_id, table_name='order_line'): pass
|
||||
else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The order line ID was invalid or not found.')
|
||||
|
||||
# ### SECTION ### Process data
|
||||
if update_order_obj_line_result := update_order_obj_line(
|
||||
order_line_id = order_line_id,
|
||||
order_line_dict_obj = order_line_obj,
|
||||
): pass
|
||||
else:
|
||||
return mk_resp(data=False, status_code=400, response=commons.response, status_message='Something failed while processing the data.') # Bad Request
|
||||
|
||||
# ### SECTION ### Return successful results
|
||||
if return_obj:
|
||||
if load_order_obj_result := load_order_obj(
|
||||
order_id = order_id,
|
||||
# inc_address = inc_address,
|
||||
# inc_contact = inc_contact,
|
||||
inc_order_line_list = inc_order_line_list,
|
||||
# inc_person = inc_person,
|
||||
).dict(by_alias=commons.by_alias, exclude_unset=commons.exclude_unset):
|
||||
log.info('Loading successful. Returning result')
|
||||
log.debug(order_dict)
|
||||
return mk_resp(data=order_dict, response=commons.response)
|
||||
elif isinstance(load_order_obj_result, list) or load_order_obj_result is None: # Empty list or None
|
||||
return mk_resp(data=None, status_code=404, response=commons.response) # Not Found
|
||||
else:
|
||||
return mk_resp(data=False, status_code=400, response=commons.response) # Bad Request
|
||||
else:
|
||||
data = {}
|
||||
data['order_id'] = order_id
|
||||
data['order_id_random'] = order_id_random
|
||||
data['order_line_id'] = order_line_id
|
||||
data['order_line_id_random'] = order_line_id_random
|
||||
return mk_resp(data=data, response=commons.response)
|
||||
# ### END ### API Order Routers ### patch_order_obj_update_line() ###
|
||||
|
||||
|
||||
# ### BEGIN ### API Order Routers ### patch_order_obj_remove_line() ###
|
||||
# Updated 2022-01-18
|
||||
@router.patch('/v3/order/{order_id}/line/{order_line_id}/remove', response_model=Resp_Body_Base)
|
||||
async def patch_order_obj_remove_line(
|
||||
order_obj: Order_Line_Base,
|
||||
order_id: str = Path(min_length=11, max_length=22),
|
||||
order_line_id: str = Path(min_length=11, max_length=22),
|
||||
|
||||
# inc_order: bool = False,
|
||||
inc_order_line_list: bool = True,
|
||||
return_obj: Optional[bool] = True,
|
||||
|
||||
commons: Common_Route_Params = Depends(common_route_params),
|
||||
):
|
||||
log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
# ### SECTION ### Secondary data validation
|
||||
order_id_random = order_id # This is used later for the response data
|
||||
order_line_id_random = order_line_id # This is used later for the response data
|
||||
|
||||
if order_id := redis_lookup_id_random(record_id_random=order_id, table_name='order'): pass
|
||||
else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The order ID was invalid or not found.')
|
||||
|
||||
if order_line_id := redis_lookup_id_random(record_id_random=order_line_id, table_name='order_line'): pass
|
||||
else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The order line ID was invalid or not found.')
|
||||
|
||||
# ### SECTION ### Process data
|
||||
if remove_order_obj_line_result := remove_order_obj_line(
|
||||
order_line_id = order_line_id,
|
||||
): pass
|
||||
else:
|
||||
return mk_resp(data=False, status_code=400, response=commons.response, status_message='Something failed while processing the data.') # Bad Request
|
||||
|
||||
# ### SECTION ### Return successful results
|
||||
if return_obj:
|
||||
if load_order_obj_result := load_order_obj(
|
||||
order_id = order_id,
|
||||
# inc_address = inc_address,
|
||||
# inc_contact = inc_contact,
|
||||
inc_order_line_list = inc_order_line_list,
|
||||
# inc_person = inc_person,
|
||||
).dict(by_alias=commons.by_alias, exclude_unset=commons.exclude_unset):
|
||||
log.info('Loading successful. Returning result')
|
||||
log.debug(order_dict)
|
||||
return mk_resp(data=order_dict, response=commons.response)
|
||||
elif isinstance(load_order_obj_result, list) or load_order_obj_result is None: # Empty list or None
|
||||
return mk_resp(data=None, status_code=404, response=commons.response) # Not Found
|
||||
else:
|
||||
return mk_resp(data=False, status_code=400, response=commons.response) # Bad Request
|
||||
else:
|
||||
data = {}
|
||||
data['order_id'] = order_id
|
||||
data['order_id_random'] = order_id_random
|
||||
data['order_line_id'] = order_line_id
|
||||
data['order_line_id_random'] = order_line_id_random
|
||||
return mk_resp(data=data, response=commons.response)
|
||||
# ### END ### API Order Routers ### patch_order_obj_remove_line() ###
|
||||
|
||||
|
||||
# ### BEGIN ### API Order Routers ### get_order_obj_li() ###
|
||||
# Updated 2022-01-18
|
||||
@router.get('/v3/{for_obj_type}/{for_obj_id}/order/list', response_model=Resp_Body_Base)
|
||||
async def get_order_obj_li(
|
||||
for_obj_type: str = Path(min_length=2, max_length=50),
|
||||
for_obj_id: str = Path(min_length=11, max_length=22),
|
||||
order_status: str = 'complete',
|
||||
order_checkout_status: str = 'complete',
|
||||
from_datetime: datetime.datetime = None,
|
||||
to_datetime: datetime.datetime = None,
|
||||
|
||||
inc_address: bool = False,
|
||||
inc_contact: bool = False,
|
||||
inc_order_cfg: bool = False,
|
||||
inc_order_line_list: bool = False,
|
||||
inc_person: bool = False,
|
||||
|
||||
commons: Common_Route_Params = Depends(common_route_params),
|
||||
):
|
||||
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
if obj_type in ['account', 'person']: pass
|
||||
else: return mk_resp(data=False, status_code=400, response=response, status_message='The object type passed was invalid or not found. Expecting "account" or "person".') # Bad Request
|
||||
|
||||
if obj_type_id := redis_lookup_id_random(record_id_random=for_obj_id, table_name=for_obj_type): pass
|
||||
else: return mk_resp(data=False, status_code=404, response=commons.response) # Not Found
|
||||
|
||||
if get_order_rec_list_result := get_order_rec_list(
|
||||
for_obj_type = for_obj_type,
|
||||
for_obj_id = for_obj_id,
|
||||
from_datetime = from_datetime,
|
||||
to_datetime = to_datetime,
|
||||
status = order_status,
|
||||
# checkout_status = order_checkout_status,
|
||||
enabled = commons.enabled,
|
||||
limit = commons.limit,
|
||||
offset = commons.offset,
|
||||
):
|
||||
order_obj_list = []
|
||||
for order_rec in get_order_rec_list_result:
|
||||
if load_order_obj_result := load_order_obj(
|
||||
order_id = order_rec.get('order_id'),
|
||||
inc_address = inc_address,
|
||||
inc_contact = inc_contact,
|
||||
inc_order_cfg = inc_order_cfg,
|
||||
inc_order_line_list = inc_order_line_list,
|
||||
inc_person = inc_person,
|
||||
enabled = commons.enabled,
|
||||
limit = commons.limit,
|
||||
by_alias = commons.by_alias,
|
||||
exclude_unset = commons.exclude_unset,
|
||||
# model_as_dict = model_as_dict,
|
||||
):
|
||||
log.debug(load_order_obj_result)
|
||||
order_obj_list.append(load_order_obj_result)
|
||||
else:
|
||||
order_obj_list.append(None)
|
||||
log.info('Loading successful. Returning result')
|
||||
log.debug(order_obj_list)
|
||||
return mk_resp(data=order_obj_list, response=commons.response)
|
||||
elif isinstance(get_order_rec_list_result, list) or get_order_rec_list_result is None: # Empty list or None
|
||||
return mk_resp(data=None, status_code=404, response=commons.response) # Not Found
|
||||
else:
|
||||
return mk_resp(data=False, status_code=400, response=commons.response) # Bad Request
|
||||
# ### END ### API Order Routers ### get_order_obj_li() ###
|
||||
|
||||
|
||||
# ### BEGIN ### API Order Routes ### get_order_obj() ###
|
||||
# NOTE 2021-08-09: Use with rework of order_cart
|
||||
# Updated 2022-12-18
|
||||
@router.get('/v3/order/{order_id}', response_model=Resp_Body_Base)
|
||||
async def get_order_obj(
|
||||
order_id: str = Path(min_length=11, max_length=22),
|
||||
|
||||
inc_address: bool = False,
|
||||
inc_contact: bool = False,
|
||||
inc_order_cfg: bool = False,
|
||||
inc_order_line_list: bool = False,
|
||||
inc_person: bool = False,
|
||||
|
||||
commons: Common_Route_Params = Depends(common_route_params),
|
||||
):
|
||||
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
if order_id := redis_lookup_id_random(record_id_random=order_id, table_name='order'): pass
|
||||
else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The order ID was invalid or not found.')
|
||||
|
||||
if load_order_obj_result := load_order_obj(
|
||||
order_id = order_id,
|
||||
inc_address = inc_address,
|
||||
inc_contact = inc_contact,
|
||||
inc_order_cfg = inc_order_cfg,
|
||||
inc_order_line_list = inc_order_line_list,
|
||||
inc_person = inc_person,
|
||||
limit = commons.limit,
|
||||
enabled = commons.enabled,
|
||||
by_alias = commons.by_alias,
|
||||
exclude_unset = commons.exclude_unset,
|
||||
# model_as_dict = model_as_dict,
|
||||
):
|
||||
log.debug(load_order_obj_result)
|
||||
order_dict = load_order_obj_result.dict(by_alias=commons.by_alias, exclude_unset=False) # NOTE NOTE NOTE NOTE exclude_unset is forced to False for now. Will return more fields than is ideal. Need to create another Order_Line_Base. Probably Order_Line_OUT_Base
|
||||
log.info('Loading successful. Returning result')
|
||||
return mk_resp(data=order_dict, response=commons.response)
|
||||
elif isinstance(load_order_obj_result, list) or load_order_obj_result is None: # Empty list or None
|
||||
return mk_resp(data=None, status_code=404, response=commons.response) # Not Found
|
||||
else:
|
||||
return mk_resp(data=False, status_code=400, response=commons.response) # Bad Request
|
||||
# ### END ### API Order Routes ### get_order_obj() ###
|
||||
|
||||
|
||||
# ### BEGIN ### API Order ### get_person_id_order_cart() ###
|
||||
# NOTE 2021-08-09: Use with rework of order_cart. The most recent (hopefully only one) "open" order for a person.
|
||||
# Updated 2022-12-18
|
||||
@router.get('/v3/person/{person_id}/order/cart', response_model=Resp_Body_Base)
|
||||
async def get_person_id_order_cart(
|
||||
person_id: str = Path(min_length=11, max_length=22),
|
||||
enabled: str = 'enabled',
|
||||
inc_order_line_list: bool = False,
|
||||
inc_order_cfg: bool = False,
|
||||
|
||||
commons: Common_Route_Params = Depends(common_route_params),
|
||||
):
|
||||
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
if person_id := redis_lookup_id_random(record_id_random=person_id, table_name='person'): pass
|
||||
else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The person ID was invalid or not found.')
|
||||
|
||||
# Query to get the one "open" order status for a person ID
|
||||
|
||||
return False
|
||||
# ### END ### API Order ### get_person_id_order_cart() ###
|
||||
|
||||
|
||||
# ### BEGIN ### API Order Routers ### delete_order_obj() ###
|
||||
# Updated 2022-01-18
|
||||
@router.delete('/v3/order/{order_id}', response_model=Resp_Body_Base)
|
||||
async def delete_order_obj(
|
||||
order_id: str = Path(min_length=11, max_length=22),
|
||||
commons: Common_Route_Params = Depends(common_route_params),
|
||||
):
|
||||
log.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
if order_id := redis_lookup_id_random(record_id_random=order_id, table_name='order'): pass
|
||||
else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The order ID was invalid or not found.')
|
||||
|
||||
obj_type = 'order'
|
||||
result = delete_obj_template(
|
||||
obj_type = obj_type,
|
||||
obj_id = obj_id,
|
||||
)
|
||||
return result
|
||||
# ### END ### API Order Routers ### delete_order_obj() ###
|
||||
@@ -16,9 +16,9 @@ from app.methods.event_person_methods import create_event_person_obj, create_upd
|
||||
# from app.methods.event_presenter_methods import create_update_event_presenter_obj_v4, get_event_presenter_rec_list, load_event_presenter_obj
|
||||
from app.methods.hosted_file_methods import load_hosted_file_obj, save_file
|
||||
|
||||
from app.models.event_models import Event_Base
|
||||
#from app.models.event_models import Event_Base
|
||||
# from app.models.event_location_models import Event_Location_Base
|
||||
from app.models.event_person_models import Event_Person_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_presenter_models import Event_Presenter_Base
|
||||
# from app.models.event_session_models import Event_Session_Base
|
||||
@@ -472,4 +472,303 @@ async def event_id_badge_import(
|
||||
if return_detail:
|
||||
return mk_resp(data=event_badge_person_li, status_message=f'Importing badges from file. Found {len(person_li)} badges.', response=commons.response)
|
||||
else:
|
||||
return mk_resp(data=event_badge_person_summary_li, status_message=f'Checked for badges from file. Found {len(event_badge_person_li)} badges.', response=commons.response)
|
||||
return mk_resp(data=event_badge_person_summary_li, status_message=f'Checked for badges from file. Found {len(event_badge_person_li)} badges.', response=commons.response)
|
||||
|
||||
|
||||
# ### BEGIN ### Zoom Events CSV Badge Import ### event_id_badge_import_zoom_csv() ###
|
||||
# Accepts a Zoom Events registrant CSV export and upserts event_person records.
|
||||
# Zoom CSV format: fixed columns (First name, Last name, Registrant email, Ticket name,
|
||||
# Unique identifier, etc.) plus per-ticket-type custom fields using the pattern
|
||||
# "FieldLabel_*_TicketTypeName". Delimiter is auto-detected (Zoom exports vary).
|
||||
# Updated 2026-04-06
|
||||
|
||||
# Notes specific to Axonius 2026
|
||||
|
||||
# SELECT id, badge_type, badge_type_code
|
||||
# FROM event_badge
|
||||
# WHERE badge_type = 'In-Person Attendee';
|
||||
|
||||
# UPDATE event_badge
|
||||
# SET badge_type_code = 'attendee'
|
||||
# WHERE badge_type = 'In-Person Attendee';
|
||||
|
||||
# SELECT id, badge_type, badge_type_code
|
||||
# FROM event_badge
|
||||
# WHERE badge_type = 'Adapt26 Sponsor';
|
||||
|
||||
# UPDATE event_badge
|
||||
# SET badge_type_code = 'sponsor'
|
||||
# WHERE badge_type = 'Adapt26 Sponsor';
|
||||
|
||||
|
||||
def _zoom_ticket_field(record: dict, field_prefix: str, ticket_name: str) -> str:
|
||||
"""
|
||||
Extracts a per-ticket-type field value from a Zoom CSV row.
|
||||
Tries the exact ticket match first, then falls back to the first non-empty value
|
||||
across all variants of that field prefix.
|
||||
"""
|
||||
exact_key = f'{field_prefix}_*_{ticket_name}'
|
||||
if val := str(record.get(exact_key, '')).strip():
|
||||
return val
|
||||
for key, val in record.items():
|
||||
if key.startswith(f'{field_prefix}_*_') and str(val).strip():
|
||||
return str(val).strip()
|
||||
return ''
|
||||
|
||||
|
||||
@router.post('/event/{event_id}/badge/import/zoom_csv', response_model=Resp_Body_Base)
|
||||
async def event_id_badge_import_zoom_csv(
|
||||
event_id: str = Path(min_length=11, max_length=22),
|
||||
file: UploadFile = File(...),
|
||||
|
||||
begin_at: int = 0,
|
||||
end_at: int = 20000,
|
||||
|
||||
return_detail: bool = False,
|
||||
|
||||
commons: Common_Route_Params = Depends(common_route_params),
|
||||
):
|
||||
"""
|
||||
Import event badges from a Zoom Events registrant CSV export.
|
||||
|
||||
Zoom exports fixed columns (First name, Last name, Registrant email, Ticket name,
|
||||
Unique identifier) plus per-ticket-type custom fields in the format
|
||||
"FieldLabel_*_TicketTypeName". The 'Unique identifier' column is used as the
|
||||
external_registration_id. Delimiter is auto-detected.
|
||||
"""
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
account_id = commons.x_account_id
|
||||
|
||||
event_id_random = event_id
|
||||
if event_id := redis_lookup_id_random(record_id_random=event_id, table_name='event'): pass
|
||||
else: return mk_resp(data=None, status_code=404, response=commons.response)
|
||||
|
||||
link_to_type = 'event'
|
||||
link_to_id = event_id
|
||||
|
||||
file_info = await save_file(
|
||||
file=file,
|
||||
account_id=account_id,
|
||||
link_to_type=link_to_type,
|
||||
link_to_id=link_to_id,
|
||||
)
|
||||
if file_info['saved']:
|
||||
log.info('File saved')
|
||||
else:
|
||||
log.error('Something may have gone wrong while saving the uploaded file?')
|
||||
return mk_resp(data=None, status_code=500, response=commons.response)
|
||||
|
||||
hosted_files_path = settings.FILES_PATH['hosted_files_root']
|
||||
subdirectory_dest = os.path.join(hosted_files_path, file_info.get('subdirectory_path'))
|
||||
hash_filename = file_info.get('hash_sha256') + '.file'
|
||||
full_file_path = pathlib.Path(os.path.join(subdirectory_dest, hash_filename))
|
||||
|
||||
if not full_file_path.exists():
|
||||
log.warning(f'Not found at full file path: {full_file_path}')
|
||||
return mk_resp(data=None, status_code=500, response=commons.response)
|
||||
|
||||
# Zoom CSV layout: row 1 = "Report generated" metadata, row 2 = blank, row 3 = headers
|
||||
# Delimiter is auto-detected (Zoom exports vary between comma and tab)
|
||||
df = pandas.read_csv(
|
||||
full_file_path,
|
||||
sep=None,
|
||||
engine='python',
|
||||
skiprows=2,
|
||||
na_filter=False,
|
||||
dtype=str,
|
||||
)
|
||||
|
||||
df_dict = df.to_dict(orient='records')
|
||||
log.info(f'Zoom CSV total record count: {len(df_dict)}')
|
||||
|
||||
loop_count = 0
|
||||
event_badge_person_li = []
|
||||
event_badge_person_summary_li = []
|
||||
|
||||
log.setLevel(logging.DEBUG)
|
||||
for record in df_dict:
|
||||
log.info(f'Loop Count: {loop_count}')
|
||||
loop_count += 1
|
||||
if loop_count <= begin_at: continue
|
||||
if loop_count > end_at: break
|
||||
|
||||
# Force use of Registrant email as the external_id for Zoom CSV imports.
|
||||
# Many Zoom exports (for this group) have a useless "Unique identifier"
|
||||
# column that contains "N/A" for every row — rely on email instead.
|
||||
email = str(record.get('Registrant email', '')).strip()
|
||||
if not email:
|
||||
log.warning('Row missing registrant email — skipping.')
|
||||
continue
|
||||
external_id = email
|
||||
|
||||
# Sanitize the Unique identifier value and only use it as the
|
||||
# external_registration_id if it appears meaningful. Treat common
|
||||
# placeholders like 'N/A'/'NA'/'UNKNOWN' as missing.
|
||||
unique_id_raw = str(record.get('Unique identifier', '')).strip()
|
||||
if unique_id_raw and unique_id_raw.upper() not in ('N/A', 'NA', 'UNKNOWN'):
|
||||
external_registration_id = unique_id_raw
|
||||
else:
|
||||
external_registration_id = None
|
||||
|
||||
ticket_name = str(record.get('Ticket name', '')).strip()
|
||||
given_name = str(record.get('First name', '')).strip()
|
||||
family_name = str(record.get('Last name', '')).strip()
|
||||
display_name = str(record.get('Display name', '')).strip()
|
||||
|
||||
# Per-ticket-type custom fields
|
||||
organization = _zoom_ticket_field(record, 'Organization', ticket_name)
|
||||
professional_title = _zoom_ticket_field(record, 'Job title', ticket_name)
|
||||
phone = (_zoom_ticket_field(record, 'Phone', ticket_name)
|
||||
or _zoom_ticket_field(record, 'Phone number', ticket_name))
|
||||
address_line_1 = (_zoom_ticket_field(record, 'Address line 1', ticket_name)
|
||||
or _zoom_ticket_field(record, 'Address', ticket_name))
|
||||
address_line_2 = _zoom_ticket_field(record, 'Address line 2', ticket_name)
|
||||
address_line_3 = _zoom_ticket_field(record, 'Address line 3', ticket_name)
|
||||
city = _zoom_ticket_field(record, 'City', ticket_name)
|
||||
state_province = _zoom_ticket_field(record, 'State/Province', ticket_name)
|
||||
state_province_abb = _zoom_ticket_field(record, 'State/Province Abb', ticket_name)
|
||||
postal_code = (_zoom_ticket_field(record, 'Postal code', ticket_name)
|
||||
or _zoom_ticket_field(record, 'Zip code', ticket_name)
|
||||
or _zoom_ticket_field(record, 'Zip/Postal Code', ticket_name))
|
||||
country = _zoom_ticket_field(record, 'Country/Region', ticket_name)
|
||||
country_alpha_2_code = _zoom_ticket_field(record, 'Country Alpha 2 Code', ticket_name)
|
||||
country_subdivision_code = _zoom_ticket_field(record, 'Country Subdivision Code', ticket_name)
|
||||
# location, full_address, location_long, location_short are computed by DB triggers
|
||||
|
||||
event_person_summary = {
|
||||
'event_id': event_id,
|
||||
'event_id_random': event_id_random,
|
||||
'external_id': external_id,
|
||||
'given_name': given_name,
|
||||
'family_name': family_name,
|
||||
'email': email,
|
||||
}
|
||||
|
||||
# TEMPORARY: Axonius-specific mapping for certain ticket / badge labels
|
||||
# to internal `badge_type_code` values. Remove after the event (~2 weeks).
|
||||
normalized_ticket = ticket_name.strip().lower()
|
||||
badge_type_code = None
|
||||
if 'sponsor' in normalized_ticket:
|
||||
badge_type_code = 'sponsor'
|
||||
elif 'attend' in normalized_ticket or 'attendee' in normalized_ticket:
|
||||
badge_type_code = 'attendee'
|
||||
if badge_type_code:
|
||||
log.info(f"Axonius mapping applied: '{ticket_name}' -> '{badge_type_code}'")
|
||||
|
||||
event_person_data = {
|
||||
'account_id': account_id,
|
||||
'event_id': event_id,
|
||||
'enable': True,
|
||||
'external_id': external_id,
|
||||
'external_registration_id': external_registration_id,
|
||||
'event_person_profile': {
|
||||
'event_id': event_id,
|
||||
'enable': True,
|
||||
'given_name': given_name,
|
||||
'family_name': family_name,
|
||||
'full_name': display_name or f'{given_name} {family_name}'.strip(),
|
||||
'email': email,
|
||||
'phone': phone,
|
||||
'address_line_1': address_line_1,
|
||||
'address_line_2': address_line_2,
|
||||
'address_line_3': address_line_3,
|
||||
'city': city,
|
||||
'state_province': state_province,
|
||||
'state_province_abb': state_province_abb,
|
||||
'postal_code': postal_code,
|
||||
'country': country,
|
||||
'country_alpha_2_code': country_alpha_2_code,
|
||||
'country_subdivision_code': country_subdivision_code,
|
||||
'professional_title': professional_title,
|
||||
'affiliations': organization,
|
||||
},
|
||||
'event_badge': {
|
||||
# 'event_id': event_id,
|
||||
'enable': True,
|
||||
'external_id': external_id,
|
||||
'external_registration_id': external_registration_id,
|
||||
'given_name': given_name,
|
||||
'family_name': family_name,
|
||||
'full_name': display_name or f'{given_name} {family_name}'.strip(),
|
||||
'email': email,
|
||||
'phone': phone,
|
||||
'address_line_1': address_line_1,
|
||||
'address_line_2': address_line_2,
|
||||
'address_line_3': address_line_3,
|
||||
'city': city,
|
||||
'state_province': state_province,
|
||||
'state_province_abb': state_province_abb,
|
||||
'postal_code': postal_code,
|
||||
'country': country,
|
||||
'country_alpha_2_code': country_alpha_2_code,
|
||||
'country_subdivision_code': country_subdivision_code,
|
||||
'professional_title': professional_title,
|
||||
'affiliations': organization,
|
||||
# TEMPORARY: Axonius export does not include a badge template id.
|
||||
# Default to the Axonius group's badge template `RKYp2HcQm9o (21)`.
|
||||
# This is a temporary hardcode — remove or replace when mapping is provided.
|
||||
'event_badge_template_id': 21,
|
||||
'event_badge_template_id_random': 'RKYp2HcQm9o',
|
||||
'badge_type': ticket_name,
|
||||
'badge_type_code': badge_type_code,
|
||||
},
|
||||
}
|
||||
|
||||
# Look up existing event_person by event_id + external_id (should be 0 or 1).
|
||||
sql_select_event_person = """
|
||||
SELECT id AS event_person_id, id_random AS event_person_id_random,
|
||||
external_id AS event_person_external_id,
|
||||
event_badge_id AS event_badge_id,
|
||||
event_person_profile_id AS event_person_profile_id
|
||||
FROM `event_person`
|
||||
WHERE event_person.event_id = :event_id
|
||||
AND event_person.external_id = :external_id
|
||||
/*LIMIT 2*/;
|
||||
"""
|
||||
|
||||
event_person_result = sql_select(sql=sql_select_event_person, data=event_person_summary)
|
||||
if event_person_result:
|
||||
# If multiple rows are returned that's an integrity problem — log it and
|
||||
# use the first row for the update to avoid creating duplicates.
|
||||
if isinstance(event_person_result, list):
|
||||
log.error(f'Found more than one Event Person with external_id={external_id}. Count: {len(event_person_result)}')
|
||||
event_person_result = event_person_result[0]
|
||||
|
||||
event_person_id = event_person_result.get('event_person_id')
|
||||
event_badge_id = event_person_result.get('event_badge_id')
|
||||
event_person_profile_id = event_person_result.get('event_person_profile_id')
|
||||
log.info(f'Found Event Person. Updating existing... Event Person ID: {event_person_id}')
|
||||
|
||||
updated_id = create_update_event_person_obj_v4(
|
||||
event_person_dict_obj=event_person_data,
|
||||
event_person_id=event_person_id,
|
||||
account_id=account_id,
|
||||
event_id=event_id,
|
||||
event_badge_id=event_badge_id,
|
||||
event_person_profile_id=event_person_profile_id,
|
||||
)
|
||||
if updated_id:
|
||||
log.warning(f'Event Person updated. ID: {updated_id}')
|
||||
else:
|
||||
log.warning(f'Event Person not updated. ID: {event_person_id}')
|
||||
else:
|
||||
log.info('No Event Person found. Creating new...')
|
||||
result_id = create_update_event_person_obj_v4(
|
||||
event_person_dict_obj=event_person_data,
|
||||
account_id=account_id,
|
||||
event_id=event_id,
|
||||
)
|
||||
if result_id:
|
||||
log.warning(f'Event Person created. ID: {result_id}')
|
||||
else:
|
||||
log.warning('Event Person not created.')
|
||||
|
||||
# Record the processed input for response summary after DB ops.
|
||||
event_badge_person_li.append(event_person_data)
|
||||
event_badge_person_summary_li.append(event_person_summary)
|
||||
|
||||
if return_detail:
|
||||
return mk_resp(data=event_badge_person_li, status_message=f'Zoom CSV import complete. Processed {len(event_badge_person_li)} records.', response=commons.response)
|
||||
else:
|
||||
return mk_resp(data=event_badge_person_summary_li, status_message=f'Zoom CSV import complete. Processed {len(event_badge_person_summary_li)} records.', response=commons.response)
|
||||
@@ -23,7 +23,7 @@ def setup_routers(app: FastAPI):
|
||||
|
||||
app.include_router(api.router, prefix='/api', tags=['API'])
|
||||
# app.include_router(flask_cfg.router, prefix='/flask_cfg', tags=['Flask CFG'], dependencies=[Depends(DeprecationParams)])
|
||||
app.include_router(importing.router, prefix='/importing', tags=['Importing'], dependencies=[Depends(DeprecationParams)])
|
||||
# app.include_router(importing.router, prefix='/importing', tags=['Importing'], dependencies=[Depends(DeprecationParams)])
|
||||
# app.include_router(sql.router, tags=['SQL']) # LEGACY (disabled) - raw SQL select endpoint, testing only
|
||||
# app.include_router(account.router, tags=['Account'], dependencies=[Depends(DeprecationParams)])
|
||||
|
||||
@@ -62,13 +62,14 @@ def setup_routers(app: FastAPI):
|
||||
# app.include_router(qr.router, tags=['QR'], dependencies=[Depends(DeprecationParams)])
|
||||
# app.include_router(site.router, tags=['Site'], dependencies=[Depends(DeprecationParams)])
|
||||
# app.include_router(site_domain.router, tags=['Site Domain'], dependencies=[Depends(DeprecationParams)])
|
||||
app.include_router(user.router, tags=['User'], dependencies=[Depends(DeprecationParams)])
|
||||
# app.include_router(user.router, tags=['User'], dependencies=[Depends(DeprecationParams)])
|
||||
app.include_router(util_email.router, tags=['Utility: Email'])
|
||||
# app.include_router(websockets.router, tags=['Websockets']) # LEGACY (disabled) - superseded by Websockets V3
|
||||
# app.include_router(websockets_redis.router, tags=['Websockets (Redis)']) # LEGACY (disabled) - superseded by Websockets V3
|
||||
app.include_router(websockets_v3.router, prefix='/v3', tags=['Websockets V3'])
|
||||
|
||||
app.include_router(e_confex.router, prefix='/e/confex', tags=['External Service: Confex'])
|
||||
app.include_router(e_cvent.router, prefix='/e/cvent', tags=['External Service: Cvent'])
|
||||
app.include_router(e_impexium.router, prefix='/e/impexium', tags=['External Service: Impexium'])
|
||||
app.include_router(e_stripe.router, prefix='/e/stripe', tags=['External Service: Stripe'])
|
||||
# ALERT: Temporarily commenting these out until needed for external service integrations. They can be re-enabled as needed.
|
||||
# app.include_router(e_confex.router, prefix='/e/confex', tags=['External Service: Confex'])
|
||||
# app.include_router(e_cvent.router, prefix='/e/cvent', tags=['External Service: Cvent'])
|
||||
# app.include_router(e_impexium.router, prefix='/e/impexium', tags=['External Service: Impexium'])
|
||||
# app.include_router(e_stripe.router, prefix='/e/stripe', tags=['External Service: Stripe'])
|
||||
|
||||
@@ -77,18 +77,20 @@ async def patch_site_domain_obj(
|
||||
@router.get('/site/domain/fqdn/{fqdn}', response_model=Resp_Body_Base)
|
||||
async def lookup_site_domain_obj(
|
||||
fqdn: str,
|
||||
# x_account_id: str = Header(...),
|
||||
# response: Response = Response,
|
||||
|
||||
commons: Common_Route_Params_Min = Depends(common_route_params_min),
|
||||
# x_account_id: str = Header(...),
|
||||
# response: Response = Response,
|
||||
access_key: Optional[str] = Query(None, min_length=4, max_length=50),
|
||||
referrer: Optional[str] = Query(None, min_length=8, max_length=150),
|
||||
commons: Common_Route_Params_Min = Depends(common_route_params_min),
|
||||
):
|
||||
log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL
|
||||
log.debug(locals())
|
||||
|
||||
|
||||
# Updated 2021-12-13
|
||||
# Updated 2021-12-13
|
||||
if site_domain_rec_list_result := lookup_site_domain_fqdn(
|
||||
fqdn = fqdn,
|
||||
access_key = access_key,
|
||||
referrer = referrer,
|
||||
enabled = commons.enabled,
|
||||
limit = commons.limit,
|
||||
offset = commons.offset
|
||||
|
||||
@@ -44,6 +44,37 @@ When the frontend first loads and doesn't know the `account_id`, it performs a "
|
||||
* Returns 200 + a list containing the `account_id` (random string ID) and `site_id` (random string ID).
|
||||
* ** デザイン Choice:** If the domain is not found, it returns **200 OK with an empty list `[]`**. It is NOT a 404.
|
||||
|
||||
> **Access Key Support**
|
||||
>
|
||||
> Some client deployments restrict their domain via an access key passed in the browser URL (e.g. `?key=abc123`). The frontend reads this param and forwards it as `access_key` in the POST body.
|
||||
>
|
||||
> **How to pass the key:**
|
||||
> ```json
|
||||
> {
|
||||
> "and": [
|
||||
> { "field": "fqdn", "op": "eq", "value": "client.example.com" },
|
||||
> { "field": "access_key", "op": "eq", "value": "abc123" }
|
||||
> ]
|
||||
> }
|
||||
> ```
|
||||
> If `key` is absent, empty, or falsy — **omit `access_key` from the payload entirely**. Do not send `"access_key": ""`.
|
||||
>
|
||||
> **Server behavior:**
|
||||
> - `site_access_key` (site-level key) takes priority. If set, all domains under that site require it.
|
||||
> - `site_domain_access_key` (domain-level key) is used as fallback when `site_access_key` is not set.
|
||||
> - A domain is **public** only when **both** key columns are NULL/empty.
|
||||
> - Falsy `access_key` values are ignored server-side as a safety net.
|
||||
> - Match → `200` with the record. No match → `200` with empty list `[]`.
|
||||
> - Do **not** use `access_code_kv_json` for this — that field is for UI features only.
|
||||
>
|
||||
> | Browser URL | `access_key` in payload | Result |
|
||||
> |---|---|---|
|
||||
> | `https://dev-demo.oneskyit.com` | *(omit)* | ✅ Returns record (public) |
|
||||
> | `https://client.example.com/?key=correct` | `"correct"` | ✅ Returns record |
|
||||
> | `https://client.example.com/` | *(omit)* | ❌ Empty (key required) |
|
||||
> | `https://client.example.com/?key=wrong` | `"wrong"` | ❌ Empty (wrong key) |
|
||||
> | `https://client.example.com/?key=` | *(omit — strip empty)* | ❌ Empty (key required) |
|
||||
>
|
||||
---
|
||||
|
||||
## 3. Standard CRUD Patterns
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Set up project root for imports
|
||||
sys.path.append(os.getcwd())
|
||||
|
||||
# 1. Initialize Mock Config Helper BEFORE other imports
|
||||
import tests.mock_config_helper
|
||||
from app.config import settings
|
||||
|
||||
# Now set some REAL values for DB connection so it actually works
|
||||
import os
|
||||
settings.DB_SERVER = "vpn-db.oneskyit.com"
|
||||
settings.DB_USER = "aether_dev"
|
||||
settings.DB_PASS = "$1sky.AE_dev.2023"
|
||||
settings.DB_NAME = "aether_dev"
|
||||
settings.DB_PORT = 3306
|
||||
settings.REDIS = {"server": "127.0.0.1", "port": 6379}
|
||||
settings.FILES_PATH = {"hosted_files_root": "/home/scott/tmp/gemini_trash"} # Dummy
|
||||
|
||||
from app.methods.event_file_methods import load_event_file_obj
|
||||
from app.db_sql import get_id_random
|
||||
|
||||
print("--- Testing get_id_random directly ---")
|
||||
print(f"event ID 1 -> {get_id_random(1, 'event')}")
|
||||
print(f"session ID 543 -> {get_id_random(543, 'event_session')}")
|
||||
print(f"presenter ID 1629 -> {get_id_random(1629, 'event_presenter')}")
|
||||
|
||||
print("\n--- Testing load_event_file_obj for a2pPIT_W28o ---")
|
||||
res = load_event_file_obj('a2pPIT_W28o', model_as_dict=True)
|
||||
if res:
|
||||
import json
|
||||
print(json.dumps(res, indent=4))
|
||||
else:
|
||||
print("Failed to load object.")
|
||||
185
tests/e2e/test_e2e_jitsi_token.py
Normal file
185
tests/e2e/test_e2e_jitsi_token.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
Jitsi JWT Token E2E Test Suite
|
||||
|
||||
Tests the /api/jitsi_token endpoint to verify:
|
||||
- Moderator tokens contain moderator=true in the JWT payload
|
||||
- Attendee tokens contain moderator=false in the JWT payload
|
||||
- Room claim is correctly scoped per request
|
||||
- Basic validation rejects malformed input
|
||||
|
||||
Run from project root:
|
||||
./environment/bin/python3 tests/e2e/test_e2e_jitsi_token.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import base64
|
||||
import time
|
||||
import requests
|
||||
|
||||
sys.path.append(os.getcwd())
|
||||
|
||||
# --- Configuration ---
|
||||
API_ROOT = "https://dev-api.oneskyit.com"
|
||||
JITSI_ENDPOINT = f"{API_ROOT}/api/jitsi_token"
|
||||
|
||||
TEST_ROOM = "idaa-test-room-001"
|
||||
TEST_NAME = "E2E Test User"
|
||||
TEST_EMAIL = "e2e-test@oneskyit.com"
|
||||
|
||||
|
||||
def print_result(label, success, message=""):
|
||||
status = "✅ PASS" if success else "❌ FAIL"
|
||||
suffix = f" — {message}" if message else ""
|
||||
print(f" [{status}] {label}{suffix}")
|
||||
|
||||
|
||||
def decode_jwt_payload(token: str) -> dict:
|
||||
"""Decode a JWT payload without signature verification (for inspection)."""
|
||||
try:
|
||||
parts = token.split(".")
|
||||
if len(parts) != 3:
|
||||
return {}
|
||||
# Add padding
|
||||
padded = parts[1] + "=" * (4 - len(parts[1]) % 4)
|
||||
return json.loads(base64.urlsafe_b64decode(padded))
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def test_moderator_token():
|
||||
"""Request a moderator JWT and verify the claim is set correctly."""
|
||||
print("\n--- Test: Moderator Token ---")
|
||||
payload = {
|
||||
"room": TEST_ROOM,
|
||||
"name": TEST_NAME,
|
||||
"email": TEST_EMAIL,
|
||||
"is_moderator": True,
|
||||
}
|
||||
resp = requests.post(JITSI_ENDPOINT, json=payload)
|
||||
print_result("HTTP 200", resp.status_code == 200, f"status={resp.status_code}")
|
||||
|
||||
if resp.status_code != 200:
|
||||
print(f" Response: {resp.text}")
|
||||
return None
|
||||
|
||||
token = resp.json().get("token")
|
||||
print_result("Token returned", bool(token))
|
||||
if not token:
|
||||
return None
|
||||
|
||||
decoded = decode_jwt_payload(token)
|
||||
print(f" Decoded payload: {json.dumps(decoded, indent=6)}")
|
||||
|
||||
moderator_claim = decoded.get("context", {}).get("user", {}).get("moderator")
|
||||
room_claim = decoded.get("room")
|
||||
|
||||
print_result("moderator == True", moderator_claim is True, f"got: {moderator_claim!r}")
|
||||
print_result("room scoped correctly", room_claim == TEST_ROOM, f"got: {room_claim!r}")
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def test_attendee_token():
|
||||
"""Request a non-moderator JWT and verify the claim is False."""
|
||||
print("\n--- Test: Attendee Token (is_moderator=False) ---")
|
||||
payload = {
|
||||
"room": TEST_ROOM,
|
||||
"name": TEST_NAME,
|
||||
"email": TEST_EMAIL,
|
||||
"is_moderator": False,
|
||||
}
|
||||
resp = requests.post(JITSI_ENDPOINT, json=payload)
|
||||
print_result("HTTP 200", resp.status_code == 200, f"status={resp.status_code}")
|
||||
|
||||
if resp.status_code != 200:
|
||||
print(f" Response: {resp.text}")
|
||||
return None
|
||||
|
||||
token = resp.json().get("token")
|
||||
print_result("Token returned", bool(token))
|
||||
if not token:
|
||||
return None
|
||||
|
||||
decoded = decode_jwt_payload(token)
|
||||
print(f" Decoded payload: {json.dumps(decoded, indent=6)}")
|
||||
|
||||
moderator_claim = decoded.get("context", {}).get("user", {}).get("moderator")
|
||||
print_result("moderator == False", moderator_claim is False, f"got: {moderator_claim!r}")
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def test_room_isolation():
|
||||
"""Verify two requests for different rooms produce different room claims."""
|
||||
print("\n--- Test: Room Isolation ---")
|
||||
rooms = ["room-alpha", "room-beta"]
|
||||
tokens = []
|
||||
for room in rooms:
|
||||
resp = requests.post(JITSI_ENDPOINT, json={
|
||||
"room": room, "name": TEST_NAME, "email": TEST_EMAIL, "is_moderator": False
|
||||
})
|
||||
if resp.status_code == 200:
|
||||
tokens.append((room, decode_jwt_payload(resp.json().get("token", ""))))
|
||||
|
||||
if len(tokens) == 2:
|
||||
match_0 = tokens[0][1].get("room") == tokens[0][0]
|
||||
match_1 = tokens[1][1].get("room") == tokens[1][0]
|
||||
print_result("room-alpha scoped", match_0, f"got: {tokens[0][1].get('room')!r}")
|
||||
print_result("room-beta scoped", match_1, f"got: {tokens[1][1].get('room')!r}")
|
||||
print_result("Rooms differ", tokens[0][1].get("room") != tokens[1][1].get("room"))
|
||||
else:
|
||||
print_result("Both requests succeeded", False, "could not get both tokens")
|
||||
|
||||
|
||||
def test_invalid_email():
|
||||
"""Verify that a malformed email is rejected with 422."""
|
||||
print("\n--- Test: Input Validation (bad email) ---")
|
||||
payload = {
|
||||
"room": TEST_ROOM,
|
||||
"name": TEST_NAME,
|
||||
"email": "not-an-email",
|
||||
"is_moderator": False,
|
||||
}
|
||||
resp = requests.post(JITSI_ENDPOINT, json=payload)
|
||||
print_result("422 on bad email", resp.status_code == 422, f"status={resp.status_code}")
|
||||
|
||||
|
||||
def test_token_expiry():
|
||||
"""Verify the exp claim is approximately 1 hour from now."""
|
||||
print("\n--- Test: Token Expiry (exp claim) ---")
|
||||
payload = {
|
||||
"room": TEST_ROOM, "name": TEST_NAME, "email": TEST_EMAIL, "is_moderator": False
|
||||
}
|
||||
resp = requests.post(JITSI_ENDPOINT, json=payload)
|
||||
if resp.status_code != 200:
|
||||
print_result("HTTP 200 (skipping exp check)", False)
|
||||
return
|
||||
|
||||
decoded = decode_jwt_payload(resp.json().get("token", ""))
|
||||
exp = decoded.get("exp")
|
||||
now = int(time.time())
|
||||
ttl = exp - now if exp else 0
|
||||
# Should be ~3600s (allow 30s window for test runtime)
|
||||
ok = 3550 < ttl <= 3600
|
||||
print_result("exp ≈ now + 3600s", ok, f"ttl={ttl}s")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
suite_start = time.time()
|
||||
print("=" * 55)
|
||||
print(" Jitsi JWT Token — E2E Test Suite")
|
||||
print(f" Endpoint: {JITSI_ENDPOINT}")
|
||||
print("=" * 55)
|
||||
|
||||
test_moderator_token()
|
||||
test_attendee_token()
|
||||
test_room_isolation()
|
||||
test_invalid_email()
|
||||
test_token_expiry()
|
||||
|
||||
elapsed = time.time() - suite_start
|
||||
print(f"\n{'=' * 55}")
|
||||
print(f" Suite completed in {elapsed:.2f}s")
|
||||
print("=" * 55)
|
||||
@@ -6,13 +6,13 @@ import os
|
||||
# --- Configuration ---
|
||||
API_ROOT = "https://dev-api.oneskyit.com"
|
||||
# Using the key provided in your examples
|
||||
API_KEY = "dFP6J9DVj9hUgIMn-fNIqg"
|
||||
API_KEY = "dFP6J9DVj9hUgIMn-fNIqg"
|
||||
# A known private journal ID from account 1
|
||||
PRIVATE_JOURNAL_ID = "SWFK-48-89-90"
|
||||
# A known public object type/ID
|
||||
PUBLIC_FQDN = "dev-app.oneskyit.com"
|
||||
# A known valid account ID random for testing restoration of access
|
||||
VALID_ACCOUNT_ID_RAND = "_XY7DXtc9MY"
|
||||
VALID_ACCOUNT_ID_RAND = "_XY7DXtc9MY"
|
||||
|
||||
def print_result(label, success, message=""):
|
||||
status = "✅ PASS" if success else "❌ FAIL"
|
||||
@@ -24,13 +24,13 @@ def test_hardened_search_leak():
|
||||
url = f"{API_ROOT}/v3/crud/journal/search"
|
||||
headers = {"x-aether-api-key": API_KEY}
|
||||
# NO account header, NO JWT
|
||||
payload = {"and": []}
|
||||
|
||||
payload = {"and": []}
|
||||
|
||||
resp = requests.post(url, headers=headers, json=payload)
|
||||
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json().get('data', [])
|
||||
# Should be 0 because all journals in DB have an account_id,
|
||||
# Should be 0 because all journals in DB have an account_id,
|
||||
# and we are now strictly filtering for account_id IS NULL.
|
||||
success = (len(data) == 0)
|
||||
print_result("Leak Blocked (Journal Search)", success, f"- Found {len(data)} records (Expected 0)")
|
||||
@@ -43,9 +43,9 @@ def test_strict_id_block():
|
||||
url = f"{API_ROOT}/v3/crud/journal/{PRIVATE_JOURNAL_ID}"
|
||||
headers = {"x-aether-api-key": API_KEY}
|
||||
# NO account header, NO JWT
|
||||
|
||||
|
||||
resp = requests.get(url, headers=headers)
|
||||
|
||||
|
||||
success = (resp.status_code == 403)
|
||||
print_result("Access Denied (Journal GET ID)", success, f"- Status: {resp.status_code} (Expected 403)")
|
||||
|
||||
@@ -55,9 +55,9 @@ def test_bootstrap_exception():
|
||||
url = f"{API_ROOT}/v3/crud/site_domain/search"
|
||||
headers = {"x-aether-api-key": API_KEY}
|
||||
payload = {"and": [{"field": "fqdn", "op": "eq", "value": PUBLIC_FQDN}]}
|
||||
|
||||
|
||||
resp = requests.post(url, headers=headers, json=payload)
|
||||
|
||||
|
||||
success = (resp.status_code == 200 and len(resp.json().get('data', [])) > 0)
|
||||
print_result("Bootstrap Allowed (Site Domain)", success, f"- Status: {resp.status_code}")
|
||||
|
||||
@@ -69,22 +69,82 @@ def test_restored_access():
|
||||
"x-aether-api-key": API_KEY,
|
||||
"x-account-id": VALID_ACCOUNT_ID_RAND
|
||||
}
|
||||
|
||||
|
||||
resp = requests.get(url, headers=headers)
|
||||
|
||||
|
||||
success = (resp.status_code == 200)
|
||||
print_result("Access Restored (Journal with Header)", success, f"- Status: {resp.status_code}")
|
||||
|
||||
|
||||
def test_site_domain_access_key():
|
||||
"""
|
||||
Verify site_domain lookup respects access_key.
|
||||
|
||||
The frontend reads the 'key' query param from the browser URL and forwards it
|
||||
as 'access_key' in the POST body. No key means a public domain is expected.
|
||||
|
||||
Valid (should return a result):
|
||||
https://dev-demo.oneskyit.com — public, no key needed
|
||||
http://idaa.localhost:5173/?key=restricted — correct key
|
||||
https://dev-idaa.oneskyit.com/?key=restricted-access — correct key
|
||||
https://sk-idaa.oneskyit.com/?key=8VTOJ0X5hvT6JdiTJsGEzQ — correct key
|
||||
|
||||
Invalid (should return empty):
|
||||
http://idaa.localhost:5173/ — key required, none given
|
||||
http://idaa.localhost:5173/?key=bad-key-example — wrong key
|
||||
https://dev-idaa.oneskyit.com/ — key required, none given
|
||||
https://dev-idaa.oneskyit.com/?key= — empty key treated as none
|
||||
https://dev-idaa.oneskyit.com/?key=any-wrong-key — wrong key
|
||||
https://sk-idaa.oneskyit.com/ — key required, none given
|
||||
https://sk-idaa.oneskyit.com/?key=another-bad-key-example — wrong key
|
||||
"""
|
||||
print("\n--- Test 5: Site Domain Access Key Behavior ---")
|
||||
url = f"{API_ROOT}/v3/crud/site_domain/search"
|
||||
headers = {"x-aether-api-key": API_KEY}
|
||||
|
||||
cases = [
|
||||
# (fqdn, key, should_pass, label)
|
||||
# --- valid ---
|
||||
("dev-demo.oneskyit.com", None, True, "public domain, no key"),
|
||||
("idaa.localhost:5173", "restricted", True, "correct key"),
|
||||
("dev-idaa.oneskyit.com", "restricted-access", True, "correct key"),
|
||||
("sk-idaa.oneskyit.com", "8VTOJ0X5hvT6JdiTJsGEzQ", True, "correct key"),
|
||||
# --- invalid ---
|
||||
("idaa.localhost:5173", None, False, "key required, none given"),
|
||||
("idaa.localhost:5173", "bad-key-example", False, "wrong key"),
|
||||
("dev-idaa.oneskyit.com", None, False, "key required, none given"),
|
||||
("dev-idaa.oneskyit.com", "", False, "empty key treated as none"),
|
||||
("dev-idaa.oneskyit.com", "any-wrong-key", False, "wrong key"),
|
||||
("sk-idaa.oneskyit.com", None, False, "key required, none given"),
|
||||
("sk-idaa.oneskyit.com", "another-bad-key-example", False, "wrong key"),
|
||||
]
|
||||
|
||||
for fqdn, key, should_pass, label in cases:
|
||||
payload = {"and": [{"field": "fqdn", "op": "eq", "value": fqdn}]}
|
||||
# Omit access_key entirely when None (no key in URL); send it when present (even if empty)
|
||||
if key is not None:
|
||||
payload["and"].append({"field": "access_key", "op": "eq", "value": key})
|
||||
|
||||
try:
|
||||
resp = requests.post(url, headers=headers, json=payload)
|
||||
data = resp.json().get('data', []) if resp.status_code == 200 else []
|
||||
success = (resp.status_code == 200 and ((len(data) > 0) == should_pass))
|
||||
tag = "VALID " if should_pass else "INVALID"
|
||||
print_result(f"[{tag}] {fqdn} key={key!r:30} ({label})", success, f"- Count: {len(data)}")
|
||||
except Exception as e:
|
||||
print_result(f"[{'VALID ' if should_pass else 'INVALID'}] {fqdn} key={key!r}", False, f"- Exception: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Starting V3 Security Hardening Verification")
|
||||
print(f"Target: {API_ROOT}")
|
||||
|
||||
|
||||
try:
|
||||
test_hardened_search_leak()
|
||||
test_strict_id_block()
|
||||
test_bootstrap_exception()
|
||||
test_restored_access()
|
||||
test_site_domain_access_key()
|
||||
except Exception as e:
|
||||
print(f"\n❌ ERROR during test execution: {e}")
|
||||
|
||||
|
||||
print("\nVerification completed.")
|
||||
|
||||
Reference in New Issue
Block a user