diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml index 214cf6076..f2b4303d6 100644 --- a/.github/workflows/deploy-sandbox.yaml +++ b/.github/workflows/deploy-sandbox.yaml @@ -25,6 +25,8 @@ jobs: || startsWith(github.head_ref, 'meoward/') || startsWith(github.head_ref, 'bob/') || startsWith(github.head_ref, 'cb/') + || startsWith(github.head_ref, 'hotgov/') + || startsWith(github.head_ref, 'litterbox/') outputs: environment: ${{ steps.var.outputs.environment}} runs-on: "ubuntu-latest" diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index f5815012c..283380236 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -16,6 +16,8 @@ on: - stable - staging - development + - litterbox + - hotgov - cb - bob - meoward diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index 06638aa05..b9393415b 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -16,6 +16,8 @@ on: options: - staging - development + - litterbox + - hotgov - cb - bob - meoward 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/ops/manifests/manifest-hotgov.yaml b/ops/manifests/manifest-hotgov.yaml new file mode 100644 index 000000000..70cc97ee7 --- /dev/null +++ b/ops/manifests/manifest-hotgov.yaml @@ -0,0 +1,32 @@ +--- +applications: +- name: getgov-hotgov + buildpacks: + - python_buildpack + path: ../../src + instances: 1 + memory: 512M + stack: cflinuxfs4 + timeout: 180 + command: ./run.sh + health-check-type: http + health-check-http-endpoint: /health + health-check-invocation-timeout: 40 + env: + # Send stdout and stderr straight to the terminal without buffering + PYTHONUNBUFFERED: yup + # Tell Django where to find its configuration + DJANGO_SETTINGS_MODULE: registrar.config.settings + # Tell Django where it is being hosted + DJANGO_BASE_URL: https://getgov-hotgov.app.cloud.gov + # Tell Django how much stuff to log + DJANGO_LOG_LEVEL: INFO + # default public site location + GETGOV_PUBLIC_SITE_URL: https://get.gov + # Flag to disable/enable features in prod environments + IS_PRODUCTION: False + routes: + - route: getgov-hotgov.app.cloud.gov + services: + - getgov-credentials + - getgov-hotgov-database diff --git a/ops/manifests/manifest-litterbox.yaml b/ops/manifests/manifest-litterbox.yaml new file mode 100644 index 000000000..ae899ef3a --- /dev/null +++ b/ops/manifests/manifest-litterbox.yaml @@ -0,0 +1,32 @@ +--- +applications: +- name: getgov-litterbox + buildpacks: + - python_buildpack + path: ../../src + instances: 1 + memory: 512M + stack: cflinuxfs4 + timeout: 180 + command: ./run.sh + health-check-type: http + health-check-http-endpoint: /health + health-check-invocation-timeout: 40 + env: + # Send stdout and stderr straight to the terminal without buffering + PYTHONUNBUFFERED: yup + # Tell Django where to find its configuration + DJANGO_SETTINGS_MODULE: registrar.config.settings + # Tell Django where it is being hosted + DJANGO_BASE_URL: https://getgov-litterbox.app.cloud.gov + # Tell Django how much stuff to log + DJANGO_LOG_LEVEL: INFO + # default public site location + GETGOV_PUBLIC_SITE_URL: https://get.gov + # Flag to disable/enable features in prod environments + IS_PRODUCTION: False + routes: + - route: getgov-litterbox.app.cloud.gov + services: + - getgov-credentials + - getgov-litterbox-database diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 229680d45..43ef9e0fa 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -383,6 +383,39 @@ class CustomLogEntryAdmin(LogEntryAdmin): change_form_template = "admin/change_form_no_submit.html" add_form_template = "admin/change_form_no_submit.html" + # Select log entry to change -> Log entries + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Log entries" + return super().changelist_view(request, extra_context=extra_context) + + # #786: Skipping on updating audit log tab titles for now + # def change_view(self, request, object_id, form_url="", extra_context=None): + # if extra_context is None: + # extra_context = {} + + # log_entry = self.get_object(request, object_id) + + # if log_entry: + # # Reset title to empty string + # extra_context["subtitle"] = "" + # extra_context["tabtitle"] = "" + + # object_repr = log_entry.object_repr # Hold name of the object + # changes = log_entry.changes + + # # Check if this is a log entry for an addition and related to the contact model + # # Created [name] -> Created [name] contact | Change log entry + # if ( + # all(new_value != "None" for field, (old_value, new_value) in changes.items()) + # and log_entry.content_type.model == "contact" + # ): + # extra_context["subtitle"] = f"Created {object_repr} contact" + # extra_context["tabtitle"] = "Change log entry" + + # return super().change_view(request, object_id, form_url, extra_context=extra_context) + class AdminSortFields: _name_sort = ["first_name", "last_name", "email"] @@ -594,7 +627,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 +658,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", { @@ -778,6 +811,14 @@ class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin): search_help_text = "Search by domain or host name." inlines = [HostIPInline] + # Select host to change -> Host + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Host" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + class HostIpResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -793,6 +834,14 @@ class HostIpAdmin(AuditedAdmin, ImportExportModelAdmin): resource_classes = [HostIpResource] model = models.HostIP + # Select host ip to change -> Host ip + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Host IP" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + class ContactResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -926,6 +975,14 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin): return super().change_view(request, object_id, form_url, extra_context=extra_context) + # Select contact to change -> Contacts + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Contacts" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + class WebsiteResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -1043,6 +1100,21 @@ class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin): else: return response + # User Domain manager [email] is manager on domain [domain name] -> + # Domain manager [email] on [domain name] + def changeform_view(self, request, object_id=None, form_url="", extra_context=None): + if extra_context is None: + extra_context = {} + + if object_id: + obj = self.get_object(request, object_id) + if obj: + email = obj.user.email + domain_name = obj.domain.name + extra_context["subtitle"] = f"Domain manager {email} on {domain_name}" + + return super().changeform_view(request, object_id, form_url, extra_context=extra_context) + class DomainInvitationAdmin(ListHeaderAdmin): """Custom domain invitation admin class.""" @@ -1079,6 +1151,14 @@ class DomainInvitationAdmin(ListHeaderAdmin): change_form_template = "django/admin/email_clipboard_change_form.html" + # Select domain invitations to change -> Domain invitations + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Domain invitations" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + class DomainInformationResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -1219,6 +1299,14 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin): readonly_fields.extend([field for field in self.analyst_readonly_fields]) return readonly_fields # Read-only fields for analysts + # Select domain information to change -> Domain information + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Domain information" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + class DomainRequestResource(FsmModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -1678,11 +1766,17 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): if next_char.isdigit(): should_apply_default_filter = True + # Select domain request to change -> Domain requests + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Domain requests" + if should_apply_default_filter: # modify the GET of the request to set the selected filter modified_get = copy.deepcopy(request.GET) modified_get["status__in"] = "submitted,in review,action needed" request.GET = modified_get + response = super().changelist_view(request, extra_context=extra_context) return response @@ -2248,6 +2342,14 @@ class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin): # If no redirection is needed, return the original response return response + # Select draft domain to change -> Draft domains + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "Draft domains" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + class PublicContactResource(resources.ModelResource): """defines how each field in the referenced model should be mapped to the corresponding fields in the @@ -2292,6 +2394,20 @@ class PublicContactAdmin(ListHeaderAdmin, ImportExportModelAdmin): change_form_template = "django/admin/email_clipboard_change_form.html" autocomplete_fields = ["domain"] + def changeform_view(self, request, object_id=None, form_url="", extra_context=None): + if extra_context is None: + extra_context = {} + + if object_id: + obj = self.get_object(request, object_id) + if obj: + name = obj.name + email = obj.email + registry_id = obj.registry_id + extra_context["subtitle"] = f"{name} <{email}> id: {registry_id}" + + return super().changeform_view(request, object_id, form_url, extra_context=extra_context) + class VerifiedByStaffAdmin(ListHeaderAdmin): list_display = ("email", "requestor", "truncated_notes", "created_at") @@ -2344,6 +2460,14 @@ class UserGroupAdmin(AuditedAdmin): def user_group(self, obj): return obj.name + # Select user groups to change -> User groups + def changelist_view(self, request, extra_context=None): + if extra_context is None: + extra_context = {} + extra_context["tabtitle"] = "User groups" + # Get the filtered values + return super().changelist_view(request, extra_context=extra_context) + class WaffleFlagAdmin(FlagAdmin): """Custom admin implementation of django-waffle's Flag class""" diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 851f3550c..9a6792dc7 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -659,6 +659,8 @@ ALLOWED_HOSTS = [ "getgov-stable.app.cloud.gov", "getgov-staging.app.cloud.gov", "getgov-development.app.cloud.gov", + "getgov-litterbox.app.cloud.gov", + "getgov-hotgov.app.cloud.gov", "getgov-cb.app.cloud.gov", "getgov-bob.app.cloud.gov", "getgov-meoward.app.cloud.gov", 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/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 91a7515c7..f94938dd1 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -123,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_request.py b/src/registrar/models/domain_request.py index 137335f94..4bf683c2f 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -43,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)" diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 705d2011c..bb0276607 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -87,7 +87,6 @@ class User(AbstractUser): phone = PhoneNumberField( null=True, blank=True, - help_text="Phone", ) middle_name = models.CharField( 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/templates/admin/base_site.html b/src/registrar/templates/admin/base_site.html index dd680cec5..db34fd893 100644 --- a/src/registrar/templates/admin/base_site.html +++ b/src/registrar/templates/admin/base_site.html @@ -26,10 +26,21 @@ {% endblock %} -{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} +{% block title %} + {% if subtitle %} + {{ subtitle }} | + {% endif %} + {% if tabtitle %} + {{ tabtitle }} | + {% else %} + {{ title }} | + {% endif %} + {{ site_title|default:_('Django site admin') }} +{% endblock %} + {% block extrastyle %}{{ block.super }} -{% endblock %} +{% endblock %} {% block header %} {% if not IS_PRODUCTION %} diff --git a/src/registrar/templates/domain_authorizing_official.html b/src/registrar/templates/domain_authorizing_official.html index aa9808c2e..e5f2f09ba 100644 --- a/src/registrar/templates/domain_authorizing_official.html +++ b/src/registrar/templates/domain_authorizing_official.html @@ -1,7 +1,7 @@ {% extends "domain_base.html" %} {% load static field_helpers url_helpers %} -{% block title %}Domain authorizing official | {{ domain.name }} | {% endblock %} +{% block title %}Authorizing official | {{ domain.name }} | {% endblock %} {% block domain_content %} {# this is right after the messages block in the parent template #} diff --git a/src/registrar/templates/domain_base.html b/src/registrar/templates/domain_base.html index 90c730028..9a869ef42 100644 --- a/src/registrar/templates/domain_base.html +++ b/src/registrar/templates/domain_base.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% load static %} -{% block title %}Domain: {{ domain.name }} | {% endblock %} +{% block title %}{{ domain.name }} | {% endblock %} {% block content %}
diff --git a/src/registrar/templates/domain_request_intro.html b/src/registrar/templates/domain_request_intro.html index 285777a80..2bfeeeef1 100644 --- a/src/registrar/templates/domain_request_intro.html +++ b/src/registrar/templates/domain_request_intro.html @@ -1,6 +1,8 @@ {% extends 'base.html' %} {% load static form_helpers url_helpers %} +{% block title %} Start a request | {% endblock %} + {% block content %}
diff --git a/src/registrar/templates/includes/gov_extended_logo.html b/src/registrar/templates/includes/gov_extended_logo.html index f08aa2c85..be6aba16b 100644 --- a/src/registrar/templates/includes/gov_extended_logo.html +++ b/src/registrar/templates/includes/gov_extended_logo.html @@ -3,7 +3,7 @@