Skip to content

Commit

Permalink
feat: #1626 Add tests pagination user role assignment endpoint (#1646)
Browse files Browse the repository at this point in the history
  • Loading branch information
ianliuwk1019 authored Nov 15, 2024
1 parent 90f63e5 commit 3c3ca61
Show file tree
Hide file tree
Showing 24 changed files with 904 additions and 95 deletions.
17 changes: 12 additions & 5 deletions client-code-gen/app-access-control-openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
}
],
"description": "Column sorting order by \u003Cbr\u003EPossible values: [asc, desc]\u003Cbr\u003E ",
"default": "asc",
"default": "desc",
"title": "Sortorder"
},
"description": "Column sorting order by \u003Cbr\u003EPossible values: [asc, desc]\u003Cbr\u003E "
Expand All @@ -131,11 +131,11 @@
"type": "null"
}
],
"description": "Column to be sorted by \u003Cbr\u003EPossible values: [user_name, user_type_code, email, full_name, role_display_name, forest_client_number]\u003Cbr\u003E ",
"default": "user_name",
"description": "Column to be sorted by \u003Cbr\u003EPossible values: [create_date, user_name, user_type_code, email, full_name, role_display_name, forest_client_number]\u003Cbr\u003E ",
"default": "create_date",
"title": "Sortby"
},
"description": "Column to be sorted by \u003Cbr\u003EPossible values: [user_name, user_type_code, email, full_name, role_display_name, forest_client_number]\u003Cbr\u003E "
"description": "Column to be sorted by \u003Cbr\u003EPossible values: [create_date, user_name, user_type_code, email, full_name, role_display_name, forest_client_number]\u003Cbr\u003E "
}
],
"responses": {
Expand Down Expand Up @@ -903,6 +903,11 @@
},
"role": {
"$ref": "#/components/schemas/FamRoleWithClientSchema"
},
"create_date": {
"type": "string",
"format": "date-time",
"title": "Create Date"
}
},
"type": "object",
Expand All @@ -911,7 +916,8 @@
"user_id",
"role_id",
"user",
"role"
"role",
"create_date"
],
"title": "FamApplicationUserRoleAssignmentGetSchema"
},
Expand Down Expand Up @@ -1774,6 +1780,7 @@
"UserRoleSortByEnum": {
"type": "string",
"enum": [
"create_date",
"user_name",
"user_type_code",
"email",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const FAMApplicationsApiAxiosParamCreator = function (configuration?: Con
* @param {number | null} [pageSize] Number of records per page
* @param {string | null} [search] Search by keyword
* @param {SortOrderEnum | null} [sortOrder] Column sorting order by <br>Possible values: [asc, desc]<br> 
* @param {UserRoleSortByEnum | null} [sortBy] Column to be sorted by <br>Possible values: [user_name, user_type_code, email, full_name, role_display_name, forest_client_number]<br> 
* @param {UserRoleSortByEnum | null} [sortBy] Column to be sorted by <br>Possible values: [create_date, user_name, user_type_code, email, full_name, role_display_name, forest_client_number]<br> 
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
Expand Down Expand Up @@ -174,7 +174,7 @@ export const FAMApplicationsApiFp = function(configuration?: Configuration) {
* @param {number | null} [pageSize] Number of records per page
* @param {string | null} [search] Search by keyword
* @param {SortOrderEnum | null} [sortOrder] Column sorting order by <br>Possible values: [asc, desc]<br> 
* @param {UserRoleSortByEnum | null} [sortBy] Column to be sorted by <br>Possible values: [user_name, user_type_code, email, full_name, role_display_name, forest_client_number]<br> 
* @param {UserRoleSortByEnum | null} [sortBy] Column to be sorted by <br>Possible values: [create_date, user_name, user_type_code, email, full_name, role_display_name, forest_client_number]<br> 
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
Expand Down Expand Up @@ -213,7 +213,7 @@ export const FAMApplicationsApiFactory = function (configuration?: Configuration
* @param {number | null} [pageSize] Number of records per page
* @param {string | null} [search] Search by keyword
* @param {SortOrderEnum | null} [sortOrder] Column sorting order by <br>Possible values: [asc, desc]<br> 
* @param {UserRoleSortByEnum | null} [sortBy] Column to be sorted by <br>Possible values: [user_name, user_type_code, email, full_name, role_display_name, forest_client_number]<br> 
* @param {UserRoleSortByEnum | null} [sortBy] Column to be sorted by <br>Possible values: [create_date, user_name, user_type_code, email, full_name, role_display_name, forest_client_number]<br> 
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
Expand Down Expand Up @@ -248,7 +248,7 @@ export interface FAMApplicationsApiInterface {
* @param {number | null} [pageSize] Number of records per page
* @param {string | null} [search] Search by keyword
* @param {SortOrderEnum | null} [sortOrder] Column sorting order by <br>Possible values: [asc, desc]<br> 
* @param {UserRoleSortByEnum | null} [sortBy] Column to be sorted by <br>Possible values: [user_name, user_type_code, email, full_name, role_display_name, forest_client_number]<br> 
* @param {UserRoleSortByEnum | null} [sortBy] Column to be sorted by <br>Possible values: [create_date, user_name, user_type_code, email, full_name, role_display_name, forest_client_number]<br> 
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof FAMApplicationsApiInterface
Expand Down Expand Up @@ -285,7 +285,7 @@ export class FAMApplicationsApi extends BaseAPI implements FAMApplicationsApiInt
* @param {number | null} [pageSize] Number of records per page
* @param {string | null} [search] Search by keyword
* @param {SortOrderEnum | null} [sortOrder] Column sorting order by <br>Possible values: [asc, desc]<br> 
* @param {UserRoleSortByEnum | null} [sortBy] Column to be sorted by <br>Possible values: [user_name, user_type_code, email, full_name, role_display_name, forest_client_number]<br> 
* @param {UserRoleSortByEnum | null} [sortBy] Column to be sorted by <br>Possible values: [create_date, user_name, user_type_code, email, full_name, role_display_name, forest_client_number]<br> 
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof FAMApplicationsApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,11 @@ export interface FamApplicationUserRoleAssignmentGetSchema {
* @memberof FamApplicationUserRoleAssignmentGetSchema
*/
'role': FamRoleWithClientSchema;
/**
*
* @type {string}
* @memberof FamApplicationUserRoleAssignmentGetSchema
*/
'create_date': string;
}

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/

export const UserRoleSortByEnum = {
CreateDate: 'create_date',
UserName: 'user_name',
UserTypeCode: 'user_type_code',
Email: 'email',
Expand Down
13 changes: 12 additions & 1 deletion server/backend/api/app/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ class EmailSendingStatus(str, Enum):
)
SENT_TO_EMAIL_SERVICE_FAILURE = "SENT_TO_EMAIL_SERVICE_FAILURE" # technical/validation failure during sending to external service.


class UserRoleSortByEnum(str, Enum):
# Note: this is not the exact model column name, requires table column mapping.
CREATE_DATE = "create_date" # first one is the default sort field
USER_NAME = "user_name"
DOMAIN = "user_type_code"
EMAIL = "email"
FULL_NAME = "full_name" # special case: first_name + last_name
ROLE_DISPLAY_NAME = "role_display_name"
FOREST_CLIENT_NUMBER = "forest_client_number"

# -------------------------------- Schema Constants ------------------------------- #
SYSTEM_ACCOUNT_NAME = "system"
USER_NAME_MAX_LEN = 20
Expand Down Expand Up @@ -143,4 +154,4 @@ class EmailSendingStatus(str, Enum):
ERROR_CODE_MISSING_KEY_ATTRIBUTE = "missing_key_attribute"
ERROR_CODE_INVALID_REQUEST_PARAMETER = "invalid_request_parameter"
ERROR_CODE_TERMS_CONDITIONS_REQUIRED = "terms_condition_required"
ERROR_CODE_UNKNOWN_STATE = "unknown_state"
ERROR_CODE_UNKNOWN_STATE = "unknown_state"
43 changes: 20 additions & 23 deletions server/backend/api/app/crud/crud_application.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import logging
from datetime import datetime

from api.app.constants import SortOrderEnum, UserRoleSortByEnum, UserType
from api.app.constants import UserRoleSortByEnum, UserType
from api.app.crud.services.paginate_service import PaginateService
from api.app.datetime_format import TIMESTAMP_FORMAT_DEFAULT
from api.app.models import model as models
from api.app.schemas import (FamApplicationUserRoleAssignmentGetSchema,
RequesterSchema)
from api.app.schemas.pagination import (PagedResultsSchema,
UserRolePageParamsSchema)
from sqlalchemy import asc, desc, func, or_, select
from sqlalchemy import Column, asc, desc, func, or_, select
from sqlalchemy.orm import Session

from . import crud_utils as crud_utils
Expand All @@ -17,7 +19,8 @@
# Local constant only, for application user/role sorting/filtering query,
# provides mapping for sortBy/filtered columns mapped to model columns.
USER_ROLE_SORT_BY_MAPPED_COLUMN = {
UserRoleSortByEnum.USER_NAME: models.FamUser.user_name, # default
UserRoleSortByEnum.CREATE_DATE: models.FamUserRoleXref.create_date, # default
UserRoleSortByEnum.USER_NAME: models.FamUser.user_name,
UserRoleSortByEnum.DOMAIN: models.FamUser.user_type_code,
UserRoleSortByEnum.EMAIL: models.FamUser.email,
UserRoleSortByEnum.FULL_NAME: models.FamUser.full_name, # this is a hybrid column
Expand All @@ -35,24 +38,6 @@ def get_application(db: Session, application_id: int):
return application


def __build_order_by_criteria(page_params: UserRolePageParamsSchema):
"""
Based on 'sort_by' and 'sort_order' page_params to build SQL "ORDER BY"
clause, e.g., ("ORDER BY app_fam.fam_user.user_name ASC") to return
for the query.
"""
# currently only sorting on 1 column at a time from frontend.
sort_by = page_params.sort_by
sort_order = page_params.sort_order
mapped_column = (
USER_ROLE_SORT_BY_MAPPED_COLUMN.get(UserRoleSortByEnum.USER_NAME)
if sort_by is None # default
else USER_ROLE_SORT_BY_MAPPED_COLUMN.get(sort_by)
)

return asc(mapped_column) if sort_order == SortOrderEnum.ASC else desc(mapped_column)


def __build_filter_criteria(page_params: UserRolePageParamsSchema):
"""
Based on 'search' keyword from page_params to build additional 'where'
Expand All @@ -66,10 +51,22 @@ def __build_filter_criteria(page_params: UserRolePageParamsSchema):
"""
search_keyword = page_params.search
filter_on_columns = USER_ROLE_SORT_BY_MAPPED_COLUMN.values()

def operate_on_column(column: Column):
"""
Determines column type to apply sql operator/function for filtering.
"""
column_type = column.type.python_type
if column_type is str:
return column.ilike(f"%{search_keyword}%")
elif column_type is datetime:
return func.to_char(column, TIMESTAMP_FORMAT_DEFAULT).ilike(f"%{search_keyword}%")


return (
or_(
# build where ... "OR" conditions for all mapped columns.
*list(map(lambda column: column.ilike(f"%{search_keyword}%"), filter_on_columns))
*list(map(lambda column: operate_on_column(column), filter_on_columns))
)
if search_keyword is not None
else None
Expand Down Expand Up @@ -142,7 +139,7 @@ def get_application_role_assignments(
paginated_service = PaginateService(
db, q,
__build_filter_criteria(page_params),
__build_order_by_criteria(page_params),
USER_ROLE_SORT_BY_MAPPED_COLUMN,
page_params
)
qresult = paginated_service.get_paginated_results(FamApplicationUserRoleAssignmentGetSchema)
Expand Down
46 changes: 31 additions & 15 deletions server/backend/api/app/crud/services/paginate_service.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,45 @@

import logging
from enum import StrEnum

from api.app.constants import T
from api.app.constants import SortOrderEnum, T
from api.app.schemas.pagination import (PagedResultsSchema, PageParamsSchema,
PageResultMetaSchema)
from pydantic import BaseModel
from sqlalchemy import ColumnElement, Select, UnaryExpression, func, select
from sqlalchemy import ColumnElement, Select, asc, desc, func, select
from sqlalchemy.orm import Session

LOGGER = logging.getLogger(__name__)

class PaginateService:
"""
A simple pagination service as a helper for simple pagination, sorting and filtering.
For each business service requires pagination it needs to provid base query, sorting
and filtering construct for this PaginateService, The service uses 'page_param' for
For each business service requires pagination it needs to provid base query, filtering construct
and sort_by columns mapping for this PaginateService, The service uses 'page_param' for
executing paged query and provides paged result.
Attributes:
db (Session): The SqlAlchemy database session.
base_query (Select): Provided base query as SqlAlchemy 'Select' statement.
filter_by_criteria (ColumnElement): Provided filter criteria for base query to be filtered by.
'None' if no need to apply filter.
order_by_criteria (UnaryExpression): Provided order_by criteria for base query.
'None' if no need to define ordering.
sort_by_column_mapping (UnaryExpression): Provided db model columns mapping specific from the
caller for base query to apply order by query.
page_param: Paging parameters for performing pagination, sorting, filtering passed from external inputs.
"""
def __init__(
self,
db: Session,
base_query: Select,
filter_by_criteria: ColumnElement[bool] | None,
order_by_criteria: UnaryExpression | None,
sort_by_column_mapping: dict[StrEnum, any],
page_param: PageParamsSchema
):
self.db = db # SqlAlchemy session.
self.base_query = base_query # 'Select' base query.
self.__filter_by_criteria = filter_by_criteria
self.__order_by_criteria = order_by_criteria
self.__page_params__ = page_param
self.order_by_column_mapping = sort_by_column_mapping
self.__page_params = page_param
self.page = page_param.page
self.size = page_param.size
self.limit = self.size
Expand All @@ -47,8 +48,8 @@ def __init__(
def get_paginated_results(self, ResultSchema: BaseModel) -> PagedResultsSchema[T]:
"""
Paginate the query results.
Main function for the service, it will apply 'filter', 'order_by' if needed and
apply paging based on calculated 'offset' and 'limit'
Main function for the service, it will apply 'filter (where clause)', 'order_by clause'
if needed and apply paging based on calculated 'offset' and 'limit'
Arguments:
ResultSchema: This is the return type Class and used for paged result conversion.
Expand All @@ -57,7 +58,7 @@ def get_paginated_results(self, ResultSchema: BaseModel) -> PagedResultsSchema[T
Paged result with Generic type 'PagedResultsSchema[T]'. Other than paged results,
the pagniation metadata are also returned.
"""
LOGGER.debug(f"Obtaining paginated results with page params: {self.__page_params__}")
LOGGER.debug(f"Obtaining paginated results with page params: {self.__page_params}")
paged_query = self.__apply_filter_by(self.base_query)
paged_query = self.__apply_order_by(paged_query)
paged_query = paged_query.offset(self.offset).limit(self.limit)
Expand Down Expand Up @@ -86,9 +87,24 @@ def __get_number_of_pages(self, count: int) -> int:
return quotient if not rest else quotient + 1

def __apply_order_by(self, q: Select) -> Select:
LOGGER.debug(f"Applying order_by criteria: {self.__order_by_criteria}")
if self.__order_by_criteria is not None:
q = q.order_by(self.__order_by_criteria)
"""
Based on 'sort_by' and 'sort_order' page_params to build SQL "ORDER BY"
clause, e.g., ("ORDER BY app_fam.fam_user.user_name ASC") to return
for the query.
"""
sort_by = self.__page_params.sort_by
sort_order = self.__page_params.sort_order
mapped_column = (
list(self.order_by_column_mapping.values())[0] # default sort_by column
if sort_by is None
else self.order_by_column_mapping.get(sort_by)
)

order_by_criteria = asc(mapped_column) if sort_order == SortOrderEnum.ASC else desc(mapped_column)

LOGGER.debug(f"Applying order_by criteria: {order_by_criteria}")
if order_by_criteria is not None:
q = q.order_by(order_by_criteria)
return q

def __apply_filter_by(self, q: Select) -> Select:
Expand Down
1 change: 1 addition & 0 deletions server/backend/api/app/datetime_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TIMESTAMP_FORMAT_DEFAULT = "YYYY-MM-DD HH24:MI:SS"
5 changes: 3 additions & 2 deletions server/backend/api/app/models/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ class FamUserRoleXref(Base):
create_date = Column(
TIMESTAMP(timezone=True, precision=6),
nullable=False,
default=datetime.datetime.now(datetime.UTC),
server_default=func.utcnow(),
comment="The date and time the record was created.",
)
update_user = Column(
Expand All @@ -725,7 +725,8 @@ class FamUserRoleXref(Base):
)
update_date = Column(
TIMESTAMP(timezone=True, precision=6),
onupdate=datetime.datetime.now(datetime.UTC),
server_default=func.utcnow(),
onupdate=func.utcnow(),
comment="The date and time the record was created or last updated.",
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@


from datetime import datetime

from pydantic import BaseModel, ConfigDict
from .fam_user_info import FamUserInfoSchema

from .fam_role_with_client import FamRoleWithClientSchema
from .fam_user_info import FamUserInfoSchema


class FamApplicationUserRoleAssignmentGetSchema(BaseModel):
user_role_xref_id: int
user_id: int
role_id: int
user: FamUserInfoSchema
role: FamRoleWithClientSchema
create_date: datetime

# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information.
model_config = ConfigDict(from_attributes=True)
Loading

0 comments on commit 3c3ca61

Please sign in to comment.