def set_time_eligibility(self, eligibility_dates): date_will_be_eligible, reason = max(eligibility_dates) if date_will_be_eligible and date_class.today() >= date_will_be_eligible: time_eligibility = TimeEligibility(status=EligibilityStatus.ELIGIBLE, reason="", date_will_be_eligible=None) else: time_eligibility = TimeEligibility( status=EligibilityStatus.INELIGIBLE, reason=reason, date_will_be_eligible=date_will_be_eligible ) self.expungement_result.time_eligibility = time_eligibility
def test_possibly_eligible(): type_eligibility = TypeEligibility(EligibilityStatus.NEEDS_MORE_ANALYSIS, "Unrecognized charge") time_eligibility = TimeEligibility(EligibilityStatus.ELIGIBLE, "Eligible under for some reason", date.today()) time_eligibility_2 = TimeEligibility(EligibilityStatus.INELIGIBLE, "Ineligible under some statute", date.max()) charge_eligibility = RecordMerger.compute_charge_eligibility( type_eligibility, [time_eligibility, time_eligibility_2] ) assert charge_eligibility.status == ChargeEligibilityStatus.POSSIBLY_ELIGIBILE assert charge_eligibility.label == "Possibly Eligible Now"
def test_multiple_will_be_eligible(): type_eligibility = TypeEligibility(EligibilityStatus.ELIGIBLE, "Eligible under some statute") time_eligibility = TimeEligibility(EligibilityStatus.ELIGIBLE, "Eligible Now", Time.THREE_YEARS_AGO) time_eligibility_2 = TimeEligibility( EligibilityStatus.INELIGIBLE, "Ineligible under some statute", Time.ONE_YEARS_FROM_NOW ) charge_eligibility = RecordMerger.compute_charge_eligibility( type_eligibility, [time_eligibility, time_eligibility_2] ) assert charge_eligibility.status == ChargeEligibilityStatus.WILL_BE_ELIGIBLE assert charge_eligibility.label == f"Eligible Now or {Time.ONE_YEARS_FROM_NOW.strftime('%b %-d, %Y')}"
def test_possibly_will_be_eligible(): type_eligibility = TypeEligibility(EligibilityStatus.NEEDS_MORE_ANALYSIS, "Unrecognized charge") time_eligibility = TimeEligibility( EligibilityStatus.INELIGIBLE, "Ineligible for some reason", Time.ONE_YEARS_FROM_NOW ) time_eligibility_2 = TimeEligibility(EligibilityStatus.INELIGIBLE, "Ineligible under some statute", date.max()) charge_eligibility = RecordMerger.compute_charge_eligibility( type_eligibility, [time_eligibility, time_eligibility_2] ) assert charge_eligibility.status == ChargeEligibilityStatus.POSSIBLY_WILL_BE_ELIGIBLE assert charge_eligibility.label == f"Possibly Eligible {Time.ONE_YEARS_FROM_NOW.strftime('%b %-d, %Y')}"
def merge_time_eligibilities( time_eligibilities: Optional[List[TimeEligibility]] ) -> Optional[TimeEligibility]: if time_eligibilities: status = RecordMerger.compute_time_eligibility_status( time_eligibilities) reasons = [ time_eligibility.reason for time_eligibility in time_eligibilities ] reason = " OR ".join(list(unique_everseen(reasons))) date_will_be_eligible = time_eligibilities[0].date_will_be_eligible if len( set([ time_eligibility.date_will_be_eligible for time_eligibility in time_eligibilities ])) == 1: unique_date = True else: unique_date = False return TimeEligibility(status=status, reason=reason, date_will_be_eligible=date_will_be_eligible, unique_date=unique_date) else: return None
def test_type_possibly_eligible_never_becomes_eligible(): type_eligibility = TypeEligibility(EligibilityStatus.NEEDS_MORE_ANALYSIS, "Unrecognized charge") time_eligibility = TimeEligibility(EligibilityStatus.INELIGIBLE, "Never eligible under some statute", date.max()) charge_eligibility = RecordMerger.compute_charge_eligibility(type_eligibility, [time_eligibility]) assert charge_eligibility.status == ChargeEligibilityStatus.INELIGIBLE assert charge_eligibility.label == "Ineligible"
def test_type_eligible_never_becomes_eligible(): type_eligibility = TypeEligibility(EligibilityStatus.ELIGIBLE, "Eligible under some statute") time_eligibility = TimeEligibility(EligibilityStatus.INELIGIBLE, "Never eligible under some statute", date.max()) charge_eligibility = RecordMerger.compute_charge_eligibility(type_eligibility, [time_eligibility]) assert charge_eligibility.status == ChargeEligibilityStatus.INELIGIBLE assert charge_eligibility.label == "Ineligible"
def test_eligible(): type_eligibility = TypeEligibility(EligibilityStatus.ELIGIBLE, "Eligible under some statute") time_eligibility = TimeEligibility(EligibilityStatus.ELIGIBLE, "Eligible under some statute", date.today()) charge_eligibility = RecordMerger.compute_charge_eligibility(type_eligibility, [time_eligibility]) assert charge_eligibility.status == ChargeEligibilityStatus.ELIGIBLE_NOW assert charge_eligibility.label == "Eligible Now"
def test_expunger_for_record_with_odd_event_table_contents( record_with_odd_event_table_contents): expunger_result = Expunger.run(record_with_odd_event_table_contents) assert expunger_result == { "CASEJD1-1": TimeEligibility( status=EligibilityStatus.INELIGIBLE, reason="Never. Type ineligible charges are always time ineligible.", date_will_be_eligible=date.max(), ), "CASEJD1-2": TimeEligibility( status=EligibilityStatus.INELIGIBLE, reason="Never. Type ineligible charges are always time ineligible.", date_will_be_eligible=date.max(), ), }
def test_expunger_for_record_with_mj_over_21(record_with_mj_over_21): expunger_result = Expunger.run(record_with_mj_over_21) assert expunger_result == { "CASEJD1-1": TimeEligibility(status=EligibilityStatus.ELIGIBLE, reason="Eligible now", date_will_be_eligible=date(2001, 3, 3)) }
def test_expunger_for_record_with_mj_under_21(record_with_mj_under_21): assert isinstance(record_with_mj_under_21.charges[0], MarijuanaUnder21) expunger_result = Expunger.run(record_with_mj_under_21) assert expunger_result == { "CASEJD1-1": TimeEligibility(status=EligibilityStatus.ELIGIBLE, reason="Eligible now", date_will_be_eligible=date_class(1999, 3, 3)) }
def set_time_ineligible(self, reason, date_of_eligibility): status = self.expungement_result.type_eligibility.status if status == EligibilityStatus.ELIGIBLE or status == EligibilityStatus.NEEDS_MORE_ANALYSIS: date_will_be_eligible = date_of_eligibility else: date_will_be_eligible = None time_eligibility = TimeEligibility( status=EligibilityStatus.INELIGIBLE, reason=reason, date_will_be_eligible=date_will_be_eligible) self.expungement_result.time_eligibility = time_eligibility
def test_expunger_for_record_with_mj_under_21_not_blocked_by_traffic( record_with_mj_under_21_and_traffic_violation): expunger_result = Expunger.run( record_with_mj_under_21_and_traffic_violation) mj_charge = record_with_mj_under_21_and_traffic_violation.charges[0] traffic_charge = record_with_mj_under_21_and_traffic_violation.charges[1] assert isinstance(mj_charge.charge_type, MarijuanaUnder21) assert isinstance(traffic_charge.charge_type, TrafficViolation) assert expunger_result["CASEJD1-1"] == TimeEligibility( status=EligibilityStatus.ELIGIBLE, reason="Eligible now", date_will_be_eligible=mj_charge.disposition.date + relativedelta(years=1), ) assert expunger_result["CASEJD1-2"] == TimeEligibility( status=EligibilityStatus.INELIGIBLE, reason="Never. Type ineligible charges are always time ineligible.", date_will_be_eligible=date.max(), )
def test_expunger_for_record_with_mj_under_21_blocked_by_blocking_conviction(): record = CrawlerFactory.create( record=YoungDoe.SINGLE_CASE_RECORD, cases={"CASEJD1": CaseDetails.CASE_MJ_AND_FELONY_CONVICTION}) expunger_result = Expunger.run(record) mj_charge = record.charges[0] felony_charge = record.charges[1] assert isinstance(mj_charge.charge_type, MarijuanaUnder21) assert isinstance(felony_charge.charge_type, FelonyClassA) assert expunger_result["CASEJD1-1"] == TimeEligibility( status=EligibilityStatus.ELIGIBLE, reason="Eligible now", date_will_be_eligible=date(1998, 3, 3) + relativedelta(years=10), ) assert expunger_result["CASEJD1-2"] == TimeEligibility( status=EligibilityStatus.INELIGIBLE, reason="Never. Type ineligible charges are always time ineligible.", date_will_be_eligible=date.max(), )
def test_expunger_for_record_with_mj_under_21_with_non_traffic_charge(): record = CrawlerFactory.create( record=YoungDoe.SINGLE_CASE_RECORD, cases={"CASEJD1": CaseDetails.CASE_MJ_AND_FUGITIVE_CONVICTION}) expunger_result = Expunger.run(record) mj_charge = record.charges[0] assert isinstance(mj_charge.charge_type, MarijuanaUnder21) assert isinstance(record.charges[1].charge_type, CivilOffense) assert expunger_result["CASEJD1-1"] == TimeEligibility( status=EligibilityStatus.ELIGIBLE, reason="Eligible now", date_will_be_eligible=mj_charge.disposition.date + relativedelta(years=3), ) assert expunger_result["CASEJD1-2"] == TimeEligibility( status=EligibilityStatus.INELIGIBLE, reason="Never. Type ineligible charges are always time ineligible.", date_will_be_eligible=date.max(), )
def merge_time_eligibilities( time_eligibilities: Optional[List[TimeEligibility]] ) -> Optional[TimeEligibility]: if time_eligibilities: status = RecordMerger.compute_time_eligibility_status( time_eligibilities) reasons = [ time_eligibility.reason for time_eligibility in time_eligibilities ] reason = " ⬥ ".join(list(unique_everseen(reasons))) date_will_be_eligible = time_eligibilities[ 0].date_will_be_eligible # TODO: Fix return TimeEligibility(status=status, reason=reason, date_will_be_eligible=date_will_be_eligible) else: return None
def run(record: Record) -> Dict[str, TimeEligibility]: """ Evaluates the expungement eligibility of a record. """ analyzable_record = Expunger._without_skippable_charges(record) ambiguous_charge_id_to_time_eligibility = {} cases = analyzable_record.cases for charge in analyzable_record.charges: eligibility_dates: List[Tuple[date, str]] = [] other_charges = [ c for c in analyzable_record.charges if c.id != charge.id and c.edit_status != EditStatus.DELETE ] other_blocking_charges = [ c for c in other_charges if c.charge_type.blocks_other_charges ] _, convictions = Case.categorize_charges(other_charges) blocking_dismissals, blocking_convictions = Case.categorize_charges( other_blocking_charges) most_recent_blocking_dismissal = Expunger._most_recent_different_case_dismissal( charge, blocking_dismissals) most_recent_blocking_conviction = Expunger._most_recent_convictions( blocking_convictions) other_convictions_all_traffic = Expunger._is_other_convictions_all_traffic( convictions) if charge.convicted(): if isinstance( charge.charge_type, MarijuanaUnder21) and other_convictions_all_traffic: eligibility_dates.append(( charge.disposition.date + relativedelta(years=1), "One year from date of conviction (137.226)", )) else: eligibility_dates.append(( charge.disposition.date + relativedelta(years=3), "Three years from date of conviction (137.225(1)(a))", )) elif charge.dismissed(): eligibility_dates.append( (charge.date, "Eligible immediately (137.225(1)(b))")) else: raise ValueError( "Charge should always convicted or dismissed at this point." ) if charge.type_eligibility.status == EligibilityStatus.INELIGIBLE: eligibility_dates.append((date.max( ), "Never. Type ineligible charges are always time ineligible." )) if charge.disposition.status == DispositionStatus.NO_COMPLAINT: eligibility_dates.append(( charge.date + relativedelta(years=1), "One year from date of no-complaint arrest (137.225(1)(b))", )) if charge.convicted() and charge.probation_revoked: eligibility_dates.append(( charge.probation_revoked + relativedelta(years=10), "Time-ineligible under 137.225(1)(c) (Probation Revoked). Inspect further if the case has multiple convictions on the case.", )) if most_recent_blocking_conviction: conviction_string = "other conviction" if charge.convicted( ) else "conviction" summary = most_recent_blocking_conviction.case(cases).summary potential = "potential " if not summary.closed() else "" eligibility_dates.append(( most_recent_blocking_conviction.disposition.date + relativedelta(years=10), f"137.225(7)(b) – Ten years from most recent {potential}{conviction_string} from case [{summary.case_number}].", )) if charge.dismissed() and most_recent_blocking_dismissal: eligibility_dates.append(( most_recent_blocking_dismissal.date + relativedelta(years=3), "Three years from most recent other arrest (137.225(8)(a))", )) if charge.convicted() and isinstance(charge.charge_type, FelonyClassB): if Expunger._calculate_has_subsequent_charge( charge, other_blocking_charges): eligibility_dates.append(( date.max(), "Never. Class B felony can have no subsequent arrests or convictions (137.225(5)(a)(A)(ii))", )) else: eligibility_dates.append(( charge.disposition.date + relativedelta(years=20), "Twenty years from date of class B felony conviction (137.225(5)(a)(A)(i))", )) if isinstance(charge.charge_type, MarijuanaViolation): date_will_be_eligible = charge.disposition.date reason = "Eligible immediately (475B.401)" else: date_will_be_eligible, reason = max(eligibility_dates) if date_will_be_eligible and date.today() >= date_will_be_eligible: time_eligibility = TimeEligibility( status=EligibilityStatus.ELIGIBLE, reason="Eligible now", date_will_be_eligible=date_will_be_eligible, ) else: time_eligibility = TimeEligibility( status=EligibilityStatus.INELIGIBLE, reason=reason, date_will_be_eligible=date_will_be_eligible) ambiguous_charge_id_to_time_eligibility[ charge.ambiguous_charge_id] = time_eligibility for case in cases: non_violation_convictions_in_case = [] violations_in_case = [] for charge in case.charges: if charge.convicted(): if "violation" in charge.level.lower(): violations_in_case.append(charge) else: non_violation_convictions_in_case.append(charge) violations_in_case.sort(key=lambda charge: charge.disposition.date, reverse=True) if len(non_violation_convictions_in_case ) == 1 and len(violations_in_case) <= 1: attractor = non_violation_convictions_in_case[0] elif len(violations_in_case) == 1: attractor = violations_in_case[0] elif len(violations_in_case) in [2, 3]: attractor = violations_in_case[1] else: attractor = None if attractor: for charge in case.charges: if (charge.type_eligibility.status != EligibilityStatus.INELIGIBLE and charge.dismissed() and ambiguous_charge_id_to_time_eligibility[ charge. ambiguous_charge_id].date_will_be_eligible > ambiguous_charge_id_to_time_eligibility[ attractor. ambiguous_charge_id].date_will_be_eligible): time_eligibility = TimeEligibility( status=ambiguous_charge_id_to_time_eligibility[ attractor.ambiguous_charge_id].status, reason= 'Time eligibility of the arrest matches conviction on the same case (the "friendly" rule)', date_will_be_eligible= ambiguous_charge_id_to_time_eligibility[ attractor. ambiguous_charge_id].date_will_be_eligible, ) ambiguous_charge_id_to_time_eligibility[ charge.ambiguous_charge_id] = time_eligibility return ambiguous_charge_id_to_time_eligibility
def set_time_eligible(self, reason=""): time_eligibility = TimeEligibility(status=EligibilityStatus.ELIGIBLE, reason=reason, date_will_be_eligible=None) self.expungement_result.time_eligibility = time_eligibility
def run(record: Record, today: date = date.today()) -> Dict[str, TimeEligibility]: """ Evaluates the expungement eligibility of a record. """ analyzable_record = Expunger._without_skippable_charges(record) ambiguous_charge_id_to_time_eligibility = {} cases = analyzable_record.cases for charge in analyzable_record.charges: eligibility_dates: List[Tuple[date, str]] = [] other_charges = [ c for c in analyzable_record.charges if c.id != charge.id and c.edit_status != EditStatus.DELETE ] other_blocking_charges = [c for c in other_charges if c.charge_type.blocks_other_charges] convictions = [c for c in other_charges if c.convicted()] blocking_convictions = [c for c in other_blocking_charges if c.convicted()] most_recent_blocking_conviction = Expunger._most_recent_convictions(blocking_convictions) other_convictions_all_traffic = Expunger._is_other_convictions_all_traffic(convictions) if charge.convicted(): if isinstance(charge.charge_type, MarijuanaUnder21) and other_convictions_all_traffic: eligibility_dates.append( ( charge.disposition.date + relativedelta(years=1), "One year from date of conviction (137.226)", ) ) elif isinstance(charge.charge_type, MarijuanaEligible): eligibility_dates.append( ( charge.disposition.date + relativedelta(years=5), "Conservatively reclassified as Class C Felony (137.226)", ) ) else: eligibility_dates.append( Expunger._single_conviction_years_by_level( charge.level, charge.disposition.date, charge.charge_type ) ) elif charge.dismissed(): eligibility_dates.append((charge.date, "Eligible immediately (137.225(1)(d))")) else: raise ValueError("Charge should always convicted or dismissed at this point.") if charge.type_eligibility.status == EligibilityStatus.INELIGIBLE: eligibility_dates.append((date.max(), "Never. Type ineligible charges are always time ineligible.")) if charge.disposition.status == DispositionStatus.NO_COMPLAINT: eligibility_dates.append( ( charge.disposition.date + relativedelta(days=60), "Sixty days from date of no-complaint disposition (137.225(1)(c))", ) ) if charge.convicted() and charge.probation_revoked: eligibility_dates.append( ( charge.probation_revoked + relativedelta(years=3), "Time-ineligible under 137.225(1)(c) (Probation Revoked). Inspect further if the case has multiple convictions on the case.", ) ) if charge.convicted() and most_recent_blocking_conviction: relative_case_summary = most_recent_blocking_conviction.case(cases).summary blocking_convictions_time_eligibility = Expunger._other_blocking_conviction_years_by_level( charge.level, most_recent_blocking_conviction.disposition.date, relative_case_summary, charge.charge_type, ) eligibility_dates.append(blocking_convictions_time_eligibility) if isinstance(charge.charge_type, MarijuanaViolation): date_will_be_eligible = charge.disposition.date reason = "Eligible immediately (475B.401)" else: date_will_be_eligible, reason = max(eligibility_dates) if date_will_be_eligible and today >= date_will_be_eligible: time_eligibility = TimeEligibility( status=EligibilityStatus.ELIGIBLE, reason="Eligible now", date_will_be_eligible=date_will_be_eligible, ) else: time_eligibility = TimeEligibility( status=EligibilityStatus.INELIGIBLE, reason=reason, date_will_be_eligible=date_will_be_eligible ) ambiguous_charge_id_to_time_eligibility[charge.ambiguous_charge_id] = time_eligibility return ambiguous_charge_id_to_time_eligibility