Skip to content

Commit

Permalink
Get admin users endpoints
Browse files Browse the repository at this point in the history
Create tests for new endpoints
Modify existing tests to pass agency agency_type non-null constraint
  • Loading branch information
maxachis committed Feb 12, 2025
1 parent 802e514 commit d9e4ab9
Show file tree
Hide file tree
Showing 26 changed files with 314 additions and 151 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,25 @@
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

foreign_key_name = "user_permissions_permission_id_fkey"


def upgrade() -> None:

Check warning on line 24 in alembic/versions/61978d612820_add_user_create_update_permission.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] alembic/versions/61978d612820_add_user_create_update_permission.py#L24 <103>

Missing docstring in public function
Raw output
./alembic/versions/61978d612820_add_user_create_update_permission.py:24:1: D103 Missing docstring in public function
op.drop_constraint(
constraint_name=foreign_key_name,
table_name="user_permissions",
type_="foreignkey",
)

op.create_foreign_key(
constraint_name=foreign_key_name,
source_table="user_permissions",
referent_table="permissions",
local_cols=["permission_id"],
remote_cols=["permission_id"],
ondelete="CASCADE",
)

op.execute(
"""
INSERT INTO PERMISSIONS(
Expand All @@ -33,9 +50,25 @@ def upgrade() -> None:


def downgrade() -> None:

Check warning on line 52 in alembic/versions/61978d612820_add_user_create_update_permission.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] alembic/versions/61978d612820_add_user_create_update_permission.py#L52 <103>

Missing docstring in public function
Raw output
./alembic/versions/61978d612820_add_user_create_update_permission.py:52:1: D103 Missing docstring in public function

op.execute(
"""
DELETE FROM PERMISSIONS
DELETE FROM PERMISSIONS
WHERE permission_name = 'user_create_update';
"""
)

op.drop_constraint(
constraint_name=foreign_key_name,
table_name="user_permissions",
type_="foreignkey",
)

op.create_foreign_key(
constraint_name=foreign_key_name,
source_table="user_permissions",
referent_table="permissions",
local_cols=["permission_id"],
remote_cols=["permission_id"],
ondelete="NO ACTION",
)
3 changes: 2 additions & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from flask_cors import CORS

from middleware.util import get_env_variable
from resources.Admin import namespace_admin
from resources.Batch import namespace_bulk
from resources.Callback import namespace_auth
from resources.DataRequests import namespace_data_requests
Expand Down Expand Up @@ -72,6 +73,7 @@
namespace_match,
namespace_locations,
namespace_metrics,
namespace_admin,
]

MY_PREFIX = "/api"
Expand Down Expand Up @@ -147,7 +149,6 @@ def get_api_with_namespaces():
description="The following is the API documentation for the PDAP Data Sources API."
"\n\nFor API help, consult [our getting started guide.](https://docs.pdap.io/api/introduction)"
"\n\nTo search the database, go to [pdap.io](https://pdap.io).",

)
for namespace in NAMESPACES:
api.add_namespace(namespace)
Expand Down
2 changes: 1 addition & 1 deletion database_client/DTOs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


class UserInfoNonSensitive(BaseModel):

Check warning on line 8 in database_client/DTOs.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] database_client/DTOs.py#L8 <101>

Missing docstring in public class
Raw output
./database_client/DTOs.py:8:1: D101 Missing docstring in public class
id: int
user_id: int
email: str
created_at: datetime
updated_at: datetime
Expand Down
39 changes: 25 additions & 14 deletions database_client/database_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from psycopg import sql, Cursor
from psycopg.rows import dict_row, tuple_row
from sqlalchemy import select, MetaData, delete, update, insert, Select, func
from sqlalchemy.orm import aliased, defaultload, load_only, selectinload
from sqlalchemy.orm import aliased, defaultload, load_only, selectinload, joinedload

Check warning on line 15 in database_client/database_client.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] database_client/database_client.py#L15 <401>

'sqlalchemy.orm.defaultload' imported but unused
Raw output
./database_client/database_client.py:15:1: F401 'sqlalchemy.orm.defaultload' imported but unused

Check warning on line 15 in database_client/database_client.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] database_client/database_client.py#L15 <401>

'sqlalchemy.orm.load_only' imported but unused
Raw output
./database_client/database_client.py:15:1: F401 'sqlalchemy.orm.load_only' imported but unused

Check warning on line 15 in database_client/database_client.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] database_client/database_client.py#L15 <401>

'sqlalchemy.orm.joinedload' imported but unused
Raw output
./database_client/database_client.py:15:1: F401 'sqlalchemy.orm.joinedload' imported but unused

from database_client.DTOs import UserInfoNonSensitive, UsersWithPermissions
from database_client.constants import METADATA_METHOD_NAMES, PAGE_SIZE
Expand Down Expand Up @@ -193,7 +193,7 @@ def get_user_id(self, email: str) -> Optional[int]:
return None
return int(results[0]["id"])

def set_user_password_digest(self, user_id: int, password_digest: str):
def update_user_password_digest(self, user_id: int, password_digest: str):
"""
Updates the password digest for a user in the database.
:param user_id:
Expand Down Expand Up @@ -590,7 +590,7 @@ def link_external_account(
)

@cursor_manager()
def add_user_permission(self, user_id: str, permission: PermissionsEnum):
def add_user_permission(self, user_id: str or int, permission: PermissionsEnum):
"""
Adds a permission to a user.
Expand Down Expand Up @@ -1455,7 +1455,7 @@ def get_user_info_by_id(self, user_id: int) -> UserInfoNonSensitive:
where_mappings={"id": user_id},
)
return UserInfoNonSensitive(
id=user_id,
user_id=user_id,
email=result["email"],
created_at=result["created_at"],
updated_at=result["updated_at"],
Expand All @@ -1464,23 +1464,34 @@ def get_user_info_by_id(self, user_id: int) -> UserInfoNonSensitive:
@session_manager
def get_users(self, page: int) -> List[UsersWithPermissions]:

Check warning on line 1465 in database_client/database_client.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] database_client/database_client.py#L1465 <102>

Missing docstring in public method
Raw output
./database_client/database_client.py:1465:1: D102 Missing docstring in public method
raw_results = self.session.execute(
select(User.id, User.email, User.created_at, User.updated_at)
select(User)
.options(selectinload(User.permissions))
.order_by(User.created_at.desc())
.limit(100)
.offset((page - 1) * 100)
).all()

return [
UsersWithPermissions(
id=user_id,
email=email,
created_at=created_at,
updated_at=updated_at,
permissions=permissions,
final_results = []

for raw_result in raw_results:
user = raw_result[0]
permissions_db = user.permissions
permissions_str = [
permission.permission_name for permission in permissions_db
]
permissions_enum = [
PermissionsEnum(permission) for permission in permissions_str
]
uwp = UsersWithPermissions(
user_id=user.id,
email=user.email,
created_at=user.created_at,
updated_at=user.updated_at,
permissions=permissions_enum,
)
for user_id, email, created_at, updated_at, permissions in raw_results
]
final_results.append(uwp)

return final_results

def get_user_email(self, user_id: int) -> str:
return self._select_single_entry_from_relation(
Expand Down
12 changes: 7 additions & 5 deletions database_client/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,18 +632,18 @@ class User(Base):
role: Mapped[Optional[text]]

# Relationships
permissions: Mapped[list[PermissionsEnum]] = relationship(
argument="PermissionsEnum",
permissions = relationship(
argument="Permission",
secondary="public.user_permissions",
primaryjoin="User.id == UserPermission.user_id",
secondaryjoin="UserPermission.permission_id == Permission.id",
secondaryjoin="UserPermission.permission_id == Permission.permission_id",
)


class Permission(Base):

Check warning on line 643 in database_client/models.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] database_client/models.py#L643 <101>

Missing docstring in public class
Raw output
./database_client/models.py:643:1: D101 Missing docstring in public class
__tablename__ = Relations.PERMISSIONS.value

id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
permission_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
permission_name: Mapped[str_255]
description: Mapped[Optional[text]]

Expand All @@ -653,7 +653,9 @@ class UserPermission(Base):

id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("public.users.id"))
permission_id: Mapped[int] = mapped_column(ForeignKey("public.permissions.id"))
permission_id: Mapped[int] = mapped_column(
ForeignKey("public.permissions.permission_id")
)


class PendingUser(Base):
Expand Down
3 changes: 2 additions & 1 deletion middleware/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ class Relations(Enum):
RECENT_SEARCHES = "recent_searches"
RECENT_SEARCHES_EXPANDED = "recent_searches_expanded"
LINK_RECENT_SEARCH_RECORD_CATEGORIES = "link_recent_search_record_categories"
PERMISSIONS = "permissions"
USER_PERMISSIONS = "user_permissions"


class JurisdictionType(Enum):
Expand Down Expand Up @@ -119,7 +121,6 @@ class AgencyType(Enum):
A list of valid agency types
"""

NONE = None
AGGREGATED = "aggregated"
COURT = "court"
POLICE = "police"
Expand Down
89 changes: 27 additions & 62 deletions middleware/primary_resource_logic/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,97 +2,62 @@
from flask import Response
from werkzeug.security import generate_password_hash

from database_client.DTOs import UserInfoNonSensitive
from database_client.DTOs import UserInfoNonSensitive, UsersWithPermissions

Check warning on line 5 in middleware/primary_resource_logic/admin.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] middleware/primary_resource_logic/admin.py#L5 <401>

'database_client.DTOs.UserInfoNonSensitive' imported but unused
Raw output
./middleware/primary_resource_logic/admin.py:5:1: F401 'database_client.DTOs.UserInfoNonSensitive' imported but unused
from database_client.database_client import DatabaseClient
from middleware.access_logic import AccessInfoPrimary

Check warning on line 7 in middleware/primary_resource_logic/admin.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] middleware/primary_resource_logic/admin.py#L7 <401>

'middleware.access_logic.AccessInfoPrimary' imported but unused
Raw output
./middleware/primary_resource_logic/admin.py:7:1: F401 'middleware.access_logic.AccessInfoPrimary' imported but unused
from middleware.db_client import DatabaseClient

from middleware.common_response_formatting import created_id_response
from middleware.dynamic_request_logic.supporting_classes import MiddlewareParameters
from middleware.enums import Relations
from middleware.primary_resource_logic.util import (
create_entry,
delete_entry,
get_entries,
get_entry_by_id,
update_entry,
)

from middleware.flask_response_manager import FlaskResponseManager
from middleware.schema_and_dto_logic.common_schemas_and_dtos import (

Check warning on line 12 in middleware/primary_resource_logic/admin.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] middleware/primary_resource_logic/admin.py#L12 <401>

'middleware.schema_and_dto_logic.common_schemas_and_dtos.GetByIDBaseDTO' imported but unused
Raw output
./middleware/primary_resource_logic/admin.py:12:1: F401 'middleware.schema_and_dto_logic.common_schemas_and_dtos.GetByIDBaseDTO' imported but unused
GetByIDBaseDTO,
GetManyBaseDTO,
)
from middleware.schema_and_dto_logic.primary_resource_dtos.admin_dtos import (
AdminUserPostDTO,
AdminUserPutDTO,
)
from middleware.schema_and_dto_logic.primary_resource_schemas.admin_schemas import AdminUserBaseSchema

ADMIN_POST_MIDDLEWARE_PARAMETERS = MiddlewareParameters(
entry_name="admin_user",
relation=Relations.USERS.value,
db_client_method=DatabaseClient.create_admin_user,
)


ADMIN_PUT_MIDDLEWARE_PARAMETERS = MiddlewareParameters(
entry_name="admin_user",
relation=Relations.USERS.value,
db_client_method=DatabaseClient.update_admin_user,
)


def get_users_admin(
db_client: DatabaseClient,
access_info: AccessInfoPrimary
) -> list[dict]:
def get_users_admin(db_client: DatabaseClient, dto: GetManyBaseDTO) -> Response:

Check warning on line 22 in middleware/primary_resource_logic/admin.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] middleware/primary_resource_logic/admin.py#L22 <103>

Missing docstring in public function
Raw output
./middleware/primary_resource_logic/admin.py:22:1: D103 Missing docstring in public function
# Return database client method
pass


def get_user_by_id_admin(
db_client: DatabaseClient,
access_info: AccessInfoPrimary,
user_id: str
) -> Response:
# Return database client method
user_info: UserInfoNonSensitive = db_client.get_user_info_by_id(user_id)

user_permissions = db_client.get_user_permissions(user_id)

return FlaskResponseManager.make_response({
"id": user_info.id,
"created_at": user_info.created_at,
"updated_at": user_info.updated_at,
"email": user_info.email,
"permissions": [permission.value for permission in user_permissions]
})
results: list[UsersWithPermissions] = db_client.get_users(page=dto.page)
return FlaskResponseManager.make_response(
{
"message": "Returning users",
"data": [user.model_dump(mode="json") for user in results],
"metadata": {"count": len(results)},
}
)


def create_admin_user(
db_client: DatabaseClient,
access_info: AccessInfoPrimary,
dto: AdminUserPostDTO
) -> Response:
def create_admin_user(db_client: DatabaseClient, dto: AdminUserPostDTO) -> Response:

Check warning on line 34 in middleware/primary_resource_logic/admin.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] middleware/primary_resource_logic/admin.py#L34 <103>

Missing docstring in public function
Raw output
./middleware/primary_resource_logic/admin.py:34:1: D103 Missing docstring in public function
# Hash password
password_digest = generate_password_hash(dto.password)

# Apply database client method for user and get id
user_id = db_client.create_new_user(
email=dto.email,
password_digest=password_digest
email=dto.email, password_digest=password_digest
)

# Apply database client method for permissions
for permission in dto.permissions:
db_client.add_user_permission(user_id, permission)

# Return response with id
return created_id_response(
new_id=str(user_id), message="User created."
)
return created_id_response(new_id=str(user_id), message="User created.")


def update_admin_user(db_client: DatabaseClient, access_info: AccessInfoPrimary, admin_user_id: str, dto: AdminUserPutDTO) -> dict:
def update_user_password(

Check warning on line 51 in middleware/primary_resource_logic/admin.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] middleware/primary_resource_logic/admin.py#L51 <103>

Missing docstring in public function
Raw output
./middleware/primary_resource_logic/admin.py:51:1: D103 Missing docstring in public function
db_client: DatabaseClient, user_id: int, dto: AdminUserPutDTO
) -> Response:
# Hash password
pass
password_digest = generate_password_hash(dto.password)

# Apply database client method
db_client.update_user_password_digest(
user_id=user_id, password_digest=password_digest
)

# Return response
# Return response
return FlaskResponseManager.make_response({"message": "User updated."})
4 changes: 3 additions & 1 deletion middleware/primary_resource_logic/reset_token_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ def change_password_wrapper(

def set_user_password(db_client: DatabaseClient, user_id: int, password: str):
password_digest = generate_password_hash(password)
db_client.set_user_password_digest(user_id=user_id, password_digest=password_digest)
db_client.update_user_password_digest(
user_id=user_id, password_digest=password_digest
)


def invalid_token_response():
Expand Down
2 changes: 1 addition & 1 deletion middleware/primary_resource_logic/user_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def get_user_by_id_wrapper(
# Check that user is either owner or admin
if (
user_id != access_info.get_user_id()
and PermissionsEnum.DB_WRITE not in access_info.permissions
and PermissionsEnum.READ_ALL_USER_INFO not in access_info.permissions

Check warning on line 53 in middleware/primary_resource_logic/user_profile.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] middleware/primary_resource_logic/user_profile.py#L53 <503>

line break before binary operator
Raw output
./middleware/primary_resource_logic/user_profile.py:53:9: W503 line break before binary operator
):
return FlaskResponseManager.make_response(
data={"message": "Forbidden."}, status_code=HTTPStatus.FORBIDDEN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,11 @@ def _build_nested_field_as_model(self, fi: FieldInfo):

def _raise_if_nonzero_parser_fields(self, fi, sorter):
if len(sorter.parser_fields) != 0:
parser_field_names = [x.field_name for x in sorter.parser_fields]
raise ValueError(
f"Nested Model `{fi.field_name}` in model `{self.model_name}` has fields that are identified as parser fields."
f"Nested Model `{fi.field_name}` in model `{self.model_name}` "
f"has fields that are identified as parser fields."
f"({parser_field_names})"
)

def _raise_if_no_model_fields(self, fi, fields):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from dataclasses import dataclass

Check warning on line 1 in middleware/schema_and_dto_logic/primary_resource_dtos/admin_dtos.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] middleware/schema_and_dto_logic/primary_resource_dtos/admin_dtos.py#L1 <100>

Missing docstring in public module
Raw output
./middleware/schema_and_dto_logic/primary_resource_dtos/admin_dtos.py:1:1: D100 Missing docstring in public module
from typing import Optional

from middleware.enums import PermissionsEnum


@dataclass
class AdminUserPostDTO:

Check warning on line 8 in middleware/schema_and_dto_logic/primary_resource_dtos/admin_dtos.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] middleware/schema_and_dto_logic/primary_resource_dtos/admin_dtos.py#L8 <101>

Missing docstring in public class
Raw output
./middleware/schema_and_dto_logic/primary_resource_dtos/admin_dtos.py:8:1: D101 Missing docstring in public class
email: str
password: str
permissions: list[str]
permissions: list[PermissionsEnum]


@dataclass
class AdminUserPutDTO:

Check warning on line 15 in middleware/schema_and_dto_logic/primary_resource_dtos/admin_dtos.py

View workflow job for this annotation

GitHub Actions / flake8

[flake8] middleware/schema_and_dto_logic/primary_resource_dtos/admin_dtos.py#L15 <101>

Missing docstring in public class
Raw output
./middleware/schema_and_dto_logic/primary_resource_dtos/admin_dtos.py:15:1: D101 Missing docstring in public class
password: Optional[str] = None
permissions: Optional[list[str]] = None
Loading

0 comments on commit d9e4ab9

Please sign in to comment.