diff --git a/backend/database/__init__.py b/backend/database/__init__.py index bacb988a..2592bf86 100644 --- a/backend/database/__init__.py +++ b/backend/database/__init__.py @@ -11,6 +11,7 @@ # Alembic only does a couple models, not all of them. from .models.agency import * +from .models.unit import * from .models.attorney import * from .models.case_document import * from .models.incident import * diff --git a/backend/database/models/agency.py b/backend/database/models/agency.py index c2008820..09048f67 100644 --- a/backend/database/models/agency.py +++ b/backend/database/models/agency.py @@ -1,4 +1,4 @@ -from ..core import db, CrudMixin +from ..core import CrudMixin, db from enum import Enum from sqlalchemy.ext.associationproxy import association_proxy @@ -22,6 +22,8 @@ class Agency(db.Model, CrudMixin): jurisdiction = db.Column(db.Enum(Jurisdiction)) # total_officers = db.Column(db.Integer) + units = db.relationship("Unit", back_populates="agency") + officer_association = db.relationship("Employment", back_populates="agency") officers = association_proxy("officer_association", "officer") diff --git a/backend/database/models/employment.py b/backend/database/models/employment.py index f5e8772c..27424c2d 100644 --- a/backend/database/models/employment.py +++ b/backend/database/models/employment.py @@ -20,15 +20,16 @@ class Employment(db.Model, CrudMixin): id = db.Column(db.Integer, primary_key=True) officer_id = db.Column(db.Integer, db.ForeignKey("officer.id")) agency_id = db.Column(db.Integer, db.ForeignKey("agency.id")) + unit_id = db.Column(db.Integer, db.ForeignKey("unit.id")) earliest_employment = db.Column(db.Text) latest_employment = db.Column(db.Text) badge_number = db.Column(db.Text) - unit = db.Column(db.Text) highest_rank = db.Column(db.Enum(Rank)) currently_employed = db.Column(db.Boolean) officer = db.relationship("Officer", back_populates="agency_association") agency = db.relationship("Agency", back_populates="officer_association") + unit = db.relationship("Unit", back_populates="officer_association") def __repr__(self): return f"" @@ -65,7 +66,6 @@ def get_highest_rank(records: list[Employment]): def merge_employment_records( records: list[Employment], - unit: str = None, currently_employed: bool = None ): """ @@ -85,17 +85,15 @@ def merge_employment_records( """ earliest_employment, latest_employment = get_employment_range(records) highest_rank = get_highest_rank(records) - if unit is None: - unit = records[0].unit if currently_employed is None: currently_employed = records[0].currently_employed return Employment( officer_id=records[0].officer_id, agency_id=records[0].agency_id, + unit_id=records[0].unit_id, badge_number=records[0].badge_number, earliest_employment=earliest_employment, latest_employment=latest_employment, - unit=unit, highest_rank=highest_rank, currently_employed=currently_employed, ) diff --git a/backend/database/models/unit.py b/backend/database/models/unit.py new file mode 100644 index 00000000..50aca467 --- /dev/null +++ b/backend/database/models/unit.py @@ -0,0 +1,25 @@ +from ..core import CrudMixin, db +from sqlalchemy.ext.associationproxy import association_proxy + + +class Unit(db.Model, CrudMixin): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.Text) + website_url = db.Column(db.Text) + phone = db.Column(db.Text) + email = db.Column(db.Text) + description = db.Column(db.Text) + address = db.Column(db.Text) + zip = db.Column(db.Text) + agency_url = db.Column(db.Text) + officers_url = db.Column(db.Text) + + commander_id = db.Column(db.Integer, db.ForeignKey('officer.id')) + agency_id = db.Column(db.Integer, db.ForeignKey('agency.id')) + + agency = db.relationship("Agency", back_populates="units") + officer_association = db.relationship('Employment', back_populates='unit') + officers = association_proxy('officer_association', 'officer') + + def __repr__(self): + return f"" diff --git a/backend/routes/agencies.py b/backend/routes/agencies.py index 83894ef1..2de83188 100644 --- a/backend/routes/agencies.py +++ b/backend/routes/agencies.py @@ -231,7 +231,6 @@ def add_officer_to_agency(agency_id: int): employment.agency_id = agency_id employment = merge_employment_records( employments.all() + [employment], - unit=record.unit, currently_employed=record.currently_employed ) diff --git a/backend/routes/officers.py b/backend/routes/officers.py index 93543867..184c8e5e 100644 --- a/backend/routes/officers.py +++ b/backend/routes/officers.py @@ -304,7 +304,6 @@ def update_employment(officer_id: int): employment.officer_id = officer_id employment = merge_employment_records( employments.all() + [employment], - unit=record.unit, currently_employed=record.currently_employed ) diff --git a/backend/schemas.py b/backend/schemas.py index b38fd5d0..33b96d2e 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -14,6 +14,7 @@ from .database.models.partner import Partner, PartnerMember, MemberRole from .database.models.incident import Incident, SourceDetails from .database.models.agency import Agency, Jurisdiction +from .database.models.unit import Unit from .database.models.officer import Officer, StateID from .database.models.employment import Employment from .database.models.accusation import Accusation @@ -122,6 +123,13 @@ def validate(auth=True, **kwargs): ] _agency_list_attributes = [ + 'units', + 'officer_association', + 'officers' +] + +_unit_list_attributes = [ + 'agency', 'officer_association', 'officers' ] @@ -179,6 +187,16 @@ def none_to_list(cls, values: Dict[str, Any]) -> Dict[str, Any]: return values +class _UnitMixin(BaseModel): + @root_validator(pre=True) + def none_to_list(cls, values: Dict[str, Any]) -> Dict[str, Any]: + values = {**values} # convert mappings to base dict type. + for i in _unit_list_attributes: + if not values.get(i): + values[i] = [] + return values + + def schema_create(model_type: DeclarativeMeta, **kwargs) -> ModelMetaclass: return sqlalchemy_to_pydantic(model_type, exclude="id", **kwargs) @@ -187,6 +205,7 @@ def schema_create(model_type: DeclarativeMeta, **kwargs) -> ModelMetaclass: _BaseCreateIncidentSchema = schema_create(Incident) _BaseCreateOfficerSchema = schema_create(Officer) _BaseCreateAgencySchema = schema_create(Agency) +_BaseCreateUnitSchema = schema_create(Unit) CreateStateIDSchema = schema_create(StateID) CreateEmploymentSchema = schema_create(Employment) CreateAccusationSchema = schema_create(Accusation) @@ -241,6 +260,20 @@ class CreateAgencySchema(_BaseCreateAgencySchema, _AgencyMixin): hq_zip: Optional[str] +class CreateUnitSchema(_BaseCreateUnitSchema, _UnitMixin): + name: str + website_url: Optional[str] + phone: Optional[str] + email: Optional[str] + description: Optional[str] + address: Optional[str] + zip: Optional[str] + agency_url: Optional[str] + officers_url: Optional[str] + commander_id: int + agency_id: int + + AddMemberSchema = sqlalchemy_to_pydantic( PartnerMember, exclude=["id", "date_joined", "partner", "user"] ) @@ -255,6 +288,7 @@ def schema_get(model_type: DeclarativeMeta, **kwargs) -> ModelMetaclass: _BaseOfficerSchema = schema_get(Officer) _BasePartnerMemberSchema = schema_get(PartnerMember) _BaseAgencySchema = schema_get(Agency) +_BaseUnitSchema = schema_get(Unit) VictimSchema = schema_get(Victim) PerpetratorSchema = schema_get(Perpetrator) TagSchema = schema_get(Tag) @@ -293,6 +327,11 @@ class OfficerSchema(_BaseOfficerSchema, _OfficerMixin): class AgencySchema(_BaseAgencySchema, _AgencyMixin): + units: List[CreateUnitSchema] + officer_association: List[CreateEmploymentSchema] + + +class UnitSchema(_BaseUnitSchema): officer_association: List[CreateEmploymentSchema] @@ -401,6 +440,18 @@ def agency_orm_to_json(agency: Agency) -> dict: ) +def unit_to_orm(unit: CreateUnitSchema) -> Unit: + """Convert the JSON unit into an ORM instance""" + orm_attrs = unit.dict() + return Unit(**orm_attrs) + + +def unit_orm_to_json(unit: Unit) -> dict: + return UnitSchema.from_orm(unit).dict( + exclude_none=True, + ) + + def employment_to_orm(employment: CreateEmploymentSchema) -> Employment: """Convert the JSON employment into an ORM instance""" orm_attrs = employment.dict()