Skip to content

Commit

Permalink
Assault convictions now provide warning. Also created general approac…
Browse files Browse the repository at this point in the history
…h for adding offense-level warnings on the Petition page
  • Loading branch information
georgehelman committed Nov 16, 2024
1 parent c65fbe6 commit ee6c319
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 13 deletions.
1 change: 1 addition & 0 deletions dear_petition/petition/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.



Expand Down
98 changes: 96 additions & 2 deletions dear_petition/petition/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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(
Expand Down
76 changes: 74 additions & 2 deletions dear_petition/petition/api/tests/test_serializers.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"]
12 changes: 10 additions & 2 deletions src/components/elements/Tooltip/Tooltip.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -43,7 +51,7 @@ export const Tooltip = ({ children, tooltipContent, placement, hideTooltip = fal
</div>
{isHovering && (
<Popover.Panel static ref={setPopperElement} style={styles.popper} {...attributes.popper}>
<TooltipContentWrapper>{tooltipContent}</TooltipContentWrapper>
<TooltipContentWrapper flexDirection={flexDirection}>{tooltipContent}</TooltipContentWrapper>
</Popover.Panel>
)}
</Popover>
Expand Down
19 changes: 12 additions & 7 deletions src/features/OffenseTable/OffenseTable.jsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -21,11 +21,9 @@ 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());

let warnings2 = [...warnings, 'warning 1', 'warning 2'];
return (
<TableRow key={offenseRecord.pk}>
<TableCell>
Expand All @@ -36,8 +34,14 @@ function OffenseRow({ offenseRecord, selected, onSelect, dob }) {
<TableCell tooltip={offenseRecord.action}>{toNormalCaseEachWord(offenseRecord.action)}</TableCell>
<TableCell tooltip={offenseRecord.severity}>{toNormalCaseEachWord(offenseRecord.severity)}</TableCell>
<TableCell>
{isValid(dob) && isBefore(new Date(offenseRecord.offense_date), dateAt18YearsOld) && (
<Tooltip tooltipContent="This offense may be a candidate for the AOC-CR-293 petition form" offset={[0, 10]}>
{warnings.length > 0 && (
<Tooltip
tooltipContent={warnings2.map((warning, index) => (
<div key={index}>{warning}</div>
))}
offset={[0, 10]}
flexDirection="column"
>
<FontAwesomeIcon className="text-xl text-red-600" icon={faExclamationTriangle} />
</Tooltip>
)}
Expand Down Expand Up @@ -95,6 +99,7 @@ function OffenseTable({ offenseRecords, selectedRows, onSelect, dob }) {
offenseRecord={offenseRecord}
onSelect={() => onSelect(offenseRecord.pk)}
dob={dob}
warnings={offenseRecord.warnings}
/>
))}
</TableBody>
Expand Down

0 comments on commit ee6c319

Please sign in to comment.