diff --git a/dear_petition/petition/api/serializers.py b/dear_petition/petition/api/serializers.py index f9b88ce1..c1135f69 100644 --- a/dear_petition/petition/api/serializers.py +++ b/dear_petition/petition/api/serializers.py @@ -7,6 +7,7 @@ from dear_petition.petition.models import ( CIPRSRecord, Contact, + Client, Batch, Offense, OffenseRecord, @@ -139,6 +140,7 @@ class Meta: "zipcode", "user", "county", + "dob", ] class ClientSerializer(ContactSerializer): @@ -147,6 +149,24 @@ class ClientSerializer(ContactSerializer): state = serializers.CharField(required=True) zipcode = serializers.CharField(required=True) user = serializers.PrimaryKeyRelatedField(read_only=True) + dob = serializers.DateField(required=False) + + class Meta: + model = Client + fields = [ + "pk", + "name", + "category", + "address1", + "address2", + "formatted_address", + "city", + "state", + "zipcode", + "user", + "county", + "dob", + ] class GeneratePetitionSerializer(serializers.Serializer): @@ -288,7 +308,7 @@ class BatchSerializer(serializers.ModelSerializer): petitions = PetitionSerializer(many=True, read_only=True) generate_letter_errors = ValidationField(method_name='get_generate_errors_data', serializer=GenerateDocumentSerializer) generate_summary_errors = ValidationField(method_name='get_generate_errors_data', serializer=GenerateDocumentSerializer) - + def get_generate_errors_data(self, obj): return {'batch': obj.pk} @@ -316,9 +336,10 @@ class BatchDetailSerializer(serializers.ModelSerializer): ) attorney = ContactSerializer(read_only=True) client_id = serializers.PrimaryKeyRelatedField( - source='client', queryset=Contact.objects.filter(category='client'), write_only=True, required=False + source='client', queryset=Client.objects.all(), write_only=True, required=False ) client = ClientSerializer(read_only=True) + client_errors = serializers.SerializerMethodField() generate_letter_errors = ValidationField(method_name='get_generate_errors_data', serializer=GenerateDocumentSerializer) generate_summary_errors = ValidationField(method_name='get_generate_errors_data', serializer=GenerateDocumentSerializer) @@ -341,6 +362,7 @@ class Meta: "client_id", "generate_letter_errors", "generate_summary_errors", + "client_errors" ] read_only_fields = ["user", "pk", "date_uploaded", "records", "petitions"] @@ -350,6 +372,14 @@ def get_petitions(self, instance): batch=instance.pk, ).order_by("county", "jurisdiction") return ParentPetitionSerializer(parent_petitions, many=True).data + + def get_client_errors(self, instance): + errors = [] + if not instance.client: + return errors + if not instance.client.dob: + errors.append("Date of birth missing. The petition generator will try its best to identify a date of birth from the records at time of petition creation.") + return errors class MyInboxSerializer(serializers.ModelSerializer): diff --git a/dear_petition/petition/api/urls.py b/dear_petition/petition/api/urls.py index 9325339c..bea5d537 100644 --- a/dear_petition/petition/api/urls.py +++ b/dear_petition/petition/api/urls.py @@ -9,6 +9,7 @@ router.register(r"offense", viewsets.OffenseViewSet) router.register(r"offenserecord", viewsets.OffenseRecordViewSet) router.register(r"contact", viewsets.ContactViewSet) +router.register(r"client", viewsets.ClientViewSet) router.register(r"batch", viewsets.BatchViewSet) router.register(r"petitions", viewsets.PetitionViewSet) router.register(r"generatedpetition", viewsets.GeneratedPetitionViewSet) diff --git a/dear_petition/petition/api/viewsets.py b/dear_petition/petition/api/viewsets.py index 1d7613b1..da79cd5a 100644 --- a/dear_petition/petition/api/viewsets.py +++ b/dear_petition/petition/api/viewsets.py @@ -237,6 +237,15 @@ def import_agencies(self, request): resources.AgencyResource().import_data(dataset, raise_errors=True) return Response({}) +class ClientViewSet(ContactViewSet): + queryset = pm.Client.objects.all() + serializer_class = serializers.ClientSerializer + + def get_queryset(self): + return pm.Client.objects.filter(user=self.request.user) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) class BatchViewSet(viewsets.ModelViewSet): diff --git a/dear_petition/petition/migrations/0061_client_alter_generatedpetition_form_type_and_more.py b/dear_petition/petition/migrations/0061_client_alter_generatedpetition_form_type_and_more.py new file mode 100644 index 00000000..47a4bb80 --- /dev/null +++ b/dear_petition/petition/migrations/0061_client_alter_generatedpetition_form_type_and_more.py @@ -0,0 +1,103 @@ +# Generated by Django 4.2.9 on 2024-05-26 17:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("petition", "0060_alter_contact_user"), + ] + + operations = [ + migrations.CreateModel( + name="Client", + fields=[ + ( + "contact_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="petition.contact", + ), + ), + ("dob", models.DateField(blank=True, null=True)), + ], + bases=("petition.contact",), + ), + migrations.AlterField( + model_name="generatedpetition", + name="form_type", + field=models.CharField( + choices=[ + ("AOC-CR-281", "AOC-CR-281"), + ("AOC-CR-285", "AOC-CR-285"), + ("AOC-CR-287", "AOC-CR-287"), + ("AOC-CR-288", "AOC-CR-288"), + ("AOC-CR-293", "AOC-CR-293"), + ("AOC-CR-297", "AOC-CR-297"), + ("AOC-CR-298", "AOC-CR-298"), + ("Addendum 3B", "Addendum 3B"), + ], + max_length=255, + ), + ), + migrations.AlterField( + model_name="petition", + name="agencies", + field=models.ManyToManyField(related_name="+", to="petition.contact"), + ), + migrations.AlterField( + model_name="petition", + name="form_type", + field=models.CharField( + choices=[ + ("AOC-CR-281", "AOC-CR-281"), + ("AOC-CR-285", "AOC-CR-285"), + ("AOC-CR-287", "AOC-CR-287"), + ("AOC-CR-288", "AOC-CR-288"), + ("AOC-CR-293", "AOC-CR-293"), + ("AOC-CR-297", "AOC-CR-297"), + ("AOC-CR-298", "AOC-CR-298"), + ("Addendum 3B", "Addendum 3B"), + ], + max_length=255, + ), + ), + migrations.AlterField( + model_name="petitiondocument", + name="agencies", + field=models.ManyToManyField(related_name="+", to="petition.contact"), + ), + migrations.AlterField( + model_name="petitiondocument", + name="form_type", + field=models.CharField( + choices=[ + ("AOC-CR-281", "AOC-CR-281"), + ("AOC-CR-285", "AOC-CR-285"), + ("AOC-CR-287", "AOC-CR-287"), + ("AOC-CR-288", "AOC-CR-288"), + ("AOC-CR-293", "AOC-CR-293"), + ("AOC-CR-297", "AOC-CR-297"), + ("AOC-CR-298", "AOC-CR-298"), + ("Addendum 3B", "Addendum 3B"), + ], + max_length=255, + ), + ), + migrations.AlterField( + model_name="batch", + name="client", + field=models.ForeignKey( + limit_choices_to={"category": "client"}, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="batches", + to="petition.client", + ), + ), + ] diff --git a/dear_petition/petition/models.py b/dear_petition/petition/models.py index 0945748c..f627db5f 100644 --- a/dear_petition/petition/models.py +++ b/dear_petition/petition/models.py @@ -25,6 +25,7 @@ from dear_petition.users.models import User from dear_petition.users import constants as uc from dear_petition.common.models import PrintableModelMixin +from dear_petition.petition.utils import resolve_dob from . import constants as pc from .constants import ( @@ -267,6 +268,7 @@ def get_queryset(self): output_field=models.BooleanField(), )) ) + class Contact(PrintableModelMixin, models.Model): name = models.CharField(max_length=512) @@ -298,11 +300,21 @@ def __str__(self): @classmethod def get_sherriff_office_by_county(cls, county: str): - print(county) qs = cls.agencies_with_sherriff_office.filter(county__iexact=county, is_sheriff_office=True) if qs.count() > 1: logger.error('Multiple agencies with sherriff department name detected. Picking first one...') return qs.first() if qs.exists() else None + + +class Client(Contact): + dob = models.DateField(null=True, blank=True) + + def save(self, *args, **kwargs): + # This is for backwards compatibility with existing data that pre-exists the multi-table inheritance paradigm + # TODO: Fully convert to multi-table inheritance paradigm + if self._state.adding: + self.category = "client" + super().save(*args, **kwargs) class Batch(PrintableModelMixin, models.Model): @@ -321,7 +333,7 @@ class Batch(PrintableModelMixin, models.Model): on_delete=models.SET_NULL, ) client = models.ForeignKey( - Contact, + Client, related_name="batches", null=True, limit_choices_to={"category": "client"}, diff --git a/dear_petition/petition/types/adult_felonies.py b/dear_petition/petition/types/adult_felonies.py index 9fc7bc43..3632eb51 100644 --- a/dear_petition/petition/types/adult_felonies.py +++ b/dear_petition/petition/types/adult_felonies.py @@ -15,15 +15,12 @@ def get_offense_records(batch, jurisdiction=""): qs = OffenseRecord.objects.filter(offense__ciprs_record__batch=batch) if jurisdiction: qs = qs.filter(offense__jurisdiction=jurisdiction) - dob = resolve_dob(qs) - if not dob: - return qs # We can't determine this petition type without the date of birth - query = build_query(dob) + query = build_query() qs = qs.filter(query) return qs.select_related("offense__ciprs_record__batch") -def build_query(dob): +def build_query(): action = Q(action=pc.CONVICTED) severity = Q(severity__iexact=pc.SEVERITIES.FELONY) today = timezone.now().date() diff --git a/dear_petition/petition/types/adult_misdemeanors.py b/dear_petition/petition/types/adult_misdemeanors.py index 577bded9..6db53dd7 100644 --- a/dear_petition/petition/types/adult_misdemeanors.py +++ b/dear_petition/petition/types/adult_misdemeanors.py @@ -15,15 +15,12 @@ def get_offense_records(batch, jurisdiction=""): qs = OffenseRecord.objects.filter(offense__ciprs_record__batch=batch) if jurisdiction: qs = qs.filter(offense__jurisdiction=jurisdiction) - dob = resolve_dob(qs) - if not dob: - return qs # We can't determine this petition type without the date of birth - query = build_query(dob) + query = build_query() qs = qs.filter(query) return qs.select_related("offense__ciprs_record__batch") -def build_query(dob): +def build_query(): action = Q(action=pc.CONVICTED) severity = Q(severity__iexact=pc.SEVERITIES.MISDEMEANOR) today = timezone.now().date() diff --git a/dear_petition/petition/types/underaged_convictions.py b/dear_petition/petition/types/underaged_convictions.py index fbfcb015..19b007d6 100644 --- a/dear_petition/petition/types/underaged_convictions.py +++ b/dear_petition/petition/types/underaged_convictions.py @@ -22,7 +22,7 @@ def get_offense_records(batch, jurisdiction=""): dob = resolve_dob(qs) if not dob: - return qs # We can't determine this petition type without the date of birth + return OffenseRecord.objects.none() # We can't determine this petition type without the date of birth if jurisdiction: qs = qs.filter(offense__ciprs_record__jurisdiction=jurisdiction) diff --git a/src/components/elements/Input/FormDateInput.jsx b/src/components/elements/Input/FormDateInput.jsx new file mode 100644 index 00000000..318bec6b --- /dev/null +++ b/src/components/elements/Input/FormDateInput.jsx @@ -0,0 +1,34 @@ +import { useController } from 'react-hook-form'; +import { AnimatePresence } from 'framer-motion'; +import { InputWrapper, InputStyled, ActualInputStyled, InputErrors } from './Input.styled'; + +const FormDateInput = ({ className, label, errors, inputProps, ...restProps }) => { + const { field, fieldState } = useController(inputProps); + const { error: inputError } = fieldState; + const error = inputError ? ( +
Invalid date
+ ) : ( + // eslint-disable-next-line react/no-array-index-key + errors?.map((errMsg, i) =>{errMsg}
) + ); + return ( +