mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-09 04:33:29 +02:00
Merge remote-tracking branch 'origin/main' into nl/2426-federal-agency-read-only-bug
This commit is contained in:
commit
2c0df91aaf
45 changed files with 2108 additions and 2395 deletions
|
@ -600,33 +600,6 @@ class ListHeaderAdmin(AuditedAdmin, OrderableFieldsMixin):
|
|||
return filters
|
||||
|
||||
|
||||
class UserContactInline(admin.StackedInline):
|
||||
"""Edit a user's profile on the user page."""
|
||||
|
||||
model = models.Contact
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
analyst_readonly_fields = [
|
||||
"user",
|
||||
"email",
|
||||
]
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Set the read-only state on form elements.
|
||||
We have 1 conditions that determine which fields are read-only:
|
||||
admin user permissions.
|
||||
"""
|
||||
|
||||
readonly_fields = list(self.readonly_fields)
|
||||
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields # Read-only fields for analysts
|
||||
|
||||
|
||||
class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||
"""Custom user admin class to use our inlines."""
|
||||
|
||||
|
@ -643,8 +616,6 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
|||
|
||||
_meta = Meta()
|
||||
|
||||
inlines = [UserContactInline]
|
||||
|
||||
list_display = (
|
||||
"username",
|
||||
"overridden_email_field",
|
||||
|
@ -922,30 +893,20 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
list_display = [
|
||||
"name",
|
||||
"email",
|
||||
"user_exists",
|
||||
]
|
||||
# this ordering effects the ordering of results
|
||||
# in autocomplete_fields for user
|
||||
# in autocomplete_fields
|
||||
ordering = ["first_name", "last_name", "email"]
|
||||
|
||||
fieldsets = [
|
||||
(
|
||||
None,
|
||||
{"fields": ["user", "first_name", "middle_name", "last_name", "title", "email", "phone"]},
|
||||
{"fields": ["first_name", "middle_name", "last_name", "title", "email", "phone"]},
|
||||
)
|
||||
]
|
||||
|
||||
autocomplete_fields = ["user"]
|
||||
|
||||
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.
|
||||
|
@ -963,10 +924,7 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
name.admin_order_field = "first_name" # type: ignore
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
analyst_readonly_fields = [
|
||||
"user",
|
||||
"email",
|
||||
]
|
||||
analyst_readonly_fields: list[str] = ["email"]
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Set the read-only state on form elements.
|
||||
|
|
|
@ -5,12 +5,3 @@ class RegistrarConfig(AppConfig):
|
|||
"""Configure signal handling for our registrar Django application."""
|
||||
|
||||
name = "registrar"
|
||||
|
||||
def ready(self):
|
||||
"""Runs when all Django applications have been loaded.
|
||||
|
||||
We use it here to load signals that connect related models.
|
||||
"""
|
||||
# noqa here because we are importing something to make the signals
|
||||
# get registered, but not using what we import
|
||||
from . import signals # noqa
|
||||
|
|
|
@ -660,6 +660,7 @@ ALLOWED_HOSTS = [
|
|||
"getgov-stable.app.cloud.gov",
|
||||
"getgov-staging.app.cloud.gov",
|
||||
"getgov-development.app.cloud.gov",
|
||||
"getgov-ms.app.cloud.gov",
|
||||
"getgov-ag.app.cloud.gov",
|
||||
"getgov-litterbox.app.cloud.gov",
|
||||
"getgov-hotgov.app.cloud.gov",
|
||||
|
|
|
@ -22,6 +22,11 @@ class UserFixture:
|
|||
"""
|
||||
|
||||
ADMINS = [
|
||||
{
|
||||
"username": "be17c826-e200-4999-9389-2ded48c43691",
|
||||
"first_name": "Matthew",
|
||||
"last_name": "Spence",
|
||||
},
|
||||
{
|
||||
"username": "5f283494-31bd-49b5-b024-a7e7cae00848",
|
||||
"first_name": "Rachid",
|
||||
|
@ -115,6 +120,11 @@ class UserFixture:
|
|||
]
|
||||
|
||||
STAFF = [
|
||||
{
|
||||
"username": "d6bf296b-fac5-47ff-9c12-f88ccc5c1b99",
|
||||
"first_name": "Matthew-Analyst",
|
||||
"last_name": "Spence-Analyst",
|
||||
},
|
||||
{
|
||||
"username": "319c490d-453b-43d9-bc4d-7d6cd8ff6844",
|
||||
"first_name": "Rachid-Analyst",
|
||||
|
|
|
@ -4,7 +4,7 @@ from .domain import (
|
|||
NameserverFormset,
|
||||
DomainSecurityEmailForm,
|
||||
DomainOrgNameAddressForm,
|
||||
ContactForm,
|
||||
UserForm,
|
||||
SeniorOfficialContactForm,
|
||||
DomainDnssecForm,
|
||||
DomainDsdataFormset,
|
||||
|
|
|
@ -16,7 +16,7 @@ from registrar.utility.errors import (
|
|||
SecurityEmailErrorCodes,
|
||||
)
|
||||
|
||||
from ..models import Contact, DomainInformation, Domain
|
||||
from ..models import Contact, DomainInformation, Domain, User
|
||||
from .common import (
|
||||
ALGORITHM_CHOICES,
|
||||
DIGEST_TYPE_CHOICES,
|
||||
|
@ -203,6 +203,63 @@ NameserverFormset = formset_factory(
|
|||
)
|
||||
|
||||
|
||||
class UserForm(forms.ModelForm):
|
||||
"""Form for updating users."""
|
||||
|
||||
email = forms.EmailField(max_length=None)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"]
|
||||
widgets = {
|
||||
"first_name": forms.TextInput,
|
||||
"middle_name": forms.TextInput,
|
||||
"last_name": forms.TextInput,
|
||||
"title": forms.TextInput,
|
||||
"email": forms.EmailInput,
|
||||
"phone": RegionalPhoneNumberWidget,
|
||||
}
|
||||
|
||||
# the database fields have blank=True so ModelForm doesn't create
|
||||
# required fields by default. Use this list in __init__ to mark each
|
||||
# of these fields as required
|
||||
required = ["first_name", "last_name", "title", "email", "phone"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# take off maxlength attribute for the phone number field
|
||||
# which interferes with out input_with_errors template tag
|
||||
self.fields["phone"].widget.attrs.pop("maxlength", None)
|
||||
|
||||
# Define a custom validator for the email field with a custom error message
|
||||
email_max_length_validator = MaxLengthValidator(320, message="Response must be less than 320 characters.")
|
||||
self.fields["email"].validators.append(email_max_length_validator)
|
||||
|
||||
for field_name in self.required:
|
||||
self.fields[field_name].required = True
|
||||
|
||||
# Set custom form label
|
||||
self.fields["middle_name"].label = "Middle name (optional)"
|
||||
|
||||
# Set custom error messages
|
||||
self.fields["first_name"].error_messages = {"required": "Enter your first name / given name."}
|
||||
self.fields["last_name"].error_messages = {"required": "Enter your last name / family name."}
|
||||
self.fields["title"].error_messages = {
|
||||
"required": "Enter your title or role in your organization (e.g., Chief Information Officer)"
|
||||
}
|
||||
self.fields["email"].error_messages = {
|
||||
"required": "Enter your email address in the required format, like name@example.com."
|
||||
}
|
||||
self.fields["phone"].error_messages["required"] = "Enter your phone number."
|
||||
self.domainInfo = None
|
||||
|
||||
def set_domain_info(self, domainInfo):
|
||||
"""Set the domain information for the form.
|
||||
The form instance is associated with the contact itself. In order to access the associated
|
||||
domain information object, this needs to be set in the form by the view."""
|
||||
self.domainInfo = domainInfo
|
||||
|
||||
|
||||
class ContactForm(forms.ModelForm):
|
||||
"""Form for updating contacts."""
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django import forms
|
||||
|
||||
from registrar.models.contact import Contact
|
||||
from registrar.models.user import User
|
||||
|
||||
from django.core.validators import MaxLengthValidator
|
||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||
|
@ -13,7 +13,7 @@ class UserProfileForm(forms.ModelForm):
|
|||
redirect = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Contact
|
||||
model = User
|
||||
fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"]
|
||||
widgets = {
|
||||
"first_name": forms.TextInput,
|
||||
|
|
|
@ -1,242 +0,0 @@
|
|||
import logging
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from registrar.management.commands.utility.terminal_helper import (
|
||||
TerminalColors,
|
||||
TerminalHelper,
|
||||
)
|
||||
from registrar.models.contact import Contact
|
||||
from registrar.models.user import User
|
||||
from registrar.models.utility.domain_helper import DomainHelper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """Copy first and last names from a contact to
|
||||
a related user if it exists and if its first and last name
|
||||
properties are null or blank strings."""
|
||||
|
||||
# ======================================================
|
||||
# ===================== ARGUMENTS =====================
|
||||
# ======================================================
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
|
||||
|
||||
# ======================================================
|
||||
# ===================== PRINTING ======================
|
||||
# ======================================================
|
||||
def print_debug_mode_statements(self, debug_on: bool):
|
||||
"""Prints additional terminal statements to indicate if --debug
|
||||
or --limitParse are in use"""
|
||||
TerminalHelper.print_conditional(
|
||||
debug_on,
|
||||
f"""{TerminalColors.OKCYAN}
|
||||
----------DEBUG MODE ON----------
|
||||
Detailed print statements activated.
|
||||
{TerminalColors.ENDC}
|
||||
""",
|
||||
)
|
||||
|
||||
def print_summary_of_findings(
|
||||
self,
|
||||
skipped_contacts,
|
||||
eligible_users,
|
||||
processed_users,
|
||||
debug_on,
|
||||
):
|
||||
"""Prints to terminal a summary of findings from
|
||||
copying first and last names from contacts to users"""
|
||||
|
||||
total_eligible_users = len(eligible_users)
|
||||
total_skipped_contacts = len(skipped_contacts)
|
||||
total_processed_users = len(processed_users)
|
||||
|
||||
logger.info(
|
||||
f"""{TerminalColors.OKGREEN}
|
||||
============= FINISHED ===============
|
||||
Skipped {total_skipped_contacts} contacts
|
||||
Found {total_eligible_users} users linked to contacts
|
||||
Processed {total_processed_users} users
|
||||
{TerminalColors.ENDC}
|
||||
""" # noqa
|
||||
)
|
||||
|
||||
# DEBUG:
|
||||
TerminalHelper.print_conditional(
|
||||
debug_on,
|
||||
f"""{TerminalColors.YELLOW}
|
||||
======= DEBUG OUTPUT =======
|
||||
Users who have a linked contact:
|
||||
{eligible_users}
|
||||
|
||||
Processed users (users who have a linked contact and a missing first or last name):
|
||||
{processed_users}
|
||||
|
||||
===== SKIPPED CONTACTS =====
|
||||
{skipped_contacts}
|
||||
|
||||
{TerminalColors.ENDC}
|
||||
""",
|
||||
)
|
||||
|
||||
# ======================================================
|
||||
# =================== USER =====================
|
||||
# ======================================================
|
||||
def update_user(self, contact: Contact, debug_on: bool):
|
||||
"""Given a contact with a first_name and last_name, find & update an existing
|
||||
corresponding user if her first_name and last_name are null.
|
||||
|
||||
Returns tuple of eligible (is linked to the contact) and processed
|
||||
(first and last are blank) users.
|
||||
"""
|
||||
|
||||
user_exists = User.objects.filter(contact=contact).exists()
|
||||
if user_exists:
|
||||
try:
|
||||
# ----------------------- UPDATE USER -----------------------
|
||||
# ---- GET THE USER
|
||||
eligible_user = User.objects.get(contact=contact)
|
||||
processed_user = None
|
||||
# DEBUG:
|
||||
TerminalHelper.print_conditional(
|
||||
debug_on,
|
||||
f"""{TerminalColors.YELLOW}
|
||||
> Found linked user for contact:
|
||||
{contact} {contact.email} {contact.first_name} {contact.last_name}
|
||||
> The linked user is {eligible_user} {eligible_user.username}
|
||||
{TerminalColors.ENDC}""", # noqa
|
||||
)
|
||||
|
||||
# Get the fields that exist on both User and Contact. Excludes id.
|
||||
common_fields = DomainHelper.get_common_fields(User, Contact)
|
||||
if "email" in common_fields:
|
||||
# Don't change the email field.
|
||||
common_fields.remove("email")
|
||||
|
||||
for field in common_fields:
|
||||
# Grab the value that contact has stored for this field
|
||||
new_value = getattr(contact, field)
|
||||
|
||||
# Set it on the user field
|
||||
setattr(eligible_user, field, new_value)
|
||||
|
||||
eligible_user.save()
|
||||
processed_user = eligible_user
|
||||
|
||||
return (
|
||||
eligible_user,
|
||||
processed_user,
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
f"""
|
||||
{TerminalColors.FAIL}
|
||||
!!! ERROR: An exception occured in the
|
||||
User table for the following user:
|
||||
{contact.email} {contact.first_name} {contact.last_name}
|
||||
|
||||
Exception is: {error}
|
||||
----------TERMINATING----------"""
|
||||
)
|
||||
sys.exit()
|
||||
else:
|
||||
return None, None
|
||||
|
||||
# ======================================================
|
||||
# ================= PROCESS CONTACTS ==================
|
||||
# ======================================================
|
||||
|
||||
def process_contacts(
|
||||
self,
|
||||
debug_on,
|
||||
skipped_contacts=[],
|
||||
eligible_users=[],
|
||||
processed_users=[],
|
||||
):
|
||||
for contact in Contact.objects.all():
|
||||
TerminalHelper.print_conditional(
|
||||
debug_on,
|
||||
f"{TerminalColors.OKCYAN}"
|
||||
"Processing Contact: "
|
||||
f"{contact.email},"
|
||||
f" {contact.first_name},"
|
||||
f" {contact.last_name}"
|
||||
f"{TerminalColors.ENDC}",
|
||||
)
|
||||
|
||||
# ======================================================
|
||||
# ====================== USER =======================
|
||||
(eligible_user, processed_user) = self.update_user(contact, debug_on)
|
||||
|
||||
debug_string = ""
|
||||
if eligible_user:
|
||||
# ---------------- UPDATED ----------------
|
||||
eligible_users.append(contact.email)
|
||||
debug_string = f"eligible user: {eligible_user}"
|
||||
if processed_user:
|
||||
processed_users.append(contact.email)
|
||||
debug_string = f"processed user: {processed_user}"
|
||||
else:
|
||||
skipped_contacts.append(contact.email)
|
||||
debug_string = f"skipped user: {contact.email}"
|
||||
|
||||
# DEBUG:
|
||||
TerminalHelper.print_conditional(
|
||||
debug_on,
|
||||
(f"{TerminalColors.OKCYAN} {debug_string} {TerminalColors.ENDC}"),
|
||||
)
|
||||
|
||||
return (
|
||||
skipped_contacts,
|
||||
eligible_users,
|
||||
processed_users,
|
||||
)
|
||||
|
||||
# ======================================================
|
||||
# ===================== HANDLE ========================
|
||||
# ======================================================
|
||||
def handle(
|
||||
self,
|
||||
**options,
|
||||
):
|
||||
"""Parse entries in Contact table
|
||||
and update valid corresponding entries in the
|
||||
User table."""
|
||||
|
||||
# grab command line arguments and store locally...
|
||||
debug_on = options.get("debug")
|
||||
|
||||
self.print_debug_mode_statements(debug_on)
|
||||
|
||||
logger.info(
|
||||
f"""{TerminalColors.OKCYAN}
|
||||
==========================
|
||||
Beginning Data Transfer
|
||||
==========================
|
||||
{TerminalColors.ENDC}"""
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"""{TerminalColors.OKCYAN}
|
||||
========= Adding Domains and Domain Invitations =========
|
||||
{TerminalColors.ENDC}"""
|
||||
)
|
||||
(
|
||||
skipped_contacts,
|
||||
eligible_users,
|
||||
processed_users,
|
||||
) = self.process_contacts(
|
||||
debug_on,
|
||||
)
|
||||
|
||||
self.print_summary_of_findings(
|
||||
skipped_contacts,
|
||||
eligible_users,
|
||||
processed_users,
|
||||
debug_on,
|
||||
)
|
|
@ -50,7 +50,7 @@ class Command(BaseCommand):
|
|||
|
||||
# Generate a file locally for upload
|
||||
with open(file_path, "w") as file:
|
||||
csv_export.export_data_federal_to_csv(file)
|
||||
csv_export.DomainDataFederal.export_data_to_csv(file)
|
||||
|
||||
if check_path and not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"Could not find newly created file at '{file_path}'")
|
||||
|
|
|
@ -49,7 +49,7 @@ class Command(BaseCommand):
|
|||
|
||||
# Generate a file locally for upload
|
||||
with open(file_path, "w") as file:
|
||||
csv_export.export_data_full_to_csv(file)
|
||||
csv_export.DomainDataFull.export_data_to_csv(file)
|
||||
|
||||
if check_path and not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"Could not find newly created file at '{file_path}'")
|
||||
|
|
|
@ -65,13 +65,6 @@ class Command(BaseCommand):
|
|||
|
||||
resourcename = f"{table_name}Resource"
|
||||
|
||||
# if table_name is Contact, clean the table first
|
||||
# User table is loaded before Contact, and signals create
|
||||
# rows in Contact table which break the import, so need
|
||||
# to be cleaned again before running import on Contact table
|
||||
if table_name == "Contact":
|
||||
self.clean_table(table_name)
|
||||
|
||||
# Define the directory and the pattern for csv filenames
|
||||
tmp_dir = "tmp"
|
||||
pattern = f"{table_name}_"
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import argparse
|
||||
import csv
|
||||
import logging
|
||||
import os
|
||||
from django.core.management import BaseCommand
|
||||
from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors
|
||||
from registrar.models import DomainInformation
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand, PopulateScriptTemplate):
|
||||
"""
|
||||
This command uses the PopulateScriptTemplate,
|
||||
which provides reusable logging and bulk updating functions for mass-updating fields.
|
||||
"""
|
||||
|
||||
help = "Loops through each valid DomainInformation object and updates its Senior Official"
|
||||
prompt_title = "Do you wish to update all Senior Officials for Domain Information?"
|
||||
|
||||
def handle(self, domain_info_csv_path, **kwargs):
|
||||
"""Loops through each valid DomainInformation object and updates its senior official field"""
|
||||
|
||||
# Check if the provided file path is valid.
|
||||
if not os.path.isfile(domain_info_csv_path):
|
||||
raise argparse.ArgumentTypeError(f"Invalid file path '{domain_info_csv_path}'")
|
||||
|
||||
# Simple check to make sure we don't accidentally pass in the wrong file. Crude but it works.
|
||||
if "information" not in domain_info_csv_path.lower():
|
||||
raise argparse.ArgumentTypeError(f"Invalid file for domain information: '{domain_info_csv_path}'")
|
||||
|
||||
# Get all ao data.
|
||||
self.ao_dict = {}
|
||||
self.ao_dict = self.read_csv_file_and_get_contacts(domain_info_csv_path)
|
||||
|
||||
self.mass_update_records(
|
||||
DomainInformation, filter_conditions={"senior_official__isnull": True}, fields_to_update=["senior_official"]
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add command line arguments."""
|
||||
parser.add_argument(
|
||||
"--domain_info_csv_path", help="A csv containing the domain information id and the contact id"
|
||||
)
|
||||
|
||||
def read_csv_file_and_get_contacts(self, file):
|
||||
dict_data = {}
|
||||
with open(file, "r") as requested_file:
|
||||
reader = csv.DictReader(requested_file)
|
||||
for row in reader:
|
||||
domain_info_id = row.get("id")
|
||||
ao_id = row.get("authorizing_official")
|
||||
if ao_id:
|
||||
ao_id = int(ao_id)
|
||||
if domain_info_id and ao_id:
|
||||
dict_data[int(domain_info_id)] = ao_id
|
||||
|
||||
return dict_data
|
||||
|
||||
def update_record(self, record: DomainInformation):
|
||||
"""Defines how we update the senior official field on each record."""
|
||||
record.senior_official_id = self.ao_dict.get(record.id)
|
||||
logger.info(f"{TerminalColors.OKCYAN}Updating {str(record)} => {record.senior_official}{TerminalColors.ENDC}")
|
||||
|
||||
def should_skip_record(self, record) -> bool: # noqa
|
||||
"""Defines the conditions in which we should skip updating a record."""
|
||||
# Don't update this record if there isn't ao data to pull from
|
||||
if self.ao_dict.get(record.id) is None:
|
||||
logger.info(
|
||||
f"{TerminalColors.YELLOW}Skipping update for {str(record)} => "
|
||||
f"Missing authorizing_official data.{TerminalColors.ENDC}"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
return False
|
|
@ -0,0 +1,81 @@
|
|||
import argparse
|
||||
import csv
|
||||
import logging
|
||||
import os
|
||||
from django.core.management import BaseCommand
|
||||
from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors
|
||||
from registrar.models import DomainRequest
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand, PopulateScriptTemplate):
|
||||
"""
|
||||
This command uses the PopulateScriptTemplate,
|
||||
which provides reusable logging and bulk updating functions for mass-updating fields.
|
||||
"""
|
||||
|
||||
help = """Loops through each valid DomainRequest object and updates its senior official field"""
|
||||
prompt_title = "Do you wish to update all Senior Officials for Domain Requests?"
|
||||
|
||||
def handle(self, domain_request_csv_path, **kwargs):
|
||||
"""Loops through each valid DomainRequest object and updates its senior official field"""
|
||||
|
||||
# Check if the provided file path is valid.
|
||||
if not os.path.isfile(domain_request_csv_path):
|
||||
raise argparse.ArgumentTypeError(f"Invalid file path '{domain_request_csv_path}'")
|
||||
|
||||
# Simple check to make sure we don't accidentally pass in the wrong file. Crude but it works.
|
||||
if "request" not in domain_request_csv_path.lower():
|
||||
raise argparse.ArgumentTypeError(f"Invalid file for domain requests: '{domain_request_csv_path}'")
|
||||
|
||||
# Get all ao data.
|
||||
self.ao_dict = {}
|
||||
self.ao_dict = self.read_csv_file_and_get_contacts(domain_request_csv_path)
|
||||
|
||||
self.mass_update_records(
|
||||
DomainRequest,
|
||||
filter_conditions={
|
||||
"senior_official__isnull": True,
|
||||
},
|
||||
fields_to_update=["senior_official"],
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add command line arguments."""
|
||||
parser.add_argument(
|
||||
"--domain_request_csv_path", help="A csv containing the domain request id and the contact id"
|
||||
)
|
||||
|
||||
def read_csv_file_and_get_contacts(self, file):
|
||||
dict_data: dict = {}
|
||||
with open(file, "r") as requested_file:
|
||||
reader = csv.DictReader(requested_file)
|
||||
for row in reader:
|
||||
domain_request_id = row.get("id")
|
||||
ao_id = row.get("authorizing_official")
|
||||
if ao_id:
|
||||
ao_id = int(ao_id)
|
||||
if domain_request_id and ao_id:
|
||||
dict_data[int(domain_request_id)] = ao_id
|
||||
|
||||
return dict_data
|
||||
|
||||
def update_record(self, record: DomainRequest):
|
||||
"""Defines how we update the federal_type field on each record."""
|
||||
record.senior_official_id = self.ao_dict.get(record.id)
|
||||
# record.senior_official = Contact.objects.get(id=contact_id)
|
||||
logger.info(f"{TerminalColors.OKCYAN}Updating {str(record)} => {record.senior_official}{TerminalColors.ENDC}")
|
||||
|
||||
def should_skip_record(self, record) -> bool: # noqa
|
||||
"""Defines the conditions in which we should skip updating a record."""
|
||||
# Don't update this record if there isn't ao data to pull from
|
||||
if self.ao_dict.get(record.id) is None:
|
||||
logger.info(
|
||||
f"{TerminalColors.YELLOW}Skipping update for {str(record)} => "
|
||||
f"Missing authorizing_official data.{TerminalColors.ENDC}"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
return False
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 4.2.10 on 2024-07-02 19:52
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0111_create_groups_v15"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveIndex(
|
||||
model_name="contact",
|
||||
name="registrar_c_user_id_4059c4_idx",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="contact",
|
||||
name="user",
|
||||
),
|
||||
]
|
|
@ -8,30 +8,15 @@ from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
|||
class Contact(TimeStampedModel):
|
||||
"""
|
||||
Contact information follows a similar pattern for each contact.
|
||||
|
||||
This model uses signals [as defined in [signals.py](../../src/registrar/signals.py)].
|
||||
When a new user is created through Login.gov, a contact object will be created and
|
||||
associated on the `user` field.
|
||||
|
||||
If the `user` object already exists, the underlying user object
|
||||
will be updated if any updates are made to it through Login.gov.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Contains meta information about this class"""
|
||||
|
||||
indexes = [
|
||||
models.Index(fields=["user"]),
|
||||
models.Index(fields=["email"]),
|
||||
]
|
||||
|
||||
user = models.OneToOneField(
|
||||
"registrar.User",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
first_name = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
|
@ -110,38 +95,6 @@ class Contact(TimeStampedModel):
|
|||
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)
|
||||
|
||||
if self.user:
|
||||
updated = False
|
||||
|
||||
# Update first name and last name if necessary
|
||||
if not self.user.first_name or not self.user.last_name:
|
||||
self.user.first_name = self.first_name
|
||||
self.user.last_name = self.last_name
|
||||
updated = True
|
||||
|
||||
# Update middle_name if necessary
|
||||
if not self.user.middle_name:
|
||||
self.user.middle_name = self.middle_name
|
||||
updated = True
|
||||
|
||||
# Update phone if necessary
|
||||
if not self.user.phone:
|
||||
self.user.phone = self.phone
|
||||
updated = True
|
||||
|
||||
# Update title if necessary
|
||||
if not self.user.title:
|
||||
self.user.title = self.title
|
||||
updated = True
|
||||
|
||||
# Save user if any updates were made
|
||||
if updated:
|
||||
self.user.save()
|
||||
|
||||
def __str__(self):
|
||||
if self.first_name or self.last_name:
|
||||
return self.get_formatted_name()
|
||||
|
|
|
@ -151,6 +151,11 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
# previously existed but has been deleted from the registry
|
||||
DELETED = "deleted", "Deleted"
|
||||
|
||||
@classmethod
|
||||
def get_state_label(cls, state: str):
|
||||
"""Returns the associated label for a given state value"""
|
||||
return cls(state).label if state else None
|
||||
|
||||
@classmethod
|
||||
def get_help_text(cls, state) -> str:
|
||||
"""Returns a help message for a desired state. If none is found, an empty string is returned"""
|
||||
|
|
|
@ -136,6 +136,13 @@ class DomainRequest(TimeStampedModel):
|
|||
@classmethod
|
||||
def get_org_label(cls, org_name: str):
|
||||
"""Returns the associated label for a given org name"""
|
||||
# This is an edgecase on domains with no org.
|
||||
# This unlikely to happen but
|
||||
# a break will occur in certain edge cases without this.
|
||||
# (more specifically, csv exports).
|
||||
if not org_name:
|
||||
return None
|
||||
|
||||
org_names = org_name.split("_election")
|
||||
if len(org_names) > 0:
|
||||
org_name = org_names[0]
|
||||
|
|
|
@ -23,10 +23,6 @@ class User(AbstractUser):
|
|||
A custom user model that performs identically to the default user model
|
||||
but can be customized later.
|
||||
|
||||
This model uses signals [as defined in [signals.py](../../src/registrar/signals.py)].
|
||||
When a new user is created through Login.gov, a contact object will be created and
|
||||
associated on the contacts `user` field.
|
||||
|
||||
If the `user` object already exists, said user object
|
||||
will be updated if any updates are made to it through Login.gov.
|
||||
"""
|
||||
|
@ -113,15 +109,11 @@ class User(AbstractUser):
|
|||
Tracks if the user finished their profile setup or not. This is so
|
||||
we can globally enforce that new users provide additional account information before proceeding.
|
||||
"""
|
||||
|
||||
# Change this to self once the user and contact objects are merged.
|
||||
# For now, since they are linked, lets test on the underlying contact object.
|
||||
user_info = self.contact # noqa
|
||||
user_values = [
|
||||
user_info.first_name,
|
||||
user_info.last_name,
|
||||
user_info.title,
|
||||
user_info.phone,
|
||||
self.first_name,
|
||||
self.last_name,
|
||||
self.title,
|
||||
self.phone,
|
||||
]
|
||||
return None not in user_values
|
||||
|
||||
|
@ -169,8 +161,13 @@ class User(AbstractUser):
|
|||
"""Return count of ineligible requests"""
|
||||
return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.INELIGIBLE).count()
|
||||
|
||||
def get_formatted_name(self):
|
||||
"""Returns the contact's name in Western order."""
|
||||
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.contact.title or self.contact.email or self.contact.phone)
|
||||
return bool(self.title or self.email or self.phone)
|
||||
|
||||
@classmethod
|
||||
def needs_identity_verification(cls, email, uuid):
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
import logging
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .models import User, Contact
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def handle_profile(sender, instance, **kwargs):
|
||||
"""Method for when a User is saved.
|
||||
|
||||
A first time registrant may have been invited, so we'll search for a matching
|
||||
Contact record, by email address, and associate them, if possible.
|
||||
|
||||
A first time registrant may not have a matching Contact, so we'll create one,
|
||||
copying the contact values we received from Login.gov in order to initialize it.
|
||||
|
||||
During subsequent login, a User record may be updated with new data from Login.gov,
|
||||
but in no case will we update contact values on an existing Contact record.
|
||||
"""
|
||||
|
||||
first_name = getattr(instance, "first_name", "")
|
||||
middle_name = getattr(instance, "middle_name", "")
|
||||
last_name = getattr(instance, "last_name", "")
|
||||
email = getattr(instance, "email", "")
|
||||
phone = getattr(instance, "phone", "")
|
||||
title = getattr(instance, "title", "")
|
||||
|
||||
is_new_user = kwargs.get("created", False)
|
||||
|
||||
if is_new_user:
|
||||
contacts = Contact.objects.filter(email=email)
|
||||
else:
|
||||
contacts = Contact.objects.filter(user=instance)
|
||||
|
||||
if len(contacts) == 0: # no matching contact
|
||||
Contact.objects.create(
|
||||
user=instance,
|
||||
first_name=first_name,
|
||||
middle_name=middle_name,
|
||||
last_name=last_name,
|
||||
email=email,
|
||||
phone=phone,
|
||||
title=title,
|
||||
)
|
||||
|
||||
if len(contacts) >= 1 and is_new_user: # a matching contact
|
||||
contacts[0].user = instance
|
||||
contacts[0].save()
|
||||
|
||||
if len(contacts) > 1: # multiple matches
|
||||
logger.warning(
|
||||
"There are multiple Contacts with the same email address."
|
||||
f" Picking #{contacts[0].id} for User #{instance.id}."
|
||||
)
|
|
@ -12,37 +12,24 @@
|
|||
|
||||
{% if user.has_contact_info %}
|
||||
{# Title #}
|
||||
{% if user.title or user.contact.title %}
|
||||
{% if user.contact.title %}
|
||||
{{ user.contact.title }}
|
||||
{% else %}
|
||||
{{ user.title }}
|
||||
{% endif %}
|
||||
{% if user.title %}
|
||||
{{ user.title }}
|
||||
<br>
|
||||
{% else %}
|
||||
None<br>
|
||||
{% endif %}
|
||||
{# Email #}
|
||||
{% if user.email or user.contact.email %}
|
||||
{% if user.contact.email %}
|
||||
{{ user.contact.email }}
|
||||
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
||||
{% else %}
|
||||
{{ user.email }}
|
||||
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
||||
{% endif %}
|
||||
{% if user.email %}
|
||||
{{ user.email }}
|
||||
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
||||
<br class="admin-icon-group__br">
|
||||
{% else %}
|
||||
None<br>
|
||||
{% endif %}
|
||||
|
||||
{# Phone #}
|
||||
{% if user.phone or user.contact.phone %}
|
||||
{% if user.contact.phone %}
|
||||
{{ user.contact.phone }}
|
||||
{% else %}
|
||||
{{ user.phone }}
|
||||
{% endif %}
|
||||
{% if user.phone %}
|
||||
{{ user.phone }}
|
||||
<br>
|
||||
{% else %}
|
||||
None<br>
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
{# Conditionally display profile #}
|
||||
{% if not has_profile_feature_flag %}
|
||||
{% url 'domain-your-contact-information' pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=request.user.contact contact='true' edit_link=url editable=domain.is_editable %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=request.user contact='true' edit_link=url editable=domain.is_editable %}
|
||||
{% endif %}
|
||||
|
||||
{% url 'domain-security-email' pk=domain.id as url %}
|
||||
|
|
|
@ -77,11 +77,11 @@
|
|||
</fieldset>
|
||||
<div>
|
||||
|
||||
<button type="submit" name="contact_setup_save_button" class="usa-button ">
|
||||
<button type="submit" name="user_setup_save_button" class="usa-button ">
|
||||
Save
|
||||
</button>
|
||||
{% if user_finished_setup and going_to_specific_page %}
|
||||
<button type="submit" name="contact_setup_submit_button" class="usa-button usa-button--outline">
|
||||
<button type="submit" name="user_setup_submit_button" class="usa-button usa-button--outline">
|
||||
{{redirect_button_text }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
</h3>
|
||||
<div class="usa-summary-box__text">
|
||||
<ul>
|
||||
<li>Full name: <b>{{ user.contact.get_formatted_name }}</b></li>
|
||||
<li>Full name: <b>{{ user.get_formatted_name }}</b></li>
|
||||
<li>Organization email: <b>{{ user.email }}</b></li>
|
||||
<li>Title or role in your organization: <b>{{ user.contact.title }}</b></li>
|
||||
<li>Phone: <b>{{ user.contact.phone.as_national }}</b></li>
|
||||
<li>Title or role in your organization: <b>{{ user.title }}</b></li>
|
||||
<li>Phone: <b>{{ user.phone.as_national }}</b></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -810,6 +810,8 @@ def create_superuser():
|
|||
user = User.objects.create_user(
|
||||
username="superuser",
|
||||
email="admin@example.com",
|
||||
first_name="first",
|
||||
last_name="last",
|
||||
is_staff=True,
|
||||
password=p,
|
||||
)
|
||||
|
@ -826,6 +828,8 @@ def create_user():
|
|||
user = User.objects.create_user(
|
||||
username="staffuser",
|
||||
email="staff@example.com",
|
||||
first_name="first",
|
||||
last_name="last",
|
||||
is_staff=True,
|
||||
password=p,
|
||||
)
|
||||
|
|
|
@ -243,15 +243,11 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
|||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Due to the relation between User <==> Contact,
|
||||
# the underlying contact has to be modified this way.
|
||||
_creator.contact.email = "meoward.jones@igorville.gov"
|
||||
_creator.contact.phone = "(555) 123 12345"
|
||||
_creator.contact.title = "Treat inspector"
|
||||
_creator.contact.save()
|
||||
|
||||
# Create a fake domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
domain_request.approve()
|
||||
|
@ -943,7 +939,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
SeniorOfficial.objects.get_or_create(first_name="alex", last_name="smoe", title="some guy")
|
||||
SeniorOfficial.objects.get_or_create(first_name="Zoup", last_name="Soup", title="title")
|
||||
|
||||
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
||||
contact, _ = Contact.objects.get_or_create(first_name="Henry", last_name="McFakerson")
|
||||
domain_request = completed_domain_request(submitter=contact, name="city1.gov")
|
||||
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
|
||||
model_admin = AuditedAdmin(DomainRequest, self.site)
|
||||
|
@ -2094,15 +2090,11 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Due to the relation between User <==> Contact,
|
||||
# the underlying contact has to be modified this way.
|
||||
_creator.contact.email = "meoward.jones@igorville.gov"
|
||||
_creator.contact.phone = "(555) 123 12345"
|
||||
_creator.contact.title = "Treat inspector"
|
||||
_creator.contact.save()
|
||||
|
||||
# Create a fake domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
|
||||
|
@ -2119,11 +2111,11 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
|
||||
# == Check for the creator == #
|
||||
|
||||
# Check for the right title, email, and phone number in the response.
|
||||
# Check for the right title and phone number in the response.
|
||||
# Email will appear more than once
|
||||
expected_creator_fields = [
|
||||
# Field, expected value
|
||||
("title", "Treat inspector"),
|
||||
("email", "meoward.jones@igorville.gov"),
|
||||
("phone", "(555) 123 12345"),
|
||||
]
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_creator_fields)
|
||||
|
@ -2140,6 +2132,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
]
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields)
|
||||
self.assertContains(response, "Testy2 Tester2")
|
||||
self.assertContains(response, "meoward.jones@igorville.gov")
|
||||
|
||||
# == Check for the senior_official == #
|
||||
self.assertContains(response, "testy@town.com", count=2)
|
||||
|
@ -2948,7 +2941,7 @@ class TestDomainInformationAdmin(TestCase):
|
|||
SeniorOfficial.objects.get_or_create(first_name="alex", last_name="smoe", title="some guy")
|
||||
SeniorOfficial.objects.get_or_create(first_name="Zoup", last_name="Soup", title="title")
|
||||
|
||||
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
||||
contact, _ = Contact.objects.get_or_create(first_name="Henry", last_name="McFakerson")
|
||||
domain_request = completed_domain_request(
|
||||
submitter=contact, name="city1244.gov", status=DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||
)
|
||||
|
@ -3167,15 +3160,11 @@ class TestDomainInformationAdmin(TestCase):
|
|||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Due to the relation between User <==> Contact,
|
||||
# the underlying contact has to be modified this way.
|
||||
_creator.contact.email = "meoward.jones@igorville.gov"
|
||||
_creator.contact.phone = "(555) 123 12345"
|
||||
_creator.contact.title = "Treat inspector"
|
||||
_creator.contact.save()
|
||||
|
||||
# Create a fake domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
domain_request.approve()
|
||||
|
@ -3197,16 +3186,16 @@ class TestDomainInformationAdmin(TestCase):
|
|||
|
||||
# == Check for the creator == #
|
||||
|
||||
# Check for the right title, email, and phone number in the response.
|
||||
# Check for the right title and phone number in the response.
|
||||
# We only need to check for the end tag
|
||||
# (Otherwise this test will fail if we change classes, etc)
|
||||
expected_creator_fields = [
|
||||
# Field, expected value
|
||||
("title", "Treat inspector"),
|
||||
("email", "meoward.jones@igorville.gov"),
|
||||
("phone", "(555) 123 12345"),
|
||||
]
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_creator_fields)
|
||||
self.assertContains(response, "meoward.jones@igorville.gov")
|
||||
|
||||
# Check for the field itself
|
||||
self.assertContains(response, "Meoward Jones")
|
||||
|
@ -4118,7 +4107,7 @@ class TestContactAdmin(TestCase):
|
|||
|
||||
readonly_fields = self.admin.get_readonly_fields(request)
|
||||
|
||||
expected_fields = ["user", "email"]
|
||||
expected_fields = ["email"]
|
||||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
|
@ -4134,15 +4123,18 @@ class TestContactAdmin(TestCase):
|
|||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def test_change_view_for_joined_contact_five_or_less(self):
|
||||
"""Create a contact, join it to 4 domain requests. The 5th join will be a user.
|
||||
Assert that the warning on the contact form lists 5 joins."""
|
||||
"""Create a contact, join it to 4 domain requests.
|
||||
Assert that the warning on the contact form lists 4 joins."""
|
||||
with less_console_noise():
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
# Create an instance of the model
|
||||
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
||||
contact, _ = Contact.objects.get_or_create(
|
||||
first_name="Henry",
|
||||
last_name="McFakerson",
|
||||
)
|
||||
|
||||
# join it to 4 domain requests. The 5th join will be a user.
|
||||
# join it to 4 domain requests.
|
||||
domain_request1 = completed_domain_request(submitter=contact, name="city1.gov")
|
||||
domain_request2 = completed_domain_request(submitter=contact, name="city2.gov")
|
||||
domain_request3 = completed_domain_request(submitter=contact, name="city3.gov")
|
||||
|
@ -4165,24 +4157,26 @@ class TestContactAdmin(TestCase):
|
|||
f"domainrequest/{domain_request3.pk}/change/'>city3.gov</a></li>"
|
||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
||||
f"domainrequest/{domain_request4.pk}/change/'>city4.gov</a></li>"
|
||||
"<li>Joined to User: <a href='/admin/registrar/"
|
||||
f"user/{self.staffuser.pk}/change/'>staff@example.com</a></li>"
|
||||
"</ul>",
|
||||
)
|
||||
|
||||
def test_change_view_for_joined_contact_five_or_more(self):
|
||||
"""Create a contact, join it to 5 domain requests. The 6th join will be a user.
|
||||
"""Create a contact, join it to 6 domain requests.
|
||||
Assert that the warning on the contact form lists 5 joins and a '1 more' ellispsis."""
|
||||
with less_console_noise():
|
||||
self.client.force_login(self.superuser)
|
||||
# Create an instance of the model
|
||||
# join it to 5 domain requests. The 6th join will be a user.
|
||||
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
||||
# join it to 6 domain requests.
|
||||
contact, _ = Contact.objects.get_or_create(
|
||||
first_name="Henry",
|
||||
last_name="McFakerson",
|
||||
)
|
||||
domain_request1 = completed_domain_request(submitter=contact, name="city1.gov")
|
||||
domain_request2 = completed_domain_request(submitter=contact, name="city2.gov")
|
||||
domain_request3 = completed_domain_request(submitter=contact, name="city3.gov")
|
||||
domain_request4 = completed_domain_request(submitter=contact, name="city4.gov")
|
||||
domain_request5 = completed_domain_request(submitter=contact, name="city5.gov")
|
||||
completed_domain_request(submitter=contact, name="city6.gov")
|
||||
with patch("django.contrib.messages.warning") as mock_warning:
|
||||
# Use the test client to simulate the request
|
||||
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
from registrar.models import (
|
||||
User,
|
||||
Contact,
|
||||
)
|
||||
|
||||
from registrar.management.commands.copy_names_from_contacts_to_users import Command
|
||||
|
||||
|
||||
class TestDataUpdates(TestCase):
|
||||
def setUp(self):
|
||||
"""We cannot setup the user details because contacts will override the first and last names in its save method
|
||||
so we will initiate the users, setup the contacts and link them, and leave the rest of the setup to the test(s).
|
||||
"""
|
||||
|
||||
self.user1 = User.objects.create(username="user1")
|
||||
self.user2 = User.objects.create(username="user2")
|
||||
self.user3 = User.objects.create(username="user3")
|
||||
self.user4 = User.objects.create(username="user4")
|
||||
# The last user created triggers the creation of a contact and attaches itself to it. See signals.
|
||||
# This bs_user defuses that situation.
|
||||
self.bs_user = User.objects.create()
|
||||
|
||||
self.contact1 = Contact.objects.create(
|
||||
user=self.user1,
|
||||
email="email1@igorville.gov",
|
||||
first_name="first1",
|
||||
last_name="last1",
|
||||
middle_name="middle1",
|
||||
title="title1",
|
||||
)
|
||||
self.contact2 = Contact.objects.create(
|
||||
user=self.user2,
|
||||
email="email2@igorville.gov",
|
||||
first_name="first2",
|
||||
last_name="last2",
|
||||
middle_name="middle2",
|
||||
title="title2",
|
||||
)
|
||||
self.contact3 = Contact.objects.create(
|
||||
user=self.user3,
|
||||
email="email3@igorville.gov",
|
||||
first_name="first3",
|
||||
last_name="last3",
|
||||
middle_name="middle3",
|
||||
title="title3",
|
||||
)
|
||||
self.contact4 = Contact.objects.create(
|
||||
email="email4@igorville.gov", first_name="first4", last_name="last4", middle_name="middle4", title="title4"
|
||||
)
|
||||
|
||||
self.command = Command()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up"""
|
||||
# Delete users and contacts
|
||||
User.objects.all().delete()
|
||||
Contact.objects.all().delete()
|
||||
|
||||
def test_script_updates_linked_users(self):
|
||||
"""Test the script that copies contact information to the user object"""
|
||||
|
||||
# Set up the users' first and last names here so
|
||||
# they that they don't get overwritten by Contact's save()
|
||||
# User with no first or last names
|
||||
self.user1.first_name = ""
|
||||
self.user1.last_name = ""
|
||||
self.user1.title = "dummytitle"
|
||||
self.user1.middle_name = "dummymiddle"
|
||||
self.user1.save()
|
||||
|
||||
# User with a first name but no last name
|
||||
self.user2.first_name = "First name but no last name"
|
||||
self.user2.last_name = ""
|
||||
self.user2.save()
|
||||
|
||||
# User with a first and last name
|
||||
self.user3.first_name = "An existing first name"
|
||||
self.user3.last_name = "An existing last name"
|
||||
self.user3.save()
|
||||
|
||||
# Call the parent method the same way we do it in the script
|
||||
skipped_contacts = []
|
||||
eligible_users = []
|
||||
processed_users = []
|
||||
(
|
||||
skipped_contacts,
|
||||
eligible_users,
|
||||
processed_users,
|
||||
) = self.command.process_contacts(
|
||||
# Set debugging to False
|
||||
False,
|
||||
skipped_contacts,
|
||||
eligible_users,
|
||||
processed_users,
|
||||
)
|
||||
|
||||
# Trigger DB refresh
|
||||
self.user1.refresh_from_db()
|
||||
self.user2.refresh_from_db()
|
||||
self.user3.refresh_from_db()
|
||||
|
||||
# Asserts
|
||||
# The user that has no first and last names will get them from the contact
|
||||
self.assertEqual(self.user1.first_name, "first1")
|
||||
self.assertEqual(self.user1.last_name, "last1")
|
||||
self.assertEqual(self.user1.middle_name, "middle1")
|
||||
self.assertEqual(self.user1.title, "title1")
|
||||
# The user that has a first but no last will be updated
|
||||
self.assertEqual(self.user2.first_name, "first2")
|
||||
self.assertEqual(self.user2.last_name, "last2")
|
||||
self.assertEqual(self.user2.middle_name, "middle2")
|
||||
self.assertEqual(self.user2.title, "title2")
|
||||
# The user that has a first and a last will be updated
|
||||
self.assertEqual(self.user3.first_name, "first3")
|
||||
self.assertEqual(self.user3.last_name, "last3")
|
||||
self.assertEqual(self.user3.middle_name, "middle3")
|
||||
self.assertEqual(self.user3.title, "title3")
|
||||
# The unlinked user will be left alone
|
||||
self.assertEqual(self.user4.first_name, "")
|
||||
self.assertEqual(self.user4.last_name, "")
|
||||
self.assertEqual(self.user4.middle_name, None)
|
||||
self.assertEqual(self.user4.title, None)
|
|
@ -1077,11 +1077,6 @@ class TestImportTables(TestCase):
|
|||
for table_name in table_names:
|
||||
mock_path_exists.assert_any_call(f"{table_name}_1.csv")
|
||||
|
||||
# Check that clean_tables is called for Contact
|
||||
mock_get_model.assert_any_call("registrar", "Contact")
|
||||
model_mock = mock_get_model.return_value
|
||||
model_mock.objects.all().delete.assert_called()
|
||||
|
||||
# Check that logger.info was called for each successful import
|
||||
for table_name in table_names:
|
||||
mock_logger.info.assert_any_call(f"Successfully imported {table_name}_1.csv into {table_name}")
|
||||
|
|
|
@ -1209,29 +1209,22 @@ class TestUser(TestCase):
|
|||
# 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.user.title = None
|
||||
self.user.email = None
|
||||
self.user.phone = None
|
||||
self.assertFalse(self.user.has_contact_info())
|
||||
|
||||
|
||||
class TestContact(TestCase):
|
||||
def setUp(self):
|
||||
self.email_for_invalid = "intern@igorville.gov"
|
||||
self.invalid_user, _ = User.objects.get_or_create(
|
||||
username=self.email_for_invalid,
|
||||
email=self.email_for_invalid,
|
||||
first_name="",
|
||||
last_name="",
|
||||
phone="",
|
||||
)
|
||||
self.invalid_contact, _ = Contact.objects.get_or_create(user=self.invalid_user)
|
||||
|
||||
self.email = "mayor@igorville.gov"
|
||||
self.user, _ = User.objects.get_or_create(
|
||||
email=self.email, first_name="Jeff", last_name="Lebowski", phone="123456789"
|
||||
)
|
||||
self.contact, _ = Contact.objects.get_or_create(user=self.user)
|
||||
self.contact, _ = Contact.objects.get_or_create(
|
||||
first_name="Jeff",
|
||||
last_name="Lebowski",
|
||||
)
|
||||
|
||||
self.contact_as_so, _ = Contact.objects.get_or_create(email="newguy@igorville.gov")
|
||||
self.domain_request = DomainRequest.objects.create(creator=self.user, senior_official=self.contact_as_so)
|
||||
|
@ -1242,98 +1235,15 @@ class TestContact(TestCase):
|
|||
Contact.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
def test_saving_contact_updates_user_first_last_names_and_phone(self):
|
||||
"""When a contact is updated, we propagate the changes to the linked user if it exists."""
|
||||
|
||||
# User and Contact are created and linked as expected.
|
||||
# An empty User object should create an empty contact.
|
||||
self.assertEqual(self.invalid_contact.first_name, "")
|
||||
self.assertEqual(self.invalid_contact.last_name, "")
|
||||
self.assertEqual(self.invalid_contact.phone, "")
|
||||
self.assertEqual(self.invalid_user.first_name, "")
|
||||
self.assertEqual(self.invalid_user.last_name, "")
|
||||
self.assertEqual(self.invalid_user.phone, "")
|
||||
|
||||
# Manually update the contact - mimicking production (pre-existing data)
|
||||
self.invalid_contact.first_name = "Joey"
|
||||
self.invalid_contact.last_name = "Baloney"
|
||||
self.invalid_contact.phone = "123456789"
|
||||
self.invalid_contact.save()
|
||||
|
||||
# Refresh the user object to reflect the changes made in the database
|
||||
self.invalid_user.refresh_from_db()
|
||||
|
||||
# Updating the contact's first and last names propagate to the user
|
||||
self.assertEqual(self.invalid_contact.first_name, "Joey")
|
||||
self.assertEqual(self.invalid_contact.last_name, "Baloney")
|
||||
self.assertEqual(self.invalid_contact.phone, "123456789")
|
||||
self.assertEqual(self.invalid_user.first_name, "Joey")
|
||||
self.assertEqual(self.invalid_user.last_name, "Baloney")
|
||||
self.assertEqual(self.invalid_user.phone, "123456789")
|
||||
|
||||
def test_saving_contact_does_not_update_user_first_last_names_and_phone(self):
|
||||
"""When a contact is updated, we avoid propagating the changes to the linked user if it already has a value"""
|
||||
|
||||
# User and Contact are created and linked as expected
|
||||
self.assertEqual(self.contact.first_name, "Jeff")
|
||||
self.assertEqual(self.contact.last_name, "Lebowski")
|
||||
self.assertEqual(self.contact.phone, "123456789")
|
||||
self.assertEqual(self.user.first_name, "Jeff")
|
||||
self.assertEqual(self.user.last_name, "Lebowski")
|
||||
self.assertEqual(self.user.phone, "123456789")
|
||||
|
||||
self.contact.first_name = "Joey"
|
||||
self.contact.last_name = "Baloney"
|
||||
self.contact.phone = "987654321"
|
||||
self.contact.save()
|
||||
|
||||
# Refresh the user object to reflect the changes made in the database
|
||||
self.user.refresh_from_db()
|
||||
|
||||
# Updating the contact's first and last names propagate to the user
|
||||
self.assertEqual(self.contact.first_name, "Joey")
|
||||
self.assertEqual(self.contact.last_name, "Baloney")
|
||||
self.assertEqual(self.contact.phone, "987654321")
|
||||
self.assertEqual(self.user.first_name, "Jeff")
|
||||
self.assertEqual(self.user.last_name, "Lebowski")
|
||||
self.assertEqual(self.user.phone, "123456789")
|
||||
|
||||
def test_saving_contact_does_not_update_user_email(self):
|
||||
"""When a contact's email is updated, the change is not propagated to the user."""
|
||||
self.contact.email = "joey.baloney@diaperville.com"
|
||||
self.contact.save()
|
||||
|
||||
# Refresh the user object to reflect the changes made in the database
|
||||
self.user.refresh_from_db()
|
||||
|
||||
# Updating the contact's email does not propagate
|
||||
self.assertEqual(self.contact.email, "joey.baloney@diaperville.com")
|
||||
self.assertEqual(self.user.email, "mayor@igorville.gov")
|
||||
|
||||
def test_saving_contact_does_not_update_user_email_when_none(self):
|
||||
"""When a contact's email is updated, and the first/last name is none,
|
||||
the change is not propagated to the user."""
|
||||
self.invalid_contact.email = "joey.baloney@diaperville.com"
|
||||
self.invalid_contact.save()
|
||||
|
||||
# Refresh the user object to reflect the changes made in the database
|
||||
self.invalid_user.refresh_from_db()
|
||||
|
||||
# Updating the contact's email does not propagate
|
||||
self.assertEqual(self.invalid_contact.email, "joey.baloney@diaperville.com")
|
||||
self.assertEqual(self.invalid_user.email, "intern@igorville.gov")
|
||||
|
||||
def test_has_more_than_one_join(self):
|
||||
"""Test the Contact model method, has_more_than_one_join"""
|
||||
# test for a contact which has one user defined
|
||||
self.assertFalse(self.contact.has_more_than_one_join("user"))
|
||||
self.assertTrue(self.contact.has_more_than_one_join("senior_official"))
|
||||
# test for a contact which is assigned as a senior official on a domain request
|
||||
self.assertFalse(self.contact_as_so.has_more_than_one_join("senior_official"))
|
||||
self.assertTrue(self.contact_as_so.has_more_than_one_join("submitted_domain_requests"))
|
||||
|
||||
def test_has_contact_info(self):
|
||||
"""Test that has_contact_info properly returns"""
|
||||
self.contact.title = "Title"
|
||||
# test with a contact with contact info defined
|
||||
self.assertTrue(self.contact.has_contact_info())
|
||||
# test with a contact without contact info defined
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
import csv
|
||||
import io
|
||||
from django.test import Client, RequestFactory
|
||||
from io import StringIO
|
||||
from registrar.models.domain_request import DomainRequest
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.utility.generic_helper import convert_queryset_to_dict
|
||||
from registrar.utility.csv_export import (
|
||||
export_data_managed_domains_to_csv,
|
||||
export_data_unmanaged_domains_to_csv,
|
||||
get_sliced_domains,
|
||||
get_sliced_requests,
|
||||
write_csv_for_domains,
|
||||
DomainDataFull,
|
||||
DomainDataType,
|
||||
DomainDataFederal,
|
||||
DomainGrowth,
|
||||
DomainManaged,
|
||||
DomainUnmanaged,
|
||||
DomainExport,
|
||||
DomainRequestExport,
|
||||
DomainRequestGrowth,
|
||||
DomainRequestDataFull,
|
||||
get_default_start_date,
|
||||
get_default_end_date,
|
||||
DomainRequestExport,
|
||||
)
|
||||
|
||||
from django.db.models import Case, When
|
||||
from django.core.management import call_command
|
||||
from unittest.mock import MagicMock, call, mock_open, patch
|
||||
from api.views import get_current_federal, get_current_full
|
||||
|
@ -45,10 +47,10 @@ class CsvReportsTest(MockDb):
|
|||
fake_open = mock_open()
|
||||
expected_file_content = [
|
||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
||||
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,\r\n"),
|
||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\r\n"),
|
||||
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,\r\n"),
|
||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,\r\n"),
|
||||
]
|
||||
# We don't actually want to write anything for a test case,
|
||||
# we just want to verify what is being written.
|
||||
|
@ -67,11 +69,12 @@ class CsvReportsTest(MockDb):
|
|||
fake_open = mock_open()
|
||||
expected_file_content = [
|
||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
||||
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||
call("adomain2.gov,Interstate,,,,, \r\n"),
|
||||
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,\r\n"),
|
||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\r\n"),
|
||||
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,\r\n"),
|
||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,\r\n"),
|
||||
call("adomain2.gov,Interstate,,,,,\r\n"),
|
||||
call("zdomain12.gov,Interstate,,,,,\r\n"),
|
||||
]
|
||||
# We don't actually want to write anything for a test case,
|
||||
# we just want to verify what is being written.
|
||||
|
@ -202,494 +205,299 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
def tearDown(self):
|
||||
super().tearDown()
|
||||
|
||||
def test_export_domains_to_writer_security_emails_and_first_ready(self):
|
||||
"""Test that export_domains_to_writer returns the
|
||||
expected security email and first_ready value"""
|
||||
@less_console_noise_decorator
|
||||
def test_domain_data_type(self):
|
||||
"""Shows security contacts, domain managers, so"""
|
||||
# Add security email information
|
||||
self.domain_1.name = "defaultsecurity.gov"
|
||||
self.domain_1.save()
|
||||
# Invoke setter
|
||||
self.domain_1.security_contact
|
||||
# Invoke setter
|
||||
self.domain_2.security_contact
|
||||
# Invoke setter
|
||||
self.domain_3.security_contact
|
||||
# Add a first ready date on the first domain. Leaving the others blank.
|
||||
self.domain_1.first_ready = get_default_start_date()
|
||||
self.domain_1.save()
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
# Call the export functions
|
||||
DomainDataType.export_data_to_csv(csv_file)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
# We expect READY domains,
|
||||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,City,State,SO,"
|
||||
"SO email,Security contact email,Domain managers,Invited domain managers\n"
|
||||
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,World War I Centennial Commission,,,, ,,,"
|
||||
"meoward@rocks.com,\n"
|
||||
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,,"
|
||||
', ,,dotgov@cisa.dhs.gov,"meoward@rocks.com, info@example.com, big_lebowski@dude.co",'
|
||||
"woofwardthethird@rocks.com\n"
|
||||
"adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,, ,,,,"
|
||||
"squeaker@rocks.com\n"
|
||||
"bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
|
||||
"bdomain5.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
|
||||
"bdomain6.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
|
||||
"ddomain3.gov,On hold,(blank),2023-11-15,Federal,Armed Forces Retirement Home,,,, ,,"
|
||||
"security@mail.gov,,\n"
|
||||
"sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
|
||||
"xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
|
||||
"zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
|
||||
"adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,, ,,registrar@dotgov.gov,"
|
||||
"meoward@rocks.com,squeaker@rocks.com\n"
|
||||
"zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,, ,,,meoward@rocks.com,\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
with less_console_noise():
|
||||
# Add security email information
|
||||
self.domain_1.name = "defaultsecurity.gov"
|
||||
self.domain_1.save()
|
||||
# Invoke setter
|
||||
self.domain_1.security_contact
|
||||
# Invoke setter
|
||||
self.domain_2.security_contact
|
||||
# Invoke setter
|
||||
self.domain_3.security_contact
|
||||
@less_console_noise_decorator
|
||||
def test_domain_data_full(self):
|
||||
"""Shows security contacts, filtered by state"""
|
||||
# Add security email information
|
||||
self.domain_1.name = "defaultsecurity.gov"
|
||||
self.domain_1.save()
|
||||
# Invoke setter
|
||||
self.domain_1.security_contact
|
||||
# Invoke setter
|
||||
self.domain_2.security_contact
|
||||
# Invoke setter
|
||||
self.domain_3.security_contact
|
||||
# Add a first ready date on the first domain. Leaving the others blank.
|
||||
self.domain_1.first_ready = get_default_start_date()
|
||||
self.domain_1.save()
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
# Call the export functions
|
||||
DomainDataFull.export_data_to_csv(csv_file)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
# We expect READY domains,
|
||||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,\n"
|
||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,dotgov@cisa.dhs.gov\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
||||
"adomain2.gov,Interstate,,,,,registrar@dotgov.gov\n"
|
||||
"zdomain12.gov,Interstate,,,,,\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
# Add a first ready date on the first domain. Leaving the others blank.
|
||||
self.domain_1.first_ready = get_default_start_date()
|
||||
self.domain_1.save()
|
||||
@less_console_noise_decorator
|
||||
def test_domain_data_federal(self):
|
||||
"""Shows security contacts, filtered by state and org type"""
|
||||
# Add security email information
|
||||
self.domain_1.name = "defaultsecurity.gov"
|
||||
self.domain_1.save()
|
||||
# Invoke setter
|
||||
self.domain_1.security_contact
|
||||
# Invoke setter
|
||||
self.domain_2.security_contact
|
||||
# Invoke setter
|
||||
self.domain_3.security_contact
|
||||
# Add a first ready date on the first domain. Leaving the others blank.
|
||||
self.domain_1.first_ready = get_default_start_date()
|
||||
self.domain_1.save()
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
# Call the export functions
|
||||
DomainDataFederal.export_data_to_csv(csv_file)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
# We expect READY domains,
|
||||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,\n"
|
||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,dotgov@cisa.dhs.gov\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
# Define columns, sort fields, and filter condition
|
||||
columns = [
|
||||
"Domain name",
|
||||
"Domain type",
|
||||
"Agency",
|
||||
"Organization name",
|
||||
"City",
|
||||
"State",
|
||||
"SO",
|
||||
"SO email",
|
||||
"Security contact email",
|
||||
"Status",
|
||||
"Expiration date",
|
||||
"First ready on",
|
||||
]
|
||||
sort_fields = ["domain__name"]
|
||||
filter_condition = {
|
||||
"domain__state__in": [
|
||||
Domain.State.READY,
|
||||
Domain.State.DNS_NEEDED,
|
||||
Domain.State.ON_HOLD,
|
||||
],
|
||||
}
|
||||
|
||||
# Call the export functions
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_condition,
|
||||
should_get_domain_managers=False,
|
||||
should_write_header=True,
|
||||
@less_console_noise_decorator
|
||||
def test_domain_growth(self):
|
||||
"""Shows ready and deleted domains within a date range, sorted"""
|
||||
# Remove "Created at" and "First ready" because we can't guess this immutable, dynamically generated test data
|
||||
columns = [
|
||||
"Domain name",
|
||||
"Domain type",
|
||||
"Agency",
|
||||
"Organization name",
|
||||
"City",
|
||||
"State",
|
||||
"Status",
|
||||
"Expiration date",
|
||||
# "Created at",
|
||||
# "First ready",
|
||||
"Deleted",
|
||||
]
|
||||
sort = {
|
||||
"custom_sort": Case(
|
||||
When(domain__state=Domain.State.READY, then="domain__created_at"),
|
||||
When(domain__state=Domain.State.DELETED, then="domain__deleted"),
|
||||
)
|
||||
}
|
||||
with patch("registrar.utility.csv_export.DomainGrowth.get_columns", return_value=columns):
|
||||
with patch("registrar.utility.csv_export.DomainGrowth.get_annotations_for_sort", return_value=sort):
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
# Call the export functions
|
||||
DomainGrowth.export_data_to_csv(
|
||||
csv_file,
|
||||
self.start_date.strftime("%Y-%m-%d"),
|
||||
self.end_date.strftime("%Y-%m-%d"),
|
||||
)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
# We expect READY domains first, created between day-2 and day+2, sorted by created_at then name
|
||||
# and DELETED domains deleted between day-2 and day+2, sorted by deleted then name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,"
|
||||
"State,Status,Expiration date, Deleted\n"
|
||||
"cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,(blank)\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,(blank)\n"
|
||||
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady(blank)\n"
|
||||
"zdomain12.govInterstateReady(blank)\n"
|
||||
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-01\n"
|
||||
"sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank),2024-04-02\n"
|
||||
"xdomain7.gov,FederalArmedForcesRetirementHome,Deleted,(blank),2024-04-02\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = (
|
||||
csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
)
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
# We expect READY domains,
|
||||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,SO,"
|
||||
"SO email,Security contact email,Status,Expiration date, First ready on\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready,(blank),2024-04-03\n"
|
||||
"adomain2.gov,Interstate,(blank),Dns needed,(blank),(blank)\n"
|
||||
"cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank),2024-04-02\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,security@mail.gov,On hold,2023-11-15,(blank)\n"
|
||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,"
|
||||
"(blank),Ready,(blank),2023-11-01\n"
|
||||
"zdomain12.govInterstateReady,(blank),2024-04-02\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
def test_write_csv_for_domains(self):
|
||||
"""Test that write_body returns the
|
||||
existing domain, test that sort by domain name works,
|
||||
test that filter works"""
|
||||
|
||||
with less_console_noise():
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
|
||||
# Define columns, sort fields, and filter condition
|
||||
columns = [
|
||||
"Domain name",
|
||||
"Domain type",
|
||||
"Agency",
|
||||
"Organization name",
|
||||
"City",
|
||||
"State",
|
||||
"SO",
|
||||
"SO email",
|
||||
"Submitter",
|
||||
"Submitter title",
|
||||
"Submitter email",
|
||||
"Submitter phone",
|
||||
"Security contact email",
|
||||
"Status",
|
||||
]
|
||||
sort_fields = ["domain__name"]
|
||||
filter_condition = {
|
||||
"domain__state__in": [
|
||||
Domain.State.READY,
|
||||
Domain.State.DNS_NEEDED,
|
||||
Domain.State.ON_HOLD,
|
||||
],
|
||||
}
|
||||
# Call the export functions
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_condition,
|
||||
should_get_domain_managers=False,
|
||||
should_write_header=True,
|
||||
)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
# We expect READY domains,
|
||||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,SO,"
|
||||
"SO email,Submitter,Submitter title,Submitter email,Submitter phone,"
|
||||
"Security contact email,Status\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
|
||||
"adomain2.gov,Interstate,Dns needed\n"
|
||||
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady\n"
|
||||
"cdomain1.gov,Federal - Executive,World War I Centennial Commission,Ready\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,On hold\n"
|
||||
"zdomain12.govInterstateReady\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
def test_write_domains_body_additional(self):
|
||||
"""An additional test for filters and multi-column sort"""
|
||||
|
||||
with less_console_noise():
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
# Define columns, sort fields, and filter condition
|
||||
columns = [
|
||||
"Domain name",
|
||||
"Domain type",
|
||||
"Agency",
|
||||
"Organization name",
|
||||
"City",
|
||||
"State",
|
||||
"Security contact email",
|
||||
]
|
||||
sort_fields = ["domain__name", "federal_agency", "generic_org_type"]
|
||||
filter_condition = {
|
||||
"generic_org_type__icontains": "federal",
|
||||
"domain__state__in": [
|
||||
Domain.State.READY,
|
||||
Domain.State.DNS_NEEDED,
|
||||
Domain.State.ON_HOLD,
|
||||
],
|
||||
}
|
||||
# Call the export functions
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_condition,
|
||||
should_get_domain_managers=False,
|
||||
should_write_header=True,
|
||||
)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
# We expect READY domains,
|
||||
# federal only
|
||||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,"
|
||||
"State,Security contact email\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home\n"
|
||||
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommission\n"
|
||||
"cdomain1.gov,Federal - Executive,World War I Centennial Commission\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
def test_write_domains_body_with_date_filter_pulls_domains_in_range(self):
|
||||
"""Test that domains that are
|
||||
1. READY and their first_ready dates are in range
|
||||
2. DELETED and their deleted dates are in range
|
||||
are pulled when the growth report conditions are applied to export_domains_to_writed.
|
||||
Test that ready domains are sorted by first_ready/deleted dates first, names second.
|
||||
|
||||
We considered testing export_data_domain_growth_to_csv which calls write_body
|
||||
and would have been easy to set up, but expected_content would contain created_at dates
|
||||
which are hard to mock.
|
||||
|
||||
TODO: Simplify if created_at is not needed for the report."""
|
||||
|
||||
with less_console_noise():
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
# Define columns, sort fields, and filter condition
|
||||
columns = [
|
||||
"Domain name",
|
||||
"Domain type",
|
||||
"Agency",
|
||||
"Organization name",
|
||||
"City",
|
||||
"State",
|
||||
"Status",
|
||||
"Expiration date",
|
||||
]
|
||||
sort_fields = [
|
||||
"created_at",
|
||||
"domain__name",
|
||||
]
|
||||
sort_fields_for_deleted_domains = [
|
||||
"domain__deleted",
|
||||
"domain__name",
|
||||
]
|
||||
filter_condition = {
|
||||
"domain__state__in": [
|
||||
Domain.State.READY,
|
||||
],
|
||||
"domain__first_ready__lte": self.end_date,
|
||||
"domain__first_ready__gte": self.start_date,
|
||||
}
|
||||
filter_conditions_for_deleted_domains = {
|
||||
"domain__state__in": [
|
||||
Domain.State.DELETED,
|
||||
],
|
||||
"domain__deleted__lte": self.end_date,
|
||||
"domain__deleted__gte": self.start_date,
|
||||
}
|
||||
|
||||
# Call the export functions
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_condition,
|
||||
should_get_domain_managers=False,
|
||||
should_write_header=True,
|
||||
)
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields_for_deleted_domains,
|
||||
filter_conditions_for_deleted_domains,
|
||||
should_get_domain_managers=False,
|
||||
should_write_header=False,
|
||||
)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
|
||||
# We expect READY domains first, created between day-2 and day+2, sorted by created_at then name
|
||||
# and DELETED domains deleted between day-2 and day+2, sorted by deleted then name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,"
|
||||
"State,Status,Expiration date\n"
|
||||
"cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,(blank)\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,(blank)\n"
|
||||
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady(blank)\n"
|
||||
"zdomain12.govInterstateReady(blank)\n"
|
||||
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank)\n"
|
||||
"sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank)\n"
|
||||
"xdomain7.gov,FederalArmedForcesRetirementHome,Deleted,(blank)\n"
|
||||
)
|
||||
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
def test_export_domains_to_writer_domain_managers(self):
|
||||
"""Test that export_domains_to_writer returns the
|
||||
expected domain managers.
|
||||
@less_console_noise_decorator
|
||||
def test_domain_managed(self):
|
||||
"""Shows ready and deleted domains by an end date, sorted
|
||||
|
||||
An invited user, woofwardthethird, should also be pulled into this report.
|
||||
|
||||
squeaker@rocks.com is invited to domain2 (DNS_NEEDED) and domain10 (No managers).
|
||||
She should show twice in this report but not in test_export_data_managed_domains_to_csv."""
|
||||
She should show twice in this report but not in test_DomainManaged."""
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
# Call the export functions
|
||||
DomainManaged.export_data_to_csv(
|
||||
csv_file,
|
||||
self.start_date.strftime("%Y-%m-%d"),
|
||||
self.end_date.strftime("%Y-%m-%d"),
|
||||
)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
# We expect the READY domain names with the domain managers: Their counts, and listing at end_date.
|
||||
expected_content = (
|
||||
"MANAGED DOMAINS COUNTS AT START DATE\n"
|
||||
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||
"School district,Election office\n"
|
||||
"0,0,0,0,0,0,0,0,0,0\n"
|
||||
"\n"
|
||||
"MANAGED DOMAINS COUNTS AT END DATE\n"
|
||||
"Total,Federal,Interstate,State or territory,Tribal,County,City,"
|
||||
"Special district,School district,Election office\n"
|
||||
"3,2,1,0,0,0,0,0,0,0\n"
|
||||
"\n"
|
||||
"Domain name,Domain type,Domain managers,Invited domain managers\n"
|
||||
"cdomain11.gov,Federal - Executive,meoward@rocks.com,\n"
|
||||
'cdomain1.gov,Federal - Executive,"meoward@rocks.com, info@example.com, big_lebowski@dude.co",'
|
||||
"woofwardthethird@rocks.com\n"
|
||||
"zdomain12.gov,Interstate,meoward@rocks.com,\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
with less_console_noise():
|
||||
@less_console_noise_decorator
|
||||
def test_domain_unmanaged(self):
|
||||
"""Shows unmanaged domains by an end date, sorted"""
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
DomainUnmanaged.export_data_to_csv(
|
||||
csv_file, self.start_date.strftime("%Y-%m-%d"), self.end_date.strftime("%Y-%m-%d")
|
||||
)
|
||||
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
|
||||
# We expect the READY domain names with the domain managers: Their counts, and listing at end_date.
|
||||
expected_content = (
|
||||
"UNMANAGED DOMAINS AT START DATE\n"
|
||||
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||
"School district,Election office\n"
|
||||
"0,0,0,0,0,0,0,0,0,0\n"
|
||||
"\n"
|
||||
"UNMANAGED DOMAINS AT END DATE\n"
|
||||
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||
"School district,Election office\n"
|
||||
"1,1,0,0,0,0,0,0,0,0\n"
|
||||
"\n"
|
||||
"Domain name,Domain type\n"
|
||||
"adomain10.gov,Federal\n"
|
||||
)
|
||||
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_growth(self):
|
||||
"""Shows submitted requests within a date range, sorted"""
|
||||
# Remove "Submitted at" because we can't guess this immutable, dynamically generated test data
|
||||
columns = [
|
||||
"Domain request",
|
||||
"Domain type",
|
||||
"Federal type",
|
||||
# "Submitted at",
|
||||
]
|
||||
with patch("registrar.utility.csv_export.DomainRequestGrowth.get_columns", return_value=columns):
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
# Define columns, sort fields, and filter condition
|
||||
columns = [
|
||||
"Domain name",
|
||||
"Status",
|
||||
"Expiration date",
|
||||
"Domain type",
|
||||
"Agency",
|
||||
"Organization name",
|
||||
"City",
|
||||
"State",
|
||||
"SO",
|
||||
"SO email",
|
||||
"Security contact email",
|
||||
]
|
||||
sort_fields = ["domain__name"]
|
||||
filter_condition = {
|
||||
"domain__state__in": [
|
||||
Domain.State.READY,
|
||||
Domain.State.DNS_NEEDED,
|
||||
Domain.State.ON_HOLD,
|
||||
],
|
||||
}
|
||||
|
||||
# Call the export functions
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_condition,
|
||||
should_get_domain_managers=True,
|
||||
should_write_header=True,
|
||||
DomainRequestGrowth.export_data_to_csv(
|
||||
csv_file,
|
||||
self.start_date.strftime("%Y-%m-%d"),
|
||||
self.end_date.strftime("%Y-%m-%d"),
|
||||
)
|
||||
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
# We expect READY domains,
|
||||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Status,Expiration date,Domain type,Agency,"
|
||||
"Organization name,City,State,SO,SO email,"
|
||||
"Security contact email,Domain manager 1,DM1 status,Domain manager 2,DM2 status,"
|
||||
"Domain manager 3,DM3 status,Domain manager 4,DM4 status\n"
|
||||
"adomain10.gov,Ready,(blank),Federal,Armed Forces Retirement Home,,,, , ,squeaker@rocks.com, I\n"
|
||||
"adomain2.gov,Dns needed,(blank),Interstate,,,,, , , ,meoward@rocks.com, R,squeaker@rocks.com, I\n"
|
||||
"cdomain11.govReady,(blank),Federal-ExecutiveWorldWarICentennialCommissionmeoward@rocks.comR\n"
|
||||
"cdomain1.gov,Ready,(blank),Federal - Executive,World War I Centennial Commission,,,"
|
||||
", , , ,meoward@rocks.com,R,info@example.com,R,big_lebowski@dude.co,R,"
|
||||
"woofwardthethird@rocks.com,I\n"
|
||||
"ddomain3.gov,On hold,(blank),Federal,Armed Forces Retirement Home,,,, , , ,,\n"
|
||||
"zdomain12.gov,Ready,(blank),Interstate,meoward@rocks.com,R\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
def test_export_data_managed_domains_to_csv(self):
|
||||
"""Test get counts for domains that have domain managers for two different dates,
|
||||
get list of managed domains at end_date.
|
||||
|
||||
An invited user, woofwardthethird, should also be pulled into this report."""
|
||||
|
||||
with less_console_noise():
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
export_data_managed_domains_to_csv(
|
||||
csv_file, self.start_date.strftime("%Y-%m-%d"), self.end_date.strftime("%Y-%m-%d")
|
||||
)
|
||||
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
|
||||
# We expect the READY domain names with the domain managers: Their counts, and listing at end_date.
|
||||
expected_content = (
|
||||
"MANAGED DOMAINS COUNTS AT START DATE\n"
|
||||
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||
"School district,Election office\n"
|
||||
"0,0,0,0,0,0,0,0,0,0\n"
|
||||
"\n"
|
||||
"MANAGED DOMAINS COUNTS AT END DATE\n"
|
||||
"Total,Federal,Interstate,State or territory,Tribal,County,City,"
|
||||
"Special district,School district,Election office\n"
|
||||
"3,2,1,0,0,0,0,0,0,0\n"
|
||||
"\n"
|
||||
"Domain name,Domain type,Domain manager 1,DM1 status,Domain manager 2,DM2 status,"
|
||||
"Domain manager 3,DM3 status,Domain manager 4,DM4 status\n"
|
||||
"cdomain11.govFederal-Executivemeoward@rocks.com, R\n"
|
||||
"cdomain1.gov,Federal - Executive,meoward@rocks.com,R,info@example.com,R,"
|
||||
"big_lebowski@dude.co,R,woofwardthethird@rocks.com,I\n"
|
||||
"zdomain12.govInterstatemeoward@rocks.com,R\n"
|
||||
)
|
||||
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
def test_export_data_unmanaged_domains_to_csv(self):
|
||||
"""Test get counts for domains that do not have domain managers for two different dates,
|
||||
get list of unmanaged domains at end_date."""
|
||||
|
||||
with less_console_noise():
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
export_data_unmanaged_domains_to_csv(
|
||||
csv_file, self.start_date.strftime("%Y-%m-%d"), self.end_date.strftime("%Y-%m-%d")
|
||||
)
|
||||
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
|
||||
# We expect the READY domain names with the domain managers: Their counts, and listing at end_date.
|
||||
expected_content = (
|
||||
"UNMANAGED DOMAINS AT START DATE\n"
|
||||
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||
"School district,Election office\n"
|
||||
"0,0,0,0,0,0,0,0,0,0\n"
|
||||
"\n"
|
||||
"UNMANAGED DOMAINS AT END DATE\n"
|
||||
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||
"School district,Election office\n"
|
||||
"1,1,0,0,0,0,0,0,0,0\n"
|
||||
"\n"
|
||||
"Domain name,Domain type\n"
|
||||
"adomain10.gov,Federal\n"
|
||||
)
|
||||
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
def test_write_requests_body_with_date_filter_pulls_requests_in_range(self):
|
||||
"""Test that requests that are
|
||||
1. SUBMITTED and their submission_date are in range
|
||||
are pulled when the growth report conditions are applied to export_requests_to_writed.
|
||||
Test that requests are sorted by requested domain name.
|
||||
"""
|
||||
|
||||
with less_console_noise():
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
# Define columns, sort fields, and filter condition
|
||||
# We'll skip submission date because it's dynamic and therefore
|
||||
# impossible to set in expected_content
|
||||
columns = ["Domain request", "Domain type", "Federal type"]
|
||||
sort_fields = [
|
||||
"requested_domain__name",
|
||||
]
|
||||
filter_condition = {
|
||||
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
"submission_date__lte": self.end_date,
|
||||
"submission_date__gte": self.start_date,
|
||||
}
|
||||
|
||||
additional_values = ["requested_domain__name"]
|
||||
all_requests = DomainRequest.objects.filter(**filter_condition).order_by(*sort_fields).distinct()
|
||||
annotated_requests = DomainRequestExport.annotate_and_retrieve_fields(all_requests, {}, additional_values)
|
||||
requests_dict = convert_queryset_to_dict(annotated_requests, is_model=False)
|
||||
DomainRequestExport.write_csv_for_requests(writer, columns, requests_dict)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
# We expect READY domains first, created between today-2 and today+2, sorted by created_at then name
|
||||
# and DELETED domains deleted between today-2 and today+2, sorted by deleted then name
|
||||
expected_content = (
|
||||
"Domain request,Domain type,Federal type\n"
|
||||
"city3.gov,Federal,Executive\n"
|
||||
|
@ -705,68 +513,82 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_full_domain_request_report(self):
|
||||
def test_domain_request_data_full(self):
|
||||
"""Tests the full domain request report."""
|
||||
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
|
||||
# Call the report. Get existing fields from the report itself.
|
||||
annotations = DomainRequestExport._full_domain_request_annotations()
|
||||
additional_values = [
|
||||
"requested_domain__name",
|
||||
"federal_agency__agency",
|
||||
"senior_official__first_name",
|
||||
"senior_official__last_name",
|
||||
"senior_official__email",
|
||||
"senior_official__title",
|
||||
"creator__first_name",
|
||||
"creator__last_name",
|
||||
"creator__email",
|
||||
"investigator__email",
|
||||
# Remove "Submitted at" because we can't guess this immutable, dynamically generated test data
|
||||
columns = [
|
||||
"Domain request",
|
||||
# "Submitted at",
|
||||
"Status",
|
||||
"Domain type",
|
||||
"Federal type",
|
||||
"Federal agency",
|
||||
"Organization name",
|
||||
"Election office",
|
||||
"City",
|
||||
"State/territory",
|
||||
"Region",
|
||||
"Creator first name",
|
||||
"Creator last name",
|
||||
"Creator email",
|
||||
"Creator approved domains count",
|
||||
"Creator active requests count",
|
||||
"Alternative domains",
|
||||
"SO first name",
|
||||
"SO last name",
|
||||
"SO email",
|
||||
"SO title/role",
|
||||
"Request purpose",
|
||||
"Request additional details",
|
||||
"Other contacts",
|
||||
"CISA regional representative",
|
||||
"Current websites",
|
||||
"Investigator",
|
||||
]
|
||||
requests = DomainRequest.objects.exclude(status=DomainRequest.DomainRequestStatus.STARTED)
|
||||
annotated_requests = DomainRequestExport.annotate_and_retrieve_fields(requests, annotations, additional_values)
|
||||
requests_dict = convert_queryset_to_dict(annotated_requests, is_model=False)
|
||||
DomainRequestExport.write_csv_for_requests(writer, DomainRequestExport.all_columns, requests_dict)
|
||||
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
expected_content = (
|
||||
# Header
|
||||
"Domain request,Submitted at,Status,Domain type,Federal type,"
|
||||
"Federal agency,Organization name,Election office,City,State/territory,"
|
||||
"Region,Creator first name,Creator last name,Creator email,Creator approved domains count,"
|
||||
"Creator active requests count,Alternative domains,SO first name,SO last name,SO email,"
|
||||
"SO title/role,Request purpose,Request additional details,Other contacts,"
|
||||
"CISA regional representative,Current websites,Investigator\n"
|
||||
# Content
|
||||
"city2.gov,,In review,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
|
||||
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
||||
"city3.gov,2024-04-02,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,"
|
||||
"cheeseville.gov | city1.gov | igorville.gov,Testy,Tester,testy@town.com,Chief Tester,"
|
||||
"Purpose of the site,CISA-first-name CISA-last-name | There is more,Meow Tester24 te2@town.com | "
|
||||
"Testy1232 Tester24 te2@town.com | Testy Tester testy2@town.com,test@igorville.com,"
|
||||
"city.com | https://www.example2.com | https://www.example.com,\n"
|
||||
"city4.gov,2024-04-02,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,Tester,"
|
||||
"testy@town.com,Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
||||
"Testy Tester testy2@town.com,cisaRep@igorville.gov,city.com,\n"
|
||||
"city5.gov,,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
|
||||
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
||||
"city6.gov,2024-04-02,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,"
|
||||
"testy@town.com,Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
||||
"Testy Tester testy2@town.com,cisaRep@igorville.gov,city.com,"
|
||||
)
|
||||
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
with patch("registrar.utility.csv_export.DomainRequestDataFull.get_columns", return_value=columns):
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
# Call the export functions
|
||||
DomainRequestDataFull.export_data_to_csv(csv_file)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
print(csv_content)
|
||||
expected_content = (
|
||||
# Header
|
||||
"Domain request,Status,Domain type,Federal type,"
|
||||
"Federal agency,Organization name,Election office,City,State/territory,"
|
||||
"Region,Creator first name,Creator last name,Creator email,Creator approved domains count,"
|
||||
"Creator active requests count,Alternative domains,SO first name,SO last name,SO email,"
|
||||
"SO title/role,Request purpose,Request additional details,Other contacts,"
|
||||
"CISA regional representative,Current websites,Investigator\n"
|
||||
# Content
|
||||
"city5.gov,,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
|
||||
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
||||
"city2.gov,,In review,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,"
|
||||
"testy@town.com,"
|
||||
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
||||
'city3.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,"cheeseville.gov, city1.gov,'
|
||||
'igorville.gov",Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,CISA-first-name '
|
||||
"CISA-last-name "
|
||||
'| There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, Testy Tester '
|
||||
'testy2@town.com"'
|
||||
',test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
|
||||
"city4.gov,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
|
||||
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester "
|
||||
"testy2@town.com"
|
||||
",cisaRep@igorville.gov,city.com,\n"
|
||||
"city6.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
|
||||
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester "
|
||||
"testy2@town.com,"
|
||||
"cisaRep@igorville.gov,city.com,\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
|
||||
class HelperFunctions(MockDb):
|
||||
|
@ -792,12 +614,12 @@ class HelperFunctions(MockDb):
|
|||
"domain__first_ready__lte": self.end_date,
|
||||
}
|
||||
# Test with distinct
|
||||
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
|
||||
managed_domains_sliced_at_end_date = DomainExport.get_sliced_domains(filter_condition)
|
||||
expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
|
||||
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
|
||||
|
||||
# Test without distinct
|
||||
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
|
||||
managed_domains_sliced_at_end_date = DomainExport.get_sliced_domains(filter_condition)
|
||||
expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
|
||||
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
|
||||
|
||||
|
@ -809,6 +631,6 @@ class HelperFunctions(MockDb):
|
|||
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
"submission_date__lte": self.end_date,
|
||||
}
|
||||
submitted_requests_sliced_at_end_date = get_sliced_requests(filter_condition)
|
||||
submitted_requests_sliced_at_end_date = DomainRequestExport.get_sliced_requests(filter_condition)
|
||||
expected_content = [3, 2, 0, 0, 0, 0, 1, 0, 0, 1]
|
||||
self.assertEqual(submitted_requests_sliced_at_end_date, expected_content)
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from registrar.models import Contact
|
||||
|
||||
|
||||
class TestUserPostSave(TestCase):
|
||||
def setUp(self):
|
||||
self.username = "test_user"
|
||||
self.first_name = "First"
|
||||
self.last_name = "Last"
|
||||
self.email = "info@example.com"
|
||||
self.phone = "202-555-0133"
|
||||
|
||||
self.preferred_first_name = "One"
|
||||
self.preferred_last_name = "Two"
|
||||
self.preferred_email = "front_desk@example.com"
|
||||
self.preferred_phone = "202-555-0134"
|
||||
|
||||
def test_user_created_without_matching_contact(self):
|
||||
"""Expect 1 Contact containing data copied from User."""
|
||||
self.assertEqual(len(Contact.objects.all()), 0)
|
||||
user = get_user_model().objects.create(
|
||||
username=self.username,
|
||||
first_name=self.first_name,
|
||||
last_name=self.last_name,
|
||||
email=self.email,
|
||||
phone=self.phone,
|
||||
)
|
||||
actual = Contact.objects.get(user=user)
|
||||
self.assertEqual(actual.first_name, self.first_name)
|
||||
self.assertEqual(actual.last_name, self.last_name)
|
||||
self.assertEqual(actual.email, self.email)
|
||||
self.assertEqual(actual.phone, self.phone)
|
||||
|
||||
def test_user_created_with_matching_contact(self):
|
||||
"""Expect 1 Contact associated, but with no data copied from User."""
|
||||
self.assertEqual(len(Contact.objects.all()), 0)
|
||||
Contact.objects.create(
|
||||
first_name=self.preferred_first_name,
|
||||
last_name=self.preferred_last_name,
|
||||
email=self.email, # must be the same, to find the match!
|
||||
phone=self.preferred_phone,
|
||||
)
|
||||
user = get_user_model().objects.create(
|
||||
username=self.username,
|
||||
first_name=self.first_name,
|
||||
last_name=self.last_name,
|
||||
email=self.email,
|
||||
)
|
||||
actual = Contact.objects.get(user=user)
|
||||
self.assertEqual(actual.first_name, self.preferred_first_name)
|
||||
self.assertEqual(actual.last_name, self.preferred_last_name)
|
||||
self.assertEqual(actual.email, self.email)
|
||||
self.assertEqual(actual.phone, self.preferred_phone)
|
||||
|
||||
def test_user_updated_without_matching_contact(self):
|
||||
"""Expect 1 Contact containing data copied from User."""
|
||||
# create the user
|
||||
self.assertEqual(len(Contact.objects.all()), 0)
|
||||
user = get_user_model().objects.create(username=self.username, first_name="", last_name="", email="", phone="")
|
||||
# delete the contact
|
||||
Contact.objects.all().delete()
|
||||
self.assertEqual(len(Contact.objects.all()), 0)
|
||||
# modify the user
|
||||
user.username = self.username
|
||||
user.first_name = self.first_name
|
||||
user.last_name = self.last_name
|
||||
user.email = self.email
|
||||
user.phone = self.phone
|
||||
user.save()
|
||||
# test
|
||||
actual = Contact.objects.get(user=user)
|
||||
self.assertEqual(actual.first_name, self.first_name)
|
||||
self.assertEqual(actual.last_name, self.last_name)
|
||||
self.assertEqual(actual.email, self.email)
|
||||
self.assertEqual(actual.phone, self.phone)
|
||||
|
||||
def test_user_updated_with_matching_contact(self):
|
||||
"""Expect 1 Contact associated, but with no data copied from User."""
|
||||
# create the user
|
||||
self.assertEqual(len(Contact.objects.all()), 0)
|
||||
user = get_user_model().objects.create(
|
||||
username=self.username,
|
||||
first_name=self.first_name,
|
||||
last_name=self.last_name,
|
||||
email=self.email,
|
||||
phone=self.phone,
|
||||
)
|
||||
# modify the user
|
||||
user.first_name = self.preferred_first_name
|
||||
user.last_name = self.preferred_last_name
|
||||
user.email = self.preferred_email
|
||||
user.phone = self.preferred_phone
|
||||
user.save()
|
||||
# test
|
||||
actual = Contact.objects.get(user=user)
|
||||
self.assertEqual(actual.first_name, self.first_name)
|
||||
self.assertEqual(actual.last_name, self.last_name)
|
||||
self.assertEqual(actual.email, self.email)
|
||||
self.assertEqual(actual.phone, self.phone)
|
|
@ -57,12 +57,10 @@ class TestWithUser(MockEppLib):
|
|||
last_name = "Last"
|
||||
email = "info@example.com"
|
||||
phone = "8003111234"
|
||||
self.user = get_user_model().objects.create(
|
||||
username=username, first_name=first_name, last_name=last_name, email=email, phone=phone
|
||||
)
|
||||
title = "test title"
|
||||
self.user.contact.title = title
|
||||
self.user.contact.save()
|
||||
self.user = get_user_model().objects.create(
|
||||
username=username, first_name=first_name, last_name=last_name, title=title, email=email, phone=phone
|
||||
)
|
||||
|
||||
username_regular_incomplete = "test_regular_user_incomplete"
|
||||
username_other_incomplete = "test_other_user_incomplete"
|
||||
|
@ -374,7 +372,10 @@ class HomeTests(TestWithUser):
|
|||
)
|
||||
|
||||
# Attach a user object to a contact (should not be deleted)
|
||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
||||
contact_user, _ = Contact.objects.get_or_create(
|
||||
first_name="Hank",
|
||||
last_name="McFakey",
|
||||
)
|
||||
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
domain_request = DomainRequest.objects.create(
|
||||
|
@ -407,17 +408,12 @@ class HomeTests(TestWithUser):
|
|||
igorville = DomainRequest.objects.filter(requested_domain__name="igorville.gov")
|
||||
self.assertFalse(igorville.exists())
|
||||
|
||||
# Check if the orphaned contact was deleted
|
||||
# Check if the orphaned contacts were deleted
|
||||
orphan = Contact.objects.filter(id=contact.id)
|
||||
self.assertFalse(orphan.exists())
|
||||
orphan = Contact.objects.filter(id=contact_user.id)
|
||||
self.assertFalse(orphan.exists())
|
||||
|
||||
# All non-orphan contacts should still exist and are unaltered
|
||||
try:
|
||||
current_user = Contact.objects.filter(id=contact_user.id).get()
|
||||
except Contact.DoesNotExist:
|
||||
self.fail("contact_user (a non-orphaned contact) was deleted")
|
||||
|
||||
self.assertEqual(current_user, contact_user)
|
||||
try:
|
||||
edge_case = Contact.objects.filter(id=contact_2.id).get()
|
||||
except Contact.DoesNotExist:
|
||||
|
@ -446,7 +442,10 @@ class HomeTests(TestWithUser):
|
|||
)
|
||||
|
||||
# Attach a user object to a contact (should not be deleted)
|
||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
||||
contact_user, _ = Contact.objects.get_or_create(
|
||||
first_name="Hank",
|
||||
last_name="McFakey",
|
||||
)
|
||||
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
domain_request = DomainRequest.objects.create(
|
||||
|
@ -560,7 +559,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
|||
self.assertContains(finish_setup_page, "Enter your phone number.")
|
||||
|
||||
# Check for the name of the save button
|
||||
self.assertContains(finish_setup_page, "contact_setup_save_button")
|
||||
self.assertContains(finish_setup_page, "user_setup_save_button")
|
||||
|
||||
# Add a phone number
|
||||
finish_setup_form = finish_setup_page.form
|
||||
|
@ -598,7 +597,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
|||
self.assertContains(finish_setup_page, "Enter your phone number.")
|
||||
|
||||
# Check for the name of the save button
|
||||
self.assertContains(finish_setup_page, "contact_setup_save_button")
|
||||
self.assertContains(finish_setup_page, "user_setup_save_button")
|
||||
|
||||
# Add a phone number
|
||||
finish_setup_form = finish_setup_page.form
|
||||
|
@ -614,7 +613,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
|||
|
||||
# Submit the form using the specific submit button to execute the redirect
|
||||
completed_setup_page = self._submit_form_webtest(
|
||||
finish_setup_form, follow=True, name="contact_setup_submit_button"
|
||||
finish_setup_form, follow=True, name="user_setup_submit_button"
|
||||
)
|
||||
self.assertEqual(completed_setup_page.status_code, 200)
|
||||
|
||||
|
@ -868,7 +867,10 @@ class UserProfileTests(TestWithUser, WebTest):
|
|||
def test_request_when_profile_feature_on(self):
|
||||
"""test that Your profile is in request page when profile feature is on"""
|
||||
|
||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
||||
contact_user, _ = Contact.objects.get_or_create(
|
||||
first_name="Hank",
|
||||
last_name="McFakerson",
|
||||
)
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
domain_request = DomainRequest.objects.create(
|
||||
creator=self.user,
|
||||
|
@ -887,7 +889,10 @@ class UserProfileTests(TestWithUser, WebTest):
|
|||
def test_request_when_profile_feature_off(self):
|
||||
"""test that Your profile is not in request page when profile feature is off"""
|
||||
|
||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
||||
contact_user, _ = Contact.objects.get_or_create(
|
||||
first_name="Hank",
|
||||
last_name="McFakerson",
|
||||
)
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
domain_request = DomainRequest.objects.create(
|
||||
creator=self.user,
|
||||
|
|
|
@ -1477,8 +1477,8 @@ class TestDomainContactInformation(TestDomainOverview):
|
|||
|
||||
def test_domain_your_contact_information_content(self):
|
||||
"""Logged-in user's contact information appears on the page."""
|
||||
self.user.contact.first_name = "Testy"
|
||||
self.user.contact.save()
|
||||
self.user.first_name = "Testy"
|
||||
self.user.save()
|
||||
page = self.app.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id}))
|
||||
self.assertContains(page, "Testy")
|
||||
|
||||
|
|
|
@ -2974,7 +2974,10 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
|
|||
)
|
||||
|
||||
# Attach a user object to a contact (should not be deleted)
|
||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
||||
contact_user, _ = Contact.objects.get_or_create(
|
||||
first_name="Hank",
|
||||
last_name="McFakey",
|
||||
)
|
||||
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
domain_request = DomainRequest.objects.create(
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -49,8 +49,10 @@ class AnalyticsView(View):
|
|||
"domain__permissions__isnull": False,
|
||||
"domain__first_ready__lte": end_date_formatted,
|
||||
}
|
||||
managed_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_managed_domains_start_date)
|
||||
managed_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_managed_domains_end_date)
|
||||
managed_domains_sliced_at_start_date = csv_export.DomainExport.get_sliced_domains(
|
||||
filter_managed_domains_start_date
|
||||
)
|
||||
managed_domains_sliced_at_end_date = csv_export.DomainExport.get_sliced_domains(filter_managed_domains_end_date)
|
||||
|
||||
filter_unmanaged_domains_start_date = {
|
||||
"domain__permissions__isnull": True,
|
||||
|
@ -60,8 +62,12 @@ class AnalyticsView(View):
|
|||
"domain__permissions__isnull": True,
|
||||
"domain__first_ready__lte": end_date_formatted,
|
||||
}
|
||||
unmanaged_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_unmanaged_domains_start_date)
|
||||
unmanaged_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_unmanaged_domains_end_date)
|
||||
unmanaged_domains_sliced_at_start_date = csv_export.DomainExport.get_sliced_domains(
|
||||
filter_unmanaged_domains_start_date
|
||||
)
|
||||
unmanaged_domains_sliced_at_end_date = csv_export.DomainExport.get_sliced_domains(
|
||||
filter_unmanaged_domains_end_date
|
||||
)
|
||||
|
||||
filter_ready_domains_start_date = {
|
||||
"domain__state__in": [models.Domain.State.READY],
|
||||
|
@ -71,8 +77,8 @@ class AnalyticsView(View):
|
|||
"domain__state__in": [models.Domain.State.READY],
|
||||
"domain__first_ready__lte": end_date_formatted,
|
||||
}
|
||||
ready_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_ready_domains_start_date)
|
||||
ready_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_ready_domains_end_date)
|
||||
ready_domains_sliced_at_start_date = csv_export.DomainExport.get_sliced_domains(filter_ready_domains_start_date)
|
||||
ready_domains_sliced_at_end_date = csv_export.DomainExport.get_sliced_domains(filter_ready_domains_end_date)
|
||||
|
||||
filter_deleted_domains_start_date = {
|
||||
"domain__state__in": [models.Domain.State.DELETED],
|
||||
|
@ -82,8 +88,10 @@ class AnalyticsView(View):
|
|||
"domain__state__in": [models.Domain.State.DELETED],
|
||||
"domain__deleted__lte": end_date_formatted,
|
||||
}
|
||||
deleted_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_deleted_domains_start_date)
|
||||
deleted_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_deleted_domains_end_date)
|
||||
deleted_domains_sliced_at_start_date = csv_export.DomainExport.get_sliced_domains(
|
||||
filter_deleted_domains_start_date
|
||||
)
|
||||
deleted_domains_sliced_at_end_date = csv_export.DomainExport.get_sliced_domains(filter_deleted_domains_end_date)
|
||||
|
||||
filter_requests_start_date = {
|
||||
"created_at__lte": start_date_formatted,
|
||||
|
@ -91,8 +99,8 @@ class AnalyticsView(View):
|
|||
filter_requests_end_date = {
|
||||
"created_at__lte": end_date_formatted,
|
||||
}
|
||||
requests_sliced_at_start_date = csv_export.get_sliced_requests(filter_requests_start_date)
|
||||
requests_sliced_at_end_date = csv_export.get_sliced_requests(filter_requests_end_date)
|
||||
requests_sliced_at_start_date = csv_export.DomainRequestExport.get_sliced_requests(filter_requests_start_date)
|
||||
requests_sliced_at_end_date = csv_export.DomainRequestExport.get_sliced_requests(filter_requests_end_date)
|
||||
|
||||
filter_submitted_requests_start_date = {
|
||||
"status": models.DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
|
@ -102,8 +110,12 @@ class AnalyticsView(View):
|
|||
"status": models.DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
"submission_date__lte": end_date_formatted,
|
||||
}
|
||||
submitted_requests_sliced_at_start_date = csv_export.get_sliced_requests(filter_submitted_requests_start_date)
|
||||
submitted_requests_sliced_at_end_date = csv_export.get_sliced_requests(filter_submitted_requests_end_date)
|
||||
submitted_requests_sliced_at_start_date = csv_export.DomainRequestExport.get_sliced_requests(
|
||||
filter_submitted_requests_start_date
|
||||
)
|
||||
submitted_requests_sliced_at_end_date = csv_export.DomainRequestExport.get_sliced_requests(
|
||||
filter_submitted_requests_end_date
|
||||
)
|
||||
|
||||
context = dict(
|
||||
# Generate a dictionary of context variables that are common across all admin templates
|
||||
|
@ -142,7 +154,7 @@ class ExportDataType(View):
|
|||
# match the CSV example with all the fields
|
||||
response = HttpResponse(content_type="text/csv")
|
||||
response["Content-Disposition"] = 'attachment; filename="domains-by-type.csv"'
|
||||
csv_export.export_data_type_to_csv(response)
|
||||
csv_export.DomainDataType.export_data_to_csv(response)
|
||||
return response
|
||||
|
||||
|
||||
|
@ -151,7 +163,7 @@ class ExportDataFull(View):
|
|||
# Smaller export based on 1
|
||||
response = HttpResponse(content_type="text/csv")
|
||||
response["Content-Disposition"] = 'attachment; filename="current-full.csv"'
|
||||
csv_export.export_data_full_to_csv(response)
|
||||
csv_export.DomainDataFull.export_data_to_csv(response)
|
||||
return response
|
||||
|
||||
|
||||
|
@ -160,7 +172,7 @@ class ExportDataFederal(View):
|
|||
# Federal only
|
||||
response = HttpResponse(content_type="text/csv")
|
||||
response["Content-Disposition"] = 'attachment; filename="current-federal.csv"'
|
||||
csv_export.export_data_federal_to_csv(response)
|
||||
csv_export.DomainDataFederal.export_data_to_csv(response)
|
||||
return response
|
||||
|
||||
|
||||
|
@ -171,63 +183,51 @@ class ExportDomainRequestDataFull(View):
|
|||
"""Returns a content disposition response for current-full-domain-request.csv"""
|
||||
response = HttpResponse(content_type="text/csv")
|
||||
response["Content-Disposition"] = 'attachment; filename="current-full-domain-request.csv"'
|
||||
csv_export.DomainRequestExport.export_full_domain_request_report(response)
|
||||
csv_export.DomainRequestDataFull.export_data_to_csv(response)
|
||||
return response
|
||||
|
||||
|
||||
class ExportDataDomainsGrowth(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Get start_date and end_date from the request's GET parameters
|
||||
# #999: not needed if we switch to django forms
|
||||
start_date = request.GET.get("start_date", "")
|
||||
end_date = request.GET.get("end_date", "")
|
||||
|
||||
response = HttpResponse(content_type="text/csv")
|
||||
response["Content-Disposition"] = f'attachment; filename="domain-growth-report-{start_date}-to-{end_date}.csv"'
|
||||
# For #999: set export_data_domain_growth_to_csv to return the resulting queryset, which we can then use
|
||||
# in context to display this data in the template.
|
||||
csv_export.export_data_domain_growth_to_csv(response, start_date, end_date)
|
||||
csv_export.DomainGrowth.export_data_to_csv(response, start_date, end_date)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class ExportDataRequestsGrowth(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Get start_date and end_date from the request's GET parameters
|
||||
# #999: not needed if we switch to django forms
|
||||
start_date = request.GET.get("start_date", "")
|
||||
end_date = request.GET.get("end_date", "")
|
||||
|
||||
response = HttpResponse(content_type="text/csv")
|
||||
response["Content-Disposition"] = f'attachment; filename="requests-{start_date}-to-{end_date}.csv"'
|
||||
# For #999: set export_data_domain_growth_to_csv to return the resulting queryset, which we can then use
|
||||
# in context to display this data in the template.
|
||||
csv_export.DomainRequestExport.export_data_requests_growth_to_csv(response, start_date, end_date)
|
||||
csv_export.DomainRequestGrowth.export_data_to_csv(response, start_date, end_date)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class ExportDataManagedDomains(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Get start_date and end_date from the request's GET parameters
|
||||
# #999: not needed if we switch to django forms
|
||||
start_date = request.GET.get("start_date", "")
|
||||
end_date = request.GET.get("end_date", "")
|
||||
response = HttpResponse(content_type="text/csv")
|
||||
response["Content-Disposition"] = f'attachment; filename="managed-domains-{start_date}-to-{end_date}.csv"'
|
||||
csv_export.export_data_managed_domains_to_csv(response, start_date, end_date)
|
||||
csv_export.DomainManaged.export_data_to_csv(response, start_date, end_date)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class ExportDataUnmanagedDomains(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Get start_date and end_date from the request's GET parameters
|
||||
# #999: not needed if we switch to django forms
|
||||
start_date = request.GET.get("start_date", "")
|
||||
end_date = request.GET.get("end_date", "")
|
||||
response = HttpResponse(content_type="text/csv")
|
||||
response["Content-Disposition"] = f'attachment; filename="unamanaged-domains-{start_date}-to-{end_date}.csv"'
|
||||
csv_export.export_data_unmanaged_domains_to_csv(response, start_date, end_date)
|
||||
response["Content-Disposition"] = f'attachment; filename="unmanaged-domains-{start_date}-to-{end_date}.csv"'
|
||||
csv_export.DomainUnmanaged.export_data_to_csv(response, start_date, end_date)
|
||||
|
||||
return response
|
||||
|
|
|
@ -40,7 +40,7 @@ from registrar.models.utility.contact_error import ContactError
|
|||
from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView
|
||||
|
||||
from ..forms import (
|
||||
ContactForm,
|
||||
UserForm,
|
||||
SeniorOfficialContactForm,
|
||||
DomainOrgNameAddressForm,
|
||||
DomainAddUserForm,
|
||||
|
@ -573,7 +573,7 @@ class DomainYourContactInformationView(DomainFormBaseView):
|
|||
"""Domain your contact information editing view."""
|
||||
|
||||
template_name = "domain_your_contact_information.html"
|
||||
form_class = ContactForm
|
||||
form_class = UserForm
|
||||
|
||||
@waffle_flag("!profile_feature") # type: ignore
|
||||
def dispatch(self, request, *args, **kwargs): # type: ignore
|
||||
|
@ -582,7 +582,7 @@ class DomainYourContactInformationView(DomainFormBaseView):
|
|||
def get_form_kwargs(self, *args, **kwargs):
|
||||
"""Add domain_info.submitter instance to make a bound form."""
|
||||
form_kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||
form_kwargs["instance"] = self.request.user.contact
|
||||
form_kwargs["instance"] = self.request.user
|
||||
return form_kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
|
|
|
@ -812,15 +812,15 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
|||
self.object = self.get_object()
|
||||
self.object.delete()
|
||||
|
||||
# Delete orphaned contacts - but only for if they are not associated with a user
|
||||
Contact.objects.filter(id__in=contacts_to_delete, user=None).delete()
|
||||
# Delete orphaned contacts
|
||||
Contact.objects.filter(id__in=contacts_to_delete).delete()
|
||||
|
||||
# After a delete occurs, do a second sweep on any returned duplicates.
|
||||
# This determines if any of these three fields share a contact, which is used for
|
||||
# the edge case where the same user may be an SO, and a submitter, for example.
|
||||
if len(duplicates) > 0:
|
||||
duplicates_to_delete, _ = self._get_orphaned_contacts(domain_request, check_db=True)
|
||||
Contact.objects.filter(id__in=duplicates_to_delete, user=None).delete()
|
||||
Contact.objects.filter(id__in=duplicates_to_delete).delete()
|
||||
|
||||
# Return a 200 response with an empty body
|
||||
return HttpResponse(status=200)
|
||||
|
|
|
@ -8,9 +8,6 @@ from django.http import QueryDict
|
|||
from django.views.generic.edit import FormMixin
|
||||
from registrar.forms.user_profile import UserProfileForm, FinishSetupProfileForm
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from registrar.models import (
|
||||
Contact,
|
||||
)
|
||||
from registrar.models.user import User
|
||||
from registrar.models.utility.generic_helper import replace_url_queryparams
|
||||
from registrar.views.utility.permission_views import UserProfilePermissionView
|
||||
|
@ -24,7 +21,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
|||
Base View for the User Profile. Handles getting and setting the User Profile
|
||||
"""
|
||||
|
||||
model = Contact
|
||||
model = User
|
||||
template_name = "profile.html"
|
||||
form_class = UserProfileForm
|
||||
base_view_name = "user-profile"
|
||||
|
@ -123,9 +120,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
|||
def get_object(self, queryset=None):
|
||||
"""Override get_object to return the logged-in user's contact"""
|
||||
self.user = self.request.user # get the logged in user
|
||||
if hasattr(self.user, "contact"): # Check if the user has a contact instance
|
||||
return self.user.contact
|
||||
return None
|
||||
return self.user
|
||||
|
||||
|
||||
class FinishProfileSetupView(UserProfileView):
|
||||
|
@ -134,7 +129,7 @@ class FinishProfileSetupView(UserProfileView):
|
|||
|
||||
template_name = "finish_profile_setup.html"
|
||||
form_class = FinishSetupProfileForm
|
||||
model = Contact
|
||||
model = User
|
||||
|
||||
base_view_name = "finish-user-profile-setup"
|
||||
|
||||
|
@ -160,11 +155,11 @@ class FinishProfileSetupView(UserProfileView):
|
|||
# Get the current form and validate it
|
||||
if form.is_valid():
|
||||
self.redirect_page = False
|
||||
if "contact_setup_save_button" in request.POST:
|
||||
if "user_setup_save_button" in request.POST:
|
||||
# Logic for when the 'Save' button is clicked, which indicates
|
||||
# user should stay on this page
|
||||
self.redirect_page = False
|
||||
elif "contact_setup_submit_button" in request.POST:
|
||||
elif "user_setup_submit_button" in request.POST:
|
||||
# Logic for when the other button is clicked, which indicates
|
||||
# the user should be taken to the redirect page
|
||||
self.redirect_page = True
|
||||
|
|
|
@ -4,7 +4,7 @@ import abc # abstract base class
|
|||
|
||||
from django.views.generic import DetailView, DeleteView, TemplateView
|
||||
from registrar.models import Domain, DomainRequest, DomainInvitation
|
||||
from registrar.models.contact import Contact
|
||||
from registrar.models.user import User
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
|
||||
from .mixins import (
|
||||
|
@ -154,9 +154,9 @@ class UserProfilePermissionView(UserProfilePermission, DetailView, abc.ABC):
|
|||
"""
|
||||
|
||||
# DetailView property for what model this is viewing
|
||||
model = Contact
|
||||
model = User
|
||||
# variable name in template context for the model object
|
||||
context_object_name = "contact"
|
||||
context_object_name = "user"
|
||||
|
||||
# Abstract property enforces NotImplementedError on an attribute.
|
||||
@property
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue