Skip to content

Commit

Permalink
Merge pull request #1713 from codeforpdx/ask-if-violations-are-traffi…
Browse files Browse the repository at this point in the history
…c-related

Ask if violations are traffic related
  • Loading branch information
wittejm authored Oct 30, 2023
2 parents 7bc1fbc + 65599b0 commit 6dc6d0d
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 16 deletions.
22 changes: 17 additions & 5 deletions src/backend/expungeservice/charge_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class ChargeClassifier:
section: str
birth_year: Optional[int]
disposition: Disposition
location: str

def classify(self) -> AmbiguousChargeTypeWithQuestion:
def classification_found(c):
Expand All @@ -64,6 +65,7 @@ def classification_found(c):
def __classifications_list(self) -> Iterator[AmbiguousChargeTypeWithQuestion]:
name = self.name.lower()
level = self.level.lower()
location = self.location.lower()
yield ChargeClassifier._juvenile_charge(self.violation_type)
yield ChargeClassifier._parking_ticket(self.violation_type)
yield ChargeClassifier._fare_violation(name)
Expand All @@ -72,7 +74,7 @@ def __classifications_list(self) -> Iterator[AmbiguousChargeTypeWithQuestion]:
yield ChargeClassifier._criminal_forfeiture(self.statute)
yield ChargeClassifier._traffic_crime(self.statute, name, level, self.disposition)
yield ChargeClassifier._marijuana_violation(name, level)
yield ChargeClassifier._violation(level, name)
yield ChargeClassifier._violation(level, name, location)
criminal_charge = next(
(
c
Expand Down Expand Up @@ -105,12 +107,22 @@ def _juvenile_charge(violation_type: str):
return AmbiguousChargeTypeWithQuestion([JuvenileCharge()])

@staticmethod
def _violation(level, name):
def _violation(level, name, location):
if "violation" in level:
if "reduced" in name or "treated as" in name:
return AmbiguousChargeTypeWithQuestion([ReducedToViolation()])
if location == "multnomah":
if "reduced" in name or "treated as" in name:
question_string = "Was the underlying charge traffic-related?"
options = {"Yes": TrafficViolation(), "No": ReducedToViolation()}
return ChargeClassifier._build_ambiguous_charge_type_with_question(question_string, options)
else:
question_string = "Was the underlying charge traffic-related?"
options = {"Yes": TrafficViolation(), "No": Violation()}
return ChargeClassifier._build_ambiguous_charge_type_with_question(question_string, options)
else:
return AmbiguousChargeTypeWithQuestion([Violation()])
if "reduced" in name or "treated as" in name:
return AmbiguousChargeTypeWithQuestion([ReducedToViolation()])
else:
return AmbiguousChargeTypeWithQuestion([Violation()])

@staticmethod
def _drug_crime(
Expand Down
3 changes: 2 additions & 1 deletion src/backend/expungeservice/charge_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ def create(ambiguous_charge_id, **kwargs) -> Tuple[AmbiguousCharge, Optional[Que
section = ChargeCreator._set_section(statute)
birth_year = kwargs.get("birth_year")
disposition = kwargs["disposition"]
location = kwargs["location"]
ambiguous_charge_type_with_questions = ChargeClassifier(
violation_type, name, statute, level, section, birth_year, disposition
violation_type, name, statute, level, section, birth_year, disposition, location
).classify()
kwargs["statute"] = statute
kwargs["ambiguous_charge_id"] = ambiguous_charge_id
Expand Down
63 changes: 63 additions & 0 deletions src/backend/expungeservice/demo_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,4 +551,67 @@ def build_search_results(
),
),
],
Alias("single", "violation", "", ""): [
OeciCase(
summary=from_dict(
data_class=CaseSummary,
data={
**shared_case_data,
"current_status": "Closed",
"name": "John Notaperson",
"case_number": "123456",
"violation_type": "Offense Violation",
"balance_due_in_cents": 0,
},
),
charges=(
from_dict(
data_class=OeciCharge,
data={
**shared_charge_data,
"ambiguous_charge_id": "123456-1",
"name": "Something minor",
"statute": "163.208",
"level": "Violation",
"date": date_class.today() - relativedelta(years=2),
"disposition": DispositionCreator.create(
date=date_class.today() - relativedelta(years=1, months=9), ruling="Convicted"
),
"balance_due_in_cents": 50000,
},
),
),
),
OeciCase(
summary=from_dict(
data_class=CaseSummary,
data={
**shared_case_data,
"current_status": "Closed",
"name": "John Notaperson",
"case_number": "1234567",
"violation_type": "Offense Violation",
"balance_due_in_cents": 0,
"location":"Benton"
},
),
charges=(
from_dict(
data_class=OeciCharge,
data={
**shared_charge_data,
"ambiguous_charge_id": "1234567-1",
"name": "Something minor",
"statute": "163.208",
"level": "Violation",
"date": date_class.today() - relativedelta(years=2),
"disposition": DispositionCreator.create(
date=date_class.today() - relativedelta(years=1, months=9), ruling="Convicted"
),
"balance_due_in_cents": 50000,
},
),
),
),
],
}
1 change: 1 addition & 0 deletions src/backend/expungeservice/record_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def _build_case(oeci_case: OeciCase, new_charges: List[Charge]) -> Tuple[Ambiguo
"balance_due_in_cents": oeci_charge.balance_due_in_cents,
"case_number": oeci_case.summary.case_number,
"violation_type": oeci_case.summary.violation_type,
"location": oeci_case.summary.location,
"birth_year": oeci_case.summary.birth_year,
"edit_status": EditStatus(oeci_charge.edit_status),
}
Expand Down
11 changes: 8 additions & 3 deletions src/backend/tests/factories/charge_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ def create(
date: date_class = None,
disposition: Disposition = DispositionCreator.empty(),
violation_type="Offense Misdemeanor",
location="Benton"
) -> Charge:
charges = cls._build_ambiguous_charge(case_number, date, disposition, level, name, statute, violation_type)
charges = cls._build_ambiguous_charge(case_number, date, disposition, level, name, statute, violation_type, location)
assert len(charges) == 1
return charges[0]

Expand All @@ -33,11 +34,12 @@ def create_ambiguous_charge(
date: date_class = None,
disposition: Disposition = DispositionCreator.empty(),
violation_type="Offense Misdemeanor",
location="Benton"
) -> AmbiguousCharge:
return cls._build_ambiguous_charge(case_number, date, disposition, level, name, statute, violation_type)
return cls._build_ambiguous_charge(case_number, date, disposition, level, name, statute, violation_type, location)

@classmethod
def _build_ambiguous_charge(cls, case_number, date, disposition, level, name, statute, violation_type):
def _build_ambiguous_charge(cls, case_number, date, disposition, level, name, statute, violation_type, location):
cls.charge_count += 1
if disposition.status != DispositionStatus.UNKNOWN and not date:
updated_date = disposition.date
Expand All @@ -53,6 +55,7 @@ def _build_ambiguous_charge(cls, case_number, date, disposition, level, name, st
"date": updated_date,
"disposition": disposition,
"violation_type": violation_type,
"location": location,
"balance_due_in_cents": 0,
"edit_status": EditStatus.UNCHANGED,
}
Expand All @@ -68,6 +71,7 @@ def create_dismissed_charge(
level="Misdemeanor Class A",
date=date_class(1901, 1, 1),
violation_type="Offense Misdemeanor",
location="Benton"
) -> Charge:
cls.charge_count += 1
disposition = DispositionCreator.create(date=date_class.today(), ruling="Dismissed")
Expand All @@ -79,6 +83,7 @@ def create_dismissed_charge(
"date": date,
"disposition": disposition,
"violation_type": violation_type,
"location": location,
"balance_due_in_cents": 0,
"edit_status": EditStatus.UNCHANGED,
}
Expand Down
52 changes: 46 additions & 6 deletions src/backend/tests/models/charge_types/test_reduced_to_violation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from expungeservice.models.expungement_result import EligibilityStatus
from expungeservice.models.charge_types.reduced_to_violation import ReducedToViolation
from expungeservice.models.charge_types.traffic_violation import TrafficViolation
from expungeservice.record_merger import RecordMerger

from tests.factories.charge_factory import ChargeFactory
from tests.models.test_charge import Dispositions
Expand Down Expand Up @@ -31,20 +33,58 @@ def test_reduced_to_violation_dismissed():
assert charge.type_eligibility.reason == "Dismissed criminal charge eligible under 137.225(1)(b)"


def test_reduced_to_violation_unrecognized_disposition():
charge = ChargeFactory.create(
def test_reduced_to_violation_multnomah_convicted():
charge = ChargeFactory.create_ambiguous_charge(
name="Theft in the Second Degree (Reduced - DA Elected)",
statute="164045",
level="Violation Class A",
disposition=Dispositions.UNRECOGNIZED_DISPOSITION,
)
disposition=Dispositions.CONVICTED,
location="Multnomah"
)[1]

assert isinstance(charge.charge_type, ReducedToViolation)
assert charge.type_eligibility.status is EligibilityStatus.ELIGIBLE
assert charge.type_eligibility.reason == "Eligible under 137.225(5)(d)"


def test_reduced_to_violation_multnomah_dismissed():
charges = ChargeFactory.create_ambiguous_charge(
name="Misdemeanor Treated as a Violation",
statute="161.566(1)",
level="Violation Class A",
disposition=Dispositions.DISMISSED,
location="Multnomah"
)

type_eligibility = RecordMerger.merge_type_eligibilities(charges)

assert type_eligibility.status is EligibilityStatus.NEEDS_MORE_ANALYSIS
assert (
type_eligibility.reason
== "Traffic Violation – Dismissed violations are eligible under 137.225(1)(b) but administrative reasons may make this difficult to expunge. OR Reduced to Violation – Dismissed criminal charge eligible under 137.225(1)(b)"
)
assert isinstance(charges[0].charge_type, TrafficViolation)
assert isinstance(charges[1].charge_type, ReducedToViolation)


def test_reduced_to_violation_multnomah_unrecognized_disposition():
charges = ChargeFactory.create_ambiguous_charge(
name="Theft in the Second Degree (Reduced - DA Elected)",
statute="164045",
level="Violation Class A",
disposition=Dispositions.UNRECOGNIZED_DISPOSITION,
location="Multnomah"

)
type_eligibility = RecordMerger.merge_type_eligibilities(charges)

assert type_eligibility.status is EligibilityStatus.NEEDS_MORE_ANALYSIS
assert (
charge.type_eligibility.reason
== "Reduced Violations are always eligible under 137.225(5)(d) for convictions, or 137.225(1)(b) for dismissals"
type_eligibility.reason
== "Traffic Violation – Always ineligible under 137.225(7)(a) (for convictions) or by omission from statute (for dismissals) OR Reduced to Violation – Reduced Violations are always eligible under 137.225(5)(d) for convictions, or 137.225(1)(b) for dismissals"
)
assert isinstance(charges[0].charge_type, TrafficViolation)
assert isinstance(charges[1].charge_type, ReducedToViolation)


def test_reduced_violation_ineligible_under_other_criterion():
Expand Down
32 changes: 31 additions & 1 deletion src/backend/tests/models/charge_types/test_violation.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from expungeservice.models.expungement_result import EligibilityStatus
from expungeservice.models.charge_types.violation import Violation
from expungeservice.record_merger import RecordMerger
from expungeservice.models.charge_types.traffic_violation import TrafficViolation

from tests.factories.charge_factory import ChargeFactory
from tests.models.test_charge import Dispositions


def test_violation_convicted():
charge = ChargeFactory.create(
name="Viol Treatment", statute="1615662", level="Violation Unclassified", disposition=Dispositions.CONVICTED
Expand All @@ -23,3 +24,32 @@ def test_violation_dismissed():
assert isinstance(charge.charge_type, Violation)
assert charge.type_eligibility.status is EligibilityStatus.ELIGIBLE
assert charge.type_eligibility.reason == "Eligible under 137.225(1)(b)"

def test_violation_multnomah_convicted():
charges = ChargeFactory.create_ambiguous_charge(
name="Viol Treatment", statute="1615662", level="Violation Unclassified", disposition=Dispositions.CONVICTED, location="Multnomah"
)
type_eligibility = RecordMerger.merge_type_eligibilities(charges)

assert type_eligibility.status is EligibilityStatus.NEEDS_MORE_ANALYSIS
assert (
type_eligibility.reason
== "Traffic Violation – Ineligible under 137.225(7)(a) OR Violation – Eligible under 137.225(5)(c)"
)
assert isinstance(charges[0].charge_type, TrafficViolation)
assert isinstance(charges[1].charge_type, Violation)


def test_violation_multnomah_dismissed():
charges = ChargeFactory.create_ambiguous_charge(
name="Viol Treatment", statute="1615662", level="Violation Unclassified", disposition=Dispositions.DISMISSED, location="Multnomah"
)
type_eligibility = RecordMerger.merge_type_eligibilities(charges)

assert type_eligibility.status is EligibilityStatus.NEEDS_MORE_ANALYSIS
assert (
type_eligibility.reason
== "Traffic Violation – Dismissed violations are eligible under 137.225(1)(b) but administrative reasons may make this difficult to expunge. OR Violation – Eligible under 137.225(1)(b)"
)
assert isinstance(charges[0].charge_type, TrafficViolation)
assert isinstance(charges[1].charge_type, Violation)

0 comments on commit 6dc6d0d

Please sign in to comment.