Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 9 additions & 3 deletions auth-api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
[tool.poetry]
[project]
name = "auth-api"
version = "3.0.9"
description = ""
authors = ["\"BC Registries and Online Services\""]
authors = [{ name = "BC Registries and Online Services" }]
readme = "README.md"
requires-python = ">=3.12,<3.13"

[tool.poetry]
packages = [
{ include = "auth_api", from = "src" }
]

[tool.poetry.dependencies]
python = "^3.12"
python = ">=3.12,<3.13"
flask-cors = "^6.0.0"
flask-migrate = "^4.0.7"
flask-moment = "^1.0.6"
Expand Down
13 changes: 9 additions & 4 deletions auth-api/src/auth_api/models/custom_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,24 @@ def filter_conditionally(self, search_criteria, model_attribute, is_like: bool =

return self.filter(model_attribute == search_criteria)

def filter_conditional_date_range(self, start_date: date, end_date: date, model_attribute):
def filter_conditional_date_range(self, start_date: date, end_date: date, model_attribute, cast_to_date=True):
"""Add query filter for a date range if present."""
# Dates in DB are stored as UTC, you may need to take into account timezones and adjust the input dates
# depending on the needs
query = self

if start_date and end_date:
return query.filter(and_(func.DATE(model_attribute) >= start_date, func.DATE(model_attribute) <= end_date))
return query.filter(
and_(
func.DATE(model_attribute) if cast_to_date else model_attribute >= start_date,
func.DATE(model_attribute) if cast_to_date else model_attribute <= end_date,
)
)

if start_date:
query = query.filter(func.DATE(model_attribute) >= start_date)
query = query.filter(func.DATE(model_attribute) if cast_to_date else model_attribute >= start_date)

if end_date:
query = query.filter(func.DATE(model_attribute) <= end_date)
query = query.filter(func.DATE(model_attribute) if cast_to_date else model_attribute <= end_date)

return query
3 changes: 3 additions & 0 deletions auth-api/src/auth_api/models/dataclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ class OrgSearch: # pylint: disable=too-many-instance-attributes
org_type: str
include_members: bool
member_search_text: str
suspended_date_from: str
suspended_date_to: str
suspension_reason_code: str
page: int
limit: int

Expand Down
17 changes: 17 additions & 0 deletions auth-api/src/auth_api/models/org.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from auth_api.exceptions.errors import Error
from auth_api.models.affiliation import Affiliation
from auth_api.models.dataclass import OrgSearch
from auth_api.utils.date import str_to_utc_dt
from auth_api.utils.enums import AccessType, InvitationStatus, InvitationType
from auth_api.utils.enums import OrgStatus as OrgStatusEnum
from auth_api.utils.roles import EXCLUDED_FIELDS, INVALID_ORG_CREATE_TYPE_CODES, VALID_STATUSES
Expand Down Expand Up @@ -193,6 +194,17 @@ def search_org(cls, search: OrgSearch):
query = cls._search_by_business_identifier(query, search.business_identifier)
query = cls._search_for_statuses(query, search.statuses)

start_date = None
end_date = None
if search.suspended_date_from:
start_date = str_to_utc_dt(search.suspended_date_from, False)
if search.suspended_date_to:
end_date = str_to_utc_dt(search.suspended_date_to, True)
if start_date or end_date:
query = query.filter_conditional_date_range(start_date, end_date, Org.suspended_on, cast_to_date=False)
if search.suspension_reason_code:
query = query.filter(Org.suspension_reason_code == search.suspension_reason_code)

query = cls.get_order_by(search, query)
pagination = query.paginate(per_page=search.limit, page=search.page)

Expand All @@ -205,6 +217,11 @@ def get_order_by(cls, search, query):
if search.id:
return query.order_by(desc(Org.id == int(search.id or -1)), Org.created.desc())

if search.statuses and (
OrgStatusEnum.SUSPENDED.value in search.statuses or OrgStatusEnum.NSF_SUSPENDED.value in search.statuses
):
return query.order_by(desc(Org.suspended_on), Org.created.desc())

return query.order_by(Org.created.desc())

@classmethod
Expand Down
27 changes: 7 additions & 20 deletions auth-api/src/auth_api/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@
# limitations under the License.
"""This model manages a Task item in the Auth Service."""

import datetime as dt
from typing import Self

import pytz
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, text
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.orm import relationship

from auth_api.models.dataclass import TaskSearch
from auth_api.utils.date import str_to_utc_dt
from auth_api.utils.enums import TaskRelationshipStatus, TaskRelationshipType, TaskStatus

from .base_model import BaseModel
Expand Down Expand Up @@ -67,14 +66,14 @@ def fetch_tasks(cls, task_search: TaskSearch):
query = query.filter(Task.type == task_search.type)
if task_search.status:
query = query.filter(Task.status.in_(task_search.status))
start_date = None
end_date = None
if task_search.start_date:
# convert PST start_date to UTC then filter
start_date_utc = cls._str_to_utc_dt(task_search.start_date, False)
query = query.filter(Task.date_submitted >= start_date_utc)
start_date = str_to_utc_dt(task_search.start_date, False)
if task_search.end_date:
# convert PST end_date to UTC then set time to end of day then filter
end_date_utc = cls._str_to_utc_dt(task_search.end_date, True)
query = query.filter(Task.date_submitted <= end_date_utc)
end_date = str_to_utc_dt(task_search.end_date, True)
if start_date or end_date:
query = query.filter_conditional_date_range(start_date, end_date, Task.date_submitted, cast_to_date=False)
if task_search.relationship_status:
query = query.filter(Task.relationship_status == task_search.relationship_status)
if task_search.modified_by:
Expand Down Expand Up @@ -149,15 +148,3 @@ def find_by_user_and_status(cls, org_id: int, status):
.filter_by(account_id=int(org_id or -1), relationship_type=TaskRelationshipType.USER.value, status=status)
.first()
)

@classmethod
def _str_to_utc_dt(cls, date: str, add_time: bool):
"""Convert ISO formatted dates into dateTime objects in UTC."""
time_zone = pytz.timezone("Canada/Pacific")
naive_dt = dt.datetime.strptime(date, "%Y-%m-%d")
local_dt = time_zone.localize(naive_dt, is_dst=None)
if add_time:
local_dt = dt.datetime(local_dt.year, local_dt.month, local_dt.day, 23, 59, 59)
utc_dt = local_dt.astimezone(pytz.utc)

return utc_dt
3 changes: 3 additions & 0 deletions auth-api/src/auth_api/resources/v1/org.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ def search_organizations():
request.args.get("orgType", None),
string_to_bool(request.args.get("includeMembers", "False")),
request.args.get("members", None),
request.args.get("suspendedDateFrom", None),
request.args.get("suspendedDateTo", None),
request.args.get("suspensionReasonCode", None),
int(request.args.get("page", 1)),
int(request.args.get("limit", 10)),
)
Expand Down
1 change: 1 addition & 0 deletions auth-api/src/auth_api/services/simple_org.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,5 @@ def get_order_by(cls, search, query):
# If searching by id, surface the perfect matches to the top
if search.id:
return query.order_by(desc(OrgModel.id == int(search.id)), OrgModel.created.desc())

return query.order_by(OrgModel.name)
30 changes: 30 additions & 0 deletions auth-api/src/auth_api/utils/date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright © 2025 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Date utility functions."""

import datetime as dt

import pytz


def str_to_utc_dt(date: str, add_time: bool):
"""Convert ISO formatted dates into dateTime objects in UTC."""
time_zone = pytz.timezone("Canada/Pacific")
naive_dt = dt.datetime.strptime(date, "%Y-%m-%d")
local_dt = time_zone.localize(naive_dt, is_dst=None)
if add_time:
local_dt = dt.datetime(local_dt.year, local_dt.month, local_dt.day, 23, 59, 59)
utc_dt = local_dt.astimezone(pytz.utc)

return utc_dt
101 changes: 100 additions & 1 deletion auth-api/tests/unit/api/test_org.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import json
import uuid
from datetime import datetime
from datetime import datetime, timedelta
from http import HTTPStatus
from unittest.mock import patch

Expand Down Expand Up @@ -3005,3 +3005,102 @@ def test_search_org_members(client, jwt, session, keycloak_mock): # pylint:disa
)
dictionary = json.loads(rv.data)
assert not dictionary["orgs"]


def test_search_org_suspended_filters(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument
"""Assert that orgs can be searched by suspended date and suspension reason code."""
headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_manage_accounts_role)

today = datetime.now()
yesterday = today - timedelta(days=1)
last_week = today - timedelta(days=7)

org1 = factory_org_model(org_info=TestOrgInfo.org1)
org1.status_code = OrgStatus.SUSPENDED.value
org1.suspended_on = yesterday
org1.suspension_reason_code = SuspensionReasonCode.OWNER_CHANGE.name
org1.save()

org2 = factory_org_model(org_info=TestOrgInfo.org2)
org2.status_code = OrgStatus.SUSPENDED.value
org2.suspended_on = last_week
org2.suspension_reason_code = SuspensionReasonCode.DISPUTE.name
org2.save()

org3 = factory_org_model(org_info=TestOrgInfo.org_with_mailing_address())
org3.status_code = OrgStatus.SUSPENDED.value
org3.suspended_on = today
org3.suspension_reason_code = SuspensionReasonCode.OWNER_CHANGE.name
org3.save()

rv = client.get(
f"/api/v1/orgs?status=SUSPENDED&suspensionReasonCode={SuspensionReasonCode.OWNER_CHANGE.name}",
headers=headers,
content_type="application/json",
)
assert rv.status_code == HTTPStatus.OK
orgs = json.loads(rv.data)
assert orgs.get("total") == 2
org_ids = [org.get("id") for org in orgs.get("orgs")]
assert org1.id in org_ids
assert org3.id in org_ids
assert org2.id not in org_ids

rv = client.get(
f"/api/v1/orgs?status=SUSPENDED&suspensionReasonCode={SuspensionReasonCode.DISPUTE.name}",
headers=headers,
content_type="application/json",
)
assert rv.status_code == HTTPStatus.OK
orgs = json.loads(rv.data)
assert orgs.get("total") == 1
assert orgs.get("orgs")[0].get("id") == org2.id

rv = client.get(
f"/api/v1/orgs?status=SUSPENDED&suspendedDateFrom={yesterday.strftime('%Y-%m-%d')}",
headers=headers,
content_type="application/json",
)
assert rv.status_code == HTTPStatus.OK
orgs = json.loads(rv.data)
assert orgs.get("total") == 2
org_ids = [org.get("id") for org in orgs.get("orgs")]
assert org1.id in org_ids
assert org3.id in org_ids
assert org2.id not in org_ids

rv = client.get(
f"/api/v1/orgs?status=SUSPENDED&suspendedDateTo={yesterday.strftime('%Y-%m-%d')}",
headers=headers,
content_type="application/json",
)
assert rv.status_code == HTTPStatus.OK
orgs = json.loads(rv.data)
assert orgs.get("total") == 2
org_ids = [org.get("id") for org in orgs.get("orgs")]
assert org1.id in org_ids
assert org2.id in org_ids
assert org3.id not in org_ids

rv = client.get(
f"/api/v1/orgs?status=SUSPENDED&suspendedDateFrom={last_week.strftime('%Y-%m-%d')}&suspendedDateTo={yesterday.strftime('%Y-%m-%d')}",
headers=headers,
content_type="application/json",
)
assert rv.status_code == HTTPStatus.OK
orgs = json.loads(rv.data)
assert orgs.get("total") == 2
org_ids = [org.get("id") for org in orgs.get("orgs")]
assert org1.id in org_ids
assert org2.id in org_ids
assert org3.id not in org_ids

rv = client.get(
f"/api/v1/orgs?status=SUSPENDED&suspendedDateFrom={last_week.strftime('%Y-%m-%d')}&suspendedDateTo={yesterday.strftime('%Y-%m-%d')}&suspensionReasonCode={SuspensionReasonCode.OWNER_CHANGE.name}",
headers=headers,
content_type="application/json",
)
assert rv.status_code == HTTPStatus.OK
orgs = json.loads(rv.data)
assert orgs.get("total") == 1
assert orgs.get("orgs")[0].get("id") == org1.id
6 changes: 3 additions & 3 deletions auth-web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion auth-web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "auth-web",
"version": "2.10.22",
"version": "2.10.23",
"appName": "Auth Web",
"sbcName": "SBC Common Components",
"private": true,
Expand Down
Loading
Loading