diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml index 583791fc1..e884c60a0 100644 --- a/.github/workflows/deploy-sandbox.yaml +++ b/.github/workflows/deploy-sandbox.yaml @@ -21,6 +21,7 @@ jobs: || startsWith(github.head_ref, 'dk/') || startsWith(github.head_ref, 'es/') || startsWith(github.head_ref, 'ky/') + || startsWith(github.head_ref, 'backup/') outputs: environment: ${{ steps.var.outputs.environment}} runs-on: "ubuntu-latest" @@ -46,9 +47,8 @@ jobs: working-directory: ./src run: docker compose run app python manage.py collectstatic --no-input - name: Deploy to cloud.gov sandbox - uses: 18f/cg-deploy-action@main + uses: cloud-gov/cg-cli-tools@main env: - DEPLOY_NOW: thanks ENVIRONMENT: ${{ needs.variables.outputs.environment }} CF_USERNAME: CF_${{ needs.variables.outputs.environment }}_USERNAME CF_PASSWORD: CF_${{ needs.variables.outputs.environment }}_PASSWORD @@ -57,7 +57,7 @@ jobs: cf_password: ${{ secrets[env.CF_PASSWORD] }} cf_org: cisa-dotgov cf_space: ${{ env.ENVIRONMENT }} - push_arguments: "-f ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml" + cf_manifest: ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml comment: runs-on: ubuntu-latest needs: [variables, deploy] diff --git a/.github/workflows/deploy-stable.yaml b/.github/workflows/deploy-stable.yaml index 0a40ac097..1e643ef9a 100644 --- a/.github/workflows/deploy-stable.yaml +++ b/.github/workflows/deploy-stable.yaml @@ -30,12 +30,10 @@ jobs: working-directory: ./src run: docker compose run app python manage.py collectstatic --no-input - name: Deploy to cloud.gov sandbox - uses: 18f/cg-deploy-action@main - env: - DEPLOY_NOW: thanks + uses: cloud-gov/cg-cli-tools@main with: cf_username: ${{ secrets.CF_STABLE_USERNAME }} cf_password: ${{ secrets.CF_STABLE_PASSWORD }} cf_org: cisa-dotgov cf_space: stable - push_arguments: "-f ops/manifests/manifest-stable.yaml" + cf_manifest: "ops/manifests/manifest-stable.yaml" diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index 1db63e2a2..fa4543637 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -9,7 +9,7 @@ on: - 'docs/**' - '**.md' - '.gitignore' - + tags: - staging-* @@ -30,12 +30,10 @@ jobs: working-directory: ./src run: docker compose run app python manage.py collectstatic --no-input - name: Deploy to cloud.gov sandbox - uses: 18f/cg-deploy-action@main - env: - DEPLOY_NOW: thanks + uses: cloud-gov/cg-cli-tools@main with: cf_username: ${{ secrets.CF_STAGING_USERNAME }} cf_password: ${{ secrets.CF_STAGING_PASSWORD }} cf_org: cisa-dotgov cf_space: staging - push_arguments: "-f ops/manifests/manifest-staging.yaml" + cf_manifest: "ops/manifests/manifest-staging.yaml" diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index b1880c830..8523af013 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -16,6 +16,7 @@ on: - stable - staging - development + - backup - ky - es - nl @@ -37,10 +38,10 @@ jobs: CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD steps: - name: Run Django migrations for ${{ github.event.inputs.environment }} - uses: 18f/cg-deploy-action@main + uses: cloud-gov/cg-cli-tools@main with: cf_username: ${{ secrets[env.CF_USERNAME] }} cf_password: ${{ secrets[env.CF_PASSWORD] }} cf_org: cisa-dotgov cf_space: ${{ github.event.inputs.environment }} - full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py migrate' --name migrate" + cf_command: "run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py migrate' --name migrate" diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index a28270a22..3848a33bd 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -16,6 +16,7 @@ on: options: - staging - development + - backup - ky - es - nl @@ -37,28 +38,28 @@ jobs: CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD steps: - name: Delete existing data for ${{ github.event.inputs.environment }} - uses: 18f/cg-deploy-action@main + uses: cloud-gov/cg-cli-tools@main with: cf_username: ${{ secrets[env.CF_USERNAME] }} cf_password: ${{ secrets[env.CF_PASSWORD] }} cf_org: cisa-dotgov cf_space: ${{ github.event.inputs.environment }} - full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py flush --no-input' --name flush" + cf_command: "run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py flush --no-input' --name flush" - name: Run Django migrations for ${{ github.event.inputs.environment }} - uses: 18f/cg-deploy-action@main + uses: cloud-gov/cg-cli-tools@main with: cf_username: ${{ secrets[env.CF_USERNAME] }} cf_password: ${{ secrets[env.CF_PASSWORD] }} cf_org: cisa-dotgov cf_space: ${{ github.event.inputs.environment }} - full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py migrate' --name migrate" + cf_command: "run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py migrate' --name migrate" - name: Load fake data for ${{ github.event.inputs.environment }} - uses: 18f/cg-deploy-action@main + uses: cloud-gov/cg-cli-tools@main with: cf_username: ${{ secrets[env.CF_USERNAME] }} cf_password: ${{ secrets[env.CF_PASSWORD] }} cf_org: cisa-dotgov cf_space: ${{ github.event.inputs.environment }} - full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py load' --name loaddata" + cf_command: "run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py load' --name loaddata" diff --git a/ops/manifests/manifest-backup.yaml b/ops/manifests/manifest-backup.yaml new file mode 100644 index 000000000..c4615d1d5 --- /dev/null +++ b/ops/manifests/manifest-backup.yaml @@ -0,0 +1,32 @@ +--- +applications: +- name: getgov-backup + 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-backup.app.cloud.gov + # Tell Django how much stuff to log + DJANGO_LOG_LEVEL: INFO + # default public site location + GETGOV_PUBLIC_SITE_URL: https://beta.get.gov + # Flag to disable/enable features in prod environments + IS_PRODUCTION: False + routes: + - route: getgov-backup.app.cloud.gov + services: + - getgov-credentials + - getgov-backup-database diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index 524fd689a..9de152b06 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -8,6 +8,7 @@ from django.test import RequestFactory from ..views import available, check_domain_available from .common import less_console_noise from registrar.tests.common import MockEppLib +from registrar.utility.errors import GenericError, GenericErrorCodes from unittest.mock import call from epplibwrapper import ( @@ -100,16 +101,25 @@ class AvailableViewTest(MockEppLib): response = available(request, domain="igorville") self.assertTrue(json.loads(response.content)["available"]) - def test_error_handling(self): - """Calling with bad strings raises an error.""" + def test_bad_string_handling(self): + """Calling with bad strings returns unavailable.""" bad_string = "blah!;" request = self.factory.get(API_BASE_PATH + bad_string) request.user = self.user response = available(request, domain=bad_string) self.assertFalse(json.loads(response.content)["available"]) - # domain set to raise error returns false for availability - error_domain_available = available(request, "errordomain.gov") - self.assertFalse(json.loads(error_domain_available.content)["available"]) + + def test_error_handling(self): + """Error thrown while calling availabilityAPI returns error.""" + request = self.factory.get(API_BASE_PATH + "errordomain.gov") + request.user = self.user + # domain set to raise error returns false for availability and error message + error_domain_response = available(request, domain="errordomain.gov") + self.assertFalse(json.loads(error_domain_response.content)["available"]) + self.assertEqual( + GenericError.get_error_message(GenericErrorCodes.CANNOT_CONTACT_REGISTRY), + json.loads(error_domain_response.content)["message"], + ) class AvailableAPITest(MockEppLib): diff --git a/src/api/views.py b/src/api/views.py index f888ee6d4..5e3ab3a89 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -5,6 +5,8 @@ from django.http import HttpResponse, JsonResponse from django.utils.safestring import mark_safe from registrar.templatetags.url_helpers import public_site_url +from registrar.utility.errors import GenericError, GenericErrorCodes + import requests from login_required import login_not_required @@ -31,7 +33,7 @@ DOMAIN_API_MESSAGES = { ), "invalid": "Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens).", "success": "That domain is available!", - "error": "Error finding domain availability.", + "error": GenericError.get_error_message(GenericErrorCodes.CANNOT_CONTACT_REGISTRY), } @@ -64,17 +66,14 @@ def check_domain_available(domain): The given domain is lowercased to match against the domains list. If the given domain doesn't end with .gov, ".gov" is added when looking for - a match. + a match. If check fails, throws a RegistryError. """ Domain = apps.get_model("registrar.Domain") - try: - if domain.endswith(".gov"): - return Domain.available(domain) - else: - # domain search string doesn't end with .gov, add it on here - return Domain.available(domain + ".gov") - except Exception: - return False + if domain.endswith(".gov"): + return Domain.available(domain) + else: + # domain search string doesn't end with .gov, add it on here + return Domain.available(domain + ".gov") @require_http_methods(["GET"]) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 6585d602a..e7030c2d3 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -322,7 +322,15 @@ class WebsiteAdmin(ListHeaderAdmin): class UserDomainRoleAdmin(ListHeaderAdmin): - """Custom domain role admin class.""" + """Custom user domain role admin class.""" + + class Meta: + """Contains meta information about this class""" + + model = models.UserDomainRole + fields = "__all__" + + _meta = Meta() # Columns list_display = [ @@ -335,10 +343,13 @@ class UserDomainRoleAdmin(ListHeaderAdmin): search_fields = [ "user__first_name", "user__last_name", + "user__email", "domain__name", "role", ] - search_help_text = "Search by user, domain, or role." + search_help_text = "Search by firstname, lastname, email, domain, or role." + + autocomplete_fields = ["user", "domain"] class DomainInvitationAdmin(ListHeaderAdmin): diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 56218c377..3afc81a35 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -245,3 +245,9 @@ h1, h2, h3 { padding-left: 90px; } } + +// Combo box +#select2-id_domain-results, +#select2-id_user-results { + width: 100%; +} diff --git a/src/registrar/assets/sass/_theme/_register-form.scss b/src/registrar/assets/sass/_theme/_register-form.scss index d0405a3c3..6d268d155 100644 --- a/src/registrar/assets/sass/_theme/_register-form.scss +++ b/src/registrar/assets/sass/_theme/_register-form.scss @@ -78,3 +78,9 @@ font-weight: font-weight('semibold'); margin-bottom: units(0.5); } + +.review__step__subheading { + color: color('primary-dark'); + font-weight: font-weight('semibold'); + margin-bottom: units(0.5); +} diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index b31590c4b..cc779911a 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -649,6 +649,7 @@ ALLOWED_HOSTS = [ "getgov-stable.app.cloud.gov", "getgov-staging.app.cloud.gov", "getgov-development.app.cloud.gov", + "getgov-backup.app.cloud.gov", "getgov-ky.app.cloud.gov", "getgov-es.app.cloud.gov", "getgov-nl.app.cloud.gov", diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index a70c23e52..03207087f 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -399,6 +399,8 @@ class AlternativeDomainForm(RegistrarForm): raise forms.ValidationError(DOMAIN_API_MESSAGES["extra_dots"], code="extra_dots") except errors.DomainUnavailableError: raise forms.ValidationError(DOMAIN_API_MESSAGES["unavailable"], code="unavailable") + except errors.RegistrySystemError: + raise forms.ValidationError(DOMAIN_API_MESSAGES["error"], code="error") except ValueError: raise forms.ValidationError(DOMAIN_API_MESSAGES["invalid"], code="invalid") return validated @@ -484,6 +486,8 @@ class DotGovDomainForm(RegistrarForm): raise forms.ValidationError(DOMAIN_API_MESSAGES["extra_dots"], code="extra_dots") except errors.DomainUnavailableError: raise forms.ValidationError(DOMAIN_API_MESSAGES["unavailable"], code="unavailable") + except errors.RegistrySystemError: + raise forms.ValidationError(DOMAIN_API_MESSAGES["error"], code="error") except ValueError: raise forms.ValidationError(DOMAIN_API_MESSAGES["invalid"], code="invalid") return validated diff --git a/src/registrar/management/commands/load_transition_domain.py b/src/registrar/management/commands/load_transition_domain.py index 6566a2f16..e1165bf9f 100644 --- a/src/registrar/management/commands/load_transition_domain.py +++ b/src/registrar/management/commands/load_transition_domain.py @@ -176,6 +176,7 @@ class Command(BaseCommand): "clienthold": TransitionDomain.StatusChoices.ON_HOLD, "created": TransitionDomain.StatusChoices.READY, "ok": TransitionDomain.StatusChoices.READY, + "unknown": TransitionDomain.StatusChoices.UNKNOWN, } mapped_status = status_maps.get(status_to_map) return mapped_status diff --git a/src/registrar/migrations/0048_alter_transitiondomain_status.py b/src/registrar/migrations/0048_alter_transitiondomain_status.py new file mode 100644 index 000000000..d67c91e4b --- /dev/null +++ b/src/registrar/migrations/0048_alter_transitiondomain_status.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.7 on 2023-12-01 17:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0047_transitiondomain_address_line_transitiondomain_city_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="transitiondomain", + name="status", + field=models.CharField( + blank=True, + choices=[("ready", "Ready"), ("on hold", "On Hold"), ("unknown", "Unknown")], + default="ready", + help_text="domain status during the transfer", + max_length=255, + verbose_name="Status", + ), + ), + ] diff --git a/src/registrar/models/transition_domain.py b/src/registrar/models/transition_domain.py index c5b9b125c..9e6d40cf1 100644 --- a/src/registrar/models/transition_domain.py +++ b/src/registrar/models/transition_domain.py @@ -5,6 +5,7 @@ from .utility.time_stamped_model import TimeStampedModel class StatusChoices(models.TextChoices): READY = "ready", "Ready" ON_HOLD = "on hold", "On Hold" + UNKNOWN = "unknown", "Unknown" class TransitionDomain(TimeStampedModel): diff --git a/src/registrar/models/utility/domain_helper.py b/src/registrar/models/utility/domain_helper.py index 49badd5d7..e43661b1d 100644 --- a/src/registrar/models/utility/domain_helper.py +++ b/src/registrar/models/utility/domain_helper.py @@ -2,6 +2,7 @@ import re from api.views import check_domain_available from registrar.utility import errors +from epplibwrapper.errors import RegistryError class DomainHelper: @@ -29,19 +30,19 @@ class DomainHelper: if not isinstance(domain, str): raise ValueError("Domain name must be a string") domain = domain.lower().strip() - if domain == "": - if blank_ok: - return domain - else: - raise errors.BlankValueError() + if domain == "" and not blank_ok: + raise errors.BlankValueError() if domain.endswith(".gov"): domain = domain[:-4] if "." in domain: raise errors.ExtraDotsError() if not DomainHelper.string_could_be_domain(domain + ".gov"): raise ValueError() - if not check_domain_available(domain): - raise errors.DomainUnavailableError() + try: + if not check_domain_available(domain): + raise errors.DomainUnavailableError() + except RegistryError as err: + raise errors.RegistrySystemError() from err return domain @classmethod diff --git a/src/registrar/templates/application_form.html b/src/registrar/templates/application_form.html index 6830033b5..c4c3b2188 100644 --- a/src/registrar/templates/application_form.html +++ b/src/registrar/templates/application_form.html @@ -22,6 +22,14 @@ {% include "includes/form_messages.html" %} {% endblock %} +{% if pending_requests_message %} +