Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#461 Assault convictions now provide warning #514

Merged
merged 2 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
18 changes: 11 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,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 (
<TableRow key={offenseRecord.pk}>
<TableCell>
Expand All @@ -36,8 +33,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={warnings.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 +98,7 @@ function OffenseTable({ offenseRecords, selectedRows, onSelect, dob }) {
offenseRecord={offenseRecord}
onSelect={() => onSelect(offenseRecord.pk)}
dob={dob}
warnings={offenseRecord.warnings}
/>
))}
</TableBody>
Expand Down