diff --git a/alembic/versions/0f529c470393_add_pending_role_ids_to_notifications.py b/alembic/versions/0f529c470393_add_pending_role_ids_to_notifications.py new file mode 100644 index 0000000..0d734e9 --- /dev/null +++ b/alembic/versions/0f529c470393_add_pending_role_ids_to_notifications.py @@ -0,0 +1,32 @@ +"""add pending_role_ids to notifications + +Revision ID: 0f529c470393 +Revises: 98df8301ae46 +Create Date: 2026-02-16 22:35:37.433719 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '0f529c470393' +down_revision: Union[str, Sequence[str], None] = '98df8301ae46' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('notification', sa.Column('pending_role_ids', sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('notification', 'pending_role_ids') + # ### end Alembic commands ### diff --git a/database_utils/models/auth.py b/database_utils/models/auth.py index a2233a1..951a3a8 100644 --- a/database_utils/models/auth.py +++ b/database_utils/models/auth.py @@ -7,6 +7,7 @@ from ..utils.timezone_utils import now_gt from datetime import datetime +from typing import Optional import uuid # Association table for many-to-many relationship between Role and Permission @@ -146,6 +147,7 @@ class Notification(Base): email = Column(String, nullable=False, unique=True) age = Column(Integer, nullable=False) password_hash = Column(String, nullable=False) + pending_role_ids: Mapped[Optional[list]] = mapped_column(JSON, nullable=True) # --- [END] Possible User data: Add User Fields --- company_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("company.id", ondelete="CASCADE"), nullable=False) diff --git a/database_utils/utils/audit_utils.py b/database_utils/utils/audit_utils.py index a6aa0c0..af7e782 100644 --- a/database_utils/utils/audit_utils.py +++ b/database_utils/utils/audit_utils.py @@ -258,3 +258,31 @@ def log_custom_operation( details=details, ip_address=ip_address ) + + +def serialize_for_audit(data: dict) -> dict: + """Convert a model column dict to JSON-safe types for audit logging. + + Handles UUID, datetime, date, and Decimal types that are not + JSON-serializable by default. + + Args: + data: Dictionary of model column values to serialize. + + Returns: + A new dictionary with all non-JSON-serializable values converted to str. + """ + from uuid import UUID + from datetime import datetime, date + from decimal import Decimal + + def _convert(v): + if isinstance(v, (UUID, datetime, date, Decimal)): + return str(v) + if isinstance(v, dict): + return {k: _convert(val) for k, val in v.items()} + if isinstance(v, list): + return [_convert(item) for item in v] + return v + + return {k: _convert(v) for k, v in data.items()} diff --git a/database_utils/utils/pagination_utils.py b/database_utils/utils/pagination_utils.py new file mode 100644 index 0000000..353894f --- /dev/null +++ b/database_utils/utils/pagination_utils.py @@ -0,0 +1,20 @@ +import math + + +def compute_pagination(page: int, page_size: int, total_count: int) -> tuple[int, int, int]: + """Compute pagination values for a SQLAlchemy query. + + Args: + page: Current page number (1-indexed). + page_size: Number of records per page. + total_count: Total number of records matching the query filters. + + Returns: + tuple: (skip, total_count, total_pages) + - skip: number of records to offset + - total_count: same as input (passed through for convenience) + - total_pages: total number of pages (minimum 1) + """ + skip = (page - 1) * page_size + total_pages = math.ceil(total_count / page_size) if total_count > 0 else 1 + return skip, total_count, total_pages