diff --git a/dear_petition/petition/README.md b/dear_petition/petition/README.md index 636fc886..1d2d19a4 100644 --- a/dear_petition/petition/README.md +++ b/dear_petition/petition/README.md @@ -14,6 +14,7 @@ 6. In dear_petition/petition/etl/load.py, add to create_batch_petitions function 7. Add to PETITION_FORM_NAMES constant in src/contstants/petitionConstants.js +8. Create a new offense record serializer for your new petition type in serializers.py and add it to the offense_record_serializer_map. diff --git a/dear_petition/petition/api/serializers.py b/dear_petition/petition/api/serializers.py index 3c522ae4..ba1afa3d 100644 --- a/dear_petition/petition/api/serializers.py +++ b/dear_petition/petition/api/serializers.py @@ -2,6 +2,7 @@ from django.urls import reverse from rest_framework import serializers from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from dateutil.relativedelta import relativedelta from dear_petition.users.models import User from dear_petition.petition.models import ( @@ -15,7 +16,14 @@ Petition, PetitionDocument, ) -from dear_petition.petition.constants import ATTACHMENT, DISMISSED, UNDERAGED_CONVICTIONS +from dear_petition.petition.constants import ( + ATTACHMENT, + DISMISSED, + UNDERAGED_CONVICTIONS, + NOT_GUILTY, + ADULT_FELONIES, + ADULT_MISDEMEANORS, +) from .fields import ValidationField @@ -92,6 +100,91 @@ class Meta: ] +class DismissedOffenseRecordSerializer(OffenseRecordSerializer): + warnings = serializers.SerializerMethodField() + + def get_warnings(self, offense_record): + warnings = [] + dob = self.get_dob(offense_record) + if dob: + eighteenth_birthday = dob + relativedelta(years=18) + if offense_record.offense.ciprs_record.offense_date.date() < eighteenth_birthday: + warnings.append("This offense may be a candidate for the AOC-CR-293 petition form") + return warnings + + class Meta: + model = OffenseRecord + fields = OffenseRecordSerializer.Meta.fields + ["warnings"] + + +class NotGuiltyOffenseRecordSerializer(OffenseRecordSerializer): + warnings = serializers.SerializerMethodField() + + def get_warnings(self, offense_record): + warnings = [] + dob = self.get_dob(offense_record) + if dob: + eighteenth_birthday = dob + relativedelta(years=18) + if offense_record.offense.ciprs_record.offense_date.date() < eighteenth_birthday: + warnings.append("This offense may be a candidate for the AOC-CR-293 petition form") + return warnings + + class Meta: + model = OffenseRecord + fields = OffenseRecordSerializer.Meta.fields + ["warnings"] + + +class UnderagedConvictionOffenseRecordSerializer(OffenseRecordSerializer): + warnings = serializers.SerializerMethodField() + + def get_warnings(self, offense_record): + warnings = [] + if "assault" in offense_record.description.lower(): + warnings.append("This is an assault conviction") + return warnings + + class Meta: + model = OffenseRecord + fields = OffenseRecordSerializer.Meta.fields + ["warnings"] + + +class AdultFelonyOffenseRecordSerializer(OffenseRecordSerializer): + warnings = serializers.SerializerMethodField() + + def get_warnings(self, offense_record): + warnings = [] + if "assault" in offense_record.description.lower(): + warnings.append("This is an assault conviction") + return warnings + + class Meta: + model = OffenseRecord + fields = OffenseRecordSerializer.Meta.fields + ["warnings"] + + +class AdultMisdemeanorOffenseRecordSerializer(OffenseRecordSerializer): + warnings = serializers.SerializerMethodField() + + def get_warnings(self, offense_record): + warnings = [] + if "assault" in offense_record.description.lower(): + warnings.append("This is an assault conviction") + return warnings + + class Meta: + model = OffenseRecord + fields = OffenseRecordSerializer.Meta.fields + ["warnings"] + + +offense_record_serializer_map = { + DISMISSED: DismissedOffenseRecordSerializer, + NOT_GUILTY: NotGuiltyOffenseRecordSerializer, + UNDERAGED_CONVICTIONS: UnderagedConvictionOffenseRecordSerializer, + ADULT_FELONIES: AdultFelonyOffenseRecordSerializer, + ADULT_MISDEMEANORS: AdultMisdemeanorOffenseRecordSerializer, +} + + class OffenseSerializer(serializers.ModelSerializer): offense_records = OffenseRecordSerializer(many=True, read_only=True) @@ -297,7 +390,8 @@ def get_attachments(self, instance): def get_offense_records(self, petition): offense_records = petition.offense_records.all() - return OffenseRecordSerializer(offense_records, many=True).data + Serializer = offense_record_serializer_map.get(petition.form_type, OffenseRecordSerializer) + return Serializer(offense_records, many=True).data def get_active_records(self, petition): return petition.offense_records.filter(petitionoffenserecord__active=True).values_list( diff --git a/dear_petition/petition/api/tests/test_serializers.py b/dear_petition/petition/api/tests/test_serializers.py index d051218b..8c817989 100644 --- a/dear_petition/petition/api/tests/test_serializers.py +++ b/dear_petition/petition/api/tests/test_serializers.py @@ -1,7 +1,16 @@ -import pytest +from datetime import timedelta, datetime -from dear_petition.petition.api.serializers import OffenseRecordSerializer +import pytest +from dear_petition.petition.api.serializers import ( + AdultFelonyOffenseRecordSerializer, + AdultMisdemeanorOffenseRecordSerializer, + DismissedOffenseRecordSerializer, + NotGuiltyOffenseRecordSerializer, + OffenseRecordSerializer, + UnderagedConvictionOffenseRecordSerializer, +) from dear_petition.petition.tests.factories import OffenseRecordFactory +import dear_petition.petition.constants as pc @pytest.mark.django_db @@ -15,3 +24,66 @@ def test_offense_date_none(self): record = OffenseRecordFactory(offense__ciprs_record__offense_date=None) serializer = OffenseRecordSerializer(record) assert serializer.data["offense_date"] is None + + def test_dismissed_record_underaged_warning(self, charged_dismissed_record): + charged_dismissed_record.offense.ciprs_record.dob = ( + charged_dismissed_record.offense.ciprs_record.offense_date.date() + - timedelta(days=365 * 16) + ) + charged_dismissed_record.offense.ciprs_record.save() + + serializer = DismissedOffenseRecordSerializer(charged_dismissed_record) + assert serializer.data["warnings"] == [ + "This offense may be a candidate for the AOC-CR-293 petition form" + ] + + def test_not_guilty_underaged_warning(self, charged_not_guilty_record): + charged_not_guilty_record.offense.ciprs_record.dob = ( + charged_not_guilty_record.offense.ciprs_record.offense_date.date() + - timedelta(days=365 * 16) + ) + charged_not_guilty_record.offense.ciprs_record.save() + + serializer = NotGuiltyOffenseRecordSerializer(charged_not_guilty_record) + assert serializer.data["warnings"] == [ + "This offense may be a candidate for the AOC-CR-293 petition form" + ] + + def test_underaged_conviction_assault_warning(self, record1, non_dismissed_offense): + record1.dob = datetime(2000, 1, 2) + record1.offense_date = datetime(2018, 1, 1) + record1.save() + + offense_record = OffenseRecordFactory( + action="CONVICTED", description="Assault", offense=non_dismissed_offense + ) + serializer = UnderagedConvictionOffenseRecordSerializer(offense_record) + assert serializer.data["warnings"] == ["This is an assault conviction"] + + def test_adult_felony_assault_warning(self, record1, non_dismissed_offense): + record1.dob = datetime(2000, 1, 2) + record1.offense_date = datetime(2019, 1, 1) + record1.save() + + offense_record = OffenseRecordFactory( + action="CONVICTED", + description="Assault", + severity=pc.SEVERITY_FELONY, + offense=non_dismissed_offense, + ) + serializer = AdultFelonyOffenseRecordSerializer(offense_record) + assert serializer.data["warnings"] == ["This is an assault conviction"] + + def test_adult_misdemeanor_assault_warning(self, record1, non_dismissed_offense): + record1.dob = datetime(2000, 1, 2) + record1.offense_date = datetime(2019, 1, 1) + record1.save() + + offense_record = OffenseRecordFactory( + action="CONVICTED", + description="Assault", + severity=pc.SEVERITY_MISDEMEANOR, + offense=non_dismissed_offense, + ) + serializer = AdultFelonyOffenseRecordSerializer(offense_record) + assert serializer.data["warnings"] == ["This is an assault conviction"] diff --git a/src/components/elements/Tooltip/Tooltip.jsx b/src/components/elements/Tooltip/Tooltip.jsx index df6cd609..c06203e2 100644 --- a/src/components/elements/Tooltip/Tooltip.jsx +++ b/src/components/elements/Tooltip/Tooltip.jsx @@ -5,6 +5,7 @@ import styled from 'styled-components'; const TooltipContentWrapper = styled.div` display: flex; + flex-direction: ${(props) => props.flexDirection}; align-items: center; background: rgb(255 255 255); z-index: 10; @@ -14,7 +15,14 @@ const TooltipContentWrapper = styled.div` padding: 1rem 0.5rem; `; -export const Tooltip = ({ children, tooltipContent, placement, hideTooltip = false, offset = [0, 0] }) => { +export const Tooltip = ({ + children, + tooltipContent, + placement, + hideTooltip = false, + offset = [0, 0], + flexDirection = 'row', +}) => { const hoverDiv = useRef(null); const [isHovering, setIsHovering] = useState(false); const [popperElement, setPopperElement] = useState(); @@ -43,7 +51,7 @@ export const Tooltip = ({ children, tooltipContent, placement, hideTooltip = fal {isHovering && ( - {tooltipContent} + {tooltipContent} )} diff --git a/src/features/OffenseTable/OffenseTable.jsx b/src/features/OffenseTable/OffenseTable.jsx index 6db70d2c..829ade1d 100644 --- a/src/features/OffenseTable/OffenseTable.jsx +++ b/src/features/OffenseTable/OffenseTable.jsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faChevronRight, faChevronDown, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; -import { formatDistance, isBefore, isValid } from 'date-fns'; +import { formatDistance, isValid } from 'date-fns'; import { TableBody, TableCell, TableHeader, TableRow, TableStyle } from '../../components/elements/Table'; import { Tooltip } from '../../components/elements/Tooltip/Tooltip'; @@ -21,11 +21,8 @@ const toNormalCaseEachWord = (str) => .reduce((acc, s) => `${acc} ${s}`); const toNormalCase = (str) => `${str.charAt(0).toUpperCase()}${str.slice(1).toLowerCase()}`; -function OffenseRow({ offenseRecord, selected, onSelect, dob }) { +function OffenseRow({ offenseRecord, selected, onSelect, dob, warnings }) { const [showDetails, setShowDetails] = useState(false); - - const dateAt18YearsOld = isValid(dob) && new Date(dob.getFullYear() + 18, dob.getMonth() + dob.getDay()); - return ( @@ -36,8 +33,14 @@ function OffenseRow({ offenseRecord, selected, onSelect, dob }) { {toNormalCaseEachWord(offenseRecord.action)} {toNormalCaseEachWord(offenseRecord.severity)} - {isValid(dob) && isBefore(new Date(offenseRecord.offense_date), dateAt18YearsOld) && ( - + {warnings.length > 0 && ( + ( +
{warning}
+ ))} + offset={[0, 10]} + flexDirection="column" + >
)} @@ -95,6 +98,7 @@ function OffenseTable({ offenseRecords, selectedRows, onSelect, dob }) { offenseRecord={offenseRecord} onSelect={() => onSelect(offenseRecord.pk)} dob={dob} + warnings={offenseRecord.warnings} /> ))}