filter functionality added on all modules.

This commit is contained in:
amee
2026-04-17 10:34:51 +05:30
parent 41c91554f2
commit df83ec42d0
13 changed files with 311 additions and 29 deletions
+8
View File
@@ -89,6 +89,10 @@ class RoleController:
page: int = 1,
page_size: int = 10,
search: Optional[str] = None,
filter_role_names: Optional[List[str]] = None,
filter_tenant_ids: Optional[List[uuid.UUID]] = None,
sort_by: Optional[str] = None,
sort_order: Optional[str] = None,
) -> RolePaginatedResponse:
tenant_id = None if current_user_tenant_id is None else current_user_tenant_id
return RoleService.get_roles_paginated(
@@ -97,4 +101,8 @@ class RoleController:
page=page,
page_size=page_size,
search=search,
filter_role_names=filter_role_names,
filter_tenant_ids=filter_tenant_ids,
sort_by=sort_by,
sort_order=sort_order,
)
@@ -1,5 +1,5 @@
import uuid
from typing import Optional
from typing import List, Optional
from sqlalchemy.orm import Session
from app.schemas.auth.subscription_plan_schema import SubscriptionPlanCreate, SubscriptionPlanUpdate
from app.services.auth.subscription_plan_service import SubscriptionPlanService
@@ -23,8 +23,28 @@ class SubscriptionPlanController:
return SubscriptionPlanService.get_all_plans(db, is_public, status)
@staticmethod
def get_paginated_plans(db: Session, page: int, page_size: int, search: Optional[str]):
return SubscriptionPlanService.get_paginated_plans(db, page, page_size, search)
def get_paginated_plans(
db: Session,
page: int,
page_size: int,
search: Optional[str],
filter_names: Optional[List[str]] = None,
statuses: Optional[List[str]] = None,
visibility: Optional[List[bool]] = None,
sort_by: Optional[str] = None,
sort_order: Optional[str] = None,
):
return SubscriptionPlanService.get_paginated_plans(
db,
page,
page_size,
search,
filter_names=filter_names,
statuses=statuses,
visibility=visibility,
sort_by=sort_by,
sort_order=sort_order,
)
@staticmethod
def delete_plan(db: Session, plan_id: uuid.UUID):
+13 -1
View File
@@ -2,7 +2,7 @@ from sqlalchemy.orm import Session
from app.schemas.auth.tenant_schema import TenantCreate, TenantUpdate
from app.services.auth.tenant_service import TenantService
import uuid
from typing import Optional
from typing import List, Optional
class TenantController:
@@ -33,6 +33,12 @@ class TenantController:
page_size: int,
search: Optional[str],
is_active: Optional[bool],
filter_tenant_names: Optional[List[str]] = None,
filter_tenant_domains: Optional[List[str]] = None,
filter_plan_ids: Optional[List[uuid.UUID]] = None,
statuses: Optional[List[bool]] = None,
sort_by: Optional[str] = None,
sort_order: Optional[str] = None,
):
return TenantService.get_tenants_paginated(
db=db,
@@ -40,4 +46,10 @@ class TenantController:
page_size=page_size,
search=search,
is_active=is_active,
filter_tenant_names=filter_tenant_names,
filter_tenant_domains=filter_tenant_domains,
filter_plan_ids=filter_plan_ids,
statuses=statuses,
sort_by=sort_by,
sort_order=sort_order,
)
+14 -2
View File
@@ -55,7 +55,13 @@ class UserController:
page: int = 1,
page_size: int = 10,
search: Optional[str] = None,
status: Optional[str] = None,
filter_names: Optional[List[str]] = None,
filter_emails: Optional[List[str]] = None,
statuses: Optional[List[str]] = None,
filter_tenant_ids: Optional[List[uuid.UUID]] = None,
filter_role_ids: Optional[List[uuid.UUID]] = None,
sort_by: Optional[str] = None,
sort_order: Optional[str] = None,
):
tenant_id = current_user.tenant_id
return UserService.get_users_paginated(
@@ -64,5 +70,11 @@ class UserController:
page=page,
page_size=page_size,
search=search,
status=status,
filter_names=filter_names,
filter_emails=filter_emails,
statuses=statuses,
filter_tenant_ids=filter_tenant_ids,
filter_role_ids=filter_role_ids,
sort_by=sort_by,
sort_order=sort_order,
)
+37 -2
View File
@@ -1,6 +1,7 @@
from typing import Optional
from typing import List, Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from sqlalchemy import asc, desc, or_, cast, String
from app.config.database import get_db
from app.middleware.auth_middleware import get_current_user, User
@@ -16,6 +17,12 @@ def get_audit_logs(
module_name: Optional[str] = Query(None, description="Filter by module name"),
action_type: Optional[str] = Query(None, description="CREATE | UPDATE | DELETE"),
performed_by_email: Optional[str] = Query(None, description="Filter by actor email"),
search: Optional[str] = Query(None, description="Search audit logs"),
module_names: Optional[List[str]] = Query(None, description="Filter by module names"),
action_types: Optional[List[str]] = Query(None, description="Filter by action types"),
performed_by_emails: Optional[List[str]] = Query(None, description="Filter by actor emails"),
sort_by: Optional[str] = Query("created_at", description="Sort by created_at, module_name, action_type, or performed_by_email"),
sort_order: Optional[str] = Query("desc", description="Sort order: asc or desc"),
limit: int = Query(50, ge=1, le=500),
offset: int = Query(0, ge=0),
):
@@ -27,16 +34,44 @@ def get_audit_logs(
if module_name:
query = query.filter(AuditLog.module_name == module_name)
if module_names:
query = query.filter(AuditLog.module_name.in_(module_names))
if action_type:
query = query.filter(AuditLog.action_type == action_type.upper())
if action_types:
normalized_action_types = [item.upper() for item in action_types]
query = query.filter(AuditLog.action_type.in_(normalized_action_types))
if performed_by_email:
query = query.filter(
AuditLog.performed_by_email.ilike(f"%{performed_by_email}%")
)
if performed_by_emails:
query = query.filter(AuditLog.performed_by_email.in_(performed_by_emails))
if search:
search_term = search.strip()
query = query.filter(
or_(
AuditLog.module_name.ilike(f"%{search_term}%"),
AuditLog.action_type.ilike(f"%{search_term}%"),
AuditLog.entity_name.ilike(f"%{search_term}%"),
AuditLog.description.ilike(f"%{search_term}%"),
AuditLog.performed_by_email.ilike(f"%{search_term}%"),
AuditLog.ip_address.ilike(f"%{search_term}%"),
cast(AuditLog.entity_id, String).ilike(f"%{search_term}%"),
)
)
total = query.count()
sort_column_map = {
"created_at": AuditLog.created_at,
"module_name": AuditLog.module_name,
"action_type": AuditLog.action_type,
"performed_by_email": AuditLog.performed_by_email,
}
sort_column = sort_column_map.get(sort_by or "created_at", AuditLog.created_at)
order_fn = asc if (sort_order or "").lower() == "asc" else desc
items = (
query.order_by(AuditLog.created_at.desc())
query.order_by(order_fn(sort_column))
.offset(offset)
.limit(limit)
.all()
+8
View File
@@ -135,6 +135,10 @@ def list_roles(
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(10, ge=1, le=100, description="Items per page"),
search: Optional[str] = Query(None, description="Search in role name or ID"),
filter_role_names: Optional[List[str]] = Query(None, description="Filter by role names"),
filter_tenant_ids: Optional[List[uuid.UUID]] = Query(None, description="Filter by tenant IDs"),
sort_by: Optional[str] = Query(None, description="Sort by role name or tenant"),
sort_order: Optional[str] = Query(None, description="Sort order: asc or desc"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
_=Depends(require_access("admin.role.read")),
@@ -145,4 +149,8 @@ def list_roles(
page=page,
page_size=page_size,
search=search,
filter_role_names=filter_role_names,
filter_tenant_ids=filter_tenant_ids,
sort_by=sort_by,
sort_order=sort_order,
)
+16 -1
View File
@@ -93,11 +93,26 @@ def list_plans(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
search: Optional[str] = Query(None),
filter_names: Optional[List[str]] = Query(None),
statuses: Optional[List[str]] = Query(None),
visibility: Optional[List[bool]] = Query(None, description="Filter by public/private visibility"),
sort_by: Optional[str] = Query(None, description="Sort by name, status, visibility, or price"),
sort_order: Optional[str] = Query(None, description="Sort order: asc or desc"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
_ = Depends(require_access("superadmin.plan.read"))
):
return SubscriptionPlanController.get_paginated_plans(db, page, page_size, search)
return SubscriptionPlanController.get_paginated_plans(
db,
page,
page_size,
search,
filter_names=filter_names,
statuses=statuses,
visibility=visibility,
sort_by=sort_by,
sort_order=sort_order,
)
@router.get("/all", response_model=List[SubscriptionPlanResponse])
def all_plans(
+12
View File
@@ -133,6 +133,12 @@ def list_tenants(
page_size: int = Query(10, ge=1, le=100, description="Items per page"),
search: Optional[str] = Query(None, description="Search in name, domain, or ID"),
is_active: Optional[bool] = Query(None, description="Filter by active status"),
filter_tenant_names: Optional[List[str]] = Query(None, description="Filter by tenant names"),
filter_tenant_domains: Optional[List[str]] = Query(None, description="Filter by tenant domains"),
filter_plan_ids: Optional[List[uuid.UUID]] = Query(None, description="Filter by plan IDs"),
statuses: Optional[List[bool]] = Query(None, description="Filter by tenant statuses"),
sort_by: Optional[str] = Query(None, description="Sort by name, domain, status, or plan"),
sort_order: Optional[str] = Query(None, description="Sort order: asc or desc"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
_ = Depends(require_access("superadmin.tenant.read")),
@@ -143,4 +149,10 @@ def list_tenants(
page_size=page_size,
search=search,
is_active=is_active,
filter_tenant_names=filter_tenant_names,
filter_tenant_domains=filter_tenant_domains,
filter_plan_ids=filter_plan_ids,
statuses=statuses,
sort_by=sort_by,
sort_order=sort_order,
)
+15 -3
View File
@@ -121,7 +121,13 @@ def list_users(
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(10, ge=1, le=100, description="Items per page"),
search: Optional[str] = Query(None, description="Search in email, name, phone, or ID"),
status: Optional[str] = Query(None, description="Filter by user status (e.g. active, inactive)"),
filter_names: Optional[List[str]] = Query(None, description="Filter by full names"),
filter_emails: Optional[List[str]] = Query(None, description="Filter by emails"),
statuses: Optional[List[str]] = Query(None, description="Filter by user statuses"),
filter_tenant_ids: Optional[List[uuid.UUID]] = Query(None, description="Filter by tenant IDs"),
filter_role_ids: Optional[List[uuid.UUID]] = Query(None, description="Filter by role IDs"),
sort_by: Optional[str] = Query(None, description="Sort by name, email, status, tenant, or role"),
sort_order: Optional[str] = Query(None, description="Sort order: asc or desc"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
_ = Depends(require_access("admin.user.read")),
@@ -132,5 +138,11 @@ def list_users(
page=page,
page_size=page_size,
search=search,
status=status,
)
filter_names=filter_names,
filter_emails=filter_emails,
statuses=statuses,
filter_tenant_ids=filter_tenant_ids,
filter_role_ids=filter_role_ids,
sort_by=sort_by,
sort_order=sort_order,
)
+20 -1
View File
@@ -3,7 +3,7 @@ import json
import logging
from typing import List, Optional
from sqlalchemy.orm import Session
from sqlalchemy import or_, cast, String
from sqlalchemy import or_, cast, String, asc, desc
from fastapi import HTTPException, status
from app.models.auth.role_model import Role
from app.models.auth.role_access_model import RoleAccess
@@ -313,6 +313,10 @@ class RoleService:
page: int = 1,
page_size: int = 10,
search: Optional[str] = None,
filter_role_names: Optional[List[str]] = None,
filter_tenant_ids: Optional[List[uuid.UUID]] = None,
sort_by: Optional[str] = None,
sort_order: Optional[str] = None,
) -> RolePaginatedResponse:
query = db.query(Role)
@@ -320,6 +324,12 @@ class RoleService:
if tenant_id is not None:
query = query.filter(Role.tenant_id == tenant_id)
if filter_role_names:
query = query.filter(Role.role_name.in_(filter_role_names))
if filter_tenant_ids:
query = query.filter(Role.tenant_id.in_(filter_tenant_ids))
if search and search.strip():
search_term = search.strip()
query = query.filter(
@@ -329,6 +339,15 @@ class RoleService:
)
)
sort_column_map = {
"name": Role.role_name,
"tenant": Role.tenant_id,
}
sort_column = sort_column_map.get(sort_by or "")
if sort_column is not None:
order_fn = desc if (sort_order or "").lower() == "desc" else asc
query = query.order_by(order_fn(sort_column))
total = query.count()
offset = (page - 1) * page_size
+35 -3
View File
@@ -1,7 +1,7 @@
import uuid
from typing import Optional
from typing import List, Optional
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import or_, cast, String
from sqlalchemy import or_, cast, String, asc, desc
from fastapi import HTTPException, status
from app.models.auth.subscription_plan_model import SubscriptionPlan
@@ -103,8 +103,28 @@ class SubscriptionPlanService:
return query.all()
@staticmethod
def get_paginated_plans(db: Session, page: int = 1, page_size: int = 10, search: Optional[str] = None) -> SubscriptionPlanPaginatedResponse:
def get_paginated_plans(
db: Session,
page: int = 1,
page_size: int = 10,
search: Optional[str] = None,
filter_names: Optional[List[str]] = None,
statuses: Optional[List[str]] = None,
visibility: Optional[List[bool]] = None,
sort_by: Optional[str] = None,
sort_order: Optional[str] = None,
) -> SubscriptionPlanPaginatedResponse:
query = db.query(SubscriptionPlan)
if filter_names:
query = query.filter(SubscriptionPlan.name.in_(filter_names))
if statuses:
query = query.filter(SubscriptionPlan.status.in_(statuses))
if visibility is not None and len(visibility) > 0:
query = query.filter(SubscriptionPlan.is_public.in_(visibility))
if search:
query = query.filter(
or_(
@@ -112,6 +132,18 @@ class SubscriptionPlanService:
cast(SubscriptionPlan.id, String).ilike(f"%{search}%")
)
)
sort_column_map = {
"name": SubscriptionPlan.name,
"price": SubscriptionPlan.price,
"status": SubscriptionPlan.status,
"visibility": SubscriptionPlan.is_public,
}
sort_column = sort_column_map.get(sort_by or "")
if sort_column is not None:
order_fn = desc if (sort_order or "").lower() == "desc" else asc
query = query.order_by(order_fn(sort_column))
total = query.count()
offset = (page - 1) * page_size
plans = query.offset(offset).limit(page_size).all()
+31 -2
View File
@@ -1,5 +1,5 @@
from sqlalchemy.orm import Session
from sqlalchemy import or_, cast, String
from sqlalchemy import or_, cast, String, asc, desc
from fastapi import HTTPException, status
from app.models.auth.tenant_model import Tenant
from app.models.auth.tenant_module_model import TenantModule
@@ -13,7 +13,7 @@ from app.schemas.auth.tenant_schema import TenantCreate, TenantUpdate, TenantPag
from app.schemas.auth.role_schema import RoleCreate, RoleUpdate
from app.services.auth.role_service import RoleService
import uuid
from typing import Optional
from typing import List, Optional
from app.services.auth.event_service import EventService
import logging
@@ -411,10 +411,25 @@ class TenantService:
page_size: int = 10,
search: Optional[str] = None,
is_active: Optional[bool] = None,
filter_tenant_names: Optional[List[str]] = None,
filter_tenant_domains: Optional[List[str]] = None,
filter_plan_ids: Optional[List[uuid.UUID]] = None,
statuses: Optional[List[bool]] = None,
sort_by: Optional[str] = None,
sort_order: Optional[str] = None,
) -> TenantPaginatedResponse:
query = db.query(Tenant)
if filter_tenant_names:
query = query.filter(Tenant.tenant_name.in_(filter_tenant_names))
if filter_tenant_domains:
query = query.filter(Tenant.tenant_domain.in_(filter_tenant_domains))
if filter_plan_ids:
query = query.filter(Tenant.plan_id.in_(filter_plan_ids))
if search and search.strip():
search_term = search.strip()
query = query.filter(
@@ -428,6 +443,20 @@ class TenantService:
if is_active is not None:
query = query.filter(Tenant.is_active == is_active)
if statuses is not None and len(statuses) > 0:
query = query.filter(Tenant.is_active.in_(statuses))
sort_column_map = {
"name": Tenant.tenant_name,
"domain": Tenant.tenant_domain,
"status": Tenant.is_active,
"plan": Tenant.plan_id,
}
sort_column = sort_column_map.get(sort_by or "")
if sort_column is not None:
order_fn = desc if (sort_order or "").lower() == "desc" else asc
query = query.order_by(order_fn(sort_column))
total = query.count()
offset = (page - 1) * page_size
+79 -11
View File
@@ -1,5 +1,5 @@
from sqlalchemy.orm import Session
from sqlalchemy import or_, cast, String
from sqlalchemy import or_, cast, String, asc, desc, func
from fastapi import HTTPException, status, BackgroundTasks
from datetime import datetime
import uuid
@@ -14,6 +14,7 @@ from app.models.auth.role_module_access_model import RoleModuleAccess
from app.models.auth.module_access_model import ModuleAccess
from app.models.auth.tenant_module_model import TenantModule
from app.models.auth.role_model import Role
from app.models.auth.tenant_model import Tenant
logger = logging.getLogger(__name__)
@@ -235,11 +236,17 @@ class UserService:
page: int = 1,
page_size: int = 10,
search: Optional[str] = None,
status: Optional[str] = None,
filter_names: Optional[List[str]] = None,
filter_emails: Optional[List[str]] = None,
statuses: Optional[List[str]] = None,
filter_tenant_ids: Optional[List[uuid.UUID]] = None,
filter_role_ids: Optional[List[uuid.UUID]] = None,
sort_by: Optional[str] = None,
sort_order: Optional[str] = None,
) -> UserPaginatedResponse:
query = db.query(User)
query = db.query(User).outerjoin(Tenant, User.tenant_id == Tenant.id).outerjoin(Role, User.role_id == Role.id)
# Scope to tenant if not superadmin
if tenant_id:
query = query.filter(User.tenant_id == tenant_id)
@@ -256,8 +263,73 @@ class UserService:
)
)
if status is not None:
query = query.filter(User.status == status)
if filter_names:
normalized_names = [name.strip() for name in filter_names if isinstance(name, str) and name.strip()]
if normalized_names:
query = query.filter(
func.trim(
func.concat(
User.first_name,
" ",
func.coalesce(User.last_name, ""),
)
).in_(normalized_names)
)
if filter_emails:
normalized_emails = [email.strip() for email in filter_emails if isinstance(email, str) and email.strip()]
if normalized_emails:
query = query.filter(User.email.in_(normalized_emails))
if statuses:
normalized_statuses = [status for status in statuses if status]
if normalized_statuses:
query = query.filter(User.status.in_(normalized_statuses))
if filter_tenant_ids:
tenant_ids: List[uuid.UUID] = []
for tenant_value in filter_tenant_ids:
try:
tenant_ids.append(
tenant_value if isinstance(tenant_value, uuid.UUID) else uuid.UUID(str(tenant_value))
)
except (ValueError, AttributeError, TypeError):
continue
if tenant_ids:
query = query.filter(User.tenant_id.in_(tenant_ids))
if filter_role_ids:
role_ids: List[uuid.UUID] = []
for role_value in filter_role_ids:
try:
role_ids.append(
role_value if isinstance(role_value, uuid.UUID) else uuid.UUID(str(role_value))
)
except (ValueError, AttributeError, TypeError):
continue
if role_ids:
query = query.filter(User.role_id.in_(role_ids))
normalized_sort_by = (sort_by or "").strip().lower()
normalized_sort_order = (sort_order or "asc").strip().lower()
sort_fn = desc if normalized_sort_order == "desc" else asc
if normalized_sort_by == "name":
query = query.order_by(
sort_fn(func.lower(User.first_name)),
sort_fn(func.lower(func.coalesce(User.last_name, ""))),
asc(User.email),
)
elif normalized_sort_by == "email":
query = query.order_by(sort_fn(func.lower(User.email)))
elif normalized_sort_by == "status":
query = query.order_by(sort_fn(func.lower(User.status)), asc(User.email))
elif normalized_sort_by == "tenant":
query = query.order_by(sort_fn(func.lower(func.coalesce(Tenant.tenant_name, ""))), asc(User.email))
elif normalized_sort_by == "role":
query = query.order_by(sort_fn(func.lower(func.coalesce(Role.role_name, ""))), asc(User.email))
else:
query = query.order_by(desc(User.created_at))
total = query.count()
@@ -276,10 +348,6 @@ class UserService:
@staticmethod
def _resolve_targets_for_role(db: Session, role_id: uuid.UUID, tenant_id: uuid.UUID = None) -> List[Dict[str, Any]]:
"""
Helper to resolve which modules/envs a role targets.
Mirrors logic in RoleService.
"""
active_modules = (
db.query(ModuleAccess.module_id)
.join(RoleModuleAccess, RoleModuleAccess.module_access_id == ModuleAccess.id)
@@ -298,7 +366,7 @@ class UserService:
tm_assignments = db.query(TenantModule).filter(
TenantModule.tenant_id == tenant_id,
TenantModule.module_id.in_(module_ids),
TenantModule.is_active == True
TenantModule.is_active == True
).all()
for tm in tm_assignments:
env_map[str(tm.module_id)] = tm.assigned_environment_slug or "prod"