diff --git a/docs/developer/README.md b/docs/developer/README.md index 7519da7a9..72f6b9f20 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -320,16 +320,6 @@ it may help to resync your laptop with time.nist.gov: sudo sntp -sS time.nist.gov ``` -### Settings -The config for the connection pool exists inside the `settings.py` file. -| Name | Purpose | -| ------------------------ | ------------------------------------------------------------------------------------------------- | -| EPP_CONNECTION_POOL_SIZE | Determines the number of concurrent sockets that should exist in the pool. | -| POOL_KEEP_ALIVE | Determines the interval in which we ping open connections in seconds. Calculated as POOL_KEEP_ALIVE / EPP_CONNECTION_POOL_SIZE | -| POOL_TIMEOUT | Determines how long we try to keep a pool alive for, before restarting it. | - -Consider updating the `POOL_TIMEOUT` or `POOL_KEEP_ALIVE` periods if the pool often restarts. If the pool only restarts after a period of inactivity, update `POOL_KEEP_ALIVE`. If it restarts during the EPP call itself, then `POOL_TIMEOUT` needs to be updated. - ## Adding a S3 instance to your sandbox This can either be done through the CLI, or through the cloud.gov dashboard. Generally, it is better to do it through the dashboard as it handles app binding for you. diff --git a/docs/operations/data_migration.md b/docs/operations/data_migration.md index e4543a28c..472362a79 100644 --- a/docs/operations/data_migration.md +++ b/docs/operations/data_migration.md @@ -668,3 +668,32 @@ Example: `cf ssh getgov-za` #### Step 1: Running the script ```docker-compose exec app ./manage.py populate_verification_type``` + + +## Copy names from contacts to users + +### Running on sandboxes + +#### Step 1: Login to CloudFoundry +```cf login -a api.fr.cloud.gov --sso``` + +#### Step 2: SSH into your environment +```cf ssh getgov-{space}``` + +Example: `cf ssh getgov-za` + +#### Step 3: Create a shell instance +```/tmp/lifecycle/shell``` + +#### Step 4: Running the script +```./manage.py copy_names_from_contacts_to_users --debug``` + +### Running locally + +#### Step 1: Running the script +```docker-compose exec app ./manage.py copy_names_from_contacts_to_users --debug``` + +##### Optional parameters +| | Parameter | Description | +|:-:|:-------------------------- |:----------------------------------------------------------------------------| +| 1 | **debug** | Increases logging detail. Defaults to False. | diff --git a/src/registrar/admin.py b/src/registrar/admin.py index f782f7c62..22022dd94 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -594,7 +594,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): None, {"fields": ("username", "password", "status", "verification_type")}, ), - ("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "email", "title")}), + ("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}), ( "Permissions", { @@ -625,7 +625,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): ) }, ), - ("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "email", "title")}), + ("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}), ( "Permissions", { diff --git a/src/registrar/management/commands/copy_names_from_contacts_to_users.py b/src/registrar/management/commands/copy_names_from_contacts_to_users.py index 50e1bea3d..384029400 100644 --- a/src/registrar/management/commands/copy_names_from_contacts_to_users.py +++ b/src/registrar/management/commands/copy_names_from_contacts_to_users.py @@ -10,6 +10,7 @@ from registrar.management.commands.utility.terminal_helper import ( ) from registrar.models.contact import Contact from registrar.models.user import User +from registrar.models.utility.domain_helper import DomainHelper logger = logging.getLogger(__name__) @@ -110,15 +111,21 @@ class Command(BaseCommand): {TerminalColors.ENDC}""", # noqa ) - # ---- UPDATE THE USER IF IT DOES NOT HAVE A FIRST AND LAST NAMES - # ---- LET'S KEEP A LIGHT TOUCH - if not eligible_user.first_name and not eligible_user.last_name: - # (expression has type "str | None", variable has type "str | int | Combinable") - # so we'll ignore type - eligible_user.first_name = contact.first_name # type: ignore - eligible_user.last_name = contact.last_name # type: ignore - eligible_user.save() - processed_user = eligible_user + # 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, diff --git a/src/registrar/migrations/0096_alter_contact_email_alter_contact_first_name_and_more.py b/src/registrar/migrations/0096_alter_contact_email_alter_contact_first_name_and_more.py new file mode 100644 index 000000000..68cbc625b --- /dev/null +++ b/src/registrar/migrations/0096_alter_contact_email_alter_contact_first_name_and_more.py @@ -0,0 +1,131 @@ +# Generated by Django 4.2.10 on 2024-05-28 14:40 + +from django.db import migrations, models +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0095_user_middle_name_user_title"), + ] + + operations = [ + migrations.AlterField( + model_name="contact", + name="email", + field=models.EmailField(blank=True, max_length=320, null=True), + ), + migrations.AlterField( + model_name="contact", + name="first_name", + field=models.CharField(blank=True, null=True, verbose_name="first name"), + ), + migrations.AlterField( + model_name="contact", + name="last_name", + field=models.CharField(blank=True, null=True, verbose_name="last name"), + ), + migrations.AlterField( + model_name="contact", + name="phone", + field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None), + ), + migrations.AlterField( + model_name="domaininformation", + name="organization_name", + field=models.CharField(blank=True, null=True), + ), + migrations.AlterField( + model_name="domaininformation", + name="zipcode", + field=models.CharField(blank=True, max_length=10, null=True, verbose_name="zip code"), + ), + migrations.AlterField( + model_name="domainrequest", + name="organization_name", + field=models.CharField(blank=True, null=True), + ), + migrations.AlterField( + model_name="domainrequest", + name="zipcode", + field=models.CharField(blank=True, max_length=10, null=True, verbose_name="zip code"), + ), + migrations.AlterField( + model_name="transitiondomain", + name="first_name", + field=models.CharField( + blank=True, help_text="First name / given name", null=True, verbose_name="first name" + ), + ), + migrations.AlterField( + model_name="transitiondomain", + name="organization_name", + field=models.CharField(blank=True, help_text="Organization name", null=True), + ), + migrations.AlterField( + model_name="transitiondomain", + name="zipcode", + field=models.CharField(blank=True, help_text="Zip code", max_length=10, null=True, verbose_name="zip code"), + ), + migrations.AlterField( + model_name="user", + name="phone", + field=phonenumber_field.modelfields.PhoneNumberField( + blank=True, help_text="Phone", max_length=128, null=True, region=None + ), + ), + migrations.AlterField( + model_name="verifiedbystaff", + name="email", + field=models.EmailField(max_length=254), + ), + migrations.AddIndex( + model_name="contact", + index=models.Index(fields=["user"], name="registrar_c_user_id_4059c4_idx"), + ), + migrations.AddIndex( + model_name="contact", + index=models.Index(fields=["email"], name="registrar_c_email_bde2de_idx"), + ), + migrations.AddIndex( + model_name="domain", + index=models.Index(fields=["name"], name="registrar_d_name_5b1956_idx"), + ), + migrations.AddIndex( + model_name="domain", + index=models.Index(fields=["state"], name="registrar_d_state_84c134_idx"), + ), + migrations.AddIndex( + model_name="domaininformation", + index=models.Index(fields=["domain"], name="registrar_d_domain__88838a_idx"), + ), + migrations.AddIndex( + model_name="domaininformation", + index=models.Index(fields=["domain_request"], name="registrar_d_domain__d1fba8_idx"), + ), + migrations.AddIndex( + model_name="domaininvitation", + index=models.Index(fields=["status"], name="registrar_d_status_e84571_idx"), + ), + migrations.AddIndex( + model_name="domainrequest", + index=models.Index(fields=["requested_domain"], name="registrar_d_request_6894eb_idx"), + ), + migrations.AddIndex( + model_name="domainrequest", + index=models.Index(fields=["approved_domain"], name="registrar_d_approve_ac4c46_idx"), + ), + migrations.AddIndex( + model_name="domainrequest", + index=models.Index(fields=["status"], name="registrar_d_status_a32b59_idx"), + ), + migrations.AddIndex( + model_name="user", + index=models.Index(fields=["username"], name="registrar_u_usernam_964b1b_idx"), + ), + migrations.AddIndex( + model_name="user", + index=models.Index(fields=["email"], name="registrar_u_email_c8f2c4_idx"), + ), + ] diff --git a/src/registrar/migrations/0097_alter_user_phone.py b/src/registrar/migrations/0097_alter_user_phone.py new file mode 100644 index 000000000..dfa5cfba8 --- /dev/null +++ b/src/registrar/migrations/0097_alter_user_phone.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.10 on 2024-06-06 18:38 + +from django.db import migrations +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0096_alter_contact_email_alter_contact_first_name_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="phone", + field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None), + ), + ] diff --git a/src/registrar/migrations/0098_alter_domainrequest_status.py b/src/registrar/migrations/0098_alter_domainrequest_status.py new file mode 100644 index 000000000..19fa1ded2 --- /dev/null +++ b/src/registrar/migrations/0098_alter_domainrequest_status.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.10 on 2024-06-07 15:27 + +from django.db import migrations +import django_fsm + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0097_alter_user_phone"), + ] + + operations = [ + migrations.AlterField( + model_name="domainrequest", + name="status", + field=django_fsm.FSMField( + choices=[ + ("in review", "In review"), + ("action needed", "Action needed"), + ("approved", "Approved"), + ("rejected", "Rejected"), + ("ineligible", "Ineligible"), + ("submitted", "Submitted"), + ("withdrawn", "Withdrawn"), + ("started", "Started"), + ], + default="started", + max_length=50, + ), + ), + ] diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index a5a6ff16c..f94938dd1 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -17,6 +17,14 @@ class Contact(TimeStampedModel): 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, @@ -28,7 +36,6 @@ class Contact(TimeStampedModel): null=True, blank=True, verbose_name="first name", - db_index=True, ) middle_name = models.CharField( null=True, @@ -38,7 +45,6 @@ class Contact(TimeStampedModel): null=True, blank=True, verbose_name="last name", - db_index=True, ) title = models.CharField( null=True, @@ -48,13 +54,11 @@ class Contact(TimeStampedModel): email = models.EmailField( null=True, blank=True, - db_index=True, max_length=320, ) phone = PhoneNumberField( null=True, blank=True, - db_index=True, ) def _get_all_relations(self): @@ -119,11 +123,21 @@ class Contact(TimeStampedModel): 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() diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index bc9508f30..26dcb89a7 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -65,6 +65,14 @@ class Domain(TimeStampedModel, DomainHelper): domain meets the required checks. """ + class Meta: + """Contains meta information about this class""" + + indexes = [ + models.Index(fields=["name"]), + models.Index(fields=["state"]), + ] + def __init__(self, *args, **kwargs): self._cache = {} super(Domain, self).__init__(*args, **kwargs) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 264e322b8..23c9e4f32 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -22,6 +22,16 @@ class DomainInformation(TimeStampedModel): the domain request once approved, so copying them that way we can make changes after its approved. Most fields here are copied from DomainRequest.""" + class Meta: + """Contains meta information about this class""" + + indexes = [ + models.Index(fields=["domain"]), + models.Index(fields=["domain_request"]), + ] + + verbose_name_plural = "Domain information" + StateTerritoryChoices = DomainRequest.StateTerritoryChoices # use the short names in Django admin @@ -111,7 +121,6 @@ class DomainInformation(TimeStampedModel): organization_name = models.CharField( null=True, blank=True, - db_index=True, ) address_line1 = models.CharField( null=True, @@ -138,7 +147,6 @@ class DomainInformation(TimeStampedModel): max_length=10, null=True, blank=True, - db_index=True, verbose_name="zip code", ) urbanization = models.CharField( @@ -336,6 +344,3 @@ class DomainInformation(TimeStampedModel): def _get_many_to_many_fields(): """Returns a set of each field.name that has the many to many relation""" return {field.name for field in DomainInformation._meta.many_to_many} # type: ignore - - class Meta: - verbose_name_plural = "Domain information" diff --git a/src/registrar/models/domain_invitation.py b/src/registrar/models/domain_invitation.py index 12082142d..c9cbc8b39 100644 --- a/src/registrar/models/domain_invitation.py +++ b/src/registrar/models/domain_invitation.py @@ -15,6 +15,13 @@ logger = logging.getLogger(__name__) class DomainInvitation(TimeStampedModel): + class Meta: + """Contains meta information about this class""" + + indexes = [ + models.Index(fields=["status"]), + ] + # Constants for status field class DomainInvitationStatus(models.TextChoices): INVITED = "invited", "Invited" diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index d4bdeea66..994fd85eb 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -25,6 +25,15 @@ logger = logging.getLogger(__name__) class DomainRequest(TimeStampedModel): """A registrant's domain request for a new domain.""" + class Meta: + """Contains meta information about this class""" + + indexes = [ + models.Index(fields=["requested_domain"]), + models.Index(fields=["approved_domain"]), + models.Index(fields=["status"]), + ] + # https://django-auditlog.readthedocs.io/en/latest/usage.html#object-history # If we note any performace degradation due to this addition, # we can query the auditlogs table in admin.py and add the results to @@ -34,14 +43,14 @@ class DomainRequest(TimeStampedModel): # Constants for choice fields class DomainRequestStatus(models.TextChoices): - STARTED = "started", "Started" - SUBMITTED = "submitted", "Submitted" IN_REVIEW = "in review", "In review" ACTION_NEEDED = "action needed", "Action needed" APPROVED = "approved", "Approved" - WITHDRAWN = "withdrawn", "Withdrawn" REJECTED = "rejected", "Rejected" INELIGIBLE = "ineligible", "Ineligible" + SUBMITTED = "submitted", "Submitted" + WITHDRAWN = "withdrawn", "Withdrawn" + STARTED = "started", "Started" class StateTerritoryChoices(models.TextChoices): ALABAMA = "AL", "Alabama (AL)" @@ -331,7 +340,6 @@ class DomainRequest(TimeStampedModel): organization_name = models.CharField( null=True, blank=True, - db_index=True, ) address_line1 = models.CharField( @@ -360,7 +368,6 @@ class DomainRequest(TimeStampedModel): null=True, blank=True, verbose_name="zip code", - db_index=True, ) urbanization = models.CharField( null=True, diff --git a/src/registrar/models/transition_domain.py b/src/registrar/models/transition_domain.py index 2dafd6da4..0b0cffcec 100644 --- a/src/registrar/models/transition_domain.py +++ b/src/registrar/models/transition_domain.py @@ -59,7 +59,6 @@ class TransitionDomain(TimeStampedModel): null=True, blank=True, help_text="Organization name", - db_index=True, ) federal_type = models.CharField( max_length=50, @@ -85,7 +84,6 @@ class TransitionDomain(TimeStampedModel): blank=True, help_text="First name / given name", verbose_name="first name", - db_index=True, ) middle_name = models.CharField( null=True, @@ -136,7 +134,6 @@ class TransitionDomain(TimeStampedModel): blank=True, verbose_name="zip code", help_text="Zip code", - db_index=True, ) def __str__(self): diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 005037925..bb0276607 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -31,6 +31,17 @@ class User(AbstractUser): will be updated if any updates are made to it through Login.gov. """ + class Meta: + indexes = [ + models.Index(fields=["username"]), + models.Index(fields=["email"]), + ] + + permissions = [ + ("analyst_access_permission", "Analyst Access Permission"), + ("full_access_permission", "Full Access Permission"), + ] + class VerificationTypeChoices(models.TextChoices): """ Users achieve access to our system in a few different ways. @@ -76,8 +87,6 @@ class User(AbstractUser): phone = PhoneNumberField( null=True, blank=True, - help_text="Phone", - db_index=True, ) middle_name = models.CharField( @@ -281,9 +290,3 @@ class User(AbstractUser): """ self.check_domain_invitations_on_login() - - class Meta: - permissions = [ - ("analyst_access_permission", "Analyst Access Permission"), - ("full_access_permission", "Full Access Permission"), - ] diff --git a/src/registrar/models/verified_by_staff.py b/src/registrar/models/verified_by_staff.py index c09dce822..1e3e21057 100644 --- a/src/registrar/models/verified_by_staff.py +++ b/src/registrar/models/verified_by_staff.py @@ -9,7 +9,6 @@ class VerifiedByStaff(TimeStampedModel): email = models.EmailField( null=False, blank=False, - db_index=True, ) requestor = models.ForeignKey( diff --git a/src/registrar/signals.py b/src/registrar/signals.py index 4e7768ef4..bc0480b2a 100644 --- a/src/registrar/signals.py +++ b/src/registrar/signals.py @@ -24,9 +24,11 @@ def handle_profile(sender, instance, **kwargs): """ 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) @@ -39,9 +41,11 @@ def handle_profile(sender, instance, **kwargs): 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 diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index e2d390471..79c545420 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -3534,7 +3534,7 @@ class TestMyUserAdmin(TestCase): ) }, ), - ("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "email", "title")}), + ("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}), ("Permissions", {"fields": ("is_active", "groups")}), ("Important dates", {"fields": ("last_login", "date_joined")}), ) diff --git a/src/registrar/tests/test_copy_names_from_contacts_to_users.py b/src/registrar/tests/test_copy_names_from_contacts_to_users.py index 032203f4e..7fcbede1e 100644 --- a/src/registrar/tests/test_copy_names_from_contacts_to_users.py +++ b/src/registrar/tests/test_copy_names_from_contacts_to_users.py @@ -23,15 +23,32 @@ class TestDataUpdates(TestCase): self.bs_user = User.objects.create() self.contact1 = Contact.objects.create( - user=self.user1, email="email1@igorville.gov", first_name="first1", last_name="last1" + 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" + 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" + 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.contact4 = Contact.objects.create(email="email4@igorville.gov", first_name="first4", last_name="last4") self.command = Command() @@ -42,14 +59,15 @@ class TestDataUpdates(TestCase): Contact.objects.all().delete() def test_script_updates_linked_users(self): - """Test the script that copies contacts' first and last names into associated users that - are eligible (first or last are blank or undefined)""" + """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 @@ -87,12 +105,20 @@ class TestDataUpdates(TestCase): # 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") - # The user that has a first but no last will be left alone - self.assertEqual(self.user2.first_name, "First name but no last name") - self.assertEqual(self.user2.last_name, "") - # The user that has a first and a last will be left alone - self.assertEqual(self.user3.first_name, "An existing first name") - self.assertEqual(self.user3.last_name, "An existing last name") + 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) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 283c884f9..3f6cc6b0e 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -455,7 +455,6 @@ def export_data_full_to_csv(csv_file): def export_data_federal_to_csv(csv_file): """Federal domains report""" - writer = csv.writer(csv_file) # define columns to include in export columns = [