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: int = 1,
page_size: int = 10, page_size: int = 10,
search: Optional[str] = None, 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: ) -> RolePaginatedResponse:
tenant_id = None if current_user_tenant_id is None else current_user_tenant_id tenant_id = None if current_user_tenant_id is None else current_user_tenant_id
return RoleService.get_roles_paginated( return RoleService.get_roles_paginated(
@@ -97,4 +101,8 @@ class RoleController:
page=page, page=page,
page_size=page_size, page_size=page_size,
search=search, 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 import uuid
from typing import Optional from typing import List, Optional
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.schemas.auth.subscription_plan_schema import SubscriptionPlanCreate, SubscriptionPlanUpdate from app.schemas.auth.subscription_plan_schema import SubscriptionPlanCreate, SubscriptionPlanUpdate
from app.services.auth.subscription_plan_service import SubscriptionPlanService from app.services.auth.subscription_plan_service import SubscriptionPlanService
@@ -23,8 +23,28 @@ class SubscriptionPlanController:
return SubscriptionPlanService.get_all_plans(db, is_public, status) return SubscriptionPlanService.get_all_plans(db, is_public, status)
@staticmethod @staticmethod
def get_paginated_plans(db: Session, page: int, page_size: int, search: Optional[str]): def get_paginated_plans(
return SubscriptionPlanService.get_paginated_plans(db, page, page_size, search) 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 @staticmethod
def delete_plan(db: Session, plan_id: uuid.UUID): 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.schemas.auth.tenant_schema import TenantCreate, TenantUpdate
from app.services.auth.tenant_service import TenantService from app.services.auth.tenant_service import TenantService
import uuid import uuid
from typing import Optional from typing import List, Optional
class TenantController: class TenantController:
@@ -33,6 +33,12 @@ class TenantController:
page_size: int, page_size: int,
search: Optional[str], search: Optional[str],
is_active: Optional[bool], 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( return TenantService.get_tenants_paginated(
db=db, db=db,
@@ -40,4 +46,10 @@ class TenantController:
page_size=page_size, page_size=page_size,
search=search, search=search,
is_active=is_active, 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: int = 1,
page_size: int = 10, page_size: int = 10,
search: Optional[str] = None, 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 tenant_id = current_user.tenant_id
return UserService.get_users_paginated( return UserService.get_users_paginated(
@@ -64,5 +70,11 @@ class UserController:
page=page, page=page,
page_size=page_size, page_size=page_size,
search=search, 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 fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import asc, desc, or_, cast, String
from app.config.database import get_db from app.config.database import get_db
from app.middleware.auth_middleware import get_current_user, User 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"), module_name: Optional[str] = Query(None, description="Filter by module name"),
action_type: Optional[str] = Query(None, description="CREATE | UPDATE | DELETE"), action_type: Optional[str] = Query(None, description="CREATE | UPDATE | DELETE"),
performed_by_email: Optional[str] = Query(None, description="Filter by actor email"), 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), limit: int = Query(50, ge=1, le=500),
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
): ):
@@ -27,16 +34,44 @@ def get_audit_logs(
if module_name: if module_name:
query = query.filter(AuditLog.module_name == 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: if action_type:
query = query.filter(AuditLog.action_type == action_type.upper()) 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: if performed_by_email:
query = query.filter( query = query.filter(
AuditLog.performed_by_email.ilike(f"%{performed_by_email}%") 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() 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 = ( items = (
query.order_by(AuditLog.created_at.desc()) query.order_by(order_fn(sort_column))
.offset(offset) .offset(offset)
.limit(limit) .limit(limit)
.all() .all()
+8
View File
@@ -135,6 +135,10 @@ def list_roles(
page: int = Query(1, ge=1, description="Page number"), page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(10, ge=1, le=100, description="Items per page"), 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"), 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), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
_=Depends(require_access("admin.role.read")), _=Depends(require_access("admin.role.read")),
@@ -145,4 +149,8 @@ def list_roles(
page=page, page=page,
page_size=page_size, page_size=page_size,
search=search, 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: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100), page_size: int = Query(10, ge=1, le=100),
search: Optional[str] = Query(None), 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), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
_ = Depends(require_access("superadmin.plan.read")) _ = 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]) @router.get("/all", response_model=List[SubscriptionPlanResponse])
def all_plans( 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"), 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"), search: Optional[str] = Query(None, description="Search in name, domain, or ID"),
is_active: Optional[bool] = Query(None, description="Filter by active status"), 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), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
_ = Depends(require_access("superadmin.tenant.read")), _ = Depends(require_access("superadmin.tenant.read")),
@@ -143,4 +149,10 @@ def list_tenants(
page_size=page_size, page_size=page_size,
search=search, search=search,
is_active=is_active, 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
@@ -121,7 +121,13 @@ def list_users(
page: int = Query(1, ge=1, description="Page number"), page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(10, ge=1, le=100, description="Items per page"), 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"), 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), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
_ = Depends(require_access("admin.user.read")), _ = Depends(require_access("admin.user.read")),
@@ -132,5 +138,11 @@ def list_users(
page=page, page=page,
page_size=page_size, page_size=page_size,
search=search, 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 import logging
from typing import List, Optional from typing import List, Optional
from sqlalchemy.orm import Session 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 fastapi import HTTPException, status
from app.models.auth.role_model import Role from app.models.auth.role_model import Role
from app.models.auth.role_access_model import RoleAccess from app.models.auth.role_access_model import RoleAccess
@@ -313,6 +313,10 @@ class RoleService:
page: int = 1, page: int = 1,
page_size: int = 10, page_size: int = 10,
search: Optional[str] = None, 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: ) -> RolePaginatedResponse:
query = db.query(Role) query = db.query(Role)
@@ -320,6 +324,12 @@ class RoleService:
if tenant_id is not None: if tenant_id is not None:
query = query.filter(Role.tenant_id == tenant_id) 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(): if search and search.strip():
search_term = search.strip() search_term = search.strip()
query = query.filter( 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() total = query.count()
offset = (page - 1) * page_size offset = (page - 1) * page_size
+35 -3
View File
@@ -1,7 +1,7 @@
import uuid import uuid
from typing import Optional from typing import List, Optional
from sqlalchemy.orm import Session, joinedload 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 fastapi import HTTPException, status
from app.models.auth.subscription_plan_model import SubscriptionPlan from app.models.auth.subscription_plan_model import SubscriptionPlan
@@ -103,8 +103,28 @@ class SubscriptionPlanService:
return query.all() return query.all()
@staticmethod @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) 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: if search:
query = query.filter( query = query.filter(
or_( or_(
@@ -112,6 +132,18 @@ class SubscriptionPlanService:
cast(SubscriptionPlan.id, String).ilike(f"%{search}%") 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() total = query.count()
offset = (page - 1) * page_size offset = (page - 1) * page_size
plans = query.offset(offset).limit(page_size).all() plans = query.offset(offset).limit(page_size).all()
+31 -2
View File
@@ -1,5 +1,5 @@
from sqlalchemy.orm import Session 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 fastapi import HTTPException, status
from app.models.auth.tenant_model import Tenant from app.models.auth.tenant_model import Tenant
from app.models.auth.tenant_module_model import TenantModule 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.schemas.auth.role_schema import RoleCreate, RoleUpdate
from app.services.auth.role_service import RoleService from app.services.auth.role_service import RoleService
import uuid import uuid
from typing import Optional from typing import List, Optional
from app.services.auth.event_service import EventService from app.services.auth.event_service import EventService
import logging import logging
@@ -411,10 +411,25 @@ class TenantService:
page_size: int = 10, page_size: int = 10,
search: Optional[str] = None, search: Optional[str] = None,
is_active: Optional[bool] = 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: ) -> TenantPaginatedResponse:
query = db.query(Tenant) 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(): if search and search.strip():
search_term = search.strip() search_term = search.strip()
query = query.filter( query = query.filter(
@@ -428,6 +443,20 @@ class TenantService:
if is_active is not None: if is_active is not None:
query = query.filter(Tenant.is_active == is_active) 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() total = query.count()
offset = (page - 1) * page_size offset = (page - 1) * page_size
+78 -10
View File
@@ -1,5 +1,5 @@
from sqlalchemy.orm import Session 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 fastapi import HTTPException, status, BackgroundTasks
from datetime import datetime from datetime import datetime
import uuid 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.module_access_model import ModuleAccess
from app.models.auth.tenant_module_model import TenantModule from app.models.auth.tenant_module_model import TenantModule
from app.models.auth.role_model import Role from app.models.auth.role_model import Role
from app.models.auth.tenant_model import Tenant
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -235,11 +236,17 @@ class UserService:
page: int = 1, page: int = 1,
page_size: int = 10, page_size: int = 10,
search: Optional[str] = None, 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: ) -> UserPaginatedResponse:
query = db.query(User).outerjoin(Tenant, User.tenant_id == Tenant.id).outerjoin(Role, User.role_id == Role.id)
query = db.query(User) # Scope to tenant if not superadmin
if tenant_id: if tenant_id:
query = query.filter(User.tenant_id == tenant_id) query = query.filter(User.tenant_id == tenant_id)
@@ -256,8 +263,73 @@ class UserService:
) )
) )
if status is not None: if filter_names:
query = query.filter(User.status == status) 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() total = query.count()
@@ -276,10 +348,6 @@ class UserService:
@staticmethod @staticmethod
def _resolve_targets_for_role(db: Session, role_id: uuid.UUID, tenant_id: uuid.UUID = None) -> List[Dict[str, Any]]: 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 = ( active_modules = (
db.query(ModuleAccess.module_id) db.query(ModuleAccess.module_id)
.join(RoleModuleAccess, RoleModuleAccess.module_access_id == ModuleAccess.id) .join(RoleModuleAccess, RoleModuleAccess.module_access_id == ModuleAccess.id)