diff --git a/alembic/dev_seeds.py b/alembic/dev_seeds.py index c5d0f70cf..b7ee41410 100644 --- a/alembic/dev_seeds.py +++ b/alembic/dev_seeds.py @@ -6,6 +6,7 @@ from backend.database.models.partner import Partner from backend.database.models.use_of_force import UseOfForce + def create_user(user): user_exists = ( db.session.query(User).filter_by(email=user.email).first() is not None @@ -14,6 +15,7 @@ def create_user(user): if not user_exists: user.create() + def create_partner(partner): partner_exists = ( db.session.query(Partner).filter_by(id=partner.id).first() is not None @@ -22,14 +24,12 @@ def create_partner(partner): if not partner_exists: partner.create() + def create_incident(key=1, date="10-01-2019", lon=84, lat=34): - base_id = 10000000 - id = base_id + key mpv = db.session.query(Partner).filter_by( name="Mapping Police Violence").first() incident = Incident( - id=id, - source_id="1", + source_id=mpv.id, date_record_created=f"{date} 00:00:00", time_of_incident=f"{date} 00:00:00", time_confidence="1", @@ -55,11 +55,12 @@ def create_incident(key=1, date="10-01-2019", lon=84, lat=34): ], use_of_force=[UseOfForce(item=f"gunshot {key}")] ) - exists = db.session.query(Incident).filter_by(id=id).first() is not None + exists = db.session.query(Incident).filter_by(id=key).first() is not None if not exists: incident.create() + def create_seeds(): create_user( User( @@ -103,7 +104,6 @@ def create_seeds(): ) create_partner( Partner( - id="1", name="Mapping Police Violence", url="https://mappingpoliceviolence.us", contact_email="info@campaignzero.org" @@ -118,4 +118,5 @@ def create_seeds(): create_incident(key=7, date="10-01-2020", lon=-118.40853, lat=33.9415889) create_incident(key=8, date="10-15-2020", lon=-84.032149, lat=33.967774) -create_seeds() \ No newline at end of file + +create_seeds() diff --git a/backend/routes/partners.py b/backend/routes/partners.py index 72058e989..5ec3be358 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -4,13 +4,14 @@ from flask import Blueprint, abort, current_app, request from flask_jwt_extended import get_jwt from flask_jwt_extended.view_decorators import jwt_required - +from flask_sqlalchemy import Pagination from ..database import Partner, PartnerMember, MemberRole, db from ..schemas import ( CreatePartnerSchema, AddMemberSchema, partner_orm_to_json, partner_member_orm_to_json, + user_orm_to_json, partner_member_to_orm, partner_to_orm, validate, @@ -54,10 +55,14 @@ def create_partner(): ) make_admin.create() - track_to_mp(request, "create_partner", { - "partner_name": partner.name, - "partner_contact": partner.contact_email - }) + track_to_mp( + request, + "create_partner", + { + "partner_name": partner.name, + "partner_contact": partner.contact_email, + }, + ) return partner_orm_to_json(created) @@ -107,12 +112,12 @@ def get_partner_members(partner_id: int): PartnerMember.partner_id == partner_id ) results = all_members.paginate( - page=q_page, per_page=q_per_page, max_per_page=100) + page=q_page, per_page=q_per_page, max_per_page=100 + ) return { "results": [ - partner_member_orm_to_json(member) - for member in results.items + partner_member_orm_to_json(member) for member in results.items ], "page": results.page, "totalPages": results.pages, @@ -136,6 +141,42 @@ class Config: } """ +@bp.route("//users", methods=["GET"]) +@jwt_required() # type: ignore +@min_role_required(UserRole.PUBLIC) +@validate() # type: ignore +def get_partner_users(partner_id: int): + # Get the page number from the query parameters (default to 1) + page = request.args.get("page", 1, type=int) + + # Get the number of items per page from the query parameters (default to 20) + per_page = request.args.get("per_page", 20, type=int) + + # Query the PartnerMember table for records with + # the given partner_id and paginate the results + pagination: Pagination = PartnerMember.query.filter_by( + partner_id=partner_id + ).paginate(page=page, per_page=per_page, error_out=False) + + # If the partner_id is invalid, return a 404 error + if pagination.total == 0: + return {"message": "Partner not found"}, 404 + + # Get the User objects associated with the members on the current page + users: list[User] = [ + User.query.get(member.user_id) for member in pagination.items + ] # type: ignore + + # Convert the User objects to dictionaries and return them as JSON + + return { + "results": [user_orm_to_json(user) for user in users], + "page": pagination.page, + "totalPages": pagination.pages, + "totalResults": pagination.total, + } + + @bp.route("//members/add", methods=["POST"]) @jwt_required() @min_role_required(UserRole.PUBLIC) @@ -157,10 +198,14 @@ def add_member_to_partner(partner_id: int): jwt_decoded = get_jwt() current_user = User.get(jwt_decoded["sub"]) - association = db.session.query(PartnerMember).filter( - PartnerMember.user_id == current_user.id, - PartnerMember.partner_id == partner_id, - ).first() + association = ( + db.session.query(PartnerMember) + .filter( + PartnerMember.user_id == current_user.id, + PartnerMember.partner_id == partner_id, + ) + .first() + ) if ( association is None @@ -187,9 +232,13 @@ def add_member_to_partner(partner_id: int): created = partner_member.create() - track_to_mp(request, "add_partner_member", { - "partner_id": partner_id, - "user_id": partner_member.user_id, - "role": partner_member.role, - }) + track_to_mp( + request, + "add_partner_member", + { + "partner_id": partner_id, + "user_id": partner_member.user_id, + "role": partner_member.role, + }, + ) return partner_member_orm_to_json(created) diff --git a/backend/schemas.py b/backend/schemas.py index b7f1a97e4..104a1c807 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -187,8 +187,7 @@ class CreatePartnerMemberSchema(BaseModel): AddMemberSchema = sqlalchemy_to_pydantic( - PartnerMember, - exclude=["id", "date_joined", "partner", "user"] + PartnerMember, exclude=["id", "date_joined", "partner", "user"] ) @@ -298,7 +297,8 @@ def partner_orm_to_json(partner: Partner) -> dict: def partner_member_to_orm( - partner_member: CreatePartnerMemberSchema) -> PartnerMember: + partner_member: CreatePartnerMemberSchema, +) -> PartnerMember: """Convert the JSON partner member into an ORM instance""" orm_attrs = partner_member.dict() return PartnerMember(**orm_attrs) @@ -308,3 +308,12 @@ def partner_member_orm_to_json(partner_member: PartnerMember) -> dict: return PartnerMemberSchema.from_orm(partner_member).dict( exclude_none=True, ) + + +def user_orm_to_json(user: User) -> Dict[str, Any]: + return UserSchema.from_orm(user).dict( + exclude={ + "password", + "email_confirmed_at", + } + ) diff --git a/backend/tests/test_partners.py b/backend/tests/test_partners.py index 5ad33d6b0..8634e6077 100644 --- a/backend/tests/test_partners.py +++ b/backend/tests/test_partners.py @@ -2,6 +2,7 @@ from backend.auth import user_manager from backend.database import Partner, PartnerMember, MemberRole from backend.database.models.user import User, UserRole +from typing import Any publisher_email = "pub@partner.com" inactive_email = "lurker@partner.com" @@ -13,18 +14,18 @@ "cpdp": { "name": "Citizens Police Data Project", "url": "https://cpdp.co", - "contact_email": "tech@invisible.institute" + "contact_email": "tech@invisible.institute", }, "mpv": { "name": "Mapping Police Violence", "url": "https://mappingpoliceviolence.us", - "contact_email": "samswey1@gmail.com" + "contact_email": "samswey1@gmail.com", }, "fe": { "name": "Fatal Encounters", "url": "https://fatalencounters.org", - "contact_email": "d.brian@fatalencounters.org" - } + "contact_email": "d.brian@fatalencounters.org", + }, } mock_users = { @@ -43,30 +44,30 @@ "member": { "email": member_email, "password": example_password, - } + }, } mock_members = { "publisher": { "user_email": publisher_email, "role": MemberRole.PUBLISHER, - "is_active": True + "is_active": True, }, "inactive": { "user_email": inactive_email, "role": MemberRole.PUBLISHER, - "is_active": False + "is_active": False, }, "admin": { "user_email": publisher_email, "role": MemberRole.ADMIN, - "is_active": True + "is_active": True, }, "member": { "user_email": publisher_email, "role": MemberRole.MEMBER, - "is_active": True - } + "is_active": True, + }, } @@ -78,8 +79,7 @@ def example_partners(client, access_token): res = client.post( "/api/v1/partners/create", json=mock, - headers={"Authorization": - "Bearer {0}".format(access_token)}, + headers={"Authorization": "Bearer {0}".format(access_token)}, ) assert res.status_code == 200 created[id] = res.json @@ -98,38 +98,38 @@ def example_members(client, db_session, example_partner, p_admin_access_token): role=UserRole.PUBLIC, first_name=id, last_name="user", - phone_number="(278) 555-7890" + phone_number="(278) 555-7890", ) db_session.add(user) db_session.commit() users[id] = user partner_obj = ( - db_session.query(Partner).filter( - Partner.name == example_partner.name - ).first() + db_session.query(Partner) + .filter(Partner.name == example_partner.name) + .first() ) for id, mock in mock_members.items(): - user_obj = ( - db_session.query(User).filter( - User.email == mock["user_email"] - ).first() + db_session.query(User) + .filter(User.email == mock["user_email"]) + .first() ) req = { "partner_id": partner_obj.id, "user_id": user_obj.id, "role": mock["role"], - "is_active": mock["is_active"] + "is_active": mock["is_active"], } res = client.post( f"/api/v1/partners/{partner_obj.id}/members/add", json=req, - headers={"Authorization": - "Bearer {0}".format(p_admin_access_token)}, + headers={ + "Authorization": "Bearer {0}".format(p_admin_access_token) + }, ) assert res.status_code == 200 created[id] = res.json @@ -140,8 +140,9 @@ def test_create_partner(db_session, example_user, example_partners): created = example_partners["mpv"] partner_obj = ( - db_session.query(Partner).filter(Partner.name == created["name"] - ).first() + db_session.query(Partner) + .filter(Partner.name == created["name"]) + .first() ) user_obj = ( @@ -149,10 +150,12 @@ def test_create_partner(db_session, example_user, example_partners): ) association_obj = ( - db_session.query(PartnerMember).filter( + db_session.query(PartnerMember) + .filter( PartnerMember.partner_id == partner_obj.id, - PartnerMember.user_id == user_obj.id - ).first() + PartnerMember.user_id == user_obj.id, + ) + .first() ) assert partner_obj.name == created["name"] @@ -222,8 +225,9 @@ def test_add_member_to_partner(db_session, example_members): created = example_members["publisher"] partner_member_obj = ( - db_session.query(PartnerMember).filter( - PartnerMember.id == created["id"]).first() + db_session.query(PartnerMember) + .filter(PartnerMember.id == created["id"]) + .first() ) assert partner_member_obj.partner_id == created["partner_id"] @@ -232,23 +236,22 @@ def test_add_member_to_partner(db_session, example_members): def test_get_partner_members( - db_session, client, example_partner, example_user, - admin_user, access_token): + db_session, client, example_partner, example_user, admin_user, access_token +): # Create partners in the database users = [] partner_obj = ( - db_session.query(Partner).filter( - Partner.name == example_partner.name).first() + db_session.query(Partner) + .filter(Partner.name == example_partner.name) + .first() ) member_obj = ( - db_session.query(User).filter( - User.email == example_user.email).first() + db_session.query(User).filter(User.email == example_user.email).first() ) admin_obj = ( - db_session.query(User).filter( - User.email == admin_user.email).first() + db_session.query(User).filter(User.email == admin_user.email).first() ) users.append(member_obj) @@ -256,8 +259,7 @@ def test_get_partner_members( for user in users: association_obj = PartnerMember( - partner_id=partner_obj.id, - user_id=user.id + partner_id=partner_obj.id, user_id=user.id ) db_session.add(association_obj) db_session.commit() @@ -265,8 +267,7 @@ def test_get_partner_members( # Test that we can get partners res = client.get( f"/api/v1/partners/{partner_obj.id}/members/", - headers={"Authorization": - "Bearer {0}".format(access_token)} + headers={"Authorization": "Bearer {0}".format(access_token)}, ) assert res.status_code == 200 @@ -274,4 +275,47 @@ def test_get_partner_members( # assert res.json["results"][0]["user"]["email"] == member_obj.email -# def deactivate_partner_member(client, example_partners, access_token): +def test_get_partner_users( + client: Any, + example_partner: Partner, + example_members: PartnerMember, + access_token: str, +) -> None: + # Test that we can get partner users + res: Any = client.get( + f"/api/v1/partners/{example_partner.id}/users", + headers={"Authorization": "Bearer {0}".format(access_token)}, + ) + assert res.status_code == 200 + data = res.get_json() + + # Verify the response structure + assert "results" in data + assert "page" in data + assert "totalPages" in data + assert "totalResults" in data + + # Verify the results + assert len(data["results"]) == len(mock_users) + 1 + + # Verify the page number + assert data["page"] == 1 + + # Verify the total pages + assert data["totalPages"] == 1 + + # Verify the total results + assert data["totalResults"] == len(mock_users) + 1 + + +def test_get_partner_users_error( + client: Any, + access_token: str, +) -> None: + # Test that we can get partner users + res: Any = client.get( + f"/api/v1/partners/{1234}/users", + headers={"Authorization": "Bearer {0}".format(access_token)}, + ) + assert res.status_code == 404 + assert res.get_json()["message"] == "Partner not found"