Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 20 additions & 4 deletions backend/routes/officers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from datetime import datetime
from typing import Optional, List

from backend.auth.jwt import min_role_required
Expand All @@ -7,6 +8,7 @@
add_pagination_wrapper)
from backend.database.models.user import UserRole, User
from backend.database.models.officer import Officer
from backend.database.models.source import Source
from backend.routes.search import create_officer_result
from .tmp.pydantic.officers import CreateOfficer, UpdateOfficer
from flask import Blueprint, abort, request, jsonify
Expand Down Expand Up @@ -134,16 +136,30 @@ class AddEmploymentListSchema(BaseModel):
@validate_request(CreateOfficer)
def create_officer():
"""Create an officer profile.

Requires a valid source_uid. The officer will be linked to the
specified data source via an UPDATED_BY citation.
"""
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(body.dict())
# except Exception as e:
# abort(400, description=str(e))
source = Source.nodes.get_or_none(uid=body.source_uid)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great. The only other thing is that the current user needs to be a member of the Source organization and have a member role of at least Publisher.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

members = RelationshipFrom(

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to hear. I see you solved that by adding line 148, correct? Let me know if I can still develop this issue.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I haven't changed anything. Just highlighting the section of code I'm talking about. Sorry I wasn't clear.

You'll need to add some logic that checks to see if the user is a member of the Source indicated by the source_uid. Something like:

    if source.members.is_connected(current_user):
        member_rel = source.members.relationship(current_user)
        if member_rel.may_publish()
          # Prodeed with Creation
        else:
        abort(403, description="No permission.")
    else:
        abort(403, description="No permission.")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am looking at this! I made new changes, and I just need to run tests.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if I can help

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Darrel, I believe the last change solves the check for membership. I think this is ready for merge.

if source is None:
abort(
422,
description=(
f"Source with UID '{body.source_uid}' not found. "
"A valid data source is required to create an officer."
),
)

officer_data = body.dict()
officer_data.pop("source_uid", None)
officer = Officer.from_dict(officer_data)

officer.citations.connect(source, {"date": datetime.now()})

logger.info(f"Officer {officer.uid} created by User {current_user.uid}")
track_to_mp(
Expand Down
1 change: 1 addition & 0 deletions backend/routes/tmp/pydantic/officers.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class CreateOfficer(BaseOfficer, BaseModel):
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")
source_uid: str = Field(..., description="The UID of the data source for this officer record")


class UpdateOfficer(BaseOfficer, BaseModel):
Expand Down
52 changes: 50 additions & 2 deletions backend/tests/test_officers.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,16 @@ def example_officers():
def test_create_officer(
client,
contributor_access_token,
example_source,
example_agency
):

# Test that we can create an officer without an agency association
request = {
"first_name": "Max",
"last_name": "Payne",
"ethnicity": "White",
"gender": "Male"
"gender": "Male",
"source_uid": example_source.uid,
}
res = client.post(
"/api/v1/officers/",
Expand All @@ -169,6 +170,53 @@ def test_create_officer(
assert officer_obj.ethnicity == request["ethnicity"]
assert officer_obj.gender == request["gender"]

source = officer_obj.citations.all()
assert len(source) == 1
assert source[0].uid == example_source.uid


def test_create_officer_without_source_uid(
client,
contributor_access_token,
):
"""POST officer without source_uid should fail with 422."""
request = {
"first_name": "Max",
"last_name": "Payne",
"ethnicity": "White",
"gender": "Male",
}
res = client.post(
"/api/v1/officers/",
json=request,
headers={
"Authorization": "Bearer {0}".format(contributor_access_token)
},
)
assert res.status_code == 422


def test_create_officer_with_invalid_source_uid(
client,
contributor_access_token,
):
"""POST officer with a non-existent source_uid should fail with 422."""
request = {
"first_name": "Max",
"last_name": "Payne",
"ethnicity": "White",
"gender": "Male",
"source_uid": "nonexistent-uid-12345",
}
res = client.post(
"/api/v1/officers/",
json=request,
headers={
"Authorization": "Bearer {0}".format(contributor_access_token)
},
)
assert res.status_code == 422


def test_get_officer(client, example_officer, access_token):
# Test that we can get it
Expand Down