Merge branch 'main' into sspj/draft-test-cases

This commit is contained in:
Seamus Johnston 2023-06-16 14:17:49 -05:00
commit 12d3f5bdb7
No known key found for this signature in database
GPG key ID: 2F21225985069105
32 changed files with 513 additions and 374 deletions

View file

@ -83,6 +83,6 @@ export GPG_TTY
## Setting up developer sandbox
We have two types of environments: stable, and sandbox. Stable gets deployed via tagged release every sprint, and developer sandboxes are given to get.gov developers to mess around in a production-like environment without disrupting stable. Each sandbox is namespaced and will automatically be deployed too when the appropriate branch syntax is used for that space in an open pull request. There are several things you need to setup to make the sandbox work for a developer.
We have three types of environments: stable, staging, and sandbox. Stable (production)and staging (pre-prod) get deployed via tagged release, and developer sandboxes are given to get.gov developers to mess around in a production-like environment without disrupting stable or staging. Each sandbox is namespaced and will automatically be deployed too when the appropriate branch syntax is used for that space in an open pull request. There are several things you need to setup to make the sandbox work for a developer.
All automation for setting up a developer sandbox is documented in the scripts for [creating a developer sandbox](../../ops/scripts/create_dev_sandbox.sh) and [removing a developer sandbox](../../ops/scripts/destroy_dev_sandbox.sh). A Cloud.gov organization administrator will have to perform the script in order to create the sandbox.

View file

@ -9,15 +9,12 @@ on:
jobs:
variables:
if: |
startsWith(github.head_ref, 'ik/')
|| startsWith(github.head_ref, 'jon')
|| startsWith(github.head_ref, 'sspj/')
|| startsWith(github.head_ref, 'mr/')
|| startsWith(github.head_ref, 'nmb/')
|| startsWith(github.head_ref, 'ab/')
startsWith(github.head_ref, 'ab/')
|| startsWith(github.head_ref, 'bl/')
|| startsWith(github.head_ref, 'rjm/')
|| startsWith(github.head_ref, 'rb/')
|| startsWith(github.head_ref, 'ko/')
|| startsWith(github.head_ref, 'gd/')
outputs:
environment: ${{ steps.var.outputs.environment}}
runs-on: "ubuntu-latest"

41
.github/workflows/deploy-staging.yaml vendored Normal file
View file

@ -0,0 +1,41 @@
# This workflow runs on pushes of tagged commits.
# "Releases" of tagged commits will deploy selected branch to staging.
name: Build and deploy staging for tagged release
on:
push:
paths-ignore:
- 'docs/**'
- '**.md'
- '.gitignore'
tags:
- staging-*
jobs:
deploy-staging:
if: ${{ github.ref_type == 'tag' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Compile USWDS assets
working-directory: ./src
run: |
docker compose run node npm install &&
docker compose run node npx gulp copyAssets &&
docker compose run node npx gulp compile
- name: Collect static assets
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
with:
cf_username: ${{ secrets.CF_STAGING_USERNAME }}
cf_password: ${{ secrets.CF_STAGING_PASSWORD }}
cf_org: cisa-getgov-prototyping
cf_space: staging
push_arguments: "-f ops/manifests/manifest-staging.yaml"

View file

@ -14,15 +14,13 @@ on:
description: Which environment should we run migrations for?
options:
- stable
- staging
- gd
- rb
- ko
- ab
- bl
- rjm
- jon
- ik
- sspj
- nmb
- mr
jobs:
migrate:

View file

@ -15,15 +15,13 @@ on:
description: Which environment should we flush and re-load data for?
options:
- stable
- staging
- gd
- rb
- ko
- ab
- bl
- rjm
- jon
- ik
- sspj
- nmb
- mr
jobs:
reset-db:

View file

@ -42,10 +42,9 @@ Optionally, load data from fixtures as well
cf run-task getgov-ENVIRONMENT --wait --command 'python manage.py load' --name loaddata
```
For the `stable` environment, developers don't have credentials so we need to
run that command using Github Actions. Go to
For the `stable` or `staging` environments, developers don't have credentials so we need to run that command using Github Actions. Go to
<https://github.com/cisagov/getgov/actions/workflows/migrate.yaml> and select
the "Run workflow" button, making sure that `stable` is selected.
the "Run workflow" button, making sure that `stable` or `staging` depending on which envirornment you desire to update.
## Getting data for fixtures

View file

@ -1,5 +1,4 @@
# Operations
========================
Some basic information and setup steps are included in this README.
@ -36,7 +35,9 @@ Binding the database in `manifest-<ENVIRONMENT>.json` automatically inserts the
# Deploy
We have two types of environments: developer "sandboxes" and `stable`. Developers can deploy locally to their sandbox whenever they want. However, only our CD service can deploy to `stable`, and it does so when we make tagged releases of `main`. This is to ensure that we have a "golden" environment to point to, and can still test things out in a sandbox space. You should make sure all of the USWDS assets are compiled and collected before deploying to your sandbox. To deploy locally to `sandbox`:
We have three types of environments: developer "sandboxes", `staging` and `stable`. Developers can deploy locally to their sandbox whenever they want. However, only our CD service can deploy to `staging` and `stable`, and it does so when we make tagged releases of `main`. For `staging`, this is done to ensure there is a non-production level test envirornment that can be used for user testing or for testing code before it is pushed to `stable`. `Staging` can be especially helpful when testing database changes or migrations that could have adververse affects in `stable`. On the other hand, `stable` is used to ensure that we have a "golden" environment to point to. We can refer to `stable` as our production environment and `staging` as our pre-production (pre-prod) environment. As such, code on main should always be tagged for `staging` before it is tagged for `stable`.
You should make sure all of the USWDS assets are compiled and collected before deploying to your sandbox. To deploy locally to `sandbox`:
For ease of use, you can run the `deploy.sh <sandbox name>` script in the `/src` directory to build the assets and deploy to your sandbox. Similarly, you could run `build.sh <sandbox name>` script to just compile and collect the assets without deploying.
@ -46,3 +47,114 @@ Your sandbox space should've been setup as part of the onboarding process. If th
We are using [WhiteNoise](http://whitenoise.evans.io/en/stable/index.html) plugin to serve our static assets on cloud.gov. This plugin is added to the `MIDDLEWARE` list in our apps `settings.py`.
Note that its a good idea to run `collectstatic` locally or in the docker container before pushing files up to your sandbox. This is because `collectstatic` relies on timestamps when deciding to whether to overwrite the existing assets in `/public`. Due the way files are uploaded, the compiled css in the `/assets/css` folder on your sandbox will have a slightly earlier timestamp than the files in `/public/css`, and consequently running `collectstatic` on your sandbox will not update `public/css` as you may expect. For convenience, both the `deploy.sh` and `build.sh` scripts will take care of that.
# Debugging
Debugging errors observed in applications running on Cloud.gov requires being
able to see the log information from the environment that the application is
running in. There are (at least) three different ways to see that information:
Cloud.gov dashboard, CloudFoundry CLI application, and Cloud.gov Kibana logging
queries. There is also SSH access into Cloud.gov containers and Github Actions
that can be used for specific tasks.
## Cloud.gov dashboard
At <https://dashboard.fr.cloud.gov/applications> there is a list for all of the
applications that a Cloud.gov user has access to. Clicking on an application
goes to a screen for that individual application, e.g.
<https://dashboard.fr.cloud.gov/applications/2oBn9LBurIXUNpfmtZCQTCHnxUM/53b88024-1492-46aa-8fb6-1429bdb35f95/summary>.
On that page is a left-hand link for "Log Stream" e.g.
<https://dashboard.fr.cloud.gov/applications/2oBn9LBurIXUNpfmtZCQTCHnxUM/53b88024-1492-46aa-8fb6-1429bdb35f95/log-stream>.
That log stream shows a stream of Cloud.gov log messages. Cloud.gov has
different layers that log requests. One is `RTR` which is the router within
Cloud.gov. Messages from our Django app are prefixed with `APP/PROC/WEB`. While
it is possible to search inside the browser for particular log messages, this
is not a sophisticated interface for querying logs.
## CloudFoundry CLI
When logged in with the CloudFoundry CLI (see
[above](#authenticating-to-cloudgov-via-the-command-line)) Cloudfoundry
application logs can be viewed with the `cf logs <application>` where
`<application>` is the name of the application in the currently targeted space.
By default `cf logs` starts a streaming view of log messages from the
application. It appears to show the same information as the dashboard web
application, but in the terminal. There is a `--recent` option that will dump
things that happened prior to the current time rather than starting a stream of
the present log messages, but that is also not a full log archive and search
system.
CloudFoundry also offers a `run-task` command that can be used to run a single
command in the running Cloud.gov container. For example, to run our Django
admin command that loads test fixture data:
```
cf run-task getgov-{environment} --command "./manage.py load" --name fixtures
```
However, this task runs asynchronously in the background without any command
output, so it can sometimes be hard to know if the command has completed and if
so, if it was successful.
## Cloud.gov Kibana
Cloud.gov provides an instance of the log query program Kibana at
<https://logs.fr.cloud.gov>. Kibana is powerful, but also complicated software
that can take time to learn how to use most effectively. A few hints:
- Set the timeframe of the display appropriately, the default is the last
15 minutes which may not show any results in some environments.
- Kibana queries and filters can be used to narrow in on particular
environments. Try the query `@source.type:APP` to focus on messages from the
Django application or `@cf.app:"getgov-{environment}"` to see results from a single
environment.
Currently, our application emits Python's default log format which is textual
and not record-based. In particular, tracebacks are on multiple lines and show
up in Kibana as multiple records that are not necessarily connected. As the
application gets closer to production, we may want to switch to a JSON log format
where errors will be captured by Kibana as a single message, however with a
slightly more difficult developer experience when reading logs by eyeball.
## SSH access
The CloudFoundry CLI provides SSH access to the running container of an
application. Use `cf ssh <application>` to SSH into the container. To make sure
that your shell is seeing the same configuration as the running application, be
sure to run `/tmp/lifecycle/shell` very first.
Inside the container, the python code should be in `/app` and you can check
there to see if the expected version of code is deployed in a particular file.
There is no hot-reloading inside the container, so it isn't possible to make
code changes there and see the results reflected in the running application.
(Templates may be read directly from disk every page load so it is possible
that you could change a page template and see the result in the application.)
Inside the container, it can be useful to run various Django admin commands
using `./manage.py`. For example, `./manage.py shell` can be used to give a
python interpreter where code can be run to modify objects in the database, say
to make a user an administrator.
## Github Actions
In order to allow some ops activities by people without CloudFoundry on a
laptop, we have some ops-related actions under
<https://github.com/cisagov/getgov/actions>.
### Migrate data
This Github action runs Django's `manage.py migrate` command on the specified
environment. **This is the first thing to try when fixing 500 errors from an
application environment**. The migrations should be idempotent, so running the
same migrations more than once should never cause an additional problem.
### Reset database
Very occasionally, there are migrations that don't succeed when run against a
database with data already in it. This action drops the database and re-creates
it with the latest model schema. Once launched, this should never be used on
the `stable` environment, but during development, it may be useful on the
various sandbox environments. After launch, some schema changes may take the
involvement of a skilled DBA to fix problems like this.

View file

@ -1,6 +1,6 @@
---
applications:
- name: getgov-ik
- name: getgov-gd
buildpacks:
- python_buildpack
path: ../../src
@ -17,13 +17,13 @@ applications:
# 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-ik.app.cloud.gov
DJANGO_BASE_URL: https://getgov-gd.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# Public site base URL
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
# default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
routes:
- route: getgov-ik.app.cloud.gov
- route: getgov-gd.app.cloud.gov
services:
- getgov-credentials
- getgov-ik-database
- getgov-gd-database

View file

@ -1,29 +0,0 @@
---
applications:
- name: getgov-nmb
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
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-nmb.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# Public site base URL
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
routes:
- route: getgov-nmb.app.cloud.gov
services:
- getgov-credentials
- getgov-nmb-database

View file

@ -1,6 +1,6 @@
---
applications:
- name: getgov-mr
- name: getgov-rb
buildpacks:
- python_buildpack
path: ../../src
@ -17,13 +17,13 @@ applications:
# 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-mr.app.cloud.gov
DJANGO_BASE_URL: https://getgov-rb.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# Public site base URL
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
# default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
routes:
- route: getgov-mr.app.cloud.gov
- route: getgov-rb.app.cloud.gov
services:
- getgov-credentials
- getgov-mr-database
- getgov-rb-database

View file

@ -1,29 +0,0 @@
---
applications:
- name: getgov-sspj
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
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-sspj.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# Public site base URL
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
routes:
- route: getgov-sspj.app.cloud.gov
services:
- getgov-credentials
- getgov-sspj-database

View file

@ -1,6 +1,6 @@
---
applications:
- name: getgov-jon
- name: getgov-staging
buildpacks:
- python_buildpack
path: ../../src
@ -17,13 +17,13 @@ applications:
# 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-jon.app.cloud.gov
DJANGO_BASE_URL: https://getgov-staging.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# Public site base URL
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
# default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
routes:
- route: getgov-jon.app.cloud.gov
- route: getgov-staging.app.cloud.gov
services:
- getgov-credentials
- getgov-jon-database
- getgov-staging-database

View file

@ -43,7 +43,7 @@ cp ops/scripts/manifest-sandbox-template.yaml ops/manifests/manifest-$1.yaml
sed -i '' "s/ENVIRONMENT/$1/" "ops/manifests/manifest-$1.yaml"
echo "Adding new environment to settings.py..."
sed -i '' '/getgov-stable.app.cloud.gov/ {a\
sed -i '' '/getgov-staging.app.cloud.gov/ {a\
'\"getgov-$1.app.cloud.gov\"',
}' src/registrar/config/settings.py
@ -65,7 +65,7 @@ done
echo "Creating new cloud.gov credentials for $1..."
django_key=$(python3 -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())')
openssl req -nodes -x509 -days 365 -newkey rsa:2048 -keyout private-$1.pem -out public-$1.crt
login_key=$(base64 private-$1.pem)
login_key=$(base64 -i private-$1.pem)
jq -n --arg django_key "$django_key" --arg login_key "$login_key" '{"DJANGO_SECRET_KEY":$django_key,"DJANGO_SECRET_LOGIN_KEY":$login_key}' > credentials-$1.json
cf cups getgov-credentials -p credentials-$1.json
@ -105,11 +105,11 @@ echo
echo "Moving on to setup Github automation..."
echo "Adding new environment to Github Actions..."
sed -i '' '/ - stable/ {a\
sed -i '' '/ - staging/ {a\
- '"$1"'
}' .github/workflows/reset-db.yaml
sed -i '' '/ - stable/ {a\
sed -i '' '/ - staging/ {a\
- '"$1"'
}' .github/workflows/migrate.yaml

View file

@ -1,14 +1,11 @@
{
"defaults": {
"concurrency": 1,
"timeout": 30000,
"hideElements": "a[href='/whoami/']"
"timeout": 30000
},
"urls": [
"http://localhost:8080/",
"http://localhost:8080/health/",
"http://localhost:8080/whoami/",
"http://localhost:8080/register/",
"http://localhost:8080/register/organization/",
"http://localhost:8080/register/org_federal/",

View file

@ -1,11 +1,13 @@
import logging
from django.contrib import admin, messages
from django.contrib.auth.admin import UserAdmin
from django.contrib.contenttypes.models import ContentType
from django.http.response import HttpResponseRedirect
from django.urls import reverse
from . import models
logger = logging.getLogger(__name__)
class AuditedAdmin(admin.ModelAdmin):
@ -78,13 +80,37 @@ class DomainAdmin(AuditedAdmin):
return super().response_change(request, obj)
class DomainApplicationAdmin(AuditedAdmin):
"""Customize the applications listing view."""
# Trigger action when a fieldset is changed
def save_model(self, request, obj, form, change):
if change: # Check if the application is being edited
# Get the original application from the database
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
if (
obj.status != original_obj.status
and obj.status == models.DomainApplication.INVESTIGATING
):
# This is a transition annotated method in model which will throw an
# error if the condition is violated. To make this work, we need to
# call it on the original object which has the right status value,
# but pass the current object which contains the up-to-date data
# for the email.
original_obj.in_review(obj)
super().save_model(request, obj, form, change)
admin.site.register(models.User, MyUserAdmin)
admin.site.register(models.UserDomainRole, AuditedAdmin)
admin.site.register(models.Contact, AuditedAdmin)
admin.site.register(models.DomainInvitation, AuditedAdmin)
admin.site.register(models.DomainApplication, AuditedAdmin)
admin.site.register(models.DomainInformation, AuditedAdmin)
admin.site.register(models.Domain, DomainAdmin)
admin.site.register(models.Host, MyHostAdmin)
admin.site.register(models.Nameserver, MyHostAdmin)
admin.site.register(models.Website, AuditedAdmin)
admin.site.register(models.DomainApplication, DomainApplicationAdmin)

View file

@ -564,15 +564,13 @@ SECURE_SSL_REDIRECT = True
# web server configurations.
ALLOWED_HOSTS = [
"getgov-stable.app.cloud.gov",
"getgov-staging.app.cloud.gov",
"getgov-gd.app.cloud.gov",
"getgov-rb.app.cloud.gov",
"getgov-ko.app.cloud.gov",
"getgov-ab.app.cloud.gov",
"getgov-bl.app.cloud.gov",
"getgov-rjm.app.cloud.gov",
"getgov-jon.app.cloud.gov",
"getgov-mr.app.cloud.gov",
"getgov-sspj.app.cloud.gov",
"getgov-nmb.app.cloud.gov",
"getgov-ik.app.cloud.gov",
"get.gov",
]

View file

@ -45,7 +45,6 @@ for step, view in [
urlpatterns = [
path("", views.index, name="home"),
path("whoami/", views.whoami, name="whoami"),
path("admin/", admin.site.urls),
path(
"application/<id>/edit/",

View file

@ -24,31 +24,6 @@ class UserFixture:
"""
ADMINS = [
{
"username": "c4a0e101-73b4-4d7d-9e5e-7f19a726a0fa",
"first_name": "Seamus",
"last_name": "Johnston",
},
{
"username": "d4c3bd84-dc3a-48bc-a3c3-f53111df2ec6",
"first_name": "Igor",
"last_name": "",
},
{
"username": "ee80bfe0-49ad-456d-8d82-e2b608a66517",
"first_name": "Logan",
"last_name": "",
},
{
"username": "2ffe71b0-cea4-4097-8fb6-7a35b901dd70",
"first_name": "Neil",
"last_name": "Martinsen-Burrell",
},
{
"username": "7185e6cd-d3c8-4adc-90a3-ceddba71d24f",
"first_name": "Jon",
"last_name": "Roberts",
},
{
"username": "5f283494-31bd-49b5-b024-a7e7cae00848",
"first_name": "Rachid",
@ -59,11 +34,6 @@ 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",

View file

@ -490,9 +490,38 @@ class DomainApplication(TimeStampedModel):
self.submitter.email,
context={"application": self},
)
logger.info(
f"Submission confirmation email sent to: {self.submitter.email}"
)
except EmailSendingError:
logger.warning("Failed to send confirmation email", exc_info=True)
def _send_in_review_email(self):
"""Send an email that this application is now in review.
The email goes to the email address that the submitter gave as their
contact information. If there is not submitter information, then do
nothing.
"""
if self.submitter is None or self.submitter.email is None:
logger.warning(
"Cannot send status change (in review) email,"
"no submitter email address."
)
return
try:
send_templated_email(
"emails/status_change_in_review.txt",
"emails/status_change_in_review_subject.txt",
self.submitter.email,
context={"application": self},
)
logging.info(f"In review email sent to: {self.submitter.email}")
except EmailSendingError:
logger.warning(
"Failed to send status change (in review) email", exc_info=True
)
@transition(field="status", source=[STARTED, WITHDRAWN], target=SUBMITTED)
def submit(self):
"""Submit an application that is started."""
@ -541,6 +570,19 @@ class DomainApplication(TimeStampedModel):
user=self.creator, domain=created_domain, role=UserDomainRole.Roles.ADMIN
)
@transition(field="status", source=SUBMITTED, target=INVESTIGATING)
def in_review(self, updated_domain_application):
"""Investigate an application that has been submitted.
This method is called in admin.py on the original application
which has the correct status value, but is passed the changed
application which has the up-to-date data that we'll use
in the email."""
# 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."""

View file

@ -152,7 +152,7 @@
<ul class="usa-nav__primary usa-accordion">
<li class="usa-nav__primary-item">
{% if user.is_authenticated %}
<a href="{% url 'whoami' %}"><span>{{ user.email }}</span></a>
<span>{{ user.email }}</span>
</li>
<li class="usa-nav__primary-item display-flex flex-align-center">
<span class="text-base"> | </span>

View file

@ -0,0 +1,39 @@
SUMMARY OF YOUR DOMAIN REQUEST
Type of organization:
{{ application.get_organization_type_display }}
Organization name and mailing address:
{% spaceless %}{{ application.organization_name }}
{{ application.address_line1 }}{% if application.address_line2 %}
{{ application.address_line2 }}{% endif %}
{{ application.city }}, {{ application.state_territory }}
{{ application.zipcode }}{% if application.urbanization %}
{{ application.urbanization }}{% endif %}{% endspaceless %}
{% if application.type_of_work %}{# if block makes one newline if it's false #}
Type of work:
{% spaceless %}{{ application.type_of_work }}{% endspaceless %}
{% endif %}
Authorizing official:
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.authorizing_official %}{% endspaceless %}
{% if application.current_websites.exists %}{# if block makes a newline #}
Current website for your organization: {% for site in application.current_websites.all %}
{% spaceless %}{{ site.website }}{% endspaceless %}
{% endfor %}{% endif %}
.gov domain:
{{ application.requested_domain.name }}
{% for site in application.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %}
{% endfor %}
Purpose of your domain:
{{ application.purpose }}
Your contact information:
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.submitter %}{% endspaceless %}
{% if application.other_contacts.all %}
Other employees from your organization:
{% for other in application.other_contacts.all %}
{% spaceless %}{% include "emails/includes/contact.txt" with contact=other %}{% endspaceless %}
{% endfor %}{% endif %}{% if application.anything_else %}
Anything else we should know?
{{ application.anything_else }}
{% endif %}

View file

@ -0,0 +1,43 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi {{ application.submitter.first_name }}.
Your .gov domain request is being reviewed.
DOMAIN REQUESTED: {{ application.requested_domain.name }}
REQUEST RECEIVED ON: {{ application.updated_at|date }}
REQUEST #: {{ application.id }}
STATUS: In review
NEED TO MAKE CHANGES?
If you need to change your request you have to first withdraw it. Once you
withdraw the request you can edit it and submit it again. Changing your request
might add to the wait time. Learn more about withdrawing your request.
<https://get.gov/help/domain-requests/#withdraw-your-domain-request>.
NEXT STEPS
- Were reviewing your request. This usually takes 20 business days.
- You can check the status of your request at any time.
<https://registrar.get.gov/application/{{ application.id }}>
- Well email you with questions or when we complete our review.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for
requesting a .gov domain.
----------------------------------------------------------------
{% include 'emails/includes/application_summary.txt' %}
----------------------------------------------------------------
The .gov team
Contact us: <https://get.gov/contact/>
Visit <https://get.gov>
{% endautoescape %}

View file

@ -0,0 +1 @@
Your .gov domain request is being reviewed

View file

@ -33,45 +33,7 @@ requesting a .gov domain.
----------------------------------------------------------------
SUMMARY OF YOUR DOMAIN REQUEST
Type of organization:
{{ application.get_organization_type_display }}
Organization name and mailing address:
{% spaceless %}{{ application.organization_name }}
{{ application.address_line1 }}{% if application.address_line2 %}
{{ application.address_line2 }}{% endif %}
{{ application.city }}, {{ application.state_territory }}
{{ application.zipcode }}{% if application.urbanization %}
{{ application.urbanization }}{% endif %}{% endspaceless %}
{% if application.type_of_work %}{# if block makes one newline if it's false #}
Type of work:
{% spaceless %}{{ application.type_of_work }}{% endspaceless %}
{% endif %}
Authorizing official:
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.authorizing_official %}{% endspaceless %}
{% if application.current_websites.exists %}{# if block makes a newline #}
Current website for your organization: {% for site in application.current_websites.all %}
{% spaceless %}{{ site.website }}{% endspaceless %}
{% endfor %}{% endif %}
.gov domain:
{{ application.requested_domain.name }}
{% for site in application.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %}
{% endfor %}
Purpose of your domain:
{{ application.purpose }}
Your contact information:
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.submitter %}{% endspaceless %}
{% if application.other_contacts.all %}
Other employees from your organization:
{% for other in application.other_contacts.all %}
{% spaceless %}{% include "emails/includes/contact.txt" with contact=other %}{% endspaceless %}
{% endfor %}{% endif %}{% if application.anything_else %}
Anything else we should know?
{{ application.anything_else }}
{% endif %}
{% include 'emails/includes/application_summary.txt' %}
----------------------------------------------------------------
The .gov team

View file

@ -44,7 +44,7 @@
<p>Domain requests from state legislatures and courts must be authorized by an agencys <strong>Chief Information Officer</strong> or <strong>highest-ranking executive</strong>.</p>
{% elif organization_type == 'tribal' %}
<p>Domain requests from federally-recognized tribal governments must be authorized by the leader of the tribe, as recognized by the <a href="https://www.bia.gov/service/tribal-leaders-directory" class="usa-link">Bureau of Indian Affairs.</a></p>
<p>Domain requests from state-recognized tribal governments must be authorized by the leader of the tribe, as determined by the states tribal recognition initiative.</p>
<p><strong>Domain requests from federally-recognized tribal governments must be authorized by the leader of the tribe</strong>, as recognized by the <a href="https://www.bia.gov/service/tribal-leaders-directory" class="usa-link">Bureau of Indian Affairs.</a></p>
<p><strong>Domain requests from state-recognized tribal governments must be authorized by the leader of the tribe</strong>, as determined by the states tribal recognition initiative.</p>
{% endif %}

View file

@ -1,10 +0,0 @@
{% extends 'base.html' %}
{% block title %} Hello {% endblock %}
{% block content %}
<main id="main-content" class="grid-container">
<p> Hello {{ user.last_name|default:"No last name given" }}, {{ user.first_name|default:"No first name given" }} &lt;{{ user.email }}&gt;! </p>
<p><a href="{% url 'logout' %}">Click here to log out</a></p>
</main>
{% endblock %}

View file

@ -8,6 +8,8 @@ from typing import List, Dict
from django.conf import settings
from django.contrib.auth import get_user_model, login
from registrar.models import Contact, DraftDomain, Website, DomainApplication
def get_handlers():
"""Obtain pointers to all StreamHandlers."""
@ -84,3 +86,74 @@ class MockSESClient(Mock):
def send_email(self, *args, **kwargs):
self.EMAILS_SENT.append({"args": args, "kwargs": kwargs})
def completed_application(
has_other_contacts=True,
has_current_website=True,
has_alternative_gov_domain=True,
has_type_of_work=True,
has_anything_else=True,
status=DomainApplication.STARTED,
user=False,
):
"""A completed domain application."""
if not user:
user = get_user_model().objects.create(username="username")
ao, _ = Contact.objects.get_or_create(
first_name="Testy",
last_name="Tester",
title="Chief Tester",
email="testy@town.com",
phone="(555) 555 5555",
)
domain, _ = DraftDomain.objects.get_or_create(name="city.gov")
alt, _ = Website.objects.get_or_create(website="city1.gov")
current, _ = Website.objects.get_or_create(website="city.com")
you, _ = Contact.objects.get_or_create(
first_name="Testy you",
last_name="Tester you",
title="Admin Tester",
email="mayor@igorville.gov",
phone="(555) 555 5556",
)
other, _ = Contact.objects.get_or_create(
first_name="Testy2",
last_name="Tester2",
title="Another Tester",
email="testy2@town.com",
phone="(555) 555 5557",
)
domain_application_kwargs = dict(
organization_type="federal",
federal_type="executive",
purpose="Purpose of the site",
is_policy_acknowledged=True,
organization_name="Testorg",
address_line1="address 1",
address_line2="address 2",
state_territory="NY",
zipcode="10002",
authorizing_official=ao,
requested_domain=domain,
submitter=you,
creator=user,
status=status,
)
if has_type_of_work:
domain_application_kwargs["type_of_work"] = "e-Government"
if has_anything_else:
domain_application_kwargs["anything_else"] = "There is more"
application, _ = DomainApplication.objects.get_or_create(
**domain_application_kwargs
)
if has_other_contacts:
application.other_contacts.add(other)
if has_current_website:
application.current_websites.add(current)
if has_alternative_gov_domain:
application.alternative_domains.add(alt)
return application

View file

@ -0,0 +1,64 @@
from django.test import TestCase, RequestFactory
from django.contrib.admin.sites import AdminSite
from registrar.admin import DomainApplicationAdmin
from registrar.models import DomainApplication, User
from .common import completed_application
from django.conf import settings
from unittest.mock import MagicMock
import boto3_mocking # type: ignore
class TestDomainApplicationAdmin(TestCase):
def setUp(self):
self.site = AdminSite()
self.factory = RequestFactory()
@boto3_mocking.patching
def test_save_model_sends_email_on_property_change(self):
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
mock_client = MagicMock()
mock_client_instance = mock_client.return_value
with boto3_mocking.clients.handler_for("sesv2", mock_client):
# Create a sample application
application = completed_application(status=DomainApplication.SUBMITTED)
# Create a mock request
request = self.factory.post(
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
)
# Create an instance of the model admin
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
# Modify the application's property
application.status = DomainApplication.INVESTIGATING
# Use the model admin's save_model method
model_admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = mock_client_instance.send_email.call_args
args, kwargs = call_args
# Retrieve the email details from the arguments
from_email = kwargs.get("FromEmailAddress")
to_email = kwargs["Destination"]["ToAddresses"][0]
email_content = kwargs["Content"]
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
# Assert or perform other checks on the email details
expected_string = "Your .gov domain request is being reviewed"
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(to_email, EMAIL)
self.assertIn(expected_string, email_body)
# Perform assertions on the mock call itself
mock_client_instance.send_email.assert_called_once()
# Cleanup
application.delete()

View file

@ -2,82 +2,14 @@
from unittest.mock import MagicMock
from django.contrib.auth import get_user_model
from django.test import TestCase
from .common import completed_application
from registrar.models import Contact, DraftDomain, Website, DomainApplication
import boto3_mocking # type: ignore
class TestEmails(TestCase):
def _completed_application(
self,
has_other_contacts=True,
has_current_website=True,
has_alternative_gov_domain=True,
has_type_of_work=True,
has_anything_else=True,
):
"""A completed domain application."""
user = get_user_model().objects.create(username="username")
ao, _ = Contact.objects.get_or_create(
first_name="Testy",
last_name="Tester",
title="Chief Tester",
email="testy@town.com",
phone="(555) 555 5555",
)
domain, _ = DraftDomain.objects.get_or_create(name="city.gov")
alt, _ = Website.objects.get_or_create(website="city1.gov")
current, _ = Website.objects.get_or_create(website="city.com")
you, _ = Contact.objects.get_or_create(
first_name="Testy you",
last_name="Tester you",
title="Admin Tester",
email="testy-admin@town.com",
phone="(555) 555 5556",
)
other, _ = Contact.objects.get_or_create(
first_name="Testy2",
last_name="Tester2",
title="Another Tester",
email="testy2@town.com",
phone="(555) 555 5557",
)
domain_application_kwargs = dict(
organization_type="federal",
federal_type="executive",
purpose="Purpose of the site",
is_policy_acknowledged=True,
organization_name="Testorg",
address_line1="address 1",
address_line2="address 2",
state_territory="NY",
zipcode="10002",
authorizing_official=ao,
requested_domain=domain,
submitter=you,
creator=user,
)
if has_type_of_work:
domain_application_kwargs["type_of_work"] = "e-Government"
if has_anything_else:
domain_application_kwargs["anything_else"] = "There is more"
application, _ = DomainApplication.objects.get_or_create(
**domain_application_kwargs
)
if has_other_contacts:
application.other_contacts.add(other)
if has_current_website:
application.current_websites.add(current)
if has_alternative_gov_domain:
application.alternative_domains.add(alt)
return application
def setUp(self):
self.mock_client_class = MagicMock()
self.mock_client = self.mock_client_class.return_value
@ -85,7 +17,7 @@ class TestEmails(TestCase):
@boto3_mocking.patching
def test_submission_confirmation(self):
"""Submission confirmation email works."""
application = self._completed_application()
application = completed_application()
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
application.submit()
@ -122,7 +54,7 @@ class TestEmails(TestCase):
@boto3_mocking.patching
def test_submission_confirmation_no_current_website_spacing(self):
"""Test line spacing without current_website."""
application = self._completed_application(has_current_website=False)
application = completed_application(has_current_website=False)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
application.submit()
_, kwargs = self.mock_client.send_email.call_args
@ -134,7 +66,7 @@ class TestEmails(TestCase):
@boto3_mocking.patching
def test_submission_confirmation_current_website_spacing(self):
"""Test line spacing with current_website."""
application = self._completed_application(has_current_website=True)
application = completed_application(has_current_website=True)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
application.submit()
_, kwargs = self.mock_client.send_email.call_args
@ -147,7 +79,7 @@ class TestEmails(TestCase):
@boto3_mocking.patching
def test_submission_confirmation_other_contacts_spacing(self):
"""Test line spacing with other contacts."""
application = self._completed_application(has_other_contacts=True)
application = completed_application(has_other_contacts=True)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
application.submit()
_, kwargs = self.mock_client.send_email.call_args
@ -160,7 +92,7 @@ class TestEmails(TestCase):
@boto3_mocking.patching
def test_submission_confirmation_no_other_contacts_spacing(self):
"""Test line spacing without other contacts."""
application = self._completed_application(has_other_contacts=False)
application = completed_application(has_other_contacts=False)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
application.submit()
_, kwargs = self.mock_client.send_email.call_args
@ -172,7 +104,7 @@ class TestEmails(TestCase):
@boto3_mocking.patching
def test_submission_confirmation_alternative_govdomain_spacing(self):
"""Test line spacing with alternative .gov domain."""
application = self._completed_application(has_alternative_gov_domain=True)
application = completed_application(has_alternative_gov_domain=True)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
application.submit()
_, kwargs = self.mock_client.send_email.call_args
@ -184,7 +116,7 @@ class TestEmails(TestCase):
@boto3_mocking.patching
def test_submission_confirmation_no_alternative_govdomain_spacing(self):
"""Test line spacing without alternative .gov domain."""
application = self._completed_application(has_alternative_gov_domain=False)
application = completed_application(has_alternative_gov_domain=False)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
application.submit()
_, kwargs = self.mock_client.send_email.call_args
@ -196,7 +128,7 @@ class TestEmails(TestCase):
@boto3_mocking.patching
def test_submission_confirmation_type_of_work_spacing(self):
"""Test line spacing with type of work."""
application = self._completed_application(has_type_of_work=True)
application = completed_application(has_type_of_work=True)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
application.submit()
_, kwargs = self.mock_client.send_email.call_args
@ -208,7 +140,7 @@ class TestEmails(TestCase):
@boto3_mocking.patching
def test_submission_confirmation_no_type_of_work_spacing(self):
"""Test line spacing without type of work."""
application = self._completed_application(has_type_of_work=False)
application = completed_application(has_type_of_work=False)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
application.submit()
_, kwargs = self.mock_client.send_email.call_args
@ -220,7 +152,7 @@ class TestEmails(TestCase):
@boto3_mocking.patching
def test_submission_confirmation_anything_else_spacing(self):
"""Test line spacing with anything else."""
application = self._completed_application(has_anything_else=True)
application = completed_application(has_anything_else=True)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
application.submit()
_, kwargs = self.mock_client.send_email.call_args
@ -231,7 +163,7 @@ class TestEmails(TestCase):
@boto3_mocking.patching
def test_submission_confirmation_no_anything_else_spacing(self):
"""Test line spacing without anything else."""
application = self._completed_application(has_anything_else=False)
application = completed_application(has_anything_else=False)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
application.submit()
_, kwargs = self.mock_client.send_email.call_args

View file

@ -5,6 +5,7 @@ from django.conf import settings
from django.test import Client, TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
from .common import completed_application
from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore
@ -39,12 +40,6 @@ class TestViews(TestCase):
response = self.client.get("/")
self.assertEqual(response.status_code, 302)
def test_whoami_page_no_user(self):
"""Whoami page not accessible without a logged-in user."""
response = self.client.get("/whoami/")
self.assertEqual(response.status_code, 302)
self.assertIn("?next=/whoami/", response.headers["Location"])
def test_application_form_not_logged_in(self):
"""Application form not accessible without a logged-in user."""
response = self.client.get("/register/")
@ -99,13 +94,6 @@ class LoggedInTests(TestWithUser):
# clean up
role.delete()
def test_whoami_page(self):
"""User information appears on the whoami page."""
response = self.client.get("/whoami/")
self.assertContains(response, self.user.first_name)
self.assertContains(response, self.user.last_name)
self.assertContains(response, self.user.email)
def test_application_form_view(self):
response = self.client.get("/register/", follow=True)
self.assertContains(
@ -1407,85 +1395,18 @@ class TestApplicationStatus(TestWithUser, WebTest):
self.app.set_user(self.user.username)
self.client.force_login(self.user)
def _completed_application(
self,
has_other_contacts=True,
has_current_website=True,
has_alternative_gov_domain=True,
has_type_of_work=True,
has_anything_else=True,
):
"""A completed domain application."""
ao, _ = Contact.objects.get_or_create(
first_name="Testy",
last_name="Tester",
title="Chief Tester",
email="testy@town.com",
phone="(555) 555 5555",
)
domain, _ = DraftDomain.objects.get_or_create(name="citystatus.gov")
alt, _ = Website.objects.get_or_create(website="city1.gov")
current, _ = Website.objects.get_or_create(website="city.com")
you, _ = Contact.objects.get_or_create(
first_name="Testy you",
last_name="Tester you",
title="Admin Tester",
email="testy-admin@town.com",
phone="(555) 555 5556",
)
other, _ = Contact.objects.get_or_create(
first_name="Testy2",
last_name="Tester2",
title="Another Tester",
email="testy2@town.com",
phone="(555) 555 5557",
)
domain_application_kwargs = dict(
organization_type="federal",
federal_type="executive",
purpose="Purpose of the site",
is_policy_acknowledged=True,
organization_name="Testorg",
address_line1="address 1",
address_line2="address 2",
state_territory="NY",
zipcode="10002",
authorizing_official=ao,
requested_domain=domain,
submitter=you,
creator=self.user,
)
if has_type_of_work:
domain_application_kwargs["type_of_work"] = "e-Government"
if has_anything_else:
domain_application_kwargs["anything_else"] = "There is more"
application, _ = DomainApplication.objects.get_or_create(
**domain_application_kwargs
)
application.status = DomainApplication.SUBMITTED
application.save()
if has_other_contacts:
application.other_contacts.add(other)
if has_current_website:
application.current_websites.add(current)
if has_alternative_gov_domain:
application.alternative_domains.add(alt)
return application
def test_application_status(self):
"""Checking application status page"""
application = self._completed_application()
application = completed_application(
status=DomainApplication.SUBMITTED, user=self.user
)
application.save()
home_page = self.app.get("/")
self.assertContains(home_page, "citystatus.gov")
self.assertContains(home_page, "city.gov")
# click the "Manage" link
detail_page = home_page.click("Manage")
self.assertContains(detail_page, "citystatus.gov")
self.assertContains(detail_page, "city.gov")
self.assertContains(detail_page, "Chief Tester")
self.assertContains(detail_page, "testy@town.com")
self.assertContains(detail_page, "Admin Tester")
@ -1493,14 +1414,16 @@ class TestApplicationStatus(TestWithUser, WebTest):
def test_application_withdraw(self):
"""Checking application status page"""
application = self._completed_application()
application = completed_application(
status=DomainApplication.SUBMITTED, user=self.user
)
application.save()
home_page = self.app.get("/")
self.assertContains(home_page, "citystatus.gov")
self.assertContains(home_page, "city.gov")
# click the "Manage" link
detail_page = home_page.click("Manage")
self.assertContains(detail_page, "citystatus.gov")
self.assertContains(detail_page, "city.gov")
self.assertContains(detail_page, "Chief Tester")
self.assertContains(detail_page, "testy@town.com")
self.assertContains(detail_page, "Admin Tester")
@ -1522,7 +1445,9 @@ class TestApplicationStatus(TestWithUser, WebTest):
def test_application_status_no_permissions(self):
"""Can't access applications without being the creator."""
application = self._completed_application()
application = completed_application(
status=DomainApplication.SUBMITTED, user=self.user
)
other_user = User()
other_user.save()
application.creator = other_user

View file

@ -12,4 +12,3 @@ from .domain import (
)
from .health import *
from .index import *
from .whoami import *

View file

@ -1,8 +0,0 @@
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
@login_required
def whoami(request):
"""This is the first page someone goes to after logging in."""
return render(request, "whoami.html")