11import logging
2- from typing import Any , cast
2+ from typing import Any , Literal , TypeAlias , cast
33
44import sqlalchemy as sa
55from common_library .exclude import Unset , is_unset
88from models_library .users import (
99 UserID ,
1010)
11+ from pydantic import validate_call
1112from simcore_postgres_database .models .groups import groups , user_to_groups
1213from simcore_postgres_database .models .products import products
1314from simcore_postgres_database .models .users import UserStatus , users
@@ -426,6 +427,11 @@ async def search_merged_pre_and_registered_users(
426427 return result .fetchall ()
427428
428429
430+ OrderKeys : TypeAlias = Literal ["email" , "current_status_created" ]
431+ OrderDirs : TypeAlias = Literal ["asc" , "desc" ]
432+
433+
434+ @validate_call (config = {"arbitrary_types_allowed" : True })
429435async def list_merged_pre_and_registered_users (
430436 engine : AsyncEngine ,
431437 connection : AsyncConnection | None = None ,
@@ -435,6 +441,7 @@ async def list_merged_pre_and_registered_users(
435441 filter_include_deleted : bool = False ,
436442 pagination_limit : int = 50 ,
437443 pagination_offset : int = 0 ,
444+ order_by : list [tuple [OrderKeys , OrderDirs ]] | None = None ,
438445) -> tuple [list [dict [str , Any ]], int ]:
439446 """Retrieves and merges users from both users and pre-registration tables.
440447
@@ -452,25 +459,29 @@ async def list_merged_pre_and_registered_users(
452459 filter_include_deleted: Whether to include deleted users
453460 pagination_limit: Maximum number of results to return
454461 pagination_offset: Number of results to skip (for pagination)
462+ order_by: List of (field, direction) tuples. Valid fields: "email", "current_status_created"
463+ Default: [("email", "asc"), ("is_pre_registered", "desc"), ("current_status_created", "desc")]
455464
456465 Returns:
457466 Tuple of (list of merged user data, total count)
458467 """
459468 # Base where conditions for both queries
460- pre_reg_where = [users_pre_registration_details .c .product_name == product_name ]
461- users_where = []
469+ pre_reg_query_conditions = [
470+ users_pre_registration_details .c .product_name == product_name
471+ ]
472+ user_conditions = []
462473
463474 # Add account request status filter if specified
464475 if filter_any_account_request_status :
465- pre_reg_where .append (
476+ pre_reg_query_conditions .append (
466477 users_pre_registration_details .c .account_request_status .in_ (
467478 filter_any_account_request_status
468479 )
469480 )
470481
471482 # Add filter for deleted users
472483 if not filter_include_deleted :
473- users_where .append (users .c .status != UserStatus .DELETED )
484+ user_conditions .append (users .c .status != UserStatus .DELETED )
474485
475486 # Create subquery for reviewer username
476487 account_request_reviewed_by_username = (
@@ -479,7 +490,7 @@ async def list_merged_pre_and_registered_users(
479490
480491 # Query for pre-registered users
481492 # We need to left join with users to identify if the pre-registered user is already in the system
482- pre_reg_query = (
493+ pre_registered_users_query = (
483494 sa .select (
484495 users_pre_registration_details .c .id ,
485496 users_pre_registration_details .c .pre_email .label ("email" ),
@@ -495,6 +506,12 @@ async def list_merged_pre_and_registered_users(
495506 users_pre_registration_details .c .user_id .label ("pre_reg_user_id" ),
496507 users_pre_registration_details .c .extras ,
497508 users_pre_registration_details .c .created ,
509+ # Computed current_status_created column
510+ sa .func .coalesce (
511+ users .c .created_at , # If user exists, use users.created_at
512+ users_pre_registration_details .c .account_request_reviewed_at , # Else if reviewed, use review date
513+ users_pre_registration_details .c .created , # Else use pre-registration created date
514+ ).label ("current_status_created" ),
498515 users_pre_registration_details .c .account_request_status ,
499516 users_pre_registration_details .c .account_request_reviewed_by ,
500517 users_pre_registration_details .c .account_request_reviewed_at ,
@@ -512,11 +529,11 @@ async def list_merged_pre_and_registered_users(
512529 users , users_pre_registration_details .c .user_id == users .c .id
513530 )
514531 )
515- .where (sa .and_ (* pre_reg_where ))
532+ .where (sa .and_ (* pre_reg_query_conditions ))
516533 )
517534
518535 # Query for users that are associated with the product through groups
519- users_query = (
536+ registered_users_query = (
520537 sa .select (
521538 sa .literal (None ).label ("id" ),
522539 users .c .email ,
@@ -532,6 +549,8 @@ async def list_merged_pre_and_registered_users(
532549 sa .literal (None ).label ("pre_reg_user_id" ),
533550 sa .literal (None ).label ("extras" ),
534551 users .c .created_at .label ("created" ),
552+ # For regular users, current_status_created is just their created_at
553+ users .c .created_at .label ("current_status_created" ),
535554 sa .literal (None ).label ("account_request_status" ),
536555 sa .literal (None ).label ("account_request_reviewed_by" ),
537556 sa .literal (None ).label ("account_request_reviewed_at" ),
@@ -549,29 +568,28 @@ async def list_merged_pre_and_registered_users(
549568 .join (groups , groups .c .gid == user_to_groups .c .gid )
550569 .join (products , products .c .group_id == groups .c .gid )
551570 )
552- .where (sa .and_ (products .c .name == product_name , * users_where ))
571+ .where (sa .and_ (products .c .name == product_name , * user_conditions ))
553572 )
554573
555574 # If filtering by account request status, we only want pre-registered users with any of those statuses
556575 # No need to union with regular users as they don't have account_request_status
557576 merged_query : sa .sql .Select | sa .sql .CompoundSelect
558577 if filter_any_account_request_status :
559- merged_query = pre_reg_query
578+ merged_query = pre_registered_users_query
560579 else :
561- merged_query = pre_reg_query .union_all (users_query )
580+ merged_query = pre_registered_users_query .union_all (registered_users_query )
562581
563582 # Add distinct on email to eliminate duplicates
564583 merged_query_subq = merged_query .subquery ()
584+
585+ # Build ordering clauses using the extracted function
586+ order_by_clauses = _build_ordering_clauses (merged_query_subq , order_by )
587+
565588 distinct_query = (
566589 sa .select (merged_query_subq )
567590 .select_from (merged_query_subq )
568591 .distinct (merged_query_subq .c .email )
569- .order_by (
570- merged_query_subq .c .email ,
571- # Prioritize pre-registration records if duplicate emails exist
572- merged_query_subq .c .is_pre_registered .desc (),
573- merged_query_subq .c .created .desc (),
574- )
592+ .order_by (* order_by_clauses )
575593 .limit (pagination_limit )
576594 .offset (pagination_offset )
577595 )
@@ -594,3 +612,42 @@ async def list_merged_pre_and_registered_users(
594612 records = result .mappings ().all ()
595613
596614 return cast (list [dict [str , Any ]], records ), total_count
615+
616+
617+ def _build_ordering_clauses (
618+ merged_query_subq : sa .sql .Subquery ,
619+ order_by : list [tuple [OrderKeys , OrderDirs ]] | None = None ,
620+ ) -> list [sa .sql .ColumnElement ]:
621+ """Build ORDER BY clauses for merged user queries.
622+
623+ Args:
624+ merged_query_subq: The merged query subquery containing all columns
625+ order_by: List of (field, direction) tuples for ordering
626+
627+ Returns:
628+ List of SQLAlchemy ordering clauses
629+ """
630+ _ordering_criteria : list [tuple [str , OrderDirs ]] = []
631+
632+ if order_by is None :
633+ # Default ordering
634+ _ordering_criteria = [
635+ ("email" , "asc" ),
636+ ("is_pre_registered" , "desc" ),
637+ ("current_status_created" , "desc" ),
638+ ]
639+ else :
640+ _ordering_criteria = list (order_by )
641+ # Always append is_pre_registered prioritization for custom ordering
642+ if not any (field == "is_pre_registered" for field , _ in order_by ):
643+ _ordering_criteria .append (("is_pre_registered" , "desc" ))
644+
645+ order_by_clauses = []
646+ for field , direction in _ordering_criteria :
647+ column = merged_query_subq .columns [field ]
648+ if direction == "asc" :
649+ order_by_clauses .append (column .asc ())
650+ else :
651+ order_by_clauses .append (column .desc ())
652+
653+ return order_by_clauses
0 commit comments