Skip to content
Closed
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
4 changes: 3 additions & 1 deletion strr-api/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
"python.testing.pytestEnabled": true,
"python-envs.defaultEnvManager": "ms-python.python:poetry",
"python-envs.defaultPackageManager": "ms-python.python:poetry"
}
120 changes: 45 additions & 75 deletions strr-api/migrations/env.py
Original file line number Diff line number Diff line change
@@ -1,104 +1,74 @@
import os
import logging
from logging.config import fileConfig

from flask import current_app

from sqlalchemy import engine_from_config, pool, create_engine
from alembic import context

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# 1. Passive Detection: Don't create an app, just look for one
try:
from flask import current_app
from strr_api import db
# If we are in a Flask context (like your Flask tests or running the web app)
if current_app:
use_flask = True
target_metadata = db.metadata
else:
raise ImportError
except (ImportError, RuntimeError):
# 2. Standalone Fallback: For your Jobs and Job-tests
use_flask = False
from strr_api.models.base_model import SimpleBaseModel
target_metadata = SimpleBaseModel.metadata

# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')


def get_engine():
return current_app.extensions['migrate'].db.engine

if use_flask:
return current_app.extensions['migrate'].db.engine
# Standalone: use the env var you set in conftest or your job environment
url = os.getenv("DATABASE_URL")
return create_engine(url, poolclass=pool.NullPool)

def get_engine_url():
try:
return get_engine().url.render_as_string(hide_password=False).replace(
'%', '%%')
except AttributeError:
if use_flask:
return str(get_engine().url).replace('%', '%%')


# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option('sqlalchemy.url', get_engine_url())
target_db = current_app.extensions['migrate'].db

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.

return os.getenv("DATABASE_URL").replace('%', '%%')

def get_metadata():
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata


def run_migrations_offline():
"""Run migrations in 'offline' mode.

This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.

Calls to context.execute() here emit the given string to the
script output.

"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
)

with context.begin_transaction():
context.run_migrations()
return target_metadata

config.set_main_option('sqlalchemy.url', get_engine_url())

def run_migrations_online():
"""Run migrations in 'online' mode.

In this scenario we need to create an Engine
and associate a connection with the context.

"""

# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')

connectable = get_engine()

with connectable.connect() as connection:
# Only pull migrate args if Flask is actually present
extra_args = current_app.extensions['migrate'].configure_args if use_flask else {}

context.configure(
connection=connection,
target_metadata=get_metadata(),
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
**extra_args
)

with context.begin_transaction():
context.run_migrations()


def run_migrations_offline():
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=get_metadata(),
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)

with context.begin_transaction():
context.run_migrations()

if context.is_offline_mode():
run_migrations_offline()
else:
Expand Down
40 changes: 39 additions & 1 deletion strr-api/poetry.lock

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

4 changes: 2 additions & 2 deletions strr-api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "strr-api"
version = "0.3.4"
version = "0.3.7"
description = ""
authors = ["thorwolpert <thor@wolpert.ca>"]
license = "BSD 3-Clause"
Expand All @@ -26,7 +26,7 @@ pytest-env = "^1.1.3"
coloredlogs = "^15.0.1"
flask-httpauth = "^4.8.0"
flasgger = "^0.9.7.1"
pydantic = "^2.7.1"
pydantic = {extras = ["email"], version = "^2.12.5"}
google-auth = "^2.29.0"
google-cloud-storage = "2.14.0"
geoalchemy2 = "^0.15.1"
Expand Down
9 changes: 9 additions & 0 deletions strr-api/src/strr_api/models/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,15 @@ def find_by_account(
Application.type == ApplicationType.RENEWAL.value,
)
)
if filter_criteria.applications_only:
# Exclude applications that have a completed registration, except renewals
# Include: no registration yet or renewal applications
query = query.filter(
db.or_(
Application.registration_id.is_(None),
Application.type == ApplicationType.RENEWAL.value,
)
)
sort_column = getattr(Application, filter_criteria.sort_by, Application.id)
if filter_criteria.sort_order and filter_criteria.sort_order.lower() == "asc":
query = query.order_by(sort_column.asc())
Expand Down
4 changes: 4 additions & 0 deletions strr-api/src/strr_api/models/dataclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class ApplicationSearch:
requirements: list[str] | None = None
include_draft_registration: bool = True
include_draft_renewal: bool = True
applications_only: bool = False
account_id: int | None = None


Expand All @@ -70,3 +71,6 @@ class RegistrationSearch:
assignee: str | None = None
requirements: list[str] | None = None
account_id: int | None = None
approval_methods: List[str] | None = None
noc_statuses: List[str] | None = None
is_set_aside: bool | None = None
31 changes: 31 additions & 0 deletions strr-api/src/strr_api/models/rental.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ def search_registrations(cls, filter_criteria: RegistrationSearch):
)
if filter_criteria.requirements:
query = cls._filter_by_registration_requirement(filter_criteria.requirements, query)
if filter_criteria.approval_methods:
query = cls._filter_by_approval_method(filter_criteria.approval_methods, query)
if filter_criteria.noc_statuses:
query = query.filter(Registration.noc_status.in_(filter_criteria.noc_statuses))
if filter_criteria.is_set_aside is True:
query = query.filter(Registration.is_set_aside == True) # noqa: E712
sort_column = getattr(Registration, filter_criteria.sort_by, Registration.id)
if filter_criteria.sort_order and filter_criteria.sort_order.lower() == "asc":
query = query.order_by(sort_column.asc())
Expand Down Expand Up @@ -423,6 +429,31 @@ def _filter_by_registration_requirement(cls, requirement: list[str], query):
query = query.filter(db.or_(*combined_conditions))
return query

@classmethod
def _filter_by_approval_method(cls, approval_methods: list[str], query):
"""Filter registrations by application approval method.

Only considers the most recent application (index 0 when sorted by
application_date desc) for each registration. Returns registrations
where that most recent application's status is in the given approval methods.
"""
if not approval_methods:
return query
# pylint: disable=import-outside-toplevel
from sqlalchemy import select

from strr_api.models.application import Application

# for each registration, get the status of the most recent application
latest_app_status_subq = (
select(Application.status)
.where(Application.registration_id == Registration.id)
.order_by(Application.application_date.desc())
.limit(1)
.scalar_subquery()
)
return query.filter(latest_app_status_subq.in_(approval_methods))


class RentalProperty(Versioned, BaseModel):
"""Rental Property"""
Expand Down
7 changes: 7 additions & 0 deletions strr-api/src/strr_api/resources/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ def get_applications():
type: boolean
default: true
description: Include draft renewal applications
- in: query
name: applicationsOnly
type: boolean
default: false
description: When true, exclude applications that have a completed registration (except renewals). For split dashboard applications table.
responses:
200:
description:
Expand All @@ -287,6 +292,7 @@ def get_applications():
requirements = request.args.getlist("requirement", None)
include_draft_registration = request.args.get("includeDraftRegistration", "true").lower() == "true"
include_draft_renewal = request.args.get("includeDraftRenewal", "true").lower() == "true"
applications_only = request.args.get("applicationsOnly", "false").lower() == "true"
if sort_by not in VALID_SORT_FIELDS:
sort_by = "id"
if sort_order not in ["asc", "desc"]:
Expand All @@ -304,6 +310,7 @@ def get_applications():
requirements=requirements,
include_draft_registration=include_draft_registration,
include_draft_renewal=include_draft_renewal,
applications_only=applications_only,
)
application_list = ApplicationService.list_applications(account_id, filter_criteria=filter_criteria)
return jsonify(application_list), HTTPStatus.OK
Expand Down
25 changes: 25 additions & 0 deletions strr-api/src/strr_api/resources/registrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,24 @@ def search_registrations():
items:
type: string
description: Requirement filter (e.g., PR, BL, PROHIBITED, NO_REQ, PR_EXEMPT_STRATA_HOTEL, PR_EXEMPT_FARM_LAND, PR_EXEMPT_FRACTIONAL_OWNERSHIP, PLATFORM_MAJOR, PLATFORM_MEDIUM, PLATFORM_MINOR, STRATA_PR, STRATA_NO_PR). Can provide multiple values.
- in: query
name: approvalMethod
type: array
items:
type: string
enum: [FULL_REVIEW_APPROVED, AUTO_APPROVED, PROVISIONALLY_APPROVED]
description: Approval method filter. Can provide multiple values.
- in: query
name: nocStatus
type: array
items:
type: string
enum: [NOC_EXPIRED, NOC_PENDING]
description: NOC status filter. Can provide multiple values.
- in: query
name: isSetAside
type: boolean
description: Filter for set aside registrations when true.
responses:
200:
description:
Expand All @@ -1034,6 +1052,10 @@ def search_registrations():
sort_order = request.args.get("sortOrder", "desc")
assignee = request.args.get("assignee", None)
requirements = request.args.getlist("requirement") or None
approval_methods = request.args.getlist("approvalMethod") or None
noc_statuses = request.args.getlist("nocStatus") or None
is_set_aside_param = request.args.get("isSetAside", None)
is_set_aside = is_set_aside_param.lower() == "true" if is_set_aside_param else None
if sort_by not in VALID_REGISTRATION_SORT_FIELDS:
sort_by = "id"
if sort_order not in ["asc", "desc"]:
Expand All @@ -1052,6 +1074,9 @@ def search_registrations():
sort_order=sort_order,
assignee=assignee,
requirements=requirements,
approval_methods=approval_methods,
noc_statuses=noc_statuses,
is_set_aside=is_set_aside,
)

registration_list = RegistrationService.search_registrations(filter_criteria=filter_criteria)
Expand Down
Loading
Loading