Merge branch 'main' into za/1816-domain-metadata-includes-organization-type

This commit is contained in:
zandercymatics 2024-04-17 14:58:05 -06:00
commit 9aa7235105
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
30 changed files with 989 additions and 152 deletions

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.
@ -779,6 +787,46 @@ class WebsiteAdmin(ListHeaderAdmin):
]
search_help_text = "Search by website."
def get_model_perms(self, request):
"""
Return empty perms dict thus hiding the model from admin index.
"""
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
if analyst_perm and not superuser_perm:
return {}
return super().get_model_perms(request)
def has_change_permission(self, request, obj=None):
"""
Allow analysts to access the change form directly via URL.
"""
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
if analyst_perm and not superuser_perm:
return True
return super().has_change_permission(request, obj)
def response_change(self, request, obj):
"""
Override to redirect users back to the previous page after saving.
"""
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
return_path = request.GET.get("return_path")
# First, call the super method to perform the standard operations and capture the response
response = super().response_change(request, obj)
# Don't redirect to the website page on save if the user is an analyst.
# Rather, just redirect back to the originating page.
if (analyst_perm and not superuser_perm) and return_path:
# Redirect to the return path if it exists
return HttpResponseRedirect(return_path)
# If no redirection is needed, return the original response
return response
class UserDomainRoleAdmin(ListHeaderAdmin):
"""Custom user domain role admin class."""
@ -1395,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"
@ -1466,7 +1538,10 @@ class DomainInformationInline(admin.StackedInline):
def has_change_permission(self, request, obj=None):
"""Custom has_change_permission override so that we can specify that
analysts can edit this through this inline, but not through the model normally"""
if request.user.has_perm("registrar.analyst_access_permission"):
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
if analyst_perm and not superuser_perm:
return True
return super().has_change_permission(request, obj)
@ -1627,12 +1702,8 @@ class DomainAdmin(ListHeaderAdmin):
# No expiration date was found. Return none.
extra_context["extended_expiration_date"] = None
return super().changeform_view(request, object_id, form_url, extra_context)
if curr_exp_date < date.today():
extra_context["extended_expiration_date"] = date.today() + relativedelta(years=years_to_extend_by)
else:
new_date = domain.registry_expiration_date + relativedelta(years=years_to_extend_by)
extra_context["extended_expiration_date"] = new_date
new_date = curr_exp_date + relativedelta(years=years_to_extend_by)
extra_context["extended_expiration_date"] = new_date
else:
extra_context["extended_expiration_date"] = None
@ -1896,6 +1967,46 @@ class DraftDomainAdmin(ListHeaderAdmin):
# in autocomplete_fields for user
ordering = ["name"]
def get_model_perms(self, request):
"""
Return empty perms dict thus hiding the model from admin index.
"""
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
if analyst_perm and not superuser_perm:
return {}
return super().get_model_perms(request)
def has_change_permission(self, request, obj=None):
"""
Allow analysts to access the change form directly via URL.
"""
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
if analyst_perm and not superuser_perm:
return True
return super().has_change_permission(request, obj)
def response_change(self, request, obj):
"""
Override to redirect users back to the previous page after saving.
"""
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
return_path = request.GET.get("return_path")
# First, call the super method to perform the standard operations and capture the response
response = super().response_change(request, obj)
# Don't redirect to the website page on save if the user is an analyst.
# Rather, just redirect back to the originating page.
if (analyst_perm and not superuser_perm) and return_path:
# Redirect to the return path if it exists
return HttpResponseRedirect(return_path)
# If no redirection is needed, return the original response
return response
class PublicContactAdmin(ListHeaderAdmin):
"""Custom PublicContact admin class."""

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

@ -495,6 +495,8 @@ address.dja-address-contact-list {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: medium;
padding-top: 3px !important;
}
}
@ -505,6 +507,7 @@ address.dja-address-contact-list {
@media screen and (min-width:768px) {
.visible-768 {
display: block;
padding-top: 0;
}
}
@ -536,6 +539,18 @@ address.dja-address-contact-list {
}
}
.dja-status-list {
border-top: solid 1px var(--border-color);
margin-left: 0 !important;
padding-left: 0 !important;
padding-top: 10px;
li {
line-height: 1.5;
font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif !important;
padding-top: 0;
padding-bottom: 0;
}
}
// Make the clipboard button "float" inside of the input box
.admin-icon-group {

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

@ -11,7 +11,11 @@ logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Loops through each valid DomainInformation and DomainRequest object and updates its organization_type value"
help = (
"Loops through each valid DomainInformation and DomainRequest object and updates its organization_type value. "
"A valid DomainInformation/DomainRequest in this sense is one that has the value None for organization_type. "
"In other words, we populate the organization_type field if it is not already populated."
)
def __init__(self):
super().__init__()
@ -26,34 +30,26 @@ class Command(BaseCommand):
self.di_skipped: List[DomainInformation] = []
# Define a global variable for all domains with election offices
self.domains_with_election_offices_set = set()
self.domains_with_election_boards_set = set()
def add_arguments(self, parser):
"""Adds command line arguments"""
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
parser.add_argument(
"domain_election_office_filename",
"domain_election_board_filename",
help=("A file that contains" " all the domains that are election offices."),
)
def handle(self, domain_election_office_filename, **kwargs):
def handle(self, domain_election_board_filename, **kwargs):
"""Loops through each valid Domain object and updates its first_created value"""
debug = kwargs.get("debug")
# Check if the provided file path is valid
if not os.path.isfile(domain_election_office_filename):
raise argparse.ArgumentTypeError(f"Invalid file path '{domain_election_office_filename}'")
if not os.path.isfile(domain_election_board_filename):
raise argparse.ArgumentTypeError(f"Invalid file path '{domain_election_board_filename}'")
with open(domain_election_office_filename, "r") as file:
for line in file:
# Remove any leading/trailing whitespace
domain = line.strip()
if domain not in self.domains_with_election_offices_set:
self.domains_with_election_offices_set.add(domain)
# Read the election office csv
self.read_election_board_file(domain_election_board_filename)
domain_requests = DomainRequest.objects.filter(
organization_type__isnull=True, requested_domain__name__isnull=False
)
domain_requests = DomainRequest.objects.filter(organization_type__isnull=True)
# Code execution will stop here if the user prompts "N"
TerminalHelper.prompt_for_execution(
@ -68,7 +64,7 @@ class Command(BaseCommand):
)
logger.info("Updating DomainRequest(s)...")
self.update_domain_requests(domain_requests, debug)
self.update_domain_requests(domain_requests)
# We should actually be targeting all fields with no value for organization type,
# but do have a value for generic_org_type. This is because there is data that we can infer.
@ -86,23 +82,55 @@ class Command(BaseCommand):
)
logger.info("Updating DomainInformation(s)...")
self.update_domain_informations(domain_infos, debug)
self.update_domain_informations(domain_infos)
def update_domain_requests(self, domain_requests, debug):
def read_election_board_file(self, domain_election_board_filename):
"""
Reads the election board file and adds each parsed domain to self.domains_with_election_boards_set.
As previously implied, this file contains information about Domains which have election boards.
The file must adhere to this format:
```
domain1.gov
domain2.gov
domain3.gov
```
(and so on)
"""
with open(domain_election_board_filename, "r") as file:
for line in file:
# Remove any leading/trailing whitespace
domain = line.strip()
if domain not in self.domains_with_election_boards_set:
self.domains_with_election_boards_set.add(domain)
def update_domain_requests(self, domain_requests):
"""
Updates the organization_type for a list of DomainRequest objects using the `sync_organization_type` function.
Results are then logged.
This function updates the following variables:
- self.request_to_update list is appended to if the field was updated successfully.
- self.request_skipped list is appended to if the field has `None` for `request.generic_org_type`.
- self.request_failed_to_update list is appended to if an exception is caught during update.
"""
for request in domain_requests:
try:
if request.generic_org_type is not None:
domain_name = request.requested_domain.name
request.is_election_board = domain_name in self.domains_with_election_offices_set
request = self.sync_organization_type(DomainRequest, request)
self.request_to_update.append(request)
domain_name = None
if request.requested_domain is not None and request.requested_domain.name is not None:
domain_name = request.requested_domain.name
if debug:
logger.info(f"Updating {request} => {request.organization_type}")
request_is_approved = request.status == DomainRequest.DomainRequestStatus.APPROVED
if request_is_approved and domain_name is not None and not request.is_election_board:
request.is_election_board = domain_name in self.domains_with_election_boards_set
self.sync_organization_type(DomainRequest, request)
self.request_to_update.append(request)
logger.info(f"Updating {request} => {request.organization_type}")
else:
self.request_skipped.append(request)
if debug:
logger.warning(f"Skipped updating {request}. No generic_org_type was found.")
logger.warning(f"Skipped updating {request}. No generic_org_type was found.")
except Exception as err:
self.request_failed_to_update.append(request)
logger.error(err)
@ -116,23 +144,44 @@ class Command(BaseCommand):
# Log what happened
log_header = "============= FINISHED UPDATE FOR DOMAINREQUEST ==============="
TerminalHelper.log_script_run_summary(
self.request_to_update, self.request_failed_to_update, self.request_skipped, debug, log_header
self.request_to_update, self.request_failed_to_update, self.request_skipped, True, log_header
)
def update_domain_informations(self, domain_informations, debug):
update_skipped_count = len(self.request_to_update)
if update_skipped_count > 0:
logger.warning(
f"""{TerminalColors.MAGENTA}
Note: Entries are skipped when generic_org_type is None
{TerminalColors.ENDC}
"""
)
def update_domain_informations(self, domain_informations):
"""
Updates the organization_type for a list of DomainInformation objects
and updates is_election_board if the domain is in the provided csv.
Results are then logged.
This function updates the following variables:
- self.di_to_update list is appended to if the field was updated successfully.
- self.di_skipped list is appended to if the field has `None` for `request.generic_org_type`.
- self.di_failed_to_update list is appended to if an exception is caught during update.
"""
for info in domain_informations:
try:
if info.generic_org_type is not None:
domain_name = info.domain.name
info.is_election_board = domain_name in self.domains_with_election_offices_set
info = self.sync_organization_type(DomainInformation, info)
if not info.is_election_board:
info.is_election_board = domain_name in self.domains_with_election_boards_set
self.sync_organization_type(DomainInformation, info)
self.di_to_update.append(info)
if debug:
logger.info(f"Updating {info} => {info.organization_type}")
logger.info(f"Updating {info} => {info.organization_type}")
else:
self.di_skipped.append(info)
if debug:
logger.warning(f"Skipped updating {info}. No generic_org_type was found.")
logger.warning(f"Skipped updating {info}. No generic_org_type was found.")
except Exception as err:
self.di_failed_to_update.append(info)
logger.error(err)
@ -146,9 +195,18 @@ class Command(BaseCommand):
# Log what happened
log_header = "============= FINISHED UPDATE FOR DOMAININFORMATION ==============="
TerminalHelper.log_script_run_summary(
self.di_to_update, self.di_failed_to_update, self.di_skipped, debug, log_header
self.di_to_update, self.di_failed_to_update, self.di_skipped, True, log_header
)
update_skipped_count = len(self.di_skipped)
if update_skipped_count > 0:
logger.warning(
f"""{TerminalColors.MAGENTA}
Note: Entries are skipped when generic_org_type is None
{TerminalColors.ENDC}
"""
)
def sync_organization_type(self, sender, instance):
"""
Updates the organization_type (without saving) to match
@ -159,7 +217,7 @@ class Command(BaseCommand):
# These have to be defined here, as you'd get a cyclical import error
# otherwise.
# For any given organization type, return the "_election" variant.
# For any given organization type, return the "_ELECTION" enum equivalent.
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
generic_org_map = DomainRequest.OrgChoicesElectionOffice.get_org_generic_to_org_election()
@ -168,7 +226,7 @@ class Command(BaseCommand):
election_org_map = DomainRequest.OrgChoicesElectionOffice.get_org_election_to_org_generic()
# Manages the "organization_type" variable and keeps in sync with
# "is_election_office" and "generic_organization_type"
# "is_election_board" and "generic_organization_type"
org_type_helper = CreateOrUpdateOrganizationTypeHelper(
sender=sender,
instance=instance,
@ -176,5 +234,4 @@ class Command(BaseCommand):
election_org_to_generic_org_map=election_org_map,
)
instance = org_type_helper.create_or_update_organization_type()
return instance
org_type_helper.create_or_update_organization_type(force_update=True)

View file

@ -0,0 +1,37 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
# It is dependent on 0079 (which populates federal agencies)
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
# in the user_group model then:
# [NOT RECOMMENDED]
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
# step 3: fake run the latest migration in the migrations list
# [RECOMMENDED]
# Alternatively:
# step 1: duplicate the migration that loads data
# step 2: docker-compose exec app ./manage.py migrate
from django.db import migrations
from registrar.models import UserGroup
from typing import Any
# For linting: RunPython expects a function reference,
# so let's give it one
def create_groups(apps, schema_editor) -> Any:
UserGroup.create_cisa_analyst_group(apps, schema_editor)
UserGroup.create_full_access_group(apps, schema_editor)
class Migration(migrations.Migration):
dependencies = [
("registrar", "0083_alter_contact_email_alter_publiccontact_email"),
]
operations = [
migrations.RunPython(
create_groups,
reverse_code=migrations.RunPython.noop,
atomic=True,
),
]

View file

@ -94,6 +94,9 @@ class Contact(TimeStampedModel):
names = [n for n in [self.first_name, self.middle_name, self.last_name] if n]
return " ".join(names) if names else "Unknown"
def has_contact_info(self):
return bool(self.title or self.email or self.phone)
def save(self, *args, **kwargs):
# Call the parent class's save method to perform the actual save
super().save(*args, **kwargs)

View file

@ -246,7 +246,7 @@ class DomainInformation(TimeStampedModel):
# These have to be defined here, as you'd get a cyclical import error
# otherwise.
# For any given organization type, return the "_election" variant.
# For any given organization type, return the "_ELECTION" enum equivalent.
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
generic_org_map = DomainRequest.OrgChoicesElectionOffice.get_org_generic_to_org_election()

View file

@ -675,7 +675,7 @@ class DomainRequest(TimeStampedModel):
# These have to be defined here, as you'd get a cyclical import error
# otherwise.
# For any given organization type, return the "_election" variant.
# For any given organization type, return the "_ELECTION" enum equivalent.
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
generic_org_map = self.OrgChoicesElectionOffice.get_org_generic_to_org_election()

View file

@ -9,6 +9,7 @@ from .domain_invitation import DomainInvitation
from .transition_domain import TransitionDomain
from .verified_by_staff import VerifiedByStaff
from .domain import Domain
from .domain_request import DomainRequest
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
@ -67,6 +68,33 @@ class User(AbstractUser):
def is_restricted(self):
return self.status == self.RESTRICTED
def get_approved_domains_count(self):
"""Return count of approved domains"""
allowed_states = [Domain.State.UNKNOWN, Domain.State.DNS_NEEDED, Domain.State.READY, Domain.State.ON_HOLD]
approved_domains_count = self.domains.filter(state__in=allowed_states).count()
return approved_domains_count
def get_active_requests_count(self):
"""Return count of active requests"""
allowed_states = [
DomainRequest.DomainRequestStatus.SUBMITTED,
DomainRequest.DomainRequestStatus.IN_REVIEW,
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
]
active_requests_count = self.domain_requests_created.filter(status__in=allowed_states).count()
return active_requests_count
def get_rejected_requests_count(self):
"""Return count of rejected requests"""
return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.REJECTED).count()
def get_ineligible_requests_count(self):
"""Return count of ineligible requests"""
return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.INELIGIBLE).count()
def has_contact_info(self):
return bool(self.contact.title or self.contact.email or self.contact.phone)
@classmethod
def needs_identity_verification(cls, email, uuid):
"""A method used by our oidc classes to test whether a user needs email/uuid verification

View file

@ -41,11 +41,6 @@ class UserGroup(Group):
"model": "domain",
"permissions": ["view_domain"],
},
{
"app_label": "registrar",
"model": "draftdomain",
"permissions": ["change_draftdomain"],
},
{
"app_label": "registrar",
"model": "user",
@ -56,11 +51,6 @@ class UserGroup(Group):
"model": "domaininvitation",
"permissions": ["add_domaininvitation", "view_domaininvitation"],
},
{
"app_label": "registrar",
"model": "website",
"permissions": ["change_website"],
},
{
"app_label": "registrar",
"model": "userdomainrole",

View file

@ -49,7 +49,7 @@ class CreateOrUpdateOrganizationTypeHelper:
self.generic_org_to_org_map = generic_org_to_org_map
self.election_org_to_generic_org_map = election_org_to_generic_org_map
def create_or_update_organization_type(self):
def create_or_update_organization_type(self, force_update=False):
"""The organization_type field on DomainRequest and DomainInformation is consituted from the
generic_org_type and is_election_board fields. To keep the organization_type
field up to date, we need to update it before save based off of those field
@ -59,6 +59,14 @@ class CreateOrUpdateOrganizationTypeHelper:
one of the excluded types (FEDERAL, INTERSTATE, SCHOOL_DISTRICT), the
organization_type is set to a corresponding election variant. Otherwise, it directly
mirrors the generic_org_type value.
args:
force_update (bool): If an existing instance has no values to change,
try to update the organization_type field (or related fields) anyway.
This is done by invoking the new instance handler.
Use to force org type to be updated to the correct value even
if no other changes were made (including is_election).
"""
# A new record is added with organization_type not defined.
@ -67,7 +75,7 @@ class CreateOrUpdateOrganizationTypeHelper:
if is_new_instance:
self._handle_new_instance()
else:
self._handle_existing_instance()
self._handle_existing_instance(force_update)
return self.instance
@ -92,7 +100,7 @@ class CreateOrUpdateOrganizationTypeHelper:
# Update the field
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
def _handle_existing_instance(self):
def _handle_existing_instance(self, force_update_when_no_are_changes_found=False):
# == Init variables == #
# Instance is already in the database, fetch its current state
current_instance = self.sender.objects.get(id=self.instance.id)
@ -109,17 +117,19 @@ class CreateOrUpdateOrganizationTypeHelper:
# This will not happen in normal flow as it is not possible otherwise.
raise ValueError("Cannot update organization_type and generic_org_type simultaneously.")
elif not organization_type_changed and (not generic_org_type_changed and not is_election_board_changed):
# No values to update - do nothing
return None
# == Program flow will halt here if there is no reason to update == #
# No changes found
if force_update_when_no_are_changes_found:
# If we want to force an update anyway, we can treat this record like
# its a new one in that we check for "None" values rather than changes.
self._handle_new_instance()
else:
# == Update the linked values == #
# Find out which field needs updating
organization_type_needs_update = generic_org_type_changed or is_election_board_changed
generic_org_type_needs_update = organization_type_changed
# == Update the linked values == #
# Find out which field needs updating
organization_type_needs_update = generic_org_type_changed or is_election_board_changed
generic_org_type_needs_update = organization_type_changed
# Update the field
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
# Update the field
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
def _update_fields(self, organization_type_needs_update, generic_org_type_needs_update):
"""

View file

@ -116,8 +116,8 @@
</button>
</span>
<p class="text-right margin-top-2 padding-right-2 margin-bottom-0 requested-domain-sticky float-right visible-768">
<strong>Requested domain:</strong> {{ original.requested_domain.name }}
<p class="padding-top-05 text-right margin-top-2 padding-right-2 margin-bottom-0 requested-domain-sticky float-right visible-768">
Requested domain: <strong>{{ original.requested_domain.name }}</strong>
</p>
{{ block.super }}
</div>

View file

@ -1,6 +1,6 @@
{% load i18n static %}
<address class="{% if no_title_top_padding %}margin-top-neg-1__detail-list{% endif %} dja-address-contact-list">
<address class="{% if no_title_top_padding %}margin-top-neg-1__detail-list{% endif %} {% if user.has_contact_info %}margin-bottom-1{% endif %} dja-address-contact-list">
{% if show_formatted_name %}
{% if contact.get_formatted_name %}
@ -10,7 +10,7 @@
{% endif %}
{% endif %}
{% if user.title or user.contact.title or user.email or user.contact.email or user.phone or user.contact.phone %}
{% if user.has_contact_info %}
{# Title #}
{% if user.title or user.contact.title %}
{% if user.contact.title %}

View file

@ -27,6 +27,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
</dl>
</div>
{% endif %}
{% elif field.field.name == "requested_domain" %}
{% with current_path=request.get_full_path %}
<a class="margin-top-05 padding-top-05" href="{% url 'admin:registrar_draftdomain_change' original.requested_domain.id %}?{{ 'return_path='|add:current_path }}">{{ original.requested_domain }}</a>
{% endwith%}
{% elif field.field.name == "current_websites" %}
{% comment %}
The "website" model is essentially just a text field.
@ -49,9 +53,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
</div>
{% elif field.field.name == "alternative_domains" %}
<div class="readonly">
{% with current_path=request.get_full_path %}
{% for alt_domain in original.alternative_domains.all %}
<a href="{% url 'admin:registrar_website_change' alt_domain.id %}">{{ alt_domain }}</a>{% if not forloop.last %}, {% endif %}
<a href="{% url 'admin:registrar_website_change' alt_domain.id %}?{{ 'return_path='|add:current_path }}">{{ alt_domain }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
{% endwith %}
</div>
{% else %}
<div class="readonly">{{ field.contents }}</div>
@ -65,6 +71,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
<label aria-label="Creator contact details"></label>
{% include "django/admin/includes/contact_detail_list.html" with user=original.creator no_title_top_padding=field.is_readonly %}
</div>
<div class="flex-container">
<label aria-label="User summary details"></label>
{% include "django/admin/includes/user_detail_list.html" with user=original.creator no_title_top_padding=field.is_readonly %}
</div>
{% elif field.field.name == "submitter" %}
<div class="flex-container">
<label aria-label="Submitter contact details"></label>

View file

@ -0,0 +1,30 @@
{% load i18n static %}
{% with approved_domains_count=user.get_approved_domains_count %}
{% with active_requests_count=user.get_active_requests_count %}
{% with rejected_requests_count=user.get_rejected_requests_count %}
{% with ineligible_requests_count=user.get_ineligible_requests_count %}
{% if approved_domains_count|add:active_requests_count|add:rejected_requests_count|add:ineligible_requests_count > 0 %}
<ul class="dja-status-list">
{% if approved_domains_count > 0 %}
{# Approved domains #}
<li>Approved domains: {{ approved_domains_count }}</li>
{% endif %}
{% if active_requests_count > 0 %}
{# Active requests #}
<li>Active requests: {{ active_requests_count }}</li>
{% endif %}
{% if rejected_requests_count > 0 %}
{# Rejected requests #}
<li>Rejected requests: {{ rejected_requests_count }}</li>
{% endif %}
{% if ineligible_requests_count > 0 %}
{# Ineligible requests #}
<li>Ineligible requests: {{ ineligible_requests_count }}</li>
{% endif %}
</ul>
{% endif %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}

View file

@ -3,7 +3,7 @@
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold" >
Next steps in this process
</h2>
<p>We received your .gov domain request. Our next step is to review your request. This usually takes 20 business days. Well email you if we have questions and when we complete our review. <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">Contact us with any questions</a>.</p>
<p>We received your .gov domain request. Our next step is to review your request. This usually takes 30 business days. Well email you if we have questions and when we complete our review. <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">Contact us with any questions</a>.</p>
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold">
Need to make changes?

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

@ -21,7 +21,16 @@ from registrar.admin import (
UserDomainRoleAdmin,
VerifiedByStaffAdmin,
)
from registrar.models import Domain, DomainRequest, DomainInformation, User, DomainInvitation, Contact, Website
from registrar.models import (
Domain,
DomainRequest,
DomainInformation,
User,
DomainInvitation,
Contact,
Website,
DraftDomain,
)
from registrar.models.user_domain_role import UserDomainRole
from registrar.models.verified_by_staff import VerifiedByStaff
from .common import (
@ -76,11 +85,10 @@ class TestDomainAdmin(MockEppLib, WebTest):
)
super().setUp()
@skip("TODO for another ticket. This test case is grabbing old db data.")
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
def test_extend_expiration_date_button(self, mock_date_today):
"""
Tests if extend_expiration_date button extends correctly
Tests if extend_expiration_date modal gives an accurate date
"""
# Create a ready domain with a preset expiration date
@ -107,17 +115,11 @@ class TestDomainAdmin(MockEppLib, WebTest):
# Follow the response
response = response.follow()
# refresh_from_db() does not work for objects with protected=True.
# https://github.com/viewflow/django-fsm/issues/89
new_domain = Domain.objects.get(id=domain.id)
# Check that the current expiration date is what we expect
self.assertEqual(new_domain.expiration_date, date(2025, 5, 25))
# Assert that everything on the page looks correct
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Extend expiration date")
self.assertContains(response, "New expiration date: <b>May 25, 2025</b>")
# Ensure the message we recieve is in line with what we expect
expected_message = "Successfully extended the expiration date."
@ -129,6 +131,7 @@ class TestDomainAdmin(MockEppLib, WebTest):
extra_tags="",
fail_silently=False,
)
mock_add_message.assert_has_calls([expected_call], 1)
@less_console_noise_decorator
@ -703,6 +706,126 @@ class TestDomainRequestAdmin(MockEppLib):
)
self.mock_client = MockSESClient()
@less_console_noise_decorator
def test_analyst_can_see_and_edit_alternative_domain(self):
"""Tests if an analyst can still see and edit the alternative domain field"""
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
)
# Create a fake domain request
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
fake_website = Website.objects.create(website="thisisatest.gov")
_domain_request.alternative_domains.add(fake_website)
_domain_request.save()
p = "userpass"
self.client.login(username="staffuser", password=p)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, _domain_request.requested_domain.name)
# Test if the page has the alternative domain
self.assertContains(response, "thisisatest.gov")
# Check that the page contains the url we expect
expected_href = reverse("admin:registrar_website_change", args=[fake_website.id])
self.assertContains(response, expected_href)
# Navigate to the website to ensure that we can still edit it
response = self.client.get(
"/admin/registrar/website/{}/change/".format(fake_website.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, "thisisatest.gov")
@less_console_noise_decorator
def test_analyst_can_see_and_edit_requested_domain(self):
"""Tests if an analyst can still see and edit the requested domain field"""
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
)
# Create a fake domain request
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
p = "userpass"
self.client.login(username="staffuser", password=p)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
follow=True,
)
# Filter to get the latest from the DB (rather than direct assignment)
requested_domain = DraftDomain.objects.filter(name=_domain_request.requested_domain.name).get()
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, requested_domain.name)
# Check that the page contains the url we expect
expected_href = reverse("admin:registrar_draftdomain_change", args=[requested_domain.id])
self.assertContains(response, expected_href)
# Navigate to the website to ensure that we can still edit it
response = self.client.get(
"/admin/registrar/draftdomain/{}/change/".format(requested_domain.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, "city.gov")
@less_console_noise_decorator
def test_analyst_can_see_current_websites(self):
"""Tests if an analyst can still see current website field"""
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
)
# Create a fake domain request
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
fake_website = Website.objects.create(website="thisisatest.gov")
_domain_request.current_websites.add(fake_website)
_domain_request.save()
p = "userpass"
self.client.login(username="staffuser", password=p)
response = self.client.get(
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, _domain_request.requested_domain.name)
# Test if the page has the current website
self.assertContains(response, "thisisatest.gov")
def test_domain_sortable(self):
"""Tests if the DomainRequest sorts by domain correctly"""
with less_console_noise():
@ -1386,7 +1509,7 @@ class TestDomainRequestAdmin(MockEppLib):
# Since we're using client to mock the request, we can only test against
# non-interpolated values
expected_content = "<strong>Requested domain:</strong>"
expected_content = "Requested domain:"
expected_content2 = '<span class="scroll-indicator"></span>'
expected_content3 = '<div class="submit-row-wrapper">'
not_expected_content = "submit-row-wrapper--analyst-view>"
@ -1415,7 +1538,7 @@ class TestDomainRequestAdmin(MockEppLib):
# Since we're using client to mock the request, we can only test against
# non-interpolated values
expected_content = "<strong>Requested domain:</strong>"
expected_content = "Requested domain:"
expected_content2 = '<span class="scroll-indicator"></span>'
expected_content3 = '<div class="submit-row-wrapper submit-row-wrapper--analyst-view">'
self.assertContains(request, expected_content)
@ -1553,6 +1676,10 @@ class TestDomainRequestAdmin(MockEppLib):
# Test for the copy link
self.assertContains(response, "usa-button__clipboard", count=4)
# Test that Creator counts display properly
self.assertNotContains(response, "Approved domains")
self.assertContains(response, "Active requests")
def test_save_model_sets_restricted_status_on_user(self):
with less_console_noise():
# make sure there is no user with this email

View file

@ -98,7 +98,7 @@ class TestPopulateOrganizationType(MockEppLib):
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True,
):
call_command("populate_organization_type", "registrar/tests/data/fake_election_domains.csv", debug=True)
call_command("populate_organization_type", "registrar/tests/data/fake_election_domains.csv")
def assert_expected_org_values_on_request_and_info(
self,
@ -107,9 +107,23 @@ class TestPopulateOrganizationType(MockEppLib):
expected_values: dict,
):
"""
This is a a helper function that ensures that:
This is a helper function that tests the following conditions:
1. DomainRequest and DomainInformation (on given objects) are equivalent
2. That generic_org_type, is_election_board, and organization_type are equal to passed in values
Args:
domain_request (DomainRequest): The DomainRequest object to test
domain_info (DomainInformation): The DomainInformation object to test
expected_values (dict): Container for what we expect is_electionboard, generic_org_type,
and organization_type to be on DomainRequest and DomainInformation.
Example:
expected_values = {
"is_election_board": False,
"generic_org_type": DomainRequest.OrganizationChoices.CITY,
"organization_type": DomainRequest.OrgChoicesElectionOffice.CITY,
}
"""
# Test domain request
@ -124,8 +138,23 @@ class TestPopulateOrganizationType(MockEppLib):
self.assertEqual(domain_info.is_election_board, expected_values["is_election_board"])
self.assertEqual(domain_info.organization_type, expected_values["organization_type"])
def do_nothing(self):
"""Does nothing for mocking purposes"""
pass
def test_request_and_info_city_not_in_csv(self):
"""Tests what happens to a city domain that is not defined in the CSV"""
"""
Tests what happens to a city domain that is not defined in the CSV.
Scenario: A domain request (of type city) is made that is not defined in the CSV file.
When a domain request is made for a city that is not listed in the CSV,
Then the `is_election_board` value should remain False,
and the `generic_org_type` and `organization_type` should both be `city`.
Expected Result: The `is_election_board` and `generic_org_type` attributes should be unchanged.
The `organization_type` field should now be `city`.
"""
city_request = self.domain_request_2
city_info = self.domain_request_2
@ -149,7 +178,17 @@ class TestPopulateOrganizationType(MockEppLib):
self.assert_expected_org_values_on_request_and_info(city_request, city_info, expected_values)
def test_request_and_info_federal(self):
"""Tests what happens to a federal domain after the script is run (should be unchanged)"""
"""
Tests what happens to a federal domain after the script is run (should be unchanged).
Scenario: A domain request (of type federal) is processed after running the populate_organization_type script.
When a federal domain request is made,
Then the `is_election_board` value should remain None,
and the `generic_org_type` and `organization_type` fields should both be `federal`.
Expected Result: The `is_election_board` and `generic_org_type` attributes should be unchanged.
The `organization_type` field should now be `federal`.
"""
federal_request = self.domain_request_1
federal_info = self.domain_info_1
@ -172,10 +211,6 @@ class TestPopulateOrganizationType(MockEppLib):
# All values should be the same
self.assert_expected_org_values_on_request_and_info(federal_request, federal_info, expected_values)
def do_nothing(self):
"""Does nothing for mocking purposes"""
pass
def test_request_and_info_tribal_add_election_office(self):
"""
Tests if a tribal domain in the election csv changes organization_type to TRIBAL - ELECTION
@ -216,11 +251,14 @@ class TestPopulateOrganizationType(MockEppLib):
self.assert_expected_org_values_on_request_and_info(tribal_request, tribal_info, expected_values)
def test_request_and_info_tribal_remove_election_office(self):
def test_request_and_info_tribal_doesnt_remove_election_office(self):
"""
Tests if a tribal domain in the election csv changes organization_type to TRIBAL
when it used to be TRIBAL - ELECTION
for the domain request and the domain info
Tests if a tribal domain in the election csv changes organization_type to TRIBAL_ELECTION
when the is_election_board is True, and generic_org_type is Tribal when it is not
present in the CSV.
To avoid overwriting data, the script should not set any domain specified as
an election_office (that doesn't exist in the CSV) to false.
"""
# Set org type fields to none to mimic an environment without this data
@ -252,10 +290,10 @@ class TestPopulateOrganizationType(MockEppLib):
except Exception as e:
self.fail(f"Could not run populate_organization_type script. Failed with exception: {e}")
# Because we don't define this in the "csv", we expect that is election board will switch to False,
# and organization_type will now be tribal
expected_values["is_election_board"] = False
expected_values["organization_type"] = DomainRequest.OrgChoicesElectionOffice.TRIBAL
# If we don't define this in the "csv", but the value was already true,
# we expect that is election board will stay True, and the org type will be tribal,
# and organization_type will now be tribal_election
expected_values["organization_type"] = DomainRequest.OrgChoicesElectionOffice.TRIBAL_ELECTION
tribal_election_request.refresh_from_db()
tribal_election_info.refresh_from_db()
self.assert_expected_org_values_on_request_and_info(

View file

@ -37,7 +37,6 @@ class TestGroups(TestCase):
"add_domaininvitation",
"view_domaininvitation",
"change_domainrequest",
"change_draftdomain",
"add_federalagency",
"change_federalagency",
"delete_federalagency",
@ -48,7 +47,6 @@ class TestGroups(TestCase):
"add_verifiedbystaff",
"change_verifiedbystaff",
"delete_verifiedbystaff",
"change_website",
]
# Get the codenames of actual permissions associated with the group

View file

@ -1004,6 +1004,8 @@ class TestUser(TestCase):
Domain.objects.all().delete()
DomainInvitation.objects.all().delete()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
DraftDomain.objects.all().delete()
TransitionDomain.objects.all().delete()
User.objects.all().delete()
UserDomainRole.objects.all().delete()
@ -1060,6 +1062,91 @@ class TestUser(TestCase):
# Domain Invitation, then save routine should be called exactly once
save_mock.assert_called_once()
def test_approved_domains_count(self):
"""Test that the correct approved domain count is returned for a user"""
# with no associated approved domains, expect this to return 0
self.assertEquals(self.user.get_approved_domains_count(), 0)
# with one approved domain, expect this to return 1
UserDomainRole.objects.get_or_create(user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
self.assertEquals(self.user.get_approved_domains_count(), 1)
# with one approved domain, expect this to return 1 (domain2 is deleted, so not considered approved)
domain2, _ = Domain.objects.get_or_create(name="igorville2.gov", state=Domain.State.DELETED)
UserDomainRole.objects.get_or_create(user=self.user, domain=domain2, role=UserDomainRole.Roles.MANAGER)
self.assertEquals(self.user.get_approved_domains_count(), 1)
# with two approved domains, expect this to return 2
domain3, _ = Domain.objects.get_or_create(name="igorville3.gov", state=Domain.State.DNS_NEEDED)
UserDomainRole.objects.get_or_create(user=self.user, domain=domain3, role=UserDomainRole.Roles.MANAGER)
self.assertEquals(self.user.get_approved_domains_count(), 2)
# with three approved domains, expect this to return 3
domain4, _ = Domain.objects.get_or_create(name="igorville4.gov", state=Domain.State.ON_HOLD)
UserDomainRole.objects.get_or_create(user=self.user, domain=domain4, role=UserDomainRole.Roles.MANAGER)
self.assertEquals(self.user.get_approved_domains_count(), 3)
# with four approved domains, expect this to return 4
domain5, _ = Domain.objects.get_or_create(name="igorville5.gov", state=Domain.State.READY)
UserDomainRole.objects.get_or_create(user=self.user, domain=domain5, role=UserDomainRole.Roles.MANAGER)
self.assertEquals(self.user.get_approved_domains_count(), 4)
def test_active_requests_count(self):
"""Test that the correct active domain requests count is returned for a user"""
# with no associated active requests, expect this to return 0
self.assertEquals(self.user.get_active_requests_count(), 0)
# with one active request, expect this to return 1
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville1.gov")
DomainRequest.objects.create(
creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.SUBMITTED
)
self.assertEquals(self.user.get_active_requests_count(), 1)
# with two active requests, expect this to return 2
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville2.gov")
DomainRequest.objects.create(
creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.IN_REVIEW
)
self.assertEquals(self.user.get_active_requests_count(), 2)
# with three active requests, expect this to return 3
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville3.gov")
DomainRequest.objects.create(
creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.ACTION_NEEDED
)
self.assertEquals(self.user.get_active_requests_count(), 3)
# with three active requests, expect this to return 3 (STARTED is not considered active)
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville4.gov")
DomainRequest.objects.create(
creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.STARTED
)
self.assertEquals(self.user.get_active_requests_count(), 3)
def test_rejected_requests_count(self):
"""Test that the correct rejected domain requests count is returned for a user"""
# with no associated rejected requests, expect this to return 0
self.assertEquals(self.user.get_rejected_requests_count(), 0)
# with one rejected request, expect this to return 1
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville1.gov")
DomainRequest.objects.create(
creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.REJECTED
)
self.assertEquals(self.user.get_rejected_requests_count(), 1)
def test_ineligible_requests_count(self):
"""Test that the correct ineligible domain requests count is returned for a user"""
# with no associated ineligible requests, expect this to return 0
self.assertEquals(self.user.get_ineligible_requests_count(), 0)
# with one ineligible request, expect this to return 1
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville1.gov")
DomainRequest.objects.create(
creator=self.user, requested_domain=draft_domain, status=DomainRequest.DomainRequestStatus.INELIGIBLE
)
self.assertEquals(self.user.get_ineligible_requests_count(), 1)
def test_has_contact_info(self):
"""Test that has_contact_info properly returns"""
# test with a user with contact info defined
self.assertTrue(self.user.has_contact_info())
# test with a user without contact info defined
self.user.contact.title = None
self.user.contact.email = None
self.user.contact.phone = None
self.assertFalse(self.user.has_contact_info())
class TestContact(TestCase):
def setUp(self):
@ -1162,6 +1249,16 @@ class TestContact(TestCase):
self.assertFalse(self.contact_as_ao.has_more_than_one_join("authorizing_official"))
self.assertTrue(self.contact_as_ao.has_more_than_one_join("submitted_domain_requests"))
def test_has_contact_info(self):
"""Test that has_contact_info properly returns"""
# test with a contact with contact info defined
self.assertTrue(self.contact.has_contact_info())
# test with a contact without contact info defined
self.contact.title = None
self.contact.email = None
self.contact.phone = None
self.assertFalse(self.contact.has_contact_info())
class TestDomainRequestCustomSave(TestCase):
"""Tests custom save behaviour on the DomainRequest object"""

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.