From ec517334348de10ea5db7e36a8409e2083590c96 Mon Sep 17 00:00:00 2001 From: Darrell Malone Jr Date: Sun, 29 Sep 2024 15:20:17 -0500 Subject: [PATCH] Updated Officer APIs - Get officer by ID - Get all officers - Create officer Fix error in officer model Add enum property for Partner MemberRole --- backend/database/models/officer.py | 22 +++-- backend/database/models/partner.py | 9 ++ backend/routes/officers.py | 38 ++++---- backend/routes/partners.py | 1 - backend/routes/tmp/pydantic/officers.py | 115 ++++++++++++++++++++++++ 5 files changed, 159 insertions(+), 26 deletions(-) create mode 100644 backend/routes/tmp/pydantic/officers.py diff --git a/backend/database/models/officer.py b/backend/database/models/officer.py index eed2a54af..4d55658d8 100644 --- a/backend/database/models/officer.py +++ b/backend/database/models/officer.py @@ -77,6 +77,13 @@ def __repr__(self): class Officer(StructuredNode, JsonSerializable): + __property_order__ = [ + "uid", "first_name", "middle_name", + "last_name", "suffix", + "race", "ethnicity", "gender", + "date_of_birth" + ] + uid = UniqueIdProperty() first_name = StringProperty() middle_name = StringProperty() @@ -89,11 +96,16 @@ class Officer(StructuredNode, JsonSerializable): # Relationships state_ids = RelationshipTo('StateID', "HAS_STATE_ID") - units = Relationship('Unit', "MEMBER_OF_UNIT") - litigation = Relationship('Litigation', "NAMED_IN") - allegations = Relationship('Allegation', "ACCUSED_OF") - investigations = Relationship('Investigation', "LEAD_BY") - commands = Relationship('Unit', "COMMANDS") + units = Relationship( + 'backend.database.models.agency.Unit', "MEMBER_OF_UNIT") + litigation = Relationship( + 'backend.database.models.litigation.Litigation', "NAMED_IN") + allegations = Relationship( + 'backend.database.models.complaint.Allegation', "ACCUSED_OF") + investigations = Relationship( + 'backend.database.models.complaint.Investigation', "LEAD_BY") + commands = Relationship( + 'backend.database.models.agency.Unit', "COMMANDS") def __repr__(self): return f"" diff --git a/backend/database/models/partner.py b/backend/database/models/partner.py index fe6d6300b..8fd49ade2 100644 --- a/backend/database/models/partner.py +++ b/backend/database/models/partner.py @@ -78,6 +78,15 @@ def __init__(self, **kwargs): date_joined = DateTimeProperty(default=datetime.now()) is_active = BooleanProperty(default=True) + @property + def role_enum(self) -> MemberRole: + """ + Get the role as a MemberRole enum. + Returns: + MemberRole: The role as a MemberRole enum. + """ + return MemberRole(self.role) + def is_administrator(self): return self.role == MemberRole.ADMIN diff --git a/backend/routes/officers.py b/backend/routes/officers.py index 4eb89dcbe..5dc80b8f3 100644 --- a/backend/routes/officers.py +++ b/backend/routes/officers.py @@ -5,10 +5,13 @@ from backend.auth.jwt import min_role_required from backend.mixpanel.mix import track_to_mp from mixpanel import MixpanelException -from backend.database.models.user import UserRole +from backend.schemas import validate_request, ordered_jsonify, paginate_results +from backend.database.models.user import UserRole, User from backend.database.models.agency import Agency from backend.database.models.officer import Officer +from .tmp.pydantic.officers import CreateOfficer, UpdateOfficer from flask import Blueprint, abort, request +from flask_jwt_extended import get_jwt from flask_jwt_extended.view_decorators import jwt_required from pydantic import BaseModel @@ -128,16 +131,21 @@ class AddEmploymentListSchema(BaseModel): @bp.route("/", methods=["POST"]) @jwt_required() @min_role_required(UserRole.CONTRIBUTOR) -#@validate(json=CreateOfficerSchema) +@validate_request(CreateOfficer) def create_officer(): """Create an officer profile. """ + logger = logging.getLogger("create_officer") + body: CreateOfficer = request.validated_body + jwt_decoded = get_jwt() + current_user = User.get(jwt_decoded["sub"]) - try: - officer = Officer.from_dict(request.context.json) - except Exception as e: - abort(400, description=str(e)) + # try: + officer = Officer.from_dict(body.dict()) + # except Exception as e: + # abort(400, description=str(e)) + logger.info(f"Officer {officer.uid} created by User {current_user.uid}") track_to_mp( request, "create_officer", @@ -149,10 +157,9 @@ def create_officer(): # Get an officer profile -@bp.route("/", methods=["GET"]) +@bp.route("/", methods=["GET"]) @jwt_required() @min_role_required(UserRole.PUBLIC) -# @validate() def get_officer(officer_id: int): """Get an officer profile. """ @@ -166,7 +173,6 @@ def get_officer(officer_id: int): @bp.route("/", methods=["GET"]) @jwt_required() @min_role_required(UserRole.PUBLIC) -# @validate() def get_all_officers(): """Get all officers. Accepts Query Parameters for pagination: @@ -178,24 +184,16 @@ def get_all_officers(): q_per_page = args.get("per_page", 20, type=int) all_officers = Officer.nodes.all() - pagination = all_officers.paginate( - page=q_page, per_page=q_per_page, max_per_page=100 - ) + results = paginate_results(all_officers, q_page, q_per_page) - return { - "results": [ - officer_orm_to_json(officer) for officer in pagination.items], - "page": pagination.page, - "totalPages": pagination.pages, - "totalResults": pagination.total, - } + return ordered_jsonify(results), 200 # Update an officer profile @bp.route("/", methods=["PUT"]) @jwt_required() @min_role_required(UserRole.CONTRIBUTOR) -#@validate(json=CreateOfficerSchema) +@validate_request(UpdateOfficer) def update_officer(officer_id: int): """Update an officer profile. """ diff --git a/backend/routes/partners.py b/backend/routes/partners.py index 2dbd122b1..9cb823af5 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -29,7 +29,6 @@ @bp.route("/", methods=["GET"]) @jwt_required() @min_role_required(UserRole.PUBLIC) -# @validate() def get_partners(partner_id: str): """Get a single partner by ID.""" p = Partner.nodes.get_or_none(uid=partner_id) diff --git a/backend/routes/tmp/pydantic/officers.py b/backend/routes/tmp/pydantic/officers.py new file mode 100644 index 000000000..572761def --- /dev/null +++ b/backend/routes/tmp/pydantic/officers.py @@ -0,0 +1,115 @@ +from pydantic import BaseModel, Field +from typing import List, Optional, Dict, Any, Union +from .common import PaginatedResponse + + +class StateId(BaseModel): + uid: Optional[str] = Field(None, description="The UUID of this state id") + state: Optional[str] = Field(None, description="The state of the state id") + id_name: Optional[str] = Field(None, description="The name of the id. For example, Tax ID, Driver's License, etc.") + value: Optional[str] = Field(None, description="The value of the id.") + + +class BaseEmployment(BaseModel): + officer_uid: Optional[str] = Field(None, description="The UID of the officer.") + agency_uid: Optional[str] = Field(None, description="The UID of the agency the officer is employed by.") + unit_uid: Optional[str] = Field(None, description="The UID of the unit the officer is assigned to.") + earliest_employment: Optional[str] = Field(None, description="The earliest known date of employment") + latest_employment: Optional[str] = Field(None, description="The latest known date of employment") + badge_number: Optional[str] = Field(None, description="The badge number of the officer") + highest_rank: Optional[str] = Field(None, description="The highest rank the officer has held during this employment.") + commander: Optional[bool] = Field(None, description="Indicates that the officer commanded the unit during this employment.") + + +class AddEmployment(BaseEmployment, BaseModel): + officer_uid: Optional[str] = Field(None, description="The UID of the officer.") + agency_uid: Optional[str] = Field(None, description="The UID of the agency the officer is employed by.") + unit_uid: Optional[str] = Field(None, description="The UID of the unit the officer is assigned to.") + earliest_employment: Optional[str] = Field(None, description="The earliest known date of employment") + latest_employment: Optional[str] = Field(None, description="The latest known date of employment") + badge_number: Optional[str] = Field(None, description="The badge number of the officer") + highest_rank: Optional[str] = Field(None, description="The highest rank the officer has held during this employment.") + commander: Optional[bool] = Field(None, description="Indicates that the officer commanded the unit during this employment.") + + +class AddEmploymentFailed(BaseModel): + agency_uid: Optional[str] = Field(None, description="The uid of the agency that could not be added.") + reason: Optional[str] = Field(None, description="The reason the employment record could not be added") + + +class AddEmploymentList(BaseModel): + agencies: Optional[List[AddEmployment]] = Field(None, description="The units to add to the officer's employment history.") + + +class Employment(BaseEmployment, BaseModel): + officer_uid: Optional[str] = Field(None, description="The UID of the officer.") + agency_uid: Optional[str] = Field(None, description="The UID of the agency the officer is employed by.") + unit_uid: Optional[str] = Field(None, description="The UID of the unit the officer is assigned to.") + earliest_employment: Optional[str] = Field(None, description="The earliest known date of employment") + latest_employment: Optional[str] = Field(None, description="The latest known date of employment") + badge_number: Optional[str] = Field(None, description="The badge number of the officer") + highest_rank: Optional[str] = Field(None, description="The highest rank the officer has held during this employment.") + commander: Optional[bool] = Field(None, description="Indicates that the officer commanded the unit during this employment.") + + +class AddEmploymentResponse(BaseModel): + created: List[Employment] = ... + failed: List[AddEmploymentFailed] = ... + total_created: int = ... + total_failed: int = ... + + +class EmploymentList(PaginatedResponse, BaseModel): + results: Optional[List[Employment]] = None + + +class BaseOfficer(BaseModel): + first_name: Optional[str] = Field(None, description="First name of the officer") + middle_name: Optional[str] = Field(None, description="Middle name of the officer") + last_name: Optional[str] = Field(None, description="Last name of the officer") + suffix: Optional[str] = Field(None, description="Suffix of the officer's name") + ethnicity: Optional[str] = Field(None, description="The ethnicity of the officer") + gender: Optional[str] = Field(None, description="The gender of the officer") + date_of_birth: Optional[str] = Field(None, description="The date of birth of the officer") + state_ids: Optional[List[StateId]] = Field(None, description="The state ids of the officer") + + +class CreateOfficer(BaseOfficer, BaseModel): + first_name: Optional[str] = Field(None, description="First name of the officer") + middle_name: Optional[str] = Field(None, description="Middle name of the officer") + last_name: Optional[str] = Field(None, description="Last name of the officer") + suffix: Optional[str] = Field(None, description="Suffix of the officer's name") + ethnicity: Optional[str] = Field(None, description="The ethnicity of the officer") + gender: Optional[str] = Field(None, description="The gender of the officer") + date_of_birth: Optional[str] = Field(None, description="The date of birth of the officer") + state_ids: Optional[List[StateId]] = Field(None, description="The state ids of the officer") + + +class UpdateOfficer(BaseOfficer, BaseModel): + first_name: Optional[str] = Field(None, description="First name of the officer") + middle_name: Optional[str] = Field(None, description="Middle name of the officer") + last_name: Optional[str] = Field(None, description="Last name of the officer") + suffix: Optional[str] = Field(None, description="Suffix of the officer's name") + ethnicity: Optional[str] = Field(None, description="The ethnicity of the officer") + gender: Optional[str] = Field(None, description="The gender of the officer") + date_of_birth: Optional[str] = Field(None, description="The date of birth of the officer") + state_ids: Optional[List[StateId]] = Field(None, description="The state ids of the officer") + + +class Officer(BaseOfficer, BaseModel): + first_name: Optional[str] = Field(None, description="First name of the officer") + middle_name: Optional[str] = Field(None, description="Middle name of the officer") + last_name: Optional[str] = Field(None, description="Last name of the officer") + suffix: Optional[str] = Field(None, description="Suffix of the officer's name") + ethnicity: Optional[str] = Field(None, description="The ethnicity of the officer") + gender: Optional[str] = Field(None, description="The gender of the officer") + date_of_birth: Optional[str] = Field(None, description="The date of birth of the officer") + state_ids: Optional[List[StateId]] = Field(None, description="The state ids of the officer") + uid: Optional[str] = Field(None, description="The uid of the officer") + employment_history: Optional[str] = Field(None, description="A link to retrieve the employment history of the officer") + allegations: Optional[str] = Field(None, description="A link to retrieve the allegations against the officer") + litigation: Optional[str] = Field(None, description="A link to retrieve the litigation against the officer") + + +class OfficerList(PaginatedResponse, BaseModel): + results: Optional[List[Officer]] = None \ No newline at end of file