Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: #1626 Add tests pagination user role assignment endpoint #1646

Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
7263c7a
Reconfig application router using dash for endpoint.
ianliuwk1019 Oct 15, 2024
1ca7282
Fix tests for router endpoint change.
ianliuwk1019 Oct 15, 2024
c689e29
add python-generics
ianliuwk1019 Oct 15, 2024
3063fca
Add paged result schema
ianliuwk1019 Oct 15, 2024
089856c
Use pydantic object for Fast API query params.
ianliuwk1019 Oct 16, 2024
96a72a4
Add pagination fields description.
ianliuwk1019 Oct 16, 2024
dceb790
Add draft service
ianliuwk1019 Oct 17, 2024
418b0ae
Call paginate_service for application user role endpoint.
ianliuwk1019 Oct 17, 2024
0a2d779
Adjust MAX_PAGE_SIZE to 100000 temporary due to frontend not ready fo…
ianliuwk1019 Oct 17, 2024
7cb2c68
Client-Code gen
ianliuwk1019 Oct 17, 2024
4f2b2f4
Add sorting function
ianliuwk1019 Oct 18, 2024
72f3470
Minor naming refactoring.
ianliuwk1019 Oct 18, 2024
f174292
Add search param
ianliuwk1019 Oct 18, 2024
7b31342
Add filter function
ianliuwk1019 Oct 18, 2024
fa56f0f
use ilike and add header
ianliuwk1019 Oct 18, 2024
5799d62
Add comments
ianliuwk1019 Oct 19, 2024
7a4cc05
Minor comment/function adjustment
ianliuwk1019 Oct 21, 2024
271c0cb
Remove GenericModel and use BaseModel
ianliuwk1019 Oct 21, 2024
f396381
Add back missed base filter.
ianliuwk1019 Oct 21, 2024
1cf74e8
Fix total count query.
ianliuwk1019 Oct 21, 2024
c72931c
Merge branch 'main' into feat/1626-paginate-get-user_role_assignment-…
ianliuwk1019 Oct 21, 2024
49c87a0
Fix tests.
ianliuwk1019 Oct 22, 2024
c060322
Abstract class for PageParamsSchema
ianliuwk1019 Oct 22, 2024
e2d82af
Remove unused python generic
ianliuwk1019 Oct 23, 2024
30032d6
Add comments.
ianliuwk1019 Oct 23, 2024
9a87f9d
Add PageResultMetaSchema for pagination meta data.
ianliuwk1019 Oct 24, 2024
49defd3
Add first default test and fixture.
ianliuwk1019 Oct 25, 2024
8aaaa9a
Add test
ianliuwk1019 Oct 25, 2024
9431b78
Minor refactoring.
ianliuwk1019 Oct 26, 2024
f4d8078
Generalized sort_by into paginate_service
ianliuwk1019 Oct 28, 2024
685efc3
Merge branch 'feat/backend-pagination-sort-search' into feat/1626-add…
ianliuwk1019 Oct 28, 2024
144fce5
Rename argument
ianliuwk1019 Oct 28, 2024
4c62053
Merge branch 'feat/1626-add-tests-pagination-user_role_assignment-end…
ianliuwk1019 Oct 28, 2024
e17c639
Add tests for sorting.
ianliuwk1019 Oct 28, 2024
9c7af0f
Add filtering test
ianliuwk1019 Oct 29, 2024
82a8af9
Add router tests
ianliuwk1019 Oct 29, 2024
a23051e
Add pagination tests for crud_application
ianliuwk1019 Oct 31, 2024
9d6779b
Add filter tests
ianliuwk1019 Nov 1, 2024
1f38353
Add few conditions for tests
ianliuwk1019 Nov 1, 2024
3cd1f2c
Add "UPDATED_DATE" as default and change model to use func.utcnow() t…
ianliuwk1019 Nov 1, 2024
da560c9
Fix broken tests due to schema change.
ianliuwk1019 Nov 1, 2024
9cd5f83
update client-code-gen
ianliuwk1019 Nov 1, 2024
aa6376d
Add some comments
ianliuwk1019 Nov 4, 2024
8fe9354
Use create_date for default sorting instead of update_date.
ianliuwk1019 Nov 4, 2024
1847a6c
Add test to validate pagination default values
ianliuwk1019 Nov 4, 2024
4eff461
Client code gen for pagination default create_date.
ianliuwk1019 Nov 4, 2024
33656f6
Use datetime formate as constant
ianliuwk1019 Nov 4, 2024
5926305
Minor refactoring.
ianliuwk1019 Nov 5, 2024
c0ac0e8
Defaut create_date sorting with sort order desc.
ianliuwk1019 Nov 14, 2024
53a8426
Fix the wrong file commit.
ianliuwk1019 Nov 14, 2024
87fe31c
Merge branch 'feat/backend-pagination-sort-search' into feat/1626-add…
ianliuwk1019 Nov 14, 2024
19c431b
Update swagger for desc sort order default.
ianliuwk1019 Nov 14, 2024
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
15 changes: 11 additions & 4 deletions client-code-gen/app-access-control-openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions server/backend/api/app/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ class SortOrderEnum(str, Enum):

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"
Expand Down
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}%")
craigyu marked this conversation as resolved.
Show resolved Hide resolved


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.
"""
ianliuwk1019 marked this conversation as resolved.
Show resolved Hide resolved
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(),
ianliuwk1019 marked this conversation as resolved.
Show resolved Hide resolved
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)
8 changes: 6 additions & 2 deletions server/backend/api/app/schemas/pagination.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

from abc import ABC
from enum import StrEnum
from typing import Generic, List

from api.app.constants import (DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, MIN_PAGE,
Expand Down Expand Up @@ -34,7 +35,8 @@ def get_fam_application_user_role_assignment(
class PageParamsSchema(BaseModel, ABC):
"""
Abstract class for request query params for backend API pagination, sorting and filtering.
This is the base schema for common fields. Endpoints need to extend this class for specific needs.
This is the base schema for common fields. Endpoints need to extend this class and override
'sort_by' for specific needs.
"""
page: int | None = Field(Query(
default=MIN_PAGE, ge=MIN_PAGE, description="Page number", alias="pageNumber"
Expand All @@ -54,10 +56,12 @@ class PageParamsSchema(BaseModel, ABC):
)
))

sort_by: StrEnum | None = None


class UserRolePageParamsSchema(PageParamsSchema):
sort_by: UserRoleSortByEnum | None = Field(Query(
default=UserRoleSortByEnum.USER_NAME, alias="sortBy",
default=UserRoleSortByEnum.CREATE_DATE, alias="sortBy",
ianliuwk1019 marked this conversation as resolved.
Show resolved Hide resolved
description=(
f'Column to be sorted by <br>Possible values: [{", ".join([enum for enum in UserRoleSortByEnum])}]<br>&nbsp;'
)
Expand Down
5 changes: 5 additions & 0 deletions server/backend/testspg/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@
"create_user": TEST_CREATOR,
}

TEST_USER_NAME_PREFIX = "TEST_USER_"
TEST_USER_NAME_IDIR_PREFIX = f"{TEST_USER_NAME_PREFIX}IDIR_"
TEST_USER_NAME_BCEID_PREFIX = f"{TEST_USER_NAME_PREFIX}BCEID_"
TEST_USER_EMAIL_SUFFIX = "fam.test.com"

# --------------------- Testing forest client numbers ----------------- #
FC_NUMBER_LEN_TOO_SHORT = "0001011"
FC_NUMBER_LEN_TOO_LONG = "000001011"
Expand Down
Loading
Loading