From cb0355306b0c79ec021da6a57af786cc0f3636c9 Mon Sep 17 00:00:00 2001 From: John Tordoff <> Date: Mon, 18 Sep 2023 16:08:26 -0400 Subject: [PATCH] add static fosterapplicationprofile to be a frozen data clone of FosterProfile --- caim_base/admin.py | 3 +- ...050_fostererapplicationprofile_and_more.py | 534 ++++++++++++++++++ caim_base/models/fosterer.py | 44 +- caim_base/views/foster_application.py | 23 +- fake_data.py | 20 +- 5 files changed, 598 insertions(+), 26 deletions(-) create mode 100644 caim_base/migrations/0050_fostererapplicationprofile_and_more.py diff --git a/caim_base/admin.py b/caim_base/admin.py index 677baa0..c863252 100644 --- a/caim_base/admin.py +++ b/caim_base/admin.py @@ -13,7 +13,7 @@ User, ) from .models.awg import AwgMember -from .models.fosterer import FostererProfile, FosterApplication +from .models.fosterer import FostererApplicationProfile, FostererProfile, FosterApplication from .models.user import UserProfile # Unregister the user admin so we can user our own @@ -83,4 +83,5 @@ class CommentAdmin(admin.ModelAdmin): admin.site.register(Awg, AwgAdmin) admin.site.register(AnimalComment, CommentAdmin) admin.site.register(FostererProfile) +admin.site.register(FostererApplicationProfile) admin.site.register(FosterApplication) diff --git a/caim_base/migrations/0050_fostererapplicationprofile_and_more.py b/caim_base/migrations/0050_fostererapplicationprofile_and_more.py new file mode 100644 index 0000000..34eb693 --- /dev/null +++ b/caim_base/migrations/0050_fostererapplicationprofile_and_more.py @@ -0,0 +1,534 @@ +# Generated by Django 4.1 on 2023-09-18 18:49 + +import caim_base.models.fosterer +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("caim_base", "0049_alter_fostererprofile_num_people_in_home"), + ] + + operations = [ + migrations.CreateModel( + name="FostererApplicationProfile", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "firstname", + models.CharField( + blank=True, default=None, max_length=64, null=True + ), + ), + ( + "lastname", + models.CharField( + blank=True, default=None, max_length=64, null=True + ), + ), + ("age", models.IntegerField(blank=True, default=None, null=True)), + ( + "email", + models.EmailField( + blank=True, default=None, max_length=255, null=True + ), + ), + ( + "phone", + phonenumber_field.modelfields.PhoneNumberField( + blank=True, default=None, max_length=128, null=True, region=None + ), + ), + ( + "street_address", + models.CharField( + blank=True, default=None, max_length=244, null=True + ), + ), + ( + "city", + models.CharField( + blank=True, default=None, max_length=32, null=True + ), + ), + ( + "state", + models.CharField( + blank=True, + choices=[ + ("AK", "Alaska"), + ("AL", "Alabama"), + ("AR", "Arkansas"), + ("AZ", "Arizona"), + ("CA", "California"), + ("CO", "Colorado"), + ("CT", "Connecticut"), + ("DC", "District of Columbia"), + ("DE", "Delaware"), + ("FL", "Florida"), + ("GA", "Georgia"), + ("HI", "Hawaii"), + ("IA", "Iowa"), + ("ID", "Idaho"), + ("IL", "Illinois"), + ("IN", "Indiana"), + ("KS", "Kansas"), + ("KY", "Kentucky"), + ("LA", "Louisiana"), + ("MA", "Massachusetts"), + ("MD", "Maryland"), + ("ME", "Maine"), + ("MI", "Michigan"), + ("MN", "Minnesota"), + ("MO", "Missouri"), + ("MS", "Mississippi"), + ("MT", "Montana"), + ("NC", "North Carolina"), + ("ND", "North Dakota"), + ("NE", "Nebraska"), + ("NH", "New Hampshire"), + ("NJ", "New Jersey"), + ("NM", "New Mexico"), + ("NV", "Nevada"), + ("NY", "New York"), + ("OH", "Ohio"), + ("OK", "Oklahoma"), + ("OR", "Oregon"), + ("PA", "Pennsylvania"), + ("RI", "Rhode Island"), + ("SC", "South Carolina"), + ("SD", "South Dakota"), + ("TN", "Tennessee"), + ("TX", "Texas"), + ("UT", "Utah"), + ("VA", "Virginia"), + ("VT", "Vermont"), + ("WA", "Washington"), + ("WI", "Wisconsin"), + ("WV", "West Virginia"), + ("WY", "Wyoming"), + ], + default=None, + max_length=2, + null=True, + ), + ), + ( + "zip_code", + models.CharField( + blank=True, default=None, max_length=16, null=True + ), + ), + ( + "type_of_animals", + caim_base.models.fosterer.ChoiceArrayField( + base_field=models.CharField( + choices=[("DOGS", "Dogs"), ("CATS", "Cats")], max_length=32 + ), + blank=True, + default=None, + null=True, + size=None, + verbose_name="Which type of animal(s) are you wanting to foster?", + ), + ), + ( + "category_of_animals", + caim_base.models.fosterer.ChoiceArrayField( + base_field=models.CharField( + choices=[ + ("ADULT_FEMALE", "Adult female"), + ("ADULT_MALE", "Adult male"), + ("PREGNANT_MOTHER", "Pregnant mom"), + ("MOTHER_WITH_BABIES", "Mom with nursing babies"), + ("BABIES", "Puppies / kittens"), + ("PIT_BULLY_BREEDS", "Pit and/or Bully breeds"), + ( + "SHEPARD_OR_MALINOIS", + "German Shepherd and/or Malinois breeds", + ), + ], + max_length=32, + ), + blank=True, + default=None, + null=True, + size=None, + verbose_name="Please check any / all that you're interested in fostering.", + ), + ), + ( + "dog_size", + caim_base.models.fosterer.ChoiceArrayField( + base_field=models.CharField( + choices=[ + ("SMALL", "Small (5 – 20 lbs)"), + ("MEDIUM", "Medium (21 – 50 lbs)"), + ("LARGE", "Large (50+ lbs)"), + ], + max_length=32, + ), + blank=True, + default=None, + null=True, + size=None, + verbose_name="If you’re interested in fostering dogs, do you have a preference about size?", + ), + ), + ( + "behavioural_attributes", + caim_base.models.fosterer.ChoiceArrayField( + base_field=models.CharField( + choices=[ + ("GOOD_WITH_DOGS", "Good with dogs"), + ("GOOD_WITH_CATS", "Good with cats"), + ("GOOD_WITH_KIDS", "Good with children"), + ], + max_length=32, + ), + blank=True, + default=None, + null=True, + size=None, + verbose_name="Please check any of the requirements you have for a foster animal.", + ), + ), + ( + "medical_issues", + models.CharField( + choices=[("YES", "Yes"), ("NO", "No")], + default=None, + max_length=32, + null=True, + verbose_name="Would you be willing to foster an animal with medical issues?", + ), + ), + ( + "special_needs", + models.CharField( + choices=[("YES", "Yes"), ("NO", "No")], + default=None, + max_length=32, + null=True, + verbose_name="Would you be willing to foster an animal with special needs?", + ), + ), + ( + "behavioral_issues", + models.CharField( + choices=[ + ("YES", "Yes"), + ( + "MINOR", + "Not severe behavioral issues, but open to minor behavioral challenges.", + ), + ("NO", "No"), + ], + default=None, + max_length=32, + null=True, + verbose_name="Would you be willing to foster an animal with behavioral issues?", + ), + ), + ( + "timeframe", + models.CharField( + choices=[ + ("MAX_2_WEEKS", "Up to 2 weeks"), + ("MAX_3_MONTHS", "Up to 3 months"), + ("ANY_DURATION", "Any duration"), + ], + default=None, + max_length=32, + null=True, + verbose_name="How long are you able to foster an animal for?", + ), + ), + ( + "num_existing_pets", + models.IntegerField( + blank=True, + default=None, + null=True, + verbose_name="How many pets do you currently have in your home?", + ), + ), + ( + "experience_given_up_pet", + models.TextField( + blank=True, + default=None, + null=True, + verbose_name="Have you ever given a pet up? If so, please describe the situation.", + ), + ), + ( + "experience_description", + models.TextField( + blank=True, + default=None, + null=True, + verbose_name="Please describe your experience with animals (personal pets, training, interactions, etc.)", + ), + ), + ( + "experience_categories", + caim_base.models.fosterer.ChoiceArrayField( + base_field=models.CharField( + choices=[ + ("HOUSE_POTTY", "House / potty training"), + ("CRATE", "Crate training"), + ("LEASH_WALKING", "Challenges with leash / walking"), + ("JUMPING", "Jumping"), + ("HIGH_ENERGY", "High energy"), + ("OBEDIENCE", "Basic obedience"), + ("PUPPIES", "Training puppies"), + ("SEPARATION_ANXIETY", "Separation anxiety"), + ("FEARS", "Fears / phobias"), + ("REACTIVITY", "Reactivity"), + ("FOOD_GUARDING", "Food guarding"), + ("MEDICAL_ISSUES", "Medical issues"), + ("GERIATRIC", "Geriatric concerns"), + ("NONE_OF_ABOVE", "None of the above"), + ], + max_length=32, + ), + blank=True, + default=None, + null=True, + size=None, + verbose_name="Have you had experience with any of the following in animals you’ve owned or fostered?", + ), + ), + ( + "reference_1", + models.TextField( + blank=True, default=None, null=True, verbose_name="Reference #1" + ), + ), + ( + "reference_2", + models.TextField( + blank=True, default=None, null=True, verbose_name="Reference #2" + ), + ), + ( + "reference_3", + models.TextField( + blank=True, default=None, null=True, verbose_name="Reference #3" + ), + ), + ( + "people_at_home", + models.TextField( + blank=True, + default=None, + null=True, + verbose_name="Please list how many people live in your home and their ages (legacy)", + ), + ), + ( + "num_people_in_home", + models.IntegerField( + blank=True, + default=None, + null=True, + verbose_name="How many people live in your home, excluding yourself?", + ), + ), + ( + "people_in_home_detail", + models.TextField( + blank=True, + default=None, + null=True, + verbose_name="Please list the following details for each person in your home, excluding yourself: Name, Relation, Age, Email address.", + ), + ), + ( + "all_in_agreement", + models.CharField( + choices=[("YES", "Yes"), ("NO", "No")], + default=None, + max_length=32, + null=True, + verbose_name="Are all members of your household in agreement about fostering?", + ), + ), + ( + "pet_allergies", + models.CharField( + choices=[("YES", "Yes"), ("NO", "No")], + default=None, + max_length=32, + null=True, + verbose_name="Does anyone in your home have pet allergies?", + ), + ), + ( + "stairs", + models.CharField( + choices=[("YES", "Yes"), ("NO", "No")], + default=None, + max_length=32, + null=True, + verbose_name="Do you have stairs that a dog would have to walk daily?", + ), + ), + ( + "yard_type", + models.CharField( + choices=[ + ("NO_YARD", "No yard"), + ("UNFENCED", "Unfenced yard"), + ("PARTIALLY_FENCED", "Partially fenced yard"), + ("FULLY_FENCED", "Fully fenced yard"), + ], + default=None, + max_length=32, + null=True, + verbose_name="Describe your yard", + ), + ), + ( + "yard_fence_over_5ft", + models.CharField( + choices=[("YES", "Yes"), ("NO", "No")], + default=None, + max_length=32, + null=True, + verbose_name="If your yard is fully fenced, is it all over 5 feet tall?", + ), + ), + ( + "rent_own", + models.CharField( + choices=[("RENT", "Rent"), ("OWN", "Own")], + default=None, + max_length=32, + null=True, + verbose_name="Do you rent or own?", + ), + ), + ( + "rent_restrictions", + models.TextField( + blank=True, + default=None, + null=True, + verbose_name="If you rent, please describe any pet restrictions that are in place.", + ), + ), + ( + "landlord_contact_text", + models.CharField( + blank=True, + default=None, + max_length=128, + null=True, + verbose_name="If you rent, please provide your landlord’s contact information (email and/or phone). We will contact them to confirm that you have approval to foster.", + ), + ), + ( + "hours_alone_description", + models.TextField( + blank=True, + default=None, + null=True, + verbose_name="How many hours per day will your foster animal be left alone?", + ), + ), + ( + "hours_alone_location", + models.TextField( + blank=True, + default=None, + null=True, + verbose_name="Where will your foster animal be kept when you're not home?", + ), + ), + ( + "sleep_location", + models.TextField( + blank=True, + default=None, + null=True, + verbose_name="Where will your foster animal sleep?", + ), + ), + ( + "other_info", + models.TextField( + blank=True, + default=None, + null=True, + verbose_name="Is there anything else you want us / rescues to know?", + ), + ), + ( + "ever_been_convicted_abuse", + models.CharField( + choices=[("YES", "Yes"), ("NO", "No")], + default=None, + max_length=32, + null=True, + verbose_name="Have you or a family / household member ever been convicted of an animal related crime (animal abuse, neglect, abandonment, etc.)?", + ), + ), + ( + "agree_share_details", + models.CharField( + choices=[("YES", "Yes"), ("NO", "No")], + default=None, + max_length=32, + null=True, + verbose_name="Do you agree that we can share the details you've provided with rescues?", + ), + ), + ( + "agree_social_media", + models.CharField( + choices=[("YES", "Yes"), ("NO", "No")], + default=None, + max_length=32, + null=True, + verbose_name="Do you agree to help promote your foster animal on social media and/or by attending adoption events and public outings?", + ), + ), + ("is_complete", models.BooleanField(default=False)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="application_profile", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AlterField( + model_name="fosterapplication", + name="fosterer", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="applications", + to="caim_base.fostererapplicationprofile", + ), + ), + ] diff --git a/caim_base/models/fosterer.py b/caim_base/models/fosterer.py index e6a7596..e4ca655 100644 --- a/caim_base/models/fosterer.py +++ b/caim_base/models/fosterer.py @@ -124,7 +124,10 @@ def __str__(self) -> str: return f"{self.firstname} {self.lastname}" -class FostererProfile(models.Model): +class AbstractFostererProfile(models.Model): + class Meta: + abstract = True + class CategoryOfAnimals(models.TextChoices): ADULT_FEMALE = "ADULT_FEMALE", "Adult female" ADULT_MALE = "ADULT_MALE", "Adult male" @@ -187,7 +190,6 @@ class RentOwn(models.TextChoices): RENT = "RENT", "Rent" OWN = "OWN", "Own" - user = models.OneToOneField(User, on_delete=models.CASCADE) firstname = models.CharField(blank=True, null=True, max_length=64, default=None) lastname = models.CharField(blank=True, null=True, max_length=64, default=None) age = models.IntegerField(blank=True, null=True, default=None) @@ -536,7 +538,7 @@ class RejectionReasons(models.TextChoices): OTHER = "OTHER", "Other" fosterer = models.ForeignKey( - FostererProfile, on_delete=models.CASCADE, related_name="applications" + 'FostererApplicationProfile', on_delete=models.CASCADE, related_name="applications" ) animal = models.ForeignKey( Animal, on_delete=models.CASCADE, related_name="applications" @@ -555,7 +557,7 @@ def __str__(self) -> str: def save(self, *args, **kwargs): if not self.id: # the first save notify_new_fosterer_application(self) - super().save() + super().save( *args, **kwargs) def get_absolute_url(self): return full_url(f"/foster/application?animal_id={self.animal.id}") @@ -573,3 +575,37 @@ class FosterApplicationAnimalSuggestion(models.Model): def __str__(self) -> str: return f"Suggestion of {self.animal} for {self.fosterer}" + + +class FostererProfile(AbstractFostererProfile): + ''' + The active profile that reflects the current user's info + ''' + user = models.OneToOneField(User, on_delete=models.CASCADE) + + def copy_to_static(self): + data = self.__dict__.copy() + data.pop('_state', None) + data.pop('id', None) + data.pop('pk', None) + data.pop('abstractfostererprofile_ptr_id', None) # I don't know what this is. + return FostererApplicationProfile.objects.create(**data) + + +class FostererApplicationProfile(AbstractFostererProfile): + ''' + The staic profile accompanies the user's foster application and doesn't change. + ''' + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="application_profile") + + @property + def application(self): + return FosterApplication.objects.get(fosterer=self) + + def save(self, *args, **kwargs): + if self.id: # the first save + raise NotImplemented('StaticFostererProfile can only be saved once.') + super().save(*args, **kwargs) + + def __str__(self) -> str: + return f"{self.firstname} {self.lastname} @{self.application.submitted_on}" diff --git a/caim_base/views/foster_application.py b/caim_base/views/foster_application.py index 5d85e13..4fb3de3 100644 --- a/caim_base/views/foster_application.py +++ b/caim_base/views/foster_application.py @@ -21,6 +21,7 @@ FostererPersonInHomeDetail, FostererProfile, FostererReferenceDetail, + FostererApplicationProfile ) @@ -28,6 +29,11 @@ @require_http_methods(["POST", "GET"]) def application(request): user = request.user + animal_id = request.POST.get("animal_id") or request.GET.get("animal_id") + try: + animal = Animal.objects.get(pk=int(animal_id)) + except Animal.DoesNotExist as e: + raise Http404("No Animal matches the given query.") from e # check if person has completed fosterer profile. if not send to fill. try: @@ -39,16 +45,8 @@ def application(request): return redirect("/fosterer") if request.method == "POST": - animal_id = request.POST.get("animal_id") - - try: - animal = Animal.objects.get(pk=animal_id) - except Animal.DoesNotExist as e: - raise Http404("No Animal matches the given query.") from e - # check if application exists already - try: - FosterApplication.objects.get(fosterer=fosterer_profile, animal=animal) + if FosterApplication.objects.filter(fosterer__user=user, animal=animal).exists(): return render( request, "foster_application/exists.html", @@ -57,17 +55,16 @@ def application(request): "pageTitle": "Foster application already exists", }, ) - except FosterApplication.DoesNotExist: - pass + static_foster_profile = FostererProfile.objects.get(user=user).copy_to_static() application = FosterApplication( - fosterer=fosterer_profile, + fosterer=static_foster_profile, animal=animal, status=FosterApplication.Statuses.PENDING, reject_reason_detail=None, ) - application.save() + application return render( request, diff --git a/fake_data.py b/fake_data.py index 111cb7f..2cd2418 100644 --- a/fake_data.py +++ b/fake_data.py @@ -244,7 +244,7 @@ def fake_foster_profile(user: User) -> FostererProfile: # if fosterer.Timeframe.ANY_DURATION in fosterer.timeframe: # fosterer.timeframe_other = fake.text(randint(5, 500)) fosterer.num_existing_pets = randint(0, 10) - fosterer.existing_pets_details = fake.text(randint(5, 500)) + # fosterer.existing_pets_details = fake.text(randint(5, 500)) fosterer.experience_description = fake.text(randint(5, 500)) # pick a random selections from these choice fields, from 1-max number of selections k = randint(1, len(fosterer.ExperienceCategories.choices)) @@ -266,10 +266,10 @@ def fake_foster_profile(user: User) -> FostererProfile: fosterer.rent_own = choice([choice[0] for choice in fosterer.RentOwn.choices]) if fosterer.rent_own == fosterer.RentOwn.RENT: fosterer.rent_restrictions = fake.text(randint(5, 50)) - fosterer.rent_ok_foster_pets = choice([choice[0] for choice in YesNo.choices]) + # fosterer.rent_ok_foster_pets = choice([choice[0] for choice in YesNo.choices]) else: fosterer.rent_restrictions = None - fosterer.rent_ok_foster_pets = YesNo.YES # field not allowing null??? + # fosterer.rent_ok_foster_pets = YesNo.YES # field not allowing null??? fosterer.hours_alone_description = fake.text(10) fosterer.hours_alone_location = fake.text(10) fosterer.sleep_location = fake.text(10) @@ -290,14 +290,18 @@ def fake_foster_profile(user: User) -> FostererProfile: def fake_fosterers(num_desired_fosterers: int): print("generating fake fosterers...") - fosterers = [] for _ in range(num_desired_fosterers): # going through the fields in the same order they appear in the model - user = User.objects.create_user(fake.user_name(), email=fake.email()) + try: + user = User.objects.create_user(fake.user_name(), email=fake.email()) + except django.db.IntegrityError: + # Random name collision gives an IntegrityError, but odds getting two IntegrityError in a row here has got + # to be very, very low. + user = User.objects.create_user(fake.user_name(), email=fake.email()) + user.save() fosterer = fake_foster_profile(user) - fosterers.append(fosterer) - FostererProfile.objects.bulk_create(fosterers) + fosterer.save() def fake_foster_application( @@ -305,7 +309,7 @@ def fake_foster_application( ) -> FosterApplication: application = FosterApplication() application.animal = animal - application.fosterer = foster_profile + application.fosterer = foster_profile.copy_to_static() application.status = choice([choice[0] for choice in application.Statuses.choices]) if application.status == application.Statuses.REJECTED: application.reject_reason = choice(