diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index e3e6a6f23..53533a951 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,6 +1,5 @@ name: Bug -description: Report a bug -title: "[Bug]: " +description: Report a bug or problem with the application labels: ["bug"] body: @@ -56,4 +55,14 @@ body: id: additional-context attributes: label: Additional Context (optional) - description: "Please include additional references, screenshots, documentation, etc. that are relevant" + description: "Please include additional references (screenshots, design links, documentation, etc.) that are relevant" + - type: textarea + id: issue-links + attributes: + label: Issue Links (optional) + description: | + What other issues does this story relate to and how? + + Example: + - 🚧 Blocked by: #123 + - 🔄 Relates to: #234 diff --git a/.github/ISSUE_TEMPLATE/user-story.yml b/.github/ISSUE_TEMPLATE/story.yml similarity index 73% rename from .github/ISSUE_TEMPLATE/user-story.yml rename to .github/ISSUE_TEMPLATE/story.yml index 0ad2f2853..6173dd9e1 100644 --- a/.github/ISSUE_TEMPLATE/user-story.yml +++ b/.github/ISSUE_TEMPLATE/story.yml @@ -1,6 +1,5 @@ -name: User Story +name: Story description: Capture actionable sprint work -title: "[Story]: " labels: ["story"] body: @@ -13,13 +12,13 @@ body: - type: textarea id: story attributes: - label: User Story + label: Story description: | - Please add the "as a, I want, so that" details that describe the user story. + Please add the "as a, I want, so that" details that describe the story. If more than one "as a, I want, so that" describes the story, add multiple. Example: - As an administrator + As an analyst I want the ability to approve a domain application so that a request can be fulfilled and a new .gov domain can be provisioned value: | @@ -33,23 +32,23 @@ body: attributes: label: Acceptance Criteria description: | - Please add the acceptance criteria using one or more "given, when, then" formulae + Please add the acceptance criteria that best describe the desired outcomes when this work is completed Example: - Given that I am an administrator who has finished reviewing a domain application + - Application sends an email when analysts approve domain requests + - Domain application status is "approved" + + Example ("given, when, then" format): + Given that I am an analyst who has finished reviewing a domain application When I click to approve a domain application Then the domain provisioning process should be initiated, and the applicant should receive an email update. - value: | - Given - When - Then validations: required: true - type: textarea id: additional-context attributes: label: Additional Context (optional) - description: "Please include additional references, screenshots, documentation, etc. that are relevant" + description: "Please include additional references (screenshots, design links, documentation, etc.) that are relevant" - type: textarea id: issue-links attributes: diff --git a/.github/workflows/deploy-stable.yaml b/.github/workflows/deploy-stable.yaml index f3c7784db..2f1a2a6b4 100644 --- a/.github/workflows/deploy-stable.yaml +++ b/.github/workflows/deploy-stable.yaml @@ -28,7 +28,7 @@ jobs: docker compose run node npx gulp compile - name: Collect static assets working-directory: ./src - run: docker compose run app python manage.py collectstatic + run: docker compose run app python manage.py collectstatic --no-input - name: Deploy to cloud.gov sandbox uses: 18f/cg-deploy-action@main env: diff --git a/.github/workflows/security-check.yaml b/.github/workflows/security-check.yaml index 39f8a80a3..dd75d5c98 100644 --- a/.github/workflows/security-check.yaml +++ b/.github/workflows/security-check.yaml @@ -72,8 +72,8 @@ jobs: # by adding MockUserLogin to settings.MIDDLEWARE run: | perl -pi \ - -e 's/"csp.middleware.CSPMiddleware",/$&"registrar.tests.common.MockUserLogin",/' \ - src/registrar/config/settings.py + -e 's/"django.contrib.auth.middleware.AuthenticationMiddleware",/$&"registrar.tests.common.MockUserLogin",/' \ + registrar/config/settings.py working-directory: ./src - name: OWASP scan diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a45b9fa72..6d34f80b5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -59,7 +59,7 @@ jobs: # by adding MockUserLogin to settings.MIDDLEWARE run: | perl -pi \ - -e 's/"csp.middleware.CSPMiddleware",/$&"registrar.tests.common.MockUserLogin",/' \ + -e 's/"django.contrib.auth.middleware.AuthenticationMiddleware",/$&"registrar.tests.common.MockUserLogin",/' \ registrar/config/settings.py - name: Start container diff --git a/README.md b/README.md index 30626117f..2685a8ec3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ # Get (your very own) .gov -======================== Welcome to the repo for a WIP brand new registrar for .gov domains. Get.gov intends to serve all government entities in the United States looking for a .gov domain to use publicly (for a website, for an email address, etc.). Here you can find the code for the registrar and other artifacts about our product strategy and research. diff --git a/docs/developer/README.md b/docs/developer/README.md index d96fa7253..bc102a64b 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -199,6 +199,8 @@ Static files (images, CSS stylesheets, JavaScripts, etc) are known as "assets". Assets are stored in `registrar/assets` during development and served from `registrar/public`. During deployment, assets are copied from `registrar/assets` into `registrar/public`. Any assets which need processing, such as USWDS Sass files, are processed before copying. +**Note:** Custom images are added to `/registrar/assets/img/registrar`, keeping them separate from the images copied over by USWDS. However, because the `/img/` directory is listed in `.gitignore`, any files added to `/registrar/assets/img/registrar` will need to be force added (i.e. `git add --force `) before they can be deployed. + We utilize the [uswds-compile tool](https://designsystem.digital.gov/documentation/getting-started/developers/phase-two-compile/) from USWDS to compile and package USWDS assets. ## Making and view style changes diff --git a/docs/developer/registry-access.md b/docs/developer/registry-access.md new file mode 100644 index 000000000..a59c8b8b7 --- /dev/null +++ b/docs/developer/registry-access.md @@ -0,0 +1,105 @@ +# Working with the registry via EPP + +## Overview of parts + +**EPP** is the protocol which describes how a registry and registrar communicate with XML over a TCP socket connection. + +**epplib** is a Python library implementation of the TCP socket connection. It has helper functions and dataclasses which can be used to send and receive the XML messages. + +**epplibwrapper** is a module in this repository which abstracts away the details of authenticating with the registry. It assists with error handling by providing error code constants and an error class with some helper methods. + +**Domain** is a Python class. It inherits from `django.db.models.Model` and is therefore part of Django's ORM and has a corresponding table in the local registrar database. Its purpose is to provide a developer-friendly interface to the registry based on *what a registrant or analyst wants to do*, not on the technical details of EPP. + +## Debugging in a Python shell + +You'll first need access to a Django shell in an environment with valid registry credentials. Only some environments are allowed access: your laptop is probably not one of them. For example: + +```shell +cf ssh getgov-ENVIRONMENT +/tmp/lifecycle/shell # this configures your environment +./manage.py shell +``` + +You'll next need to import some code. + +``` +from epplibwrapper import CLIENT as registry, commands +from epplib.models import common +``` + +Finally, you'll need to craft a request and send it. + +``` +request = ... +response = registry.send(request) +``` + +Note that you'll need to attest that the data you are sending has been sanitized to remove malicious or invalid strings. Use `send(..., cleaned=True)` to do that. + +See below for some example commands to send. Replace example data with data which makes sense for your debugging scenario. Other commands are available; see the source code of epplib for more options. + + +### Get info about a contact + +``` +request = commands.InfoContact(id='sh8013') +``` + +### Create a new contact + +``` +DF = common.DiscloseField +di = common.Disclose(flag=False, fields={DF.FAX, DF.VOICE, DF.ADDR}, types={DF.ADDR: "loc"}) +addr = common.ContactAddr(street=['123 Example Dr.'], city='Dulles', pc='20166-6503', cc='US', sp='VA') +pi = common.PostalInfo(name='John Doe', addr=addr, org="Example Inc.", type="loc") +ai = common.ContactAuthInfo(pw='feedabee') + +request = commands.CreateContact(id='sh8013', postal_info=pi, email='jdoe@example.com', voice='+1.7035555555', fax='+1.7035555556', auth_info=ai, disclose=di, vat=None, ident=None, notify_email=None) +``` + +### Create a new domain + +``` +ai = common.DomainAuthInfo(pw='feedabee') +request = commands.CreateDomain(name="okay.gov", registrant="sh8013", auth_info=ai) +``` + +### Create a host object + +``` +request = commands.CreateHost(name="ns1.okay.gov", addrs=[common.Ip(addr="127.0.0.1"), common.Ip(addr="0:0:0:0:0:0:0:1", ip="v6")]) +``` + +### Check if a host is available + +``` +request = commands.CheckHost(["ns2.okay.gov"]) +``` + +### Update a domain + +``` +request = commands.UpdateDomain(name="okay.gov", add=[common.HostObjSet(["ns1.okay.gov"])]) +``` + +``` +request = commands.UpdateDomain(name="okay.gov", add=[common.DomainContact(contact="sh8014", type="tech")]) +``` + +### How to see the raw XML + +To see the XML of a command before the request is sent, call `request.xml()`. + +To see the XML of the response, you must send the command using a different method. + +``` +registry._client.connect() +registry._client.send(registry._login) + +request = commands.InfoDomain(name="ok.gov") + +registry._client.transport.send(request.xml()) +response = registry._client.transport.receive() +``` + +This is helpful for debugging situations where epplib is not correctly or fully parsing the XML returned from the registry. diff --git a/docs/developer/user-permissions.md b/docs/developer/user-permissions.md index 281a58c1b..af5aa1259 100644 --- a/docs/developer/user-permissions.md +++ b/docs/developer/user-permissions.md @@ -22,12 +22,22 @@ role or set of permissions that they have. We use a `UserDomainRole` ## Permission decorator The Django objects that need to be permission controlled are various views. -For that purpose, we add a very simple permission mixin -[`DomainPermission`](../../src/registrar/views/utility/mixins.py) that can be -added to a view to require that (a) there is a logged-in user and (b) that the -logged in user has a role that permits access to that view. This mixin is the -place where the details of the permissions are enforced. It can allow a view -to load, or deny access with various status codes, e.g. "403 Forbidden". +For that purpose, we have a View subclass to enforce user permissions on a +domain called +[`DomainPermissionView`](../../src/registrar/views/utility/permission_views.py) +that can be added to a view to require that (a) there is a logged-in user and +(b) that the logged in user has a role that permits access to that view. This +mixin is the place where the details of the permissions are enforced. It can +allow a view to load, or deny access with various status codes, e.g. "403 +Forbidden". + +In addition, we now require all of our application views to have a logged-in +user by using a Django middleware that makes every request "login required". +This is slightly belt-and-suspenders because our permissions view also checks +that the request includes a logged in user, but it avoids accidentally creating +content that is publicly available by accident. We can specifically mark a view +as "not login required" if we do need to have publicly accessible content (such +as health checks used by our platform). ## Adding roles diff --git a/src/Pipfile b/src/Pipfile index 857505daf..6900b0bcf 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -24,9 +24,9 @@ django-fsm = "*" django-phonenumber-field = {extras = ["phonenumberslite"], version = "*"} boto3 = "*" typing-extensions ='*' +django-login-required-middleware = "*" fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"} - [dev-packages] django-debug-toolbar = "*" nplusone = "*" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 5b518dcf1..d13ed6382 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "fd7d0efa9a87dfe4b2bb228ee0e7978fba16c7cfdd3c443870900cfe899e2cfd" + "sha256": "1242c67b31261243a35128410d4a928fca3729ddac13b8c8e25adf31445c6328" }, "pipfile-spec": 6, "requires": {}, @@ -306,6 +306,13 @@ "index": "pypi", "version": "==2.8.1" }, + "django-login-required-middleware": { + "hashes": [ + "sha256:847ae9a69fd7a07618ed53192b3c06946af70a0caf6d0f4eb40a8f37593cd970" + ], + "index": "pypi", + "version": "==0.9.0" + }, "django-phonenumber-field": { "extras": [ "phonenumberslite" @@ -791,11 +798,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c", - "sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98" + "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", + "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5" ], "index": "pypi", - "version": "==4.6.2" + "version": "==4.6.3" }, "urllib3": { "hashes": [ @@ -951,19 +958,19 @@ }, "django-stubs": { "hashes": [ - "sha256:93baff824f0a056e71036b423b942a74f07b909e45e3fa38185b910f597c5c08", - "sha256:d2c671989efb3f7b0fa91e461909ad5a5a52155fe7fe6d1f2058cb88e3afb123" + "sha256:66477bdba25407623f4079205e58f3c7265a4f0d8f7c9f540a6edc16f8883a5b", + "sha256:8c15d5f7b05926805cfb25f2bfbf3509c37792fbd8aec5aedea358b85d8bccd5" ], "index": "pypi", - "version": "==4.2.0" + "version": "==4.2.1" }, "django-stubs-ext": { "hashes": [ - "sha256:55b2e3077f883e0131a7596f8ff8b19f8fc3ca325a3318ccacf5331acb2601e4", - "sha256:7789f0caeca7152fef07ad6b94dec7310a05d0b8dab77f7979e19db0037b5127" + "sha256:2696d6f7d8538341b060cffa9565c72ea797e866687e040b86d29cad8799e5fe", + "sha256:4b6b63e49f4ba30d93ec46f87507648c99c9de6911e651ad69db7084fd5b2f4e" ], - "markers": "python_version >= '3.7'", - "version": "==4.2.0" + "markers": "python_version >= '3.8'", + "version": "==4.2.1" }, "django-webtest": { "hashes": [ @@ -1306,11 +1313,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c", - "sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98" + "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26", + "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5" ], "index": "pypi", - "version": "==4.6.2" + "version": "==4.6.3" }, "urllib3": { "hashes": [ diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index 061b6274c..39ddba071 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -104,6 +104,9 @@ class AvailableAPITest(TestCase): def test_available_post(self): """Cannot post to the /available/ API endpoint.""" + # have to log in to test the correct thing now that we require login + # for all URLs by default + self.client.force_login(self.user) with less_console_noise(): response = self.client.post(API_BASE_PATH + "nonsense") self.assertEqual(response.status_code, 405) diff --git a/src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss b/src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss index 8c06730bf..905ab872c 100644 --- a/src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss +++ b/src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss @@ -124,7 +124,6 @@ h2 { } /* Make "placeholder" links visually obvious */ -a[href^="https://federalist-"]::after, a[href$="todo"]::after { background-color: yellow; color: color(blue-80v); diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 54eb35d9f..b8b32df41 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -48,6 +48,7 @@ env_db_url = env.dj_db_url("DATABASE_URL") env_debug = env.bool("DJANGO_DEBUG", default=False) env_log_level = env.str("DJANGO_LOG_LEVEL", "DEBUG") env_base_url = env.str("DJANGO_BASE_URL") +env_getgov_public_site_url = env.str("GETGOV_PUBLIC_SITE_URL", "") secret_login_key = b64decode(secret("DJANGO_SECRET_LOGIN_KEY", "")) secret_key = secret("DJANGO_SECRET_KEY") @@ -62,8 +63,6 @@ secret_registry_key = b64decode(secret("REGISTRY_KEY", "")) secret_registry_key_passphrase = secret("REGISTRY_KEY_PASSPHRASE", "") secret_registry_hostname = secret("REGISTRY_HOSTNAME") -secret_getgov_public_site_url = secret("GETGOV_PUBLIC_SITE_URL", "") - # region: Basic Django Config-----------------------------------------------### # Build paths inside the project like this: BASE_DIR / "subdir". @@ -134,6 +133,8 @@ MIDDLEWARE = [ "django.middleware.csrf.CsrfViewMiddleware", # add `user` (the currently-logged-in user) to incoming HttpRequest objects "django.contrib.auth.middleware.AuthenticationMiddleware", + # Require login for every single request by default + "login_required.middleware.LoginRequiredMiddleware", # provide framework for displaying messages to the user, see documentation "django.contrib.messages.middleware.MessageMiddleware", # provide clickjacking protection via the X-Frame-Options header @@ -461,6 +462,12 @@ AUTHENTICATION_BACKENDS = [ # the login_required() decorator, LoginRequiredMixin, or AccessMixin LOGIN_URL = "/openid/login" +# We don't want the OIDC app to be login-required because then it can't handle +# the initial login requests without erroring. +LOGIN_REQUIRED_IGNORE_PATHS = [ + r"/openid/(.+)$", +] + # where to go after logging out LOGOUT_REDIRECT_URL = "home" @@ -509,7 +516,7 @@ STATIC_URL = "public/" # Base URL of our separate static public website. Used by the # {% public_site_url subdir/path %} template tag -GETGOV_PUBLIC_SITE_URL = secret_getgov_public_site_url +GETGOV_PUBLIC_SITE_URL = env_getgov_public_site_url # endregion # region: Registry----------------------------------------------------------### diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index a19da4791..0ef9fbe36 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -88,6 +88,11 @@ urlpatterns = [ views.DomainYourContactInformationView.as_view(), name="domain-your-contact-information", ), + path( + "domain//org-name-address", + views.DomainOrgNameAddressView.as_view(), + name="domain-org-name-address", + ), path( "domain//authorizing-official", views.DomainAuthorizingOfficialView.as_view(), diff --git a/src/registrar/fixtures.py b/src/registrar/fixtures.py index 438f6723b..02a6915aa 100644 --- a/src/registrar/fixtures.py +++ b/src/registrar/fixtures.py @@ -59,6 +59,21 @@ class UserFixture: "first_name": "Alysia", "last_name": "Broddrick", }, + { + "username": "55a3bc26-cd1d-4a5c-a8c0-7e1f561ef7f4", + "first_name": "Michelle", + "last_name": "Rago", + }, + { + "username": "8f8e7293-17f7-4716-889b-1990241cbd39", + "first_name": "Katherine", + "last_name": "Osos", + }, + { + "username": "70488e0a-e937-4894-a28c-16f5949effd4", + "first_name": "Gaby", + "last_name": "DiSarli", + }, ] @classmethod diff --git a/src/registrar/forms/__init__.py b/src/registrar/forms/__init__.py index 364740211..13f75563f 100644 --- a/src/registrar/forms/__init__.py +++ b/src/registrar/forms/__init__.py @@ -3,5 +3,6 @@ from .domain import ( DomainAddUserForm, NameserverFormset, DomainSecurityEmailForm, + DomainOrgNameAddressForm, ContactForm, ) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index e6fbc8fee..f14448bcf 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -1,11 +1,12 @@ """Forms for domain management.""" from django import forms +from django.core.validators import RegexValidator from django.forms import formset_factory from phonenumber_field.widgets import RegionalPhoneNumberWidget -from ..models import Contact +from ..models import Contact, DomainInformation class DomainAddUserForm(forms.Form): @@ -64,3 +65,77 @@ class DomainSecurityEmailForm(forms.Form): """Form for adding or editing a security email to a domain.""" security_email = forms.EmailField(label="Security email") + + +class DomainOrgNameAddressForm(forms.ModelForm): + + """Form for updating the organization name and mailing address.""" + + zipcode = forms.CharField( + label="Zip code", + validators=[ + RegexValidator( + "^[0-9]{5}(?:-[0-9]{4})?$|^$", + message="Enter a zip code in the form of 12345 or 12345-6789.", + ) + ], + ) + + class Meta: + model = DomainInformation + fields = [ + "federal_agency", + "organization_name", + "address_line1", + "address_line2", + "city", + "state_territory", + "zipcode", + "urbanization", + ] + error_messages = { + "federal_agency": { + "required": "Select the federal agency for your organization." + }, + "organization_name": {"required": "Enter the name of your organization."}, + "address_line1": { + "required": "Enter the street address of your organization." + }, + "city": {"required": "Enter the city where your organization is located."}, + "state_territory": { + "required": "Select the state, territory, or military post where your" + "organization is located." + }, + } + widgets = { + # We need to set the required attributed for federal_agency and + # state/territory because for these fields we are creating an individual + # instance of the Select. For the other fields we use the for loop to set + # the class's required attribute to true. + "federal_agency": forms.Select( + attrs={"required": True}, choices=DomainInformation.AGENCY_CHOICES + ), + "organization_name": forms.TextInput, + "address_line1": forms.TextInput, + "address_line2": forms.TextInput, + "city": forms.TextInput, + "state_territory": forms.Select( + attrs={ + "required": True, + }, + choices=DomainInformation.StateTerritoryChoices.choices, + ), + "urbanization": forms.TextInput, + } + + # 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 = ["organization_name", "address_line1", "city", "zipcode"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field_name in self.required: + self.fields[field_name].required = True + self.fields["state_territory"].widget.attrs.pop("maxlength", None) + self.fields["zipcode"].widget.attrs.pop("maxlength", None) diff --git a/src/registrar/migrations/0027_alter_domaininformation_address_line1_and_more.py b/src/registrar/migrations/0027_alter_domaininformation_address_line1_and_more.py new file mode 100644 index 000000000..9f362c956 --- /dev/null +++ b/src/registrar/migrations/0027_alter_domaininformation_address_line1_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.1 on 2023-06-09 16:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0026_alter_domainapplication_address_line2_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="domaininformation", + name="address_line1", + field=models.TextField( + blank=True, + help_text="Street address", + null=True, + verbose_name="Street address", + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="address_line2", + field=models.TextField( + blank=True, + help_text="Street address line 2", + null=True, + verbose_name="Street address line 2", + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="state_territory", + field=models.CharField( + blank=True, + help_text="State, territory, or military post", + max_length=2, + null=True, + verbose_name="State, territory, or military post", + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="urbanization", + field=models.TextField( + blank=True, + help_text="Urbanization (Puerto Rico only)", + null=True, + verbose_name="Urbanization (Puerto Rico only)", + ), + ), + ] diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 3dae3d4a0..8a32333bf 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -577,6 +577,10 @@ class DomainApplication(TimeStampedModel): # When an application is moved to in review, we need to send a # confirmation email. This is a side-effect of the state transition updated_domain_application._send_in_review_email() + + @transition(field="status", source=[SUBMITTED, INVESTIGATING], target=WITHDRAWN) + def withdraw(self): + """Withdraw an application that has been submitted.""" # ## Form policies ### # diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 78b68a3fa..b12039e73 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -100,11 +100,13 @@ class DomainInformation(TimeStampedModel): null=True, blank=True, help_text="Street address", + verbose_name="Street address", ) address_line2 = models.TextField( null=True, blank=True, help_text="Street address line 2", + verbose_name="Street address line 2", ) city = models.TextField( null=True, @@ -116,6 +118,7 @@ class DomainInformation(TimeStampedModel): null=True, blank=True, help_text="State, territory, or military post", + verbose_name="State, territory, or military post", ) zipcode = models.CharField( max_length=10, @@ -128,6 +131,7 @@ class DomainInformation(TimeStampedModel): null=True, blank=True, help_text="Urbanization (Puerto Rico only)", + verbose_name="Urbanization (Puerto Rico only)", ) type_of_work = models.TextField( diff --git a/src/registrar/templates/401.html b/src/registrar/templates/401.html index 9fe0194ed..a419dab53 100644 --- a/src/registrar/templates/401.html +++ b/src/registrar/templates/401.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load i18n static %} +{% load url_helpers %} {% block title %}{% translate "Unauthorized | " %}{% endblock %} @@ -25,7 +26,7 @@ Would you like to try logging in again?

- If you would like help with this error contact us + If you'd like help with this error contact us .

{% if log_identifier %} diff --git a/src/registrar/templates/403.html b/src/registrar/templates/403.html index cc8c98656..93d32f65a 100644 --- a/src/registrar/templates/403.html +++ b/src/registrar/templates/403.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load i18n static %} +{% load url_helpers %} {% block title %}{% translate "Forbidden | " %}{% endblock %} @@ -22,12 +23,12 @@ {% endif %}

You must be an authorized user and need to be signed in to view this page. - Would you like to try logging in again? + Would you like to try logging in again?

- If you would like help with this error contact us + If you'd like help with this error contact us .

- + {% if log_identifier %}

Here's a unique identifier for this error.

{{ log_identifier }}

diff --git a/src/registrar/templates/404.html b/src/registrar/templates/404.html index 76a301187..deec71ce2 100644 --- a/src/registrar/templates/404.html +++ b/src/registrar/templates/404.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load i18n static %} +{% load url_helpers %} {% block title %}{% translate "Page not found | " %}{% endblock %} @@ -14,7 +15,7 @@ {% translate "Status 404" %} -

Try going to the homepage. If you can’t find what you’re looking for, contact us. +

Try going to the homepage. If you can’t find what you’re looking for, contact us .

diff --git a/src/registrar/templates/500.html b/src/registrar/templates/500.html index 7b7e1dfed..5749c0ad3 100644 --- a/src/registrar/templates/500.html +++ b/src/registrar/templates/500.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load i18n static %} +{% load url_helpers %} {% block title %}{% translate "Server error | " %}{% endblock %} @@ -18,7 +19,7 @@ {% else %}

Sorry! Try waiting a few minutes and then reloading the page. - Contact us if you need help. + contact us if you need help.

{% endif %} diff --git a/src/registrar/templates/application_authorizing_official.html b/src/registrar/templates/application_authorizing_official.html index 6648816ce..dead1974f 100644 --- a/src/registrar/templates/application_authorizing_official.html +++ b/src/registrar/templates/application_authorizing_official.html @@ -1,22 +1,19 @@ {% extends 'application_form.html' %} -{% load field_helpers %} +{% load field_helpers url_helpers %} {% block form_instructions %}

Who is the authorizing official for your organization?

-

Your authorizing official is the person within your organization who can authorize - your domain request. This is generally the highest-ranking or highest-elected official - in your organization. Read more about who can serve as an - authorizing official.

+

Your authorizing official is the person within your organization who can authorize your domain request. This is generally the highest-ranking or highest-elected official in your organization.

{% include "includes/ao_example.html" %}
-

We’ll contact your authorizing official to let them know that you made this request - and to double check that they approve it.

+

We might contact your authorizing official, or their office, to double check that they approve this request. Read more about who can serve as an authorizing official.

+ {% endblock %} diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html index a6b9d1037..8b53d5695 100644 --- a/src/registrar/templates/application_dotgov_domain.html +++ b/src/registrar/templates/application_dotgov_domain.html @@ -1,9 +1,9 @@ {% extends 'application_form.html' %} -{% load static field_helpers %} +{% load static field_helpers url_helpers %} {% block form_instructions %} -

Before requesting a .gov domain, please make sure it - meets our naming requirements. Your domain name must: +

Before requesting a .gov domain, please make sure it + meets our naming requirements. Your domain name must: