Merge branch 'main' of https://github.com/cisagov/manage.get.gov into rh/2909-new-agency-field

This commit is contained in:
Rebecca Hsieh 2024-04-17 13:04:52 -07:00
commit e74d98ca71
No known key found for this signature in database
7 changed files with 248 additions and 16 deletions

View file

@ -56,6 +56,13 @@ cf ssh getgov-ENVIRONMENT
./manage.py dumpdata
```
## Access certain table in the database
1. `cf connect-to-service getgov-ENVIRONMENT getgov-ENVIRONMENT-database` gets you into whichever environments database you'd like
2. `\c [table name here that starts cgaws...etc];` connects to the [cgaws...etc] table
3. `\dt` retrieves information about that table and displays it
4. Make sure the table you are looking for exists. For this example, we are looking for `django_migrations`
5. Run `SELECT * FROM django_migrations;` to see everything that's in it!
## Dropping and re-creating the database
For your sandbox environment, it might be necessary to start the database over from scratch.

View file

@ -121,3 +121,19 @@ https://cisa-corp.slack.com/archives/C05BGB4L5NF/p1697810600723069
2. `./manage.py migrate model_name_here file_name_WITH_create` (run the last data creation migration AND ONLY THAT ONE)
3. `./manage.py migrate --fake model_name_here most_recent_file_name` (fake migrate the last migration in the migration list)
4. `./manage.py load` (rerun fixtures)
### Scenario 9: Inconsistent Migration History
If you see `django.db.migrations.exceptions.InconsistentMigrationHistory` error, or when you run `./manage.py showmigrations` it looks like:
[x] 0056_example_migration
[ ] 0057_other_migration
[x] 0058_some_other_migration
1. Go to [database-access.md](../database-access.md#access-certain-table-in-the-database) to see the commands on how to access a certain table in the database.
2. In this case, we want to remove the migration "history" from the `django_migrations` table
3. Once you are in the `cgaws...` table, select the `django_migrations` table with the command `SELECT * FROM django_migrations;`
4. Find the id of the "history" you want to delete. This will be the one in the far left column. For this example, let's pretend the id is 101.
5. Run `DELETE FROM django_migrations WHERE id=101;` where 101 is an example id as seen above.
6. Go to your shell and run `./manage.py showmigrations` to make sure your migrations are now back to the right state. Most likely you will see several unapplied migrations.
7. If you still have unapplied migrations, run `./manage.py migrate`. If an error occurs saying one has already been applied, fake that particular migration `./manage.py migrate --fake model_name_here migration_number` and then run the normal `./manage.py migrate` command to then apply those migrations that come after the one that threw the error.

View file

@ -663,6 +663,7 @@ class ContactAdmin(ListHeaderAdmin):
list_display = [
"contact",
"email",
"user_exists",
]
# this ordering effects the ordering of results
# in autocomplete_fields for user
@ -679,6 +680,13 @@ class ContactAdmin(ListHeaderAdmin):
change_form_template = "django/admin/email_clipboard_change_form.html"
def user_exists(self, obj):
"""Check if the Contact has a related User"""
return "Yes" if obj.user is not None else "No"
user_exists.short_description = "Is user" # type: ignore
user_exists.admin_order_field = "user" # type: ignore
# We name the custom prop 'contact' because linter
# is not allowing a short_description attr on it
# This gets around the linter limitation, for now.
@ -1435,12 +1443,36 @@ class DomainRequestAdmin(ListHeaderAdmin):
"""
Override changelist_view to set the selected value of status filter.
"""
# there are two conditions which should set the default selected filter:
# 1 - there are no query parameters in the request and the request is the
# initial request for this view
# 2 - there are no query parameters in the request and the referring url is
# the change view for a domain request
should_apply_default_filter = False
# use http_referer in order to distinguish between request as a link from another page
# and request as a removal of all filters
http_referer = request.META.get("HTTP_REFERER", "")
# if there are no query parameters in the request
# and the request is the initial request for this view
if not bool(request.GET) and request.path not in http_referer:
if not bool(request.GET):
# if the request is the initial request for this view
if request.path not in http_referer:
should_apply_default_filter = True
# elif the request is a referral from changelist view or from
# domain request change view
elif request.path in http_referer:
# find the index to determine the referring url after the path
index = http_referer.find(request.path)
# Check if there is a character following the path in http_referer
next_char_index = index + len(request.path)
if index + next_char_index < len(http_referer):
next_char = http_referer[next_char_index]
# Check if the next character is a digit, if so, this indicates
# a change view for domain request
if next_char.isdigit():
should_apply_default_filter = True
if should_apply_default_filter:
# modify the GET of the request to set the selected filter
modified_get = copy.deepcopy(request.GET)
modified_get["status__in"] = "submitted,in review,action needed"

View file

@ -530,7 +530,7 @@ function hideDeletedForms() {
let isDotgovDomain = document.querySelector(".dotgov-domain-form");
// The Nameservers formset features 2 required and 11 optionals
if (isNameserversForm) {
cloneIndex = 2;
// cloneIndex = 2;
formLabel = "Name server";
// DNSSEC: DS Data
} else if (isDsDataForm) {
@ -766,3 +766,21 @@ function toggleTwoDomElements(ele1, ele2, index) {
}
})();
/**
* An IIFE that disables the delete buttons on nameserver forms on page load if < 3 forms
*
*/
(function nameserversFormListener() {
let isNameserversForm = document.querySelector(".nameservers-form");
if (isNameserversForm) {
let forms = document.querySelectorAll(".repeatable-form");
if (forms.length < 3) {
// Hide the delete buttons on the 2 nameservers
forms.forEach((form) => {
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
deleteButton.setAttribute("disabled", "true");
});
});
}
}
})();

View file

@ -83,25 +83,34 @@ class DomainNameserverForm(forms.Form):
# after clean_fields. it is used to determine form level errors.
# is_valid is typically called from view during a post
cleaned_data = super().clean()
self.clean_empty_strings(cleaned_data)
server = cleaned_data.get("server", "")
# remove ANY spaces in the server field
server = server.replace(" ", "")
# lowercase the server
server = server.lower()
server = server.replace(" ", "").lower()
cleaned_data["server"] = server
ip = cleaned_data.get("ip", None)
# remove ANY spaces in the ip field
ip = cleaned_data.get("ip", "")
ip = ip.replace(" ", "")
cleaned_data["ip"] = ip
domain = cleaned_data.get("domain", "")
ip_list = self.extract_ip_list(ip)
# validate if the form has a server or an ip
# Capture the server_value
server_value = self.cleaned_data.get("server")
# Validate if the form has a server or an ip
if (ip and ip_list) or server:
self.validate_nameserver_ip_combo(domain, server, ip_list)
# Re-set the server value:
# add_error which is called on validate_nameserver_ip_combo will clean-up (delete) any invalid data.
# We need that data because we need to know the total server entries (even if invalid) in the formset
# clean method where we determine whether a blank first and/or second entry should throw a required error.
self.cleaned_data["server"] = server_value
return cleaned_data
def clean_empty_strings(self, cleaned_data):
@ -149,6 +158,19 @@ class BaseNameserverFormset(forms.BaseFormSet):
"""
Check for duplicate entries in the formset.
"""
# Check if there are at least two valid servers
valid_servers_count = sum(
1 for form in self.forms if form.cleaned_data.get("server") and form.cleaned_data.get("server").strip()
)
if valid_servers_count >= 2:
# If there are, remove the "At least two name servers are required" error from each form
# This will allow for successful submissions when the first or second entries are blanked
# but there are enough entries total
for form in self.forms:
if form.errors.get("server") == ["At least two name servers are required."]:
form.errors.pop("server")
if any(self.errors):
# Don't bother validating the formset unless each form is valid on its own
return
@ -156,10 +178,13 @@ class BaseNameserverFormset(forms.BaseFormSet):
data = []
duplicates = []
for form in self.forms:
for index, form in enumerate(self.forms):
if form.cleaned_data:
value = form.cleaned_data["server"]
if value in data:
# We need to make sure not to trigger the duplicate error in case the first and second nameservers
# are empty. If there are enough records in the formset, that error is an unecessary blocker.
# If there aren't, the required error will block the submit.
if value in data and not (form.cleaned_data.get("server", "").strip() == "" and index == 1):
form.add_error(
"server",
NameserverError(code=nsErrorCodes.DUPLICATE_HOST, nameserver=value),

View file

@ -1152,6 +1152,18 @@ class MockEppLib(TestCase):
],
)
infoDomainFourHosts = fakedEppObject(
"fournameserversDomain.gov",
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
contacts=[],
hosts=[
"ns1.my-nameserver-1.com",
"ns1.my-nameserver-2.com",
"ns1.cats-are-superior3.com",
"ns1.explosive-chicken-nuggets.com",
],
)
infoDomainNoHost = fakedEppObject(
"my-nameserver.gov",
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
@ -1452,7 +1464,9 @@ class MockEppLib(TestCase):
)
def mockInfoDomainCommands(self, _request, cleaned):
request_name = getattr(_request, "name", None)
request_name = getattr(_request, "name", None).lower()
print(request_name)
# Define a dictionary to map request names to data and extension values
request_mappings = {
@ -1474,7 +1488,8 @@ class MockEppLib(TestCase):
"nameserverwithip.gov": (self.infoDomainHasIP, None),
"namerserversubdomain.gov": (self.infoDomainCheckHostIPCombo, None),
"freeman.gov": (self.InfoDomainWithContacts, None),
"threenameserversDomain.gov": (self.infoDomainThreeHosts, None),
"threenameserversdomain.gov": (self.infoDomainThreeHosts, None),
"fournameserversdomain.gov": (self.infoDomainFourHosts, None),
"defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None),
"adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None),
"defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),

View file

@ -5,7 +5,7 @@ from django.conf import settings
from django.urls import reverse
from django.contrib.auth import get_user_model
from .common import MockSESClient, create_user # type: ignore
from .common import MockEppLib, MockSESClient, create_user # type: ignore
from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore
@ -71,11 +71,14 @@ class TestWithDomainPermissions(TestWithUser):
# that inherit this setUp
self.domain_dnssec_none, _ = Domain.objects.get_or_create(name="dnssec-none.gov")
self.domain_with_four_nameservers, _ = Domain.objects.get_or_create(name="fournameserversDomain.gov")
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dsdata)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_multdsdata)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dnssec_none)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_four_nameservers)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_ip)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_just_nameserver)
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_on_hold)
@ -98,6 +101,11 @@ class TestWithDomainPermissions(TestWithUser):
domain=self.domain_dnssec_none,
role=UserDomainRole.Roles.MANAGER,
)
UserDomainRole.objects.get_or_create(
user=self.user,
domain=self.domain_with_four_nameservers,
role=UserDomainRole.Roles.MANAGER,
)
UserDomainRole.objects.get_or_create(
user=self.user,
domain=self.domain_with_ip,
@ -727,7 +735,7 @@ class TestDomainManagers(TestDomainOverview):
self.assertContains(home_page, self.domain.name)
class TestDomainNameservers(TestDomainOverview):
class TestDomainNameservers(TestDomainOverview, MockEppLib):
def test_domain_nameservers(self):
"""Can load domain's nameservers page."""
page = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
@ -974,6 +982,117 @@ class TestDomainNameservers(TestDomainOverview):
page = result.follow()
self.assertContains(page, "The name servers for this domain have been updated")
def test_domain_nameservers_can_blank_out_first_or_second_one_if_enough_entries(self):
"""Nameserver form submits successfully with 2 valid inputs, even if the first or
second entries are blanked out.
Uses self.app WebTest because we need to interact with forms.
"""
nameserver1 = ""
nameserver2 = "ns2.igorville.gov"
nameserver3 = "ns3.igorville.gov"
valid_ip = ""
valid_ip_2 = "128.0.0.2"
valid_ip_3 = "128.0.0.3"
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page.form["form-0-server"] = nameserver1
nameservers_page.form["form-0-ip"] = valid_ip
nameservers_page.form["form-1-server"] = nameserver2
nameservers_page.form["form-1-ip"] = valid_ip_2
nameservers_page.form["form-2-server"] = nameserver3
nameservers_page.form["form-2-ip"] = valid_ip_3
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a successful post, response should be a 302
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page = result.follow()
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
nameserver1 = "ns1.igorville.gov"
nameserver2 = ""
nameserver3 = "ns3.igorville.gov"
valid_ip = "128.0.0.1"
valid_ip_2 = ""
valid_ip_3 = "128.0.0.3"
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page.form["form-0-server"] = nameserver1
nameservers_page.form["form-0-ip"] = valid_ip
nameservers_page.form["form-1-server"] = nameserver2
nameservers_page.form["form-1-ip"] = valid_ip_2
nameservers_page.form["form-2-server"] = nameserver3
nameservers_page.form["form-2-ip"] = valid_ip_3
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a successful post, response should be a 302
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page = result.follow()
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
def test_domain_nameservers_can_blank_out_first_and_second_one_if_enough_entries(self):
"""Nameserver form submits successfully with 2 valid inputs, even if the first and
second entries are blanked out.
Uses self.app WebTest because we need to interact with forms.
"""
# We need to start with a domain with 4 nameservers otherwise the formset in the test environment
# will only have 3 forms
nameserver1 = ""
nameserver2 = ""
nameserver3 = "ns3.igorville.gov"
nameserver4 = "ns4.igorville.gov"
valid_ip = ""
valid_ip_2 = ""
valid_ip_3 = ""
valid_ip_4 = ""
nameservers_page = self.app.get(
reverse("domain-dns-nameservers", kwargs={"pk": self.domain_with_four_nameservers.id})
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Minimal check to ensure the form is loaded correctly
self.assertEqual(nameservers_page.form["form-0-server"].value, "ns1.my-nameserver-1.com")
self.assertEqual(nameservers_page.form["form-3-server"].value, "ns1.explosive-chicken-nuggets.com")
nameservers_page.form["form-0-server"] = nameserver1
nameservers_page.form["form-0-ip"] = valid_ip
nameservers_page.form["form-1-server"] = nameserver2
nameservers_page.form["form-1-ip"] = valid_ip_2
nameservers_page.form["form-2-server"] = nameserver3
nameservers_page.form["form-2-ip"] = valid_ip_3
nameservers_page.form["form-3-server"] = nameserver4
nameservers_page.form["form-3-ip"] = valid_ip_4
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a successful post, response should be a 302
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
reverse("domain-dns-nameservers", kwargs={"pk": self.domain_with_four_nameservers.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
nameservers_page = result.follow()
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
def test_domain_nameservers_form_invalid(self):
"""Nameserver form does not submit with invalid data.