Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@
"files.insertFinalNewline": true,
"python.testing.pytestEnabled": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.tabSize": 4,
"editor.codeActionsOnSave": {
"source.organizeImports": "never"
"source.organizeImports": "never",
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit"
}
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
Expand Down
4 changes: 3 additions & 1 deletion requirements-base.in
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ duo-client
email-validator
emails
fastapi==0.115.12
fastapi-filter
advanced-alchemy
google-api-python-client
google-auth-oauthlib
h11
Expand Down Expand Up @@ -54,7 +56,7 @@ slowapi
spacy==3.8.5
sqlalchemy-filters
sqlalchemy-utils
sqlalchemy==2.0.8
sqlalchemy
statsmodels
tabulate
tenacity
Expand Down
12 changes: 10 additions & 2 deletions requirements-base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#
--index-url https://pypi.netflix.net/simple

advanced-alchemy==1.4.4
# via -r requirements-base.in
aiocache==0.12.3
# via -r requirements-base.in
aiofiles==24.1.0
Expand All @@ -17,7 +19,9 @@ aiohttp==3.11.12
aiosignal==1.3.2
# via aiohttp
alembic==1.14.1
# via -r requirements-base.in
# via
# -r requirements-base.in
# advanced-alchemy
annotated-types==0.7.0
# via pydantic
anyio==4.8.0
Expand Down Expand Up @@ -148,6 +152,8 @@ googleapis-common-protos==1.67.0
# via google-api-core
graphql-core==3.2.6
# via hypothesis-graphql
greenlet==3.2.3
# via advanced-alchemy
h11==0.14.0
# via
# -r requirements-base.in
Expand Down Expand Up @@ -457,9 +463,10 @@ spacy-legacy==3.0.12
# via spacy
spacy-loggers==1.0.5
# via spacy
sqlalchemy==2.0.8
sqlalchemy==2.0.41
# via
# -r requirements-base.in
# advanced-alchemy
# alembic
# sqlalchemy-filters
# sqlalchemy-utils
Expand Down Expand Up @@ -509,6 +516,7 @@ typer==0.15.1
typing-extensions==4.13.2
# via
# -r requirements-base.in
# advanced-alchemy
# alembic
# anyio
# fastapi
Expand Down
269 changes: 269 additions & 0 deletions src/dispatch/case/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
"""FastAPI-Filter implementation for Case filtering."""

from __future__ import annotations

from datetime import datetime

from fastapi_filter.contrib.sqlalchemy import Filter
from pydantic import Field
from sqlalchemy.orm import Query
from sqlalchemy.orm.query import Query
Copy link

Copilot AI Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate import of Query; remove the redundant import on this line to clean up dependencies.

Suggested change
from sqlalchemy.orm.query import Query

Copilot uses AI. Check for mistakes.

from dispatch.case.enums import CaseResolutionReason, CaseStatus
from dispatch.case.models import Case
from dispatch.enums import Visibility


class CaseFilter(Filter):
"""FastAPI-Filter implementation for Case filtering."""

# Basic field filters
id__in: list[int] | None = Field(None, description="Filter by case IDs")
name__ilike: str | None = Field(None, description="Search by case name (case-insensitive)")
title__ilike: str | None = Field(None, description="Search by case title (case-insensitive)")
description__ilike: str | None = Field(
None, description="Search by case description (case-insensitive)"
)
resolution__ilike: str | None = Field(
None, description="Search by resolution text (case-insensitive)"
)

# Enum filters
resolution_reason__in: list[CaseResolutionReason] | None = Field(
None, description="Filter by resolution reasons"
)
status__in: list[CaseStatus] | None = Field(None, description="Filter by case statuses")
visibility__in: list[Visibility] | None = Field(None, description="Filter by visibility levels")

# Location and team filters
participants_team__ilike: str | None = Field(None, description="Search by participants team")
participants_location__ilike: str | None = Field(
None, description="Search by participants location"
)

# Boolean filters
dedicated_channel: bool | None = Field(None, description="Filter by dedicated channel status")
event: bool | None = Field(None, description="Filter by event status")

# Date range filters for reported_at
reported_at__gte: datetime | None = Field(
None, description="Cases reported on or after this date"
)
reported_at__lte: datetime | None = Field(
None, description="Cases reported on or before this date"
)
reported_at__gt: datetime | None = Field(None, description="Cases reported after this date")
reported_at__lt: datetime | None = Field(None, description="Cases reported before this date")

# Date range filters for triage_at
triage_at__gte: datetime | None = Field(None, description="Cases triaged on or after this date")
triage_at__lte: datetime | None = Field(
None, description="Cases triaged on or before this date"
)
triage_at__gt: datetime | None = Field(None, description="Cases triaged after this date")
triage_at__lt: datetime | None = Field(None, description="Cases triaged before this date")

# Date range filters for escalated_at
escalated_at__gte: datetime | None = Field(
None, description="Cases escalated on or after this date"
)
escalated_at__lte: datetime | None = Field(
None, description="Cases escalated on or before this date"
)
escalated_at__gt: datetime | None = Field(None, description="Cases escalated after this date")
escalated_at__lt: datetime | None = Field(None, description="Cases escalated before this date")

# Date range filters for closed_at
closed_at__gte: datetime | None = Field(None, description="Cases closed on or after this date")
closed_at__lte: datetime | None = Field(None, description="Cases closed on or before this date")
closed_at__gt: datetime | None = Field(None, description="Cases closed after this date")
closed_at__lt: datetime | None = Field(None, description="Cases closed before this date")

# Date range filters for created_at
created_at__gte: datetime | None = Field(
None, description="Cases created on or after this date"
)
created_at__lte: datetime | None = Field(
None, description="Cases created on or before this date"
)
created_at__gt: datetime | None = Field(None, description="Cases created after this date")
created_at__lt: datetime | None = Field(None, description="Cases created before this date")

# Date range filters for updated_at
updated_at__gte: datetime | None = Field(
None, description="Cases updated on or after this date"
)
updated_at__lte: datetime | None = Field(
None, description="Cases updated on or before this date"
)
updated_at__gt: datetime | None = Field(None, description="Cases updated after this date")
updated_at__lt: datetime | None = Field(None, description="Cases updated before this date")

# Relationship filters
case_type_id__in: list[int] | None = Field(None, description="Filter by case type IDs")
case_priority_id__in: list[int] | None = Field(None, description="Filter by case priority IDs")
case_severity_id__in: list[int] | None = Field(None, description="Filter by case severity IDs")

# Participant relationship filters
assignee_id__in: list[int] | None = Field(
None, description="Filter by assignee participant IDs"
)
reporter_id__in: list[int] | None = Field(
None, description="Filter by reporter participant IDs"
)

# Tag relationship filters
tag_id__in: list[int] | None = Field(None, description="Filter by tag IDs")
tag_type_id__in: list[int] | None = Field(None, description="Filter by tag type IDs")

# Project filter
project_id__in: list[int] | None = Field(None, description="Filter by project IDs")

# Full-text search
search: str | None = Field(None, description="Full-text search across multiple fields")

class Constants(Filter.Constants):
model: type = Case
search_model_fields: list[str] = ["name", "title", "description"]

def filter(self, query: Query[Case]) -> Query[Case]:
"""Enhanced filter method with custom email filtering and permission handling."""
from sqlalchemy.orm import aliased

# Apply standard FastAPI-Filter filtering first
query = super().filter(query)

# Custom email filtering logic (moved from views)
if hasattr(self, "_assignee_emails") and self._assignee_emails:
from dispatch.individual.models import IndividualContact
from dispatch.participant.models import Participant

assignee_participant = aliased(Participant)
assignee_individual = aliased(IndividualContact)

query = (
query.join(assignee_participant, Case.assignee_id == assignee_participant.id)
.join(
assignee_individual,
assignee_participant.individual_contact_id == assignee_individual.id,
)
.filter(assignee_individual.email.in_(self._assignee_emails))
)

if hasattr(self, "_reporter_emails") and self._reporter_emails:
from dispatch.individual.models import IndividualContact
from dispatch.participant.models import Participant

reporter_participant = aliased(Participant)
reporter_individual = aliased(IndividualContact)

query = (
query.join(reporter_participant, Case.reporter_id == reporter_participant.id)
.join(
reporter_individual,
reporter_participant.individual_contact_id == reporter_individual.id,
)
.filter(reporter_individual.email.in_(self._reporter_emails))
)

return query

def set_email_filters(
self, assignee_emails: list[str] | None = None, reporter_emails: list[str] | None = None
):
"""Set email filters for participant filtering."""
if assignee_emails:
self._assignee_emails = assignee_emails
if reporter_emails:
self._reporter_emails = reporter_emails


# Alternative approach using separate filters for different concerns
class CaseBasicFilter(Filter):
Copy link

Copilot AI Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] These alternative filter classes (e.g., CaseBasicFilter, CaseStatusFilter, etc.) are not referenced in the PR; consider moving them to a separate module or removing unused code to improve maintainability.

Copilot uses AI. Check for mistakes.
"""Basic field filters for cases."""

id__in: list[int] | None = None
name__icontains: str | None = None
title__icontains: str | None = None
description__icontains: str | None = None
resolution__icontains: str | None = None
dedicated_channel: bool | None = None
event: bool | None = None

class Constants(Filter.Constants):
model: type = Case


class CaseStatusFilter(Filter):
"""Status and enum-based filters for cases."""

resolution_reason__in: list[CaseResolutionReason] | None = None
status__in: list[CaseStatus] | None = None
visibility__in: list[Visibility] | None = None

class Constants(Filter.Constants):
model: type = Case


class CaseDateFilter(Filter):
"""Date range filters for cases."""

# Reported date filters
reported_at__gte: datetime | None = None
reported_at__lte: datetime | None = None
reported_at__gt: datetime | None = None
reported_at__lt: datetime | None = None

# Triage date filters
triage_at__gte: datetime | None = None
triage_at__lte: datetime | None = None
triage_at__gt: datetime | None = None
triage_at__lt: datetime | None = None

# Escalation date filters
escalated_at__gte: datetime | None = None
escalated_at__lte: datetime | None = None
escalated_at__gt: datetime | None = None
escalated_at__lt: datetime | None = None

# Closure date filters
closed_at__gte: datetime | None = None
closed_at__lte: datetime | None = None
closed_at__gt: datetime | None = None
closed_at__lt: datetime | None = None

# Creation date filters
created_at__gte: datetime | None = None
created_at__lte: datetime | None = None
created_at__gt: datetime | None = None
created_at__lt: datetime | None = None

# Update date filters
updated_at__gte: datetime | None = None
updated_at__lte: datetime | None = None
updated_at__gt: datetime | None = None
updated_at__lt: datetime | None = None

class Constants(Filter.Constants):
model: type = Case


class CaseRelationshipFilter(Filter):
"""Relationship-based filters for cases."""

# Core relationships
case_type_id__in: list[int] | None = None
case_priority_id__in: list[int] | None = None
case_severity_id__in: list[int] | None = None
project_id__in: list[int] | None = None

# Participant relationships
assignee_id__in: list[int] | None = None
reporter_id__in: list[int] | None = None

# Tag relationships
tag_id__in: list[int] | None = None
tag_type_id__in: list[int] | None = None

class Constants(Filter.Constants):
model: type = Case
Loading
Loading