diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index ceb215a4d..7b96af5ee 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -652,6 +652,9 @@ SESSION_COOKIE_SAMESITE = "Lax" # instruct browser to only send cookie via HTTPS SESSION_COOKIE_SECURE = True +# session engine to cache session information +SESSION_ENGINE = "django.contrib.sessions.backends.cache" + # ~ Set by django.middleware.clickjacking.XFrameOptionsMiddleware # prevent clickjacking by instructing the browser not to load # our site within an iframe diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 5cfcc2475..ad1fbebd6 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -260,7 +260,6 @@ class Domain(TimeStampedModel, DomainHelper): """Creates the host object in the registry doesn't add the created host to the domain returns ErrorCode (int)""" - logger.info("Creating host") if addrs is not None: addresses = [epp.Ip(addr=addr) for addr in addrs] request = commands.CreateHost(name=host, addrs=addresses) @@ -1245,7 +1244,6 @@ class Domain(TimeStampedModel, DomainHelper): count = 0 while not exitEarly and count < 3: try: - logger.info("Getting domain info from epp") req = commands.InfoDomain(name=self.name) domainInfoResponse = registry.send(req, cleaned=True) exitEarly = True @@ -1684,74 +1682,84 @@ class Domain(TimeStampedModel, DomainHelper): """Contact registry for info about a domain.""" try: # get info from registry - dataResponse = self._get_or_create_domain() - data = dataResponse.res_data[0] - # extract properties from response - # (Ellipsis is used to mean "null") - cache = { - "auth_info": getattr(data, "auth_info", ...), - "_contacts": getattr(data, "contacts", ...), - "cr_date": getattr(data, "cr_date", ...), - "ex_date": getattr(data, "ex_date", ...), - "_hosts": getattr(data, "hosts", ...), - "name": getattr(data, "name", ...), - "registrant": getattr(data, "registrant", ...), - "statuses": getattr(data, "statuses", ...), - "tr_date": getattr(data, "tr_date", ...), - "up_date": getattr(data, "up_date", ...), - } - # remove null properties (to distinguish between "a value of None" and null) - cleaned = {k: v for k, v in cache.items() if v is not ...} + data_response = self._get_or_create_domain() + cache = self._extract_data_from_response(data_response) + + # remove null properties (to distinguish between "a value of None" and null) + cleaned = self._remove_null_properties(cache) - # statuses can just be a list no need to keep the epp object if "statuses" in cleaned: cleaned["statuses"] = [status.state for status in cleaned["statuses"]] - # get extensions info, if there is any - # DNSSECExtension is one possible extension, make sure to handle - # only DNSSECExtension and not other type extensions - returned_extensions = dataResponse.extensions - cleaned["dnssecdata"] = None - for extension in returned_extensions: - if isinstance(extension, extensions.DNSSECExtension): - cleaned["dnssecdata"] = extension + cleaned["dnssecdata"] = self._get_dnssec_data(data_response.extensions) + # Capture and store old hosts and contacts from cache if they exist old_cache_hosts = self._cache.get("hosts") old_cache_contacts = self._cache.get("contacts") - # get contact info, if there are any - if ( - fetch_contacts - and "_contacts" in cleaned - and isinstance(cleaned["_contacts"], list) - and len(cleaned["_contacts"]) > 0 - ): - cleaned["contacts"] = self._fetch_contacts(cleaned["_contacts"]) - # We're only getting contacts, so retain the old - # hosts that existed in cache (if they existed) - # and pass them along. + if fetch_contacts: + cleaned["contacts"] = self._get_contacts(cleaned.get("_contacts", [])) if old_cache_hosts is not None: + logger.debug("resetting cleaned['hosts'] to old_cache_hosts") cleaned["hosts"] = old_cache_hosts - # get nameserver info, if there are any - if ( - fetch_hosts - and "_hosts" in cleaned - and isinstance(cleaned["_hosts"], list) - and len(cleaned["_hosts"]) - ): - cleaned["hosts"] = self._fetch_hosts(cleaned["_hosts"]) - # We're only getting hosts, so retain the old - # contacts that existed in cache (if they existed) - # and pass them along. + if fetch_hosts: + cleaned["hosts"] = self._get_hosts(cleaned.get("_hosts", [])) if old_cache_contacts is not None: cleaned["contacts"] = old_cache_contacts - # replace the prior cache with new data + self._cache = cleaned except RegistryError as e: logger.error(e) + def _extract_data_from_response(self, data_response): + data = data_response.res_data[0] + return { + "auth_info": getattr(data, "auth_info", ...), + "_contacts": getattr(data, "contacts", ...), + "cr_date": getattr(data, "cr_date", ...), + "ex_date": getattr(data, "ex_date", ...), + "_hosts": getattr(data, "hosts", ...), + "name": getattr(data, "name", ...), + "registrant": getattr(data, "registrant", ...), + "statuses": getattr(data, "statuses", ...), + "tr_date": getattr(data, "tr_date", ...), + "up_date": getattr(data, "up_date", ...), + } + + def _remove_null_properties(self, cache): + return {k: v for k, v in cache.items() if v is not ...} + + def _get_dnssec_data(self, response_extensions): + # get extensions info, if there is any + # DNSSECExtension is one possible extension, make sure to handle + # only DNSSECExtension and not other type extensions + dnssec_data = None + for extension in response_extensions: + if isinstance(extension, extensions.DNSSECExtension): + dnssec_data = extension + return dnssec_data + + def _get_contacts(self, contacts): + choices = PublicContact.ContactTypeChoices + # We expect that all these fields get populated, + # so we can create these early, rather than waiting. + cleaned_contacts = { + choices.ADMINISTRATIVE: None, + choices.SECURITY: None, + choices.TECHNICAL: None, + } + if contacts and isinstance(contacts, list) and len(contacts) > 0: + cleaned_contacts = self._fetch_contacts(contacts) + return cleaned_contacts + + def _get_hosts(self, hosts): + cleaned_hosts = [] + if hosts and isinstance(hosts, list): + cleaned_hosts = self._fetch_hosts(hosts) + return cleaned_hosts + def _get_or_create_public_contact(self, public_contact: PublicContact): """Tries to find a PublicContact object in our DB. If it can't, it'll create it. Returns PublicContact""" diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index d961a4591..aa71a7551 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -53,7 +53,81 @@ from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView logger = logging.getLogger(__name__) -class DomainView(DomainPermissionView): +class DomainBaseView(DomainPermissionView): + """ + Base View for the Domain. Handles getting and setting the domain + in session cache on GETs. Also provides methods for getting + and setting the domain in cache + """ + + def get(self, request, *args, **kwargs): + self._get_domain(request) + context = self.get_context_data(object=self.object) + return self.render_to_response(context) + + def _get_domain(self, request): + """ + get domain from session cache or from db and set + to self.object + set session to self for downstream functions to + update session cache + """ + self.session = request.session + # domain:private_key is the session key to use for + # caching the domain in the session + domain_pk = "domain:" + str(self.kwargs.get("pk")) + cached_domain = self.session.get(domain_pk) + + if cached_domain: + self.object = cached_domain + else: + self.object = self.get_object() + self._update_session_with_domain() + + def _update_session_with_domain(self): + """ + update domain in the session cache + """ + domain_pk = "domain:" + str(self.kwargs.get("pk")) + self.session[domain_pk] = self.object + + +class DomainFormBaseView(DomainBaseView, FormMixin): + """ + Form Base View for the Domain. Handles getting and setting + domain in cache when dealing with domain forms. Provides + implementations of post, form_valid and form_invalid. + """ + + def post(self, request, *args, **kwargs): + """Form submission posts to this view. + + This post method harmonizes using DomainBaseView and FormMixin + """ + self._get_domain(request) + form = self.get_form() + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + # updates session cache with domain + self._update_session_with_domain() + + # superclass has the redirect + return super().form_valid(form) + + def form_invalid(self, form): + # updates session cache with domain + self._update_session_with_domain() + + # superclass has the redirect + return super().form_invalid(form) + + +class DomainView(DomainBaseView): + """Domain detail overview page.""" template_name = "domain_detail.html" @@ -61,10 +135,10 @@ class DomainView(DomainPermissionView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - default_email = Domain().get_default_security_contact().email + default_email = self.object.get_default_security_contact().email context["default_security_email"] = default_email - security_email = self.get_object().get_security_email() + security_email = self.object.get_security_email() if security_email is None or security_email == default_email: context["security_email"] = None return context @@ -72,7 +146,7 @@ class DomainView(DomainPermissionView): return context -class DomainOrgNameAddressView(DomainPermissionView, FormMixin): +class DomainOrgNameAddressView(DomainFormBaseView): """Organization name and mailing address view""" model = Domain @@ -83,25 +157,13 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin): def get_form_kwargs(self, *args, **kwargs): """Add domain_info.organization_name instance to make a bound form.""" form_kwargs = super().get_form_kwargs(*args, **kwargs) - form_kwargs["instance"] = self.get_object().domain_info + form_kwargs["instance"] = self.object.domain_info return form_kwargs def get_success_url(self): """Redirect to the overview page for the domain.""" return reverse("domain-org-name-address", kwargs={"pk": self.object.pk}) - def post(self, request, *args, **kwargs): - """Form submission posts to this view. - - This post method harmonizes using DetailView and FormMixin together. - """ - self.object = self.get_object() - form = self.get_form() - if form.is_valid(): - return self.form_valid(form) - else: - return self.form_invalid(form) - def form_valid(self, form): """The form is valid, save the organization name and mailing address.""" form.save() @@ -114,7 +176,7 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin): return super().form_valid(form) -class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin): +class DomainAuthorizingOfficialView(DomainFormBaseView): """Domain authorizing official editing view.""" model = Domain @@ -125,25 +187,13 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin): def get_form_kwargs(self, *args, **kwargs): """Add domain_info.authorizing_official instance to make a bound form.""" form_kwargs = super().get_form_kwargs(*args, **kwargs) - form_kwargs["instance"] = self.get_object().domain_info.authorizing_official + form_kwargs["instance"] = self.object.domain_info.authorizing_official return form_kwargs def get_success_url(self): """Redirect to the overview page for the domain.""" return reverse("domain-authorizing-official", kwargs={"pk": self.object.pk}) - def post(self, request, *args, **kwargs): - """Form submission posts to this view. - - This post method harmonizes using DetailView and FormMixin together. - """ - self.object = self.get_object() - form = self.get_form() - if form.is_valid(): - return self.form_valid(form) - else: - return self.form_invalid(form) - def form_valid(self, form): """The form is valid, save the authorizing official.""" form.save() @@ -156,13 +206,13 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin): return super().form_valid(form) -class DomainDNSView(DomainPermissionView): +class DomainDNSView(DomainBaseView): """DNS Information View.""" template_name = "domain_dns.html" -class DomainNameserversView(DomainPermissionView, FormMixin): +class DomainNameserversView(DomainFormBaseView): """Domain nameserver editing view.""" template_name = "domain_nameservers.html" @@ -170,8 +220,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin): def get_initial(self): """The initial value for the form (which is a formset here).""" - domain = self.get_object() - nameservers = domain.nameservers + nameservers = self.object.nameservers initial_data = [] if nameservers is not None: @@ -207,16 +256,6 @@ class DomainNameserversView(DomainPermissionView, FormMixin): form.fields["server"].required = False return formset - def post(self, request, *args, **kwargs): - """Formset submission posts to this view.""" - self.object = self.get_object() - formset = self.get_form() - - if formset.is_valid(): - return self.form_valid(formset) - else: - return self.form_invalid(formset) - def form_valid(self, formset): """The formset is valid, perform something with it.""" @@ -229,8 +268,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin): except KeyError: # no server information in this field, skip it pass - domain = self.get_object() - domain.nameservers = nameservers + self.object.nameservers = nameservers messages.success( self.request, "The name servers for this domain have been updated." @@ -240,7 +278,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin): return super().form_valid(formset) -class DomainDNSSECView(DomainPermissionView, FormMixin): +class DomainDNSSECView(DomainFormBaseView): """Domain DNSSEC editing view.""" template_name = "domain_dnssec.html" @@ -250,9 +288,7 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): """The initial value for the form (which is a formset here).""" context = super().get_context_data(**kwargs) - self.domain = self.get_object() - - has_dnssec_records = self.domain.dnssecdata is not None + has_dnssec_records = self.object.dnssecdata is not None # Create HTML for the modal button modal_button = ( @@ -269,16 +305,16 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): def get_success_url(self): """Redirect to the DNSSEC page for the domain.""" - return reverse("domain-dns-dnssec", kwargs={"pk": self.domain.pk}) + return reverse("domain-dns-dnssec", kwargs={"pk": self.object.pk}) def post(self, request, *args, **kwargs): """Form submission posts to this view.""" - self.domain = self.get_object() + self._get_domain(request) form = self.get_form() if form.is_valid(): if "disable_dnssec" in request.POST: try: - self.domain.dnssecdata = {} + self.object.dnssecdata = {} except RegistryError as err: errmsg = "Error removing existing DNSSEC record(s)." logger.error(errmsg + ": " + err) @@ -293,7 +329,7 @@ class DomainDNSSECView(DomainPermissionView, FormMixin): return self.form_valid(form) -class DomainDsDataView(DomainPermissionView, FormMixin): +class DomainDsDataView(DomainFormBaseView): """Domain DNSSEC ds data editing view.""" template_name = "domain_dsdata.html" @@ -302,8 +338,7 @@ class DomainDsDataView(DomainPermissionView, FormMixin): def get_initial(self): """The initial value for the form (which is a formset here).""" - domain = self.get_object() - dnssecdata: extensions.DNSSECExtension = domain.dnssecdata + dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata initial_data = [] if dnssecdata is not None: @@ -344,8 +379,7 @@ class DomainDsDataView(DomainPermissionView, FormMixin): # set the dnssec_ds_confirmed flag in the context for this view # based either on the existence of DS Data in the domain, # or on the flag stored in the session - domain = self.get_object() - dnssecdata: extensions.DNSSECExtension = domain.dnssecdata + dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata if dnssecdata is not None and dnssecdata.dsData is not None: self.request.session["dnssec_ds_confirmed"] = True @@ -357,7 +391,7 @@ class DomainDsDataView(DomainPermissionView, FormMixin): def post(self, request, *args, **kwargs): """Formset submission posts to this view.""" - self.object = self.get_object() + self._get_domain(request) formset = self.get_form() if "confirm-ds" in request.POST: @@ -397,9 +431,8 @@ class DomainDsDataView(DomainPermissionView, FormMixin): # as valid; this can happen if form has been added but # not been interacted with; in that case, want to ignore pass - domain = self.get_object() try: - domain.dnssecdata = dnssecdata + self.object.dnssecdata = dnssecdata except RegistryError as err: errmsg = "Error updating DNSSEC data in the registry." logger.error(errmsg) @@ -414,7 +447,7 @@ class DomainDsDataView(DomainPermissionView, FormMixin): return super().form_valid(formset) -class DomainKeyDataView(DomainPermissionView, FormMixin): +class DomainKeyDataView(DomainFormBaseView): """Domain DNSSEC key data editing view.""" template_name = "domain_keydata.html" @@ -423,8 +456,7 @@ class DomainKeyDataView(DomainPermissionView, FormMixin): def get_initial(self): """The initial value for the form (which is a formset here).""" - domain = self.get_object() - dnssecdata: extensions.DNSSECExtension = domain.dnssecdata + dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata initial_data = [] if dnssecdata is not None: @@ -465,8 +497,7 @@ class DomainKeyDataView(DomainPermissionView, FormMixin): # set the dnssec_key_confirmed flag in the context for this view # based either on the existence of Key Data in the domain, # or on the flag stored in the session - domain = self.get_object() - dnssecdata: extensions.DNSSECExtension = domain.dnssecdata + dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata if dnssecdata is not None and dnssecdata.keyData is not None: self.request.session["dnssec_key_confirmed"] = True @@ -478,7 +509,7 @@ class DomainKeyDataView(DomainPermissionView, FormMixin): def post(self, request, *args, **kwargs): """Formset submission posts to this view.""" - self.object = self.get_object() + self._get_domain(request) formset = self.get_form() if "confirm-key" in request.POST: @@ -517,9 +548,8 @@ class DomainKeyDataView(DomainPermissionView, FormMixin): except KeyError: # no server information in this field, skip it pass - domain = self.get_object() try: - domain.dnssecdata = dnssecdata + self.object.dnssecdata = dnssecdata except RegistryError as err: errmsg = "Error updating DNSSEC data in the registry." logger.error(errmsg) @@ -534,7 +564,7 @@ class DomainKeyDataView(DomainPermissionView, FormMixin): return super().form_valid(formset) -class DomainYourContactInformationView(DomainPermissionView, FormMixin): +class DomainYourContactInformationView(DomainFormBaseView): """Domain your contact information editing view.""" template_name = "domain_your_contact_information.html" @@ -550,16 +580,6 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin): """Redirect to the your contact information for the domain.""" return reverse("domain-your-contact-information", kwargs={"pk": self.object.pk}) - def post(self, request, *args, **kwargs): - """Form submission posts to this view.""" - self.object = self.get_object() - form = self.get_form() - if form.is_valid(): - # there is a valid email address in the form - return self.form_valid(form) - else: - return self.form_invalid(form) - def form_valid(self, form): """The form is valid, call setter in model.""" @@ -574,7 +594,7 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin): return super().form_valid(form) -class DomainSecurityEmailView(DomainPermissionView, FormMixin): +class DomainSecurityEmailView(DomainFormBaseView): """Domain security email editing view.""" template_name = "domain_security_email.html" @@ -582,9 +602,8 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin): def get_initial(self): """The initial value for the form.""" - domain = self.get_object() initial = super().get_initial() - security_contact = domain.security_contact + security_contact = self.object.security_contact if security_contact is None or security_contact.email == "dotgov@cisa.dhs.gov": initial["security_email"] = None return initial @@ -595,16 +614,6 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin): """Redirect to the security email page for the domain.""" return reverse("domain-security-email", kwargs={"pk": self.object.pk}) - def post(self, request, *args, **kwargs): - """Form submission posts to this view.""" - self.object = self.get_object() - form = self.get_form() - if form.is_valid(): - # there is a valid email address in the form - return self.form_valid(form) - else: - return self.form_invalid(form) - def form_valid(self, form): """The form is valid, call setter in model.""" @@ -615,8 +624,7 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin): if new_email is None or new_email.strip() == "": new_email = PublicContact.get_default_security().email - domain = self.get_object() - contact = domain.security_contact + contact = self.object.security_contact # If no default is created for security_contact, # then we cannot connect to the registry. @@ -647,13 +655,13 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin): return redirect(self.get_success_url()) -class DomainUsersView(DomainPermissionView): +class DomainUsersView(DomainBaseView): """User management page in the domain details.""" template_name = "domain_users.html" -class DomainAddUserView(DomainPermissionView, FormMixin): +class DomainAddUserView(DomainFormBaseView): """Inside of a domain's user management, a form for adding users. Multiple inheritance is used here for permissions, form handling, and @@ -666,15 +674,6 @@ class DomainAddUserView(DomainPermissionView, FormMixin): def get_success_url(self): return reverse("domain-users", kwargs={"pk": self.object.pk}) - def post(self, request, *args, **kwargs): - self.object = self.get_object() - form = self.get_form() - if form.is_valid(): - # there is a valid email address in the form - return self.form_valid(form) - else: - return self.form_invalid(form) - def _domain_abs_url(self): """Get an absolute URL for this domain.""" return self.request.build_absolute_uri(