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}
/>
))}