diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 315798c59..4c5baf55d 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -34,6 +34,34 @@ class RegistrarForm(forms.Form): self.application = kwargs.pop("application", None) super(RegistrarForm, self).__init__(*args, **kwargs) + def has_more_than_one_join(self, db_obj, rel, related_name): + """Helper for finding whether an object is joined more than once.""" + # threshold is the number of related objects that are acceptable + # when determining if related objects exist. threshold is 0 for most + # relationships. if the relationship is related_name, we know that + # there is already exactly 1 acceptable relationship (the one we are + # attempting to delete), so the threshold is 1 + threshold = 1 if rel == related_name else 0 + + # Raise a KeyError if rel is not a defined field on the db_obj model + # This will help catch any errors in reverse_join config on forms + if rel not in [field.name for field in db_obj._meta.get_fields()]: + raise KeyError(f"{rel} is not a defined field on the {db_obj._meta.model_name} model.") + + # if attr rel in db_obj is not None, then test if reference object(s) exist + if getattr(db_obj, rel) is not None: + field = db_obj._meta.get_field(rel) + if isinstance(field, OneToOneField): + # if the rel field is a OneToOne field, then we have already + # determined that the object exists (is not None) + return True + elif isinstance(field, ForeignObjectRel): + # if the rel field is a ManyToOne or ManyToMany, then we need + # to determine if the count of related objects is greater than + # the threshold + return getattr(db_obj, rel).count() > threshold + return False + def to_database(self, obj: DomainApplication | Contact): """ Adds this form's cleaned data to `obj` and saves `obj`. @@ -351,13 +379,27 @@ class AboutYourOrganizationForm(RegistrarForm): class AuthorizingOfficialForm(RegistrarForm): + JOIN = "authorizing_official" + REVERSE_JOINS = [ + "user", + "authorizing_official", + "submitted_applications", + "contact_applications", + "information_authorizing_official", + "submitted_applications_information", + "contact_applications_information", + ] + def to_database(self, obj): if not self.is_valid(): return contact = getattr(obj, "authorizing_official", None) - if contact is not None: + if contact is not None and not any(self.has_more_than_one_join(contact, rel, "authorizing_official") for rel in self.REVERSE_JOINS): + # if contact exists in the database and is not joined to other entities super().to_database(contact) else: + # no contact exists OR contact exists which is joined also to other entities; + # in either case, create a new contact and update it contact = Contact() super().to_database(contact) obj.authorizing_official = contact @@ -549,13 +591,27 @@ class PurposeForm(RegistrarForm): class YourContactForm(RegistrarForm): + JOIN = "submitter" + REVERSE_JOINS = [ + "user", + "authorizing_official", + "submitted_applications", + "contact_applications", + "information_authorizing_official", + "submitted_applications_information", + "contact_applications_information", + ] + def to_database(self, obj): if not self.is_valid(): return contact = getattr(obj, "submitter", None) - if contact is not None: + if contact is not None and not any(self.has_more_than_one_join(contact, rel, "submitted_applications") for rel in self.REVERSE_JOINS): + # if contact exists in the database and is not joined to other entities super().to_database(contact) else: + # no contact exists OR contact exists which is joined also to other entities; + # in either case, create a new contact and update it contact = Contact() super().to_database(contact) obj.submitter = contact