Merge remote-tracking branch 'origin/main' into rh/2406-clipboard

This commit is contained in:
CocoByte 2024-07-30 12:52:19 -06:00
commit 57ba291434
No known key found for this signature in database
GPG key ID: BBFAA2526384C97F
80 changed files with 6492 additions and 4224 deletions

View file

@ -19,12 +19,13 @@ There are several tools we use locally that you will need to have.
- If you are using Windows, installation information can be found [here](https://github.com/cloudfoundry/cli/wiki/V8-CLI-Installation-Guide#installers-and-compressed-binaries)
- Alternatively, for Windows, [consider using chocolately](https://community.chocolatey.org/packages/cloudfoundry-cli/7.2.0)
- [ ] Make sure you have `gpg` >2.1.7. Run `gpg --version` to check. If not, [install gnupg](https://formulae.brew.sh/formula/gnupg)
- Alternatively, you can skip this step and [use ssh keys](#setting-up-commit-signing-with-ssh) instead
- [ ] Install the [Github CLI](https://cli.github.com/)
## Access
### Steps for the onboardee
- [ ] Setup [commit signing in Github](#setting-up-commit-signing) and with git locally.
- [ ] Setup commit signing in Github and with git locally using either [gpg](#setting-up-commit-signing-with-gpg) or [ssh](#setting-up-commit-signing-with-ssh).
- [ ] [Create a cloud.gov account](https://cloud.gov/docs/getting-started/accounts/)
- [ ] Email github@cisa.dhs.gov (cc: Cameron) to add you to the [CISA Github organization](https://github.com/getgov) and [.gov Team](https://github.com/orgs/cisagov/teams/gov).
- [ ] Ensure you can login to your cloud.gov account via the CLI
@ -51,7 +52,7 @@ cf login -a api.fr.cloud.gov --sso
- [ ] [Contributing Policy](https://github.com/cisagov/dotgov/tree/main/CONTRIBUTING.md)
## Setting up commit signing
## Setting up commit signing with GPG
Follow the instructions [here](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key) to generate a new GPG key (default configurations are okay) and add it to your GPG keys on Github.
@ -72,6 +73,22 @@ when setting up your key in Github.
Now test commit signing is working by checking out a branch (`yourname/test-commit-signing`) and making some small change to a file. Commit the change (it should prompt you for your GPG credential) and push it to Github. Look on Github at your branch and ensure the commit is `verified`.
## Setting up commit signing with SSH
Follow the instructions [here](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#generating-a-new-ssh-key) to generate a new SSH key and [add it to your SSH keys on Github](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account). Note that you need to add the key as a signing key.
Configure your key locally:
```bash
git config --global gpg.format ssh
git config --global commit.gpgsign true
git config --global user.signingkey <YOUR KEY>
```
Where `<YOUR KEY>` is the path to the private key you generated when running `ssh-keygen`. Usually this is located in ~\.ssh\.
Now test commit signing is working by checking out a branch (`yourinitials/test-commit-signing`) and making some small change to a file. Commit the change (it should prompt you for your key passphrase) and push it to Github. Look on Github at your branch and ensure the commit is `verified`.
### MacOS
**Note:** if you are on a mac and not able to successfully create a signed commit, getting the following error:
```zsh

View file

@ -28,6 +28,7 @@ on:
- ab
- rjm
- dk
- ms
jobs:
createcachetable:

View file

@ -0,0 +1,92 @@
# Manually deploy a branch of choice to an environment of choice.
name: Manual Build and Deploy
run-name: Manually build and deploy branch to sandbox of choice
on:
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
default: 'backup'
type: 'choice'
options:
- ab
- backup
- cb
- dk
- es
- gd
- ko
- ky
- nl
- rb
- rh
- rjm
- meoward
- bob
- hotgov
- litterbox
- ms
# GitHub Actions has no "good" way yet to dynamically input branches
branch:
description: 'Branch to deploy'
required: true
default: 'main'
type: string
jobs:
variables:
runs-on: ubuntu-latest
steps:
- name: Setting global variables
uses: actions/github-script@v6
id: var
with:
script: |
core.setOutput('environment', '${{ github.head_ref }}'.split("/")[0]);
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Compile USWDS assets
working-directory: ./src
run: |
docker compose run node npm install npm@latest &&
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: cloud-gov/cg-cli-tools@main
env:
ENVIRONMENT: ${{ github.event.inputs.environment }}
CF_USERNAME: CF_${{ github.event.inputs.environment }}_USERNAME
CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD
with:
cf_username: ${{ secrets[env.CF_USERNAME] }}
cf_password: ${{ secrets[env.CF_PASSWORD] }}
cf_org: cisa-dotgov
cf_space: ${{ env.ENVIRONMENT }}
cf_manifest: ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml
comment:
runs-on: ubuntu-latest
needs: [deploy]
steps:
- uses: actions/github-script@v6
env:
ENVIRONMENT: ${{ github.event.inputs.environment }}
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
body: '🥳 Successfully deployed to developer sandbox **[${{ env.ENVIRONMENT }}](https://getgov-${{ env.ENVIRONMENT }}.app.cloud.gov/)**.'
})

View file

@ -22,6 +22,7 @@ jobs:
- name: Compile USWDS assets
working-directory: ./src
run: |
docker compose run node npm install npm@latest &&
docker compose run node npm install &&
docker compose run node npx gulp copyAssets &&
docker compose run node npx gulp compile

View file

@ -47,6 +47,7 @@ jobs:
- name: Compile USWDS assets
working-directory: ./src
run: |
docker compose run node npm install npm@latest &&
docker compose run node npm install &&
docker compose run node npx gulp copyAssets &&
docker compose run node npx gulp compile

View file

@ -0,0 +1,18 @@
name: Notify users based on issue labels
on:
issues:
types: [labeled]
pull_request:
types: [labeled]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- uses: jenschelkopf/issue-label-notification-action@1.3
with:
recipients: |
design-review=@Katherine-Osos
message: 'cc/ {recipients} — adding you to this **{label}** issue!'

View file

@ -429,6 +429,10 @@ class ViewsTest(TestCase):
# Create a mock request
request = self.factory.get("/some-url")
request.session = {"acr_value": ""}
# Mock user and its attributes
mock_user = MagicMock()
mock_user.is_authenticated = True
request.user = mock_user
# Ensure that the CLIENT instance used in login_callback is the mock
# patch _requires_step_up_auth to return False
with patch("djangooidc.views._requires_step_up_auth", return_value=False), patch(

View file

@ -1,4 +1,3 @@
version: "3.0"
services:
app:
build: .

View file

@ -3,10 +3,10 @@ from dateutil.tz import tzlocal # type: ignore
from unittest.mock import MagicMock, patch
from pathlib import Path
from django.test import TestCase
from api.tests.common import less_console_noise_decorator
from gevent.exceptions import ConcurrentObjectUseError
from epplibwrapper.client import EPPLibWrapper
from epplibwrapper.errors import RegistryError, LoginError
from .common import less_console_noise
import logging
try:
@ -24,14 +24,15 @@ logger = logging.getLogger(__name__)
class TestClient(TestCase):
"""Test the EPPlibwrapper client"""
@less_console_noise_decorator
def fake_result(self, code, msg):
"""Helper function to create a fake Result object"""
return Result(code=code, msg=msg, res_data=[], cl_tr_id="cl_tr_id", sv_tr_id="sv_tr_id")
@less_console_noise_decorator
@patch("epplibwrapper.client.Client")
def test_initialize_client_success(self, mock_client):
"""Test when the initialize_client is successful"""
with less_console_noise():
# Mock the Client instance and its methods
mock_connect = MagicMock()
# Create a mock Result instance
@ -53,10 +54,10 @@ class TestClient(TestCase):
# Assert that _client is not None after initialization
self.assertIsNotNone(wrapper._client)
@less_console_noise_decorator
@patch("epplibwrapper.client.Client")
def test_initialize_client_transport_error(self, mock_client):
"""Test when the send(login) step of initialize_client raises a TransportError."""
with less_console_noise():
# Mock the Client instance and its methods
mock_connect = MagicMock()
mock_send = MagicMock(side_effect=TransportError("Transport error"))
@ -72,10 +73,10 @@ class TestClient(TestCase):
# the raised exception
wrapper._initialize_client()
@less_console_noise_decorator
@patch("epplibwrapper.client.Client")
def test_initialize_client_login_error(self, mock_client):
"""Test when the send(login) step of initialize_client returns (2400) comamnd failed code."""
with less_console_noise():
# Mock the Client instance and its methods
mock_connect = MagicMock()
# Create a mock Result instance
@ -98,10 +99,10 @@ class TestClient(TestCase):
# the raised exception
wrapper._initialize_client()
@less_console_noise_decorator
@patch("epplibwrapper.client.Client")
def test_initialize_client_unknown_exception(self, mock_client):
"""Test when the send(login) step of initialize_client raises an unexpected Exception."""
with less_console_noise():
# Mock the Client instance and its methods
mock_connect = MagicMock()
mock_send = MagicMock(side_effect=Exception("Unknown exception"))
@ -117,6 +118,7 @@ class TestClient(TestCase):
# the raised exception
wrapper._initialize_client()
@less_console_noise_decorator
@patch("epplibwrapper.client.Client")
def test_initialize_client_fails_recovers_with_send_command(self, mock_client):
"""Test when the initialize_client fails on the connect() step. And then a subsequent
@ -126,7 +128,6 @@ class TestClient(TestCase):
Initialization step fails at app init
Send command fails (with 2400 code) prompting retry
Client closes and re-initializes, and command is sent successfully"""
with less_console_noise():
# Mock the Client instance and its methods
# close() should return successfully
mock_close = MagicMock()
@ -176,6 +177,7 @@ class TestClient(TestCase):
# passed send(login), passed send("InfoDomainCommand")
self.assertEquals(mock_send.call_count, 4)
@less_console_noise_decorator
@patch("epplibwrapper.client.Client")
def test_send_command_failed_retries_and_fails_again(self, mock_client):
"""Test when the send("InfoDomainCommand) call fails with a 2400, prompting a retry
@ -185,7 +187,6 @@ class TestClient(TestCase):
Initialization succeeds
Send command fails (with 2400 code) prompting retry
Client closes and re-initializes, and command fails again with 2400"""
with less_console_noise():
# Mock the Client instance and its methods
# connect() and close() should succeed throughout
mock_connect = MagicMock()
@ -221,6 +222,7 @@ class TestClient(TestCase):
# send(login), send(command)
self.assertEquals(mock_send.call_count, 5)
@less_console_noise_decorator
@patch("epplibwrapper.client.Client")
def test_send_command_failure_prompts_successful_retry(self, mock_client):
"""Test when the send("InfoDomainCommand) call fails with a 2400, prompting a retry
@ -229,7 +231,6 @@ class TestClient(TestCase):
Initialization succeeds
Send command fails (with 2400 code) prompting retry
Client closes and re-initializes, and command succeeds"""
with less_console_noise():
# Mock the Client instance and its methods
# connect() and close() should succeed throughout
mock_connect = MagicMock()
@ -263,6 +264,7 @@ class TestClient(TestCase):
# send() is called 5 times: send(login), send(command) fail, send(logout), send(login), send(command)
self.assertEquals(mock_send.call_count, 5)
@less_console_noise_decorator
def fake_failure_send_concurrent_threads(self, command=None, cleaned=None):
"""
Raises a ConcurrentObjectUseError, which gevent throws when accessing
@ -277,6 +279,7 @@ class TestClient(TestCase):
"""
pass # noqa
@less_console_noise_decorator
def fake_success_send(self, command=None, cleaned=None):
"""
Simulates receiving a success response from EPP.
@ -292,6 +295,7 @@ class TestClient(TestCase):
)
return mock
@less_console_noise_decorator
def fake_info_domain_received(self, command=None, cleaned=None):
"""
Simulates receiving a response by reading from a predefined XML file.
@ -300,6 +304,7 @@ class TestClient(TestCase):
xml = (location).read_bytes()
return xml
@less_console_noise_decorator
def get_fake_epp_result(self):
"""Mimics a return from EPP by returning a dictionary in the same format"""
result = {
@ -338,6 +343,7 @@ class TestClient(TestCase):
}
return result
@less_console_noise_decorator
def test_send_command_close_failure_recovers(self):
"""
Validates the resilience of the connection handling mechanism
@ -350,7 +356,6 @@ class TestClient(TestCase):
- Subsequently, the client re-initializes the connection.
- A retry of the command execution post-reinitialization succeeds.
"""
expected_result = self.get_fake_epp_result()
wrapper = None
# Trigger a retry

View file

@ -9,6 +9,8 @@ from django.db.models.functions import Concat, Coalesce
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django_fsm import get_available_FIELD_transitions, FSMField
from registrar.models.domain_group import DomainGroup
from registrar.models.suborganization import Suborganization
from waffle.decorators import flag_is_active
from django.contrib import admin, messages
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
@ -35,6 +37,7 @@ from django_admin_multiple_choice_list_filter.list_filters import MultipleChoice
from import_export import resources
from import_export.admin import ImportExportModelAdmin
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext_lazy as _
@ -90,6 +93,31 @@ class UserResource(resources.ModelResource):
model = models.User
class FilteredSelectMultipleArrayWidget(FilteredSelectMultiple):
"""Custom widget to allow for editing an ArrayField in a widget similar to filter_horizontal widget"""
def __init__(self, verbose_name, is_stacked=False, choices=(), **kwargs):
super().__init__(verbose_name, is_stacked, **kwargs)
self.choices = choices
def value_from_datadict(self, data, files, name):
values = super().value_from_datadict(data, files, name)
return values or []
def get_context(self, name, value, attrs):
if value is None:
value = []
elif isinstance(value, str):
value = value.split(",")
# alter self.choices to be a list of selected and unselected choices, based on value;
# order such that selected choices come before unselected choices
self.choices = [(choice, label) for choice, label in self.choices if choice in value] + [
(choice, label) for choice, label in self.choices if choice not in value
]
context = super().get_context(name, value, attrs)
return context
class MyUserAdminForm(UserChangeForm):
"""This form utilizes the custom widget for its class's ManyToMany UIs.
@ -102,6 +130,14 @@ class MyUserAdminForm(UserChangeForm):
widgets = {
"groups": NoAutocompleteFilteredSelectMultiple("groups", False),
"user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False),
"portfolio_roles": FilteredSelectMultipleArrayWidget(
"portfolio_roles", is_stacked=False, choices=User.UserPortfolioRoleChoices.choices
),
"portfolio_additional_permissions": FilteredSelectMultipleArrayWidget(
"portfolio_additional_permissions",
is_stacked=False,
choices=User.UserPortfolioPermissionChoices.choices,
),
}
def __init__(self, *args, **kwargs):
@ -652,18 +688,49 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
"is_superuser",
"groups",
"user_permissions",
"portfolio",
"portfolio_roles",
"portfolio_additional_permissions",
)
},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
autocomplete_fields = [
"portfolio",
]
readonly_fields = ("verification_type",)
# Hide Username (uuid), Groups and Permissions
# Q: Now that we're using Groups and Permissions,
# do we expose those to analysts to view?
analyst_fieldsets = (
(
None,
{
"fields": (
"status",
"verification_type",
)
},
),
("User profile", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
(
"Permissions",
{
"fields": (
"is_active",
"groups",
"portfolio",
"portfolio_roles",
"portfolio_additional_permissions",
)
},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
# TODO: delete after we merge organization feature
analyst_fieldsets_no_portfolio = (
(
None,
{
@ -703,6 +770,27 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
"last_name",
"title",
"email",
"phone",
"Permissions",
"is_active",
"groups",
"Important dates",
"last_login",
"date_joined",
"portfolio",
"portfolio_roles",
"portfolio_additional_permissions",
]
# TODO: delete after we merge organization feature
analyst_readonly_fields_no_portfolio = [
"User profile",
"first_name",
"middle_name",
"last_name",
"title",
"email",
"phone",
"Permissions",
"is_active",
"groups",
@ -783,8 +871,12 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
# Show all fields for all access users
return super().get_fieldsets(request, obj)
elif request.user.has_perm("registrar.analyst_access_permission"):
if flag_is_active(request, "organization_feature"):
# show analyst_fieldsets for analysts
return self.analyst_fieldsets
else:
# TODO: delete after we merge organization feature
return self.analyst_fieldsets_no_portfolio
else:
# any admin user should belong to either full_access_group
# or cisa_analyst_group
@ -798,7 +890,11 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
else:
# Return restrictive Read-only fields for analysts and
# users who might not belong to groups
if flag_is_active(request, "organization_feature"):
return self.analyst_readonly_fields
else:
# TODO: delete after we merge organization feature
return self.analyst_readonly_fields_no_portfolio
def change_view(self, request, object_id, form_url="", extra_context=None):
"""Add user's related domains and requests to context"""
@ -1001,6 +1097,16 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
def save_model(self, request, obj, form, change):
# Clear warning messages before saving
storage = messages.get_messages(request)
storage.used = False
for message in storage:
if message.level == messages.WARNING:
storage.used = True
return super().save_model(request, obj, form, change)
class SeniorOfficialAdmin(ListHeaderAdmin):
"""Custom Senior Official Admin class."""
@ -1285,10 +1391,11 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
]
# Readonly fields for analysts and superusers
readonly_fields = ("other_contacts", "is_election_board", "federal_agency")
readonly_fields = ("other_contacts", "is_election_board")
# Read only that we'll leverage for CISA Analysts
analyst_readonly_fields = [
"federal_agency",
"creator",
"type_of_work",
"more_organization_information",
@ -1601,12 +1708,12 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"current_websites",
"alternative_domains",
"is_election_board",
"federal_agency",
"status_history",
)
# Read only that we'll leverage for CISA Analysts
analyst_readonly_fields = [
"federal_agency",
"creator",
"about_your_organization",
"requested_domain",
@ -2659,26 +2766,38 @@ class VerifiedByStaffAdmin(ListHeaderAdmin):
class PortfolioAdmin(ListHeaderAdmin):
# NOTE: these are just placeholders. Not part of ACs (haven't been defined yet). Update in future tickets.
change_form_template = "django/admin/portfolio_change_form.html"
list_display = ("organization_name", "federal_agency", "creator")
search_fields = ["organization_name"]
search_help_text = "Search by organization name."
# readonly_fields = [
# "requestor",
# ]
# Creates select2 fields (with search bars)
autocomplete_fields = [
"creator",
"federal_agency",
]
def change_view(self, request, object_id, form_url="", extra_context=None):
"""Add related suborganizations and domain groups"""
obj = self.get_object(request, object_id)
# ---- Domain Groups
domain_groups = DomainGroup.objects.filter(portfolio=obj)
# ---- Suborganizations
suborganizations = Suborganization.objects.filter(portfolio=obj)
extra_context = {"domain_groups": domain_groups, "suborganizations": suborganizations}
return super().change_view(request, object_id, form_url, extra_context)
def save_model(self, request, obj, form, change):
if obj.creator is not None:
# ---- update creator ----
# Set the creator field to the current admin user
obj.creator = request.user if request.user.is_authenticated else None
# ---- update organization name ----
# org name will be the same as federal agency, if it is federal,
# otherwise it will be the actual org name. If nothing is entered for
@ -2687,7 +2806,6 @@ class PortfolioAdmin(ListHeaderAdmin):
is_federal = obj.organization_type == DomainRequest.OrganizationChoices.FEDERAL
if is_federal and obj.organization_name is None:
obj.organization_name = obj.federal_agency.agency
super().save_model(request, obj, form, change)

View file

@ -305,6 +305,8 @@ function addOrRemoveSessionBoolean(name, add){
// "to" select list
checkToListThenInitWidget('id_groups_to', 0);
checkToListThenInitWidget('id_user_permissions_to', 0);
checkToListThenInitWidget('id_portfolio_roles_to', 0);
checkToListThenInitWidget('id_portfolio_additional_permissions_to', 0);
})();
// Function to check for the existence of the "to" select list element in the DOM, and if and when found,

View file

@ -657,6 +657,34 @@ function hideDeletedForms() {
});
}
// Checks for if we want to display Urbanization or not
document.addEventListener('DOMContentLoaded', function() {
var stateTerritoryField = document.querySelector('select[name="organization_contact-state_territory"]');
if (!stateTerritoryField) {
return; // Exit if the field not found
}
setupUrbanizationToggle(stateTerritoryField);
});
function setupUrbanizationToggle(stateTerritoryField) {
var urbanizationField = document.getElementById('urbanization-field');
function toggleUrbanizationField() {
// Checking specifically for Puerto Rico only
if (stateTerritoryField.value === 'PR') {
urbanizationField.style.display = 'block';
} else {
urbanizationField.style.display = 'none';
}
}
toggleUrbanizationField();
stateTerritoryField.addEventListener('change', toggleUrbanizationField);
}
/**
* An IIFE that attaches a click handler for our dynamic formsets
*
@ -1140,6 +1168,7 @@ document.addEventListener('DOMContentLoaded', function() {
const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
const statusIndicator = document.querySelector('.domain__filter-indicator');
const statusToggle = document.querySelector('.usa-button--filter');
const noPortfolioFlag = document.getElementById('no-portfolio-js-flag');
/**
* Loads rows in the domains list, as well as updates pagination around the domains list
@ -1173,8 +1202,20 @@ document.addEventListener('DOMContentLoaded', function() {
const expirationDateFormatted = expirationDate ? expirationDate.toLocaleDateString('en-US', options) : '';
const expirationDateSortValue = expirationDate ? expirationDate.getTime() : '';
const actionUrl = domain.action_url;
const suborganization = domain.suborganization ? domain.suborganization : '';
const row = document.createElement('tr');
let markupForSuborganizationRow = '';
if (!noPortfolioFlag) {
markupForSuborganizationRow = `
<td>
<span class="${suborganization ? 'ellipsis ellipsis--30 vertical-align-middle' : ''}" aria-label="${suborganization}" title="${suborganization}">${suborganization}</span>
</td>
`
}
row.innerHTML = `
<th scope="row" role="rowheader" data-label="Domain name">
${domain.name}
@ -1195,6 +1236,7 @@ document.addEventListener('DOMContentLoaded', function() {
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#info_outline"></use>
</svg>
</td>
${markupForSuborganizationRow}
<td>
<a href="${actionUrl}">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
@ -1826,6 +1868,9 @@ document.addEventListener('DOMContentLoaded', function() {
}
function setupListener(){
document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) {
// Get the "{field_name}" and "edit-button"
let fieldIdParts = button.id.split("__")
@ -1834,12 +1879,61 @@ document.addEventListener('DOMContentLoaded', function() {
// When the edit button is clicked, show the input field under it
handleEditButtonClick(fieldName, button);
let editableFormGroup = button.parentElement.parentElement.parentElement;
if (editableFormGroup){
let readonlyField = editableFormGroup.querySelector(".input-with-edit-button__readonly-field")
let inputField = document.getElementById(`id_${fieldName}`);
if (!inputField || !readonlyField) {
return;
}
let inputFieldValue = inputField.value
if (inputFieldValue || fieldName == "full_name"){
if (fieldName == "full_name"){
let firstName = document.querySelector("#id_first_name");
let middleName = document.querySelector("#id_middle_name");
let lastName = document.querySelector("#id_last_name");
if (firstName && lastName && firstName.value && lastName.value) {
let values = [firstName.value, middleName.value, lastName.value]
readonlyField.innerHTML = values.join(" ");
}else {
let fullNameField = document.querySelector('#full_name__edit-button-readonly');
let svg = fullNameField.querySelector("svg use")
if (svg) {
const currentHref = svg.getAttribute('xlink:href');
if (currentHref) {
const parts = currentHref.split('#');
if (parts.length === 2) {
// Keep the path before '#' and replace the part after '#' with 'invalid'
const newHref = parts[0] + '#error';
svg.setAttribute('xlink:href', newHref);
fullNameField.classList.add("input-with-edit-button__error")
label = fullNameField.querySelector(".input-with-edit-button__readonly-field")
label.innerHTML = "Unknown";
}
}
}
}
// Technically, the full_name field is optional, but we want to display it as required.
// This style is applied to readonly fields (gray text). This just removes it, as
// this is difficult to achieve otherwise by modifying the .readonly property.
if (readonlyField.classList.contains("text-base")) {
readonlyField.classList.remove("text-base")
}
}else {
readonlyField.innerHTML = inputFieldValue
}
}
}
}
});
}
function showInputOnErrorFields(){
document.addEventListener('DOMContentLoaded', function() {
// Get all input elements within the form
let form = document.querySelector("#finish-profile-setup-form");
let inputs = form ? form.querySelectorAll("input") : null;
@ -1878,9 +1972,9 @@ document.addEventListener('DOMContentLoaded', function() {
});
};
// Hookup all edit buttons to the `handleEditButtonClick` function
setupListener();
// Show the input fields if an error exists
showInputOnErrorFields();
})();

View file

@ -29,52 +29,14 @@ body {
#wrapper.dashboard {
background-color: color('primary-lightest');
padding-top: units(5);
padding-top: units(5)!important;
}
.usa-logo {
@include at-media(desktop) {
margin-top: units(2);
}
#wrapper.dashboard--portfolio {
background-color: color('gray-1');
padding-top: units(4)!important;
}
.usa-logo__text {
@include typeset('sans', 'xl', 2);
color: color('primary-darker');
}
.usa-nav__primary {
margin-top:units(1);
}
.usa-nav__primary-username {
display: inline-block;
padding: units(1) units(2);
max-width: 208px;
overflow: hidden;
text-overflow: ellipsis;
@include at-media(desktop) {
padding: units(2);
max-width: 500px;
}
}
@include at-media(desktop) {
.usa-nav__primary-item:not(:first-child) {
position: relative;
}
.usa-nav__primary-item:not(:first-child)::before {
content: '';
position: absolute;
top: 50%;
left: 0;
width: 0; /* No width since it's a border */
height: 40%;
border-left: solid 1px color('base-light');
transform: translateY(-50%);
}
}
.section--outlined {
background-color: color('white');
@ -110,6 +72,29 @@ body {
}
}
.section--outlined__header--no-portfolio {
.section--outlined__search,
.section--outlined__utility-button {
margin-top: units(2);
}
@include at-media(tablet) {
display: flex;
column-gap: units(3);
.section--outlined__search,
.section--outlined__utility-button {
margin-top: 0;
}
.section--outlined__search {
flex-grow: 4;
// Align right
max-width: 383px;
margin-left: auto;
}
}
}
.break-word {
word-break: break-word;
}
@ -136,10 +121,6 @@ footer {
color: color('primary');
}
.usa-identifier__logo {
height: units(7);
}
abbr[title] {
// workaround for underlining abbr element
border-bottom: none;
@ -179,47 +160,35 @@ abbr[title] {
cursor: pointer;
}
.input-with-edit-button {
svg.usa-icon {
width: 1.5em !important;
height: 1.5em !important;
color: #{$dhs-green};
position: absolute;
}
&.input-with-edit-button__error {
svg.usa-icon {
color: #{$dhs-red};
}
div.readonly-field {
color: #{$dhs-red};
}
}
}
// We need to deviate from some default USWDS styles here
// in this particular case, so we have to override this.
.usa-form .usa-button.readonly-edit-button {
margin-top: 0px !important;
padding-top: 0px !important;
svg {
width: 1.25em !important;
height: 1.25em !important;
}
}
// Define some styles for the .gov header/logo
.usa-logo button {
color: #{$dhs-dark-gray-85};
font-weight: 700;
font-family: family('sans');
font-size: 1.6rem;
line-height: 1.1;
}
.usa-logo button.usa-button--unstyled.disabled-button:hover{
color: #{$dhs-dark-gray-85};
}
.padding--8-8-9 {
padding: 8px 8px 9px !important;
}
.ellipsis {
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ellipsis--23 {
max-width: 23ch;
}
.ellipsis--30 {
max-width: 30ch;
}
.ellipsis--50 {
max-width: 50ch;
}
.vertical-align-middle {
vertical-align: middle;
}
@include at-media(desktop) {
.ellipsis--desktop-50 {
max-width: 50ch;
}
}

View file

@ -162,6 +162,34 @@ a.usa-button--unstyled:visited {
}
}
.input-with-edit-button {
svg.usa-icon {
width: 1.5em !important;
height: 1.5em !important;
color: #{$dhs-green};
position: absolute;
}
&.input-with-edit-button__error {
svg.usa-icon {
color: #{$dhs-red};
}
div.readonly-field {
color: #{$dhs-red};
}
}
}
// We need to deviate from some default USWDS styles here
// in this particular case, so we have to override this.
.usa-form .usa-button.readonly-edit-button {
margin-top: 0px !important;
padding-top: 0px !important;
svg {
width: 1.25em !important;
height: 1.25em !important;
}
}
.usa-button--filter {
width: auto;
// For mobile stacking
@ -177,3 +205,12 @@ a.usa-button--unstyled:visited {
}
}
.usa-icon.usa-icon--big {
margin: 0;
height: 1.5em;
width: 1.5em;
}
.margin-right-neg-4px {
margin-right: -4px;
}

View file

@ -0,0 +1,121 @@
@use "uswds-core" as *;
@use "cisa_colors" as *;
// Define some styles for the .gov header/logo
.usa-logo button {
color: #{$dhs-dark-gray-85};
font-weight: 700;
font-family: family('sans');
font-size: 1.6rem;
line-height: 1.1;
}
.usa-logo button:hover{
color: #{$dhs-dark-gray-85};
}
.usa-header {
.usa-logo {
@include at-media(desktop) {
margin-top: units(2);
}
}
.usa-logo__text {
@include typeset('sans', 'xl', 2);
}
.usa-nav__username {
max-width: 208px;
min-height: units(2);
@include at-media(desktop) {
max-width: 500px;
}
}
.padding-y-0 {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
}
.usa-header--basic {
.usa-logo__text {
color: color('primary-darker');
}
.usa-nav__username {
padding: units(1) units(2);
@include at-media(desktop) {
padding: units(2);
}
}
.usa-nav__primary {
margin-top:units(1);
}
@include at-media(desktop) {
.usa-nav__primary-item:not(:first-child) {
position: relative;
}
.usa-nav__primary-item:not(:first-child)::before {
content: '';
position: absolute;
top: 50%;
left: 0;
width: 0; /* No width since it's a border */
height: 40%;
border-left: solid 1px color('base-light');
transform: translateY(-50%);
}
}
}
.usa-header--extended {
@include at-media(desktop) {
background-color: color('primary-darker');
border-top: solid 1px color('base-light');
border-bottom: solid 1px color('base-lighter');
.usa-logo__text a,
.usa-logo__text button,
.usa-logo__text button:hover {
color: color('white');
}
.usa-nav {
background-color: color('primary-lightest');
}
.usa-nav__primary-item:last-child {
margin-left: auto;
.usa-nav-link {
margin-right: units(-2);
}
}
.usa-nav__primary {
.usa-nav-link,
.usa-nav-link:hover,
.usa-nav-link:active {
color: color('primary');
font-weight: font-weight('normal');
font-size: 16px;
}
.usa-current,
.usa-current:hover,
.usa-current:active {
font-weight: font-weight('bold');
}
}
.usa-nav__secondary {
// I don't know why USWDS has this at 2 rem, which puts it out of alignment
right: 3rem;
color: color('white');
bottom: 4.3rem;
.usa-nav-link,
.usa-nav-link:hover,
.usa-nav-link:active {
font-weight: font-weight('bold');
color: color('primary-lighter');
font-size: 16px;
}
}
> .usa-navbar {
// This is a dangerous override to USWDS, necessary because we have a tooltip on the logo
overflow: visible;
}
}
}

View file

@ -0,0 +1,9 @@
@use "uswds-core" as *;
.usa-banner {
background-color: color('primary-darker');
}
.usa-identifier__logo {
height: units(7);
}

View file

@ -1,7 +1,7 @@
@use "uswds-core" as *;
.dotgov-table {
a {
.dotgov-table a,
.usa-link--icon {
display: flex;
align-items: flex-start;
color: color('primary');
@ -9,10 +9,6 @@
&:visited {
color: color('primary');
}
}
}
a {
.usa-icon {
// align icon with x height
margin-top: units(0.5);

View file

@ -34,22 +34,6 @@
pointer-events: none;
}
}
// Ticket #1510
// @include at-media('desktop') {
// th:first-child {
// width: 220px;
// }
// th:nth-child(2) {
// width: 175px;
// }
// th:nth-child(3) {
// width: 130px;
// }
// th:nth-child(5) {
// width: 130px;
// }
// }
}
.dotgov-table {
@ -96,46 +80,3 @@
}
}
}
@media (min-width: 1040px){
.domain-requests__table {
th:nth-of-type(1) {
width: 200px;
}
th:nth-of-type(2) {
width: 158px;
}
th:nth-of-type(3) {
width: 120px;
}
th:nth-of-type(4) {
width: 95px;
}
th:nth-of-type(5) {
width: 85px;
}
}
}
@media (min-width: 1040px){
.domains__table {
th:nth-of-type(1) {
width: 200px;
}
th:nth-of-type(2) {
width: 158px;
}
th:nth-of-type(3) {
width: 215px;
}
th:nth-of-type(4) {
width: 95px;
}
}
}

View file

@ -21,6 +21,8 @@
@forward "alerts";
@forward "tables";
@forward "sidenav";
@forward "identifier";
@forward "header";
@forward "register-form";
/*--------------------------------------------------

View file

@ -240,6 +240,11 @@ TEMPLATES = [
"registrar.context_processors.canonical_path",
"registrar.context_processors.is_demo_site",
"registrar.context_processors.is_production",
"registrar.context_processors.org_user_status",
"registrar.context_processors.add_portfolio_to_context",
"registrar.context_processors.add_path_to_context",
"registrar.context_processors.add_has_profile_feature_flag_to_context",
"registrar.context_processors.portfolio_permissions",
],
},
},

View file

@ -9,7 +9,7 @@ from django.urls import include, path
from django.views.generic import RedirectView
from registrar import views
from registrar.views.admin_views import (
from registrar.views.report_views import (
ExportDataDomainsGrowth,
ExportDataFederal,
ExportDataFull,
@ -19,13 +19,14 @@ from registrar.views.admin_views import (
ExportDataUnmanagedDomains,
AnalyticsView,
ExportDomainRequestDataFull,
ExportDataTypeUser,
)
from registrar.views.domain_request import Step
from registrar.views.domain_requests_json import get_domain_requests_json
from registrar.views.domains_json import get_domains_json
from registrar.views.utility import always_404
from registrar.views.portfolios import portfolio_domains, portfolio_domain_requests
from registrar.views.portfolios import PortfolioDomainsView, PortfolioDomainRequestsView, PortfolioOrganizationView
from api.views import available, get_current_federal, get_current_full
@ -61,14 +62,19 @@ urlpatterns = [
path("", views.index, name="home"),
path(
"portfolio/<int:portfolio_id>/domains/",
portfolio_domains,
PortfolioDomainsView.as_view(),
name="portfolio-domains",
),
path(
"portfolio/<int:portfolio_id>/domain_requests/",
portfolio_domain_requests,
PortfolioDomainRequestsView.as_view(),
name="portfolio-domain-requests",
),
path(
"portfolio/<int:portfolio_id>/organization/",
PortfolioOrganizationView.as_view(),
name="portfolio-organization",
),
path(
"admin/logout/",
RedirectView.as_view(pattern_name="logout", permanent=False),
@ -119,6 +125,11 @@ urlpatterns = [
name="analytics",
),
path("admin/", admin.site.urls),
path(
"reports/export_data_type_user/",
ExportDataTypeUser.as_view(),
name="export_data_type_user",
),
path(
"domain-request/<id>/edit/",
views.DomainRequestWizard.as_view(),

View file

@ -1,4 +1,5 @@
from django.conf import settings
from waffle.decorators import flag_is_active
def language_code(request):
@ -36,3 +37,49 @@ def is_demo_site(request):
def is_production(request):
"""Add a boolean if this is our production site."""
return {"IS_PRODUCTION": settings.IS_PRODUCTION}
def org_user_status(request):
if request.user.is_authenticated:
is_org_user = request.user.is_org_user(request)
else:
is_org_user = False
return {
"is_org_user": is_org_user,
}
def add_portfolio_to_context(request):
return {"portfolio": getattr(request, "portfolio", None)}
def add_path_to_context(request):
return {"path": getattr(request, "path", None)}
def add_has_profile_feature_flag_to_context(request):
return {"has_profile_feature_flag": flag_is_active(request, "profile_feature")}
def portfolio_permissions(request):
"""Make portfolio permissions for the request user available in global context"""
try:
if not request.user or not request.user.is_authenticated:
return {
"has_base_portfolio_permission": False,
"has_domains_portfolio_permission": False,
"has_domain_requests_portfolio_permission": False,
}
return {
"has_base_portfolio_permission": request.user.has_base_portfolio_permission(),
"has_domains_portfolio_permission": request.user.has_domains_portfolio_permission(),
"has_domain_requests_portfolio_permission": request.user.has_domain_requests_portfolio_permission(),
}
except AttributeError:
# Handles cases where request.user might not exist
return {
"has_base_portfolio_permission": False,
"has_domains_portfolio_permission": False,
"has_domain_requests_portfolio_permission": False,
}

View file

@ -10,3 +10,6 @@ from .domain import (
DomainDsdataFormset,
DomainDsdataForm,
)
from .portfolio import (
PortfolioOrgAddressForm,
)

View file

@ -458,7 +458,7 @@ class DomainOrgNameAddressForm(forms.ModelForm):
# 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"]
required = ["organization_name", "address_line1", "city", "state_territory", "zipcode"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View file

@ -0,0 +1,69 @@
"""Forms for portfolio."""
import logging
from django import forms
from django.core.validators import RegexValidator
from ..models import DomainInformation, Portfolio
logger = logging.getLogger(__name__)
class PortfolioOrgAddressForm(forms.ModelForm):
"""Form for updating the portfolio org mailing address."""
zipcode = forms.CharField(
label="Zip code",
validators=[
RegexValidator(
"^[0-9]{5}(?:-[0-9]{4})?$|^$",
message="Enter a zip code in the required format, like 12345 or 12345-6789.",
)
],
)
class Meta:
model = Portfolio
fields = [
"address_line1",
"address_line2",
"city",
"state_territory",
"zipcode",
# "urbanization",
]
error_messages = {
"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 State/territory
# because for this 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.
"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 = ["address_line1", "city", "state_territory", "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)

View file

@ -71,7 +71,7 @@ class UserProfileForm(forms.ModelForm):
class FinishSetupProfileForm(UserProfileForm):
"""Form for updating user profile."""
full_name = forms.CharField(required=True, label="Full name")
full_name = forms.CharField(required=False, label="Full name")
def clean(self):
cleaned_data = super().clean()
@ -93,4 +93,7 @@ class FinishSetupProfileForm(UserProfileForm):
self.fields["title"].label = "Title or role in your organization"
# Define the "full_name" value
self.fields["full_name"].initial = self.instance.get_formatted_name()
full_name = None
if self.instance.first_name and self.instance.last_name:
full_name = self.instance.get_formatted_name()
self.fields["full_name"].initial = full_name

View file

@ -0,0 +1,80 @@
# Generated by Django 4.2.10 on 2024-07-22 19:19
from django.conf import settings
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("registrar", "0112_remove_contact_registrar_c_user_id_4059c4_idx_and_more"),
]
operations = [
migrations.AddField(
model_name="user",
name="portfolio",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="user",
to="registrar.portfolio",
),
),
migrations.AddField(
model_name="user",
name="portfolio_additional_permissions",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(
choices=[
("view_all_domains", "View all domains and domain reports"),
("view_managed_domains", "View managed domains"),
("edit_domains", "User is a manager on a domain"),
("view_member", "View members"),
("edit_member", "Create and edit members"),
("view_all_requests", "View all requests"),
("view_created_requests", "View created requests"),
("edit_requests", "Create and edit requests"),
("view_portfolio", "View organization"),
("edit_portfolio", "Edit organization"),
],
max_length=50,
),
blank=True,
help_text="Select one or more additional permissions.",
null=True,
size=None,
),
),
migrations.AddField(
model_name="user",
name="portfolio_roles",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(
choices=[
("organization_admin", "Admin"),
("organization_admin_read_only", "Admin read only"),
("organization_member", "Member"),
],
max_length=50,
),
blank=True,
help_text="Select one or more roles.",
null=True,
size=None,
),
),
migrations.AlterField(
model_name="portfolio",
name="creator",
field=models.ForeignKey(
help_text="Associated user",
on_delete=django.db.models.deletion.PROTECT,
related_name="created_portfolios",
to=settings.AUTH_USER_MODEL,
),
),
]

View file

@ -6,11 +6,6 @@ from registrar.models.federal_agency import FederalAgency
from .utility.time_stamped_model import TimeStampedModel
# def get_default_federal_agency():
# """returns non-federal agency"""
# return FederalAgency.objects.filter(agency="Non-Federal Agency").first()
class Portfolio(TimeStampedModel):
"""
Portfolio is used for organizing domains/domain-requests into
@ -23,7 +18,13 @@ class Portfolio(TimeStampedModel):
# Stores who created this model. If no creator is specified in DJA,
# then the creator will default to the current request user"""
creator = models.ForeignKey("registrar.User", on_delete=models.PROTECT, help_text="Associated user", unique=False)
creator = models.ForeignKey(
"registrar.User",
on_delete=models.PROTECT,
help_text="Associated user",
related_name="created_portfolios",
unique=False,
)
notes = models.TextField(
null=True,

View file

@ -11,6 +11,8 @@ from .transition_domain import TransitionDomain
from .verified_by_staff import VerifiedByStaff
from .domain import Domain
from .domain_request import DomainRequest
from django.contrib.postgres.fields import ArrayField
from waffle.decorators import flag_is_active
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
@ -60,6 +62,57 @@ class User(AbstractUser):
# after they login.
FIXTURE_USER = "fixture_user", "Created by fixtures"
class UserPortfolioRoleChoices(models.TextChoices):
"""
Roles make it easier for admins to look at
"""
ORGANIZATION_ADMIN = "organization_admin", "Admin"
ORGANIZATION_ADMIN_READ_ONLY = "organization_admin_read_only", "Admin read only"
ORGANIZATION_MEMBER = "organization_member", "Member"
class UserPortfolioPermissionChoices(models.TextChoices):
""" """
VIEW_ALL_DOMAINS = "view_all_domains", "View all domains and domain reports"
VIEW_MANAGED_DOMAINS = "view_managed_domains", "View managed domains"
# EDIT_DOMAINS is really self.domains. We add is hear and leverage it in has_permission
# so we have one way to test for portfolio and domain edit permissions
# Do we need to check for portfolio domains specifically?
# NOTE: A user on an org can currently invite a user outside the org
EDIT_DOMAINS = "edit_domains", "User is a manager on a domain"
VIEW_MEMBER = "view_member", "View members"
EDIT_MEMBER = "edit_member", "Create and edit members"
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
VIEW_CREATED_REQUESTS = "view_created_requests", "View created requests"
EDIT_REQUESTS = "edit_requests", "Create and edit requests"
VIEW_PORTFOLIO = "view_portfolio", "View organization"
EDIT_PORTFOLIO = "edit_portfolio", "Edit organization"
PORTFOLIO_ROLE_PERMISSIONS = {
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
UserPortfolioPermissionChoices.VIEW_MEMBER,
UserPortfolioPermissionChoices.EDIT_MEMBER,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
],
UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
UserPortfolioPermissionChoices.VIEW_MEMBER,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
],
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
],
}
# #### Constants for choice fields ####
RESTRICTED = "restricted"
STATUS_CHOICES = ((RESTRICTED, RESTRICTED),)
@ -80,6 +133,34 @@ class User(AbstractUser):
related_name="users",
)
portfolio = models.ForeignKey(
"registrar.Portfolio",
null=True,
blank=True,
related_name="user",
on_delete=models.SET_NULL,
)
portfolio_roles = ArrayField(
models.CharField(
max_length=50,
choices=UserPortfolioRoleChoices.choices,
),
null=True,
blank=True,
help_text="Select one or more roles.",
)
portfolio_additional_permissions = ArrayField(
models.CharField(
max_length=50,
choices=UserPortfolioPermissionChoices.choices,
),
null=True,
blank=True,
help_text="Select one or more additional permissions.",
)
phone = PhoneNumberField(
null=True,
blank=True,
@ -115,7 +196,8 @@ class User(AbstractUser):
self.title,
self.phone,
]
return None not in user_values
return None not in user_values and "" not in user_values
def __str__(self):
# this info is pulled from Login.gov
@ -169,6 +251,57 @@ class User(AbstractUser):
def has_contact_info(self):
return bool(self.title or self.email or self.phone)
def _get_portfolio_permissions(self):
"""
Retrieve the permissions for the user's portfolio roles.
"""
portfolio_permissions = set() # Use a set to avoid duplicate permissions
if self.portfolio_roles:
for role in self.portfolio_roles:
if role in self.PORTFOLIO_ROLE_PERMISSIONS:
portfolio_permissions.update(self.PORTFOLIO_ROLE_PERMISSIONS[role])
if self.portfolio_additional_permissions:
portfolio_permissions.update(self.portfolio_additional_permissions)
return list(portfolio_permissions) # Convert back to list if necessary
def _has_portfolio_permission(self, portfolio_permission):
"""The views should only call this function when testing for perms and not rely on roles."""
# EDIT_DOMAINS === user is a manager on a domain (has UserDomainRole)
# NOTE: Should we check whether the domain is in the portfolio?
if portfolio_permission == self.UserPortfolioPermissionChoices.EDIT_DOMAINS and self.domains.exists():
return True
if not self.portfolio:
return False
portfolio_permissions = self._get_portfolio_permissions()
return portfolio_permission in portfolio_permissions
# the methods below are checks for individual portfolio permissions. they are defined here
# to make them easier to call elsewhere throughout the application
def has_base_portfolio_permission(self):
return self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO)
def has_domains_portfolio_permission(self):
return (
self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
# or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_DOMAINS)
)
def has_edit_domains_portfolio_permission(self):
return self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_DOMAINS)
def has_domain_requests_portfolio_permission(self):
return (
self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
# or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_REQUESTS)
)
@classmethod
def needs_identity_verification(cls, email, uuid):
"""A method used by our oidc classes to test whether a user needs email/uuid verification
@ -287,3 +420,7 @@ class User(AbstractUser):
"""
self.check_domain_invitations_on_login()
def is_org_user(self, request):
has_organization_feature_flag = flag_is_active(request, "organization_feature")
return has_organization_feature_flag and self.has_base_portfolio_permission()

View file

@ -170,18 +170,11 @@ class CreateOrUpdateOrganizationTypeHelper:
# There is no avenue for this to occur in the UI,
# as such - this can only occur if the object is initialized in this way.
# Or if there are pre-existing data.
logger.debug(
"create_or_update_organization_type() -> is_election_board "
f"cannot exist for {generic_org_type}. Setting to None."
)
self.instance.is_election_board = None
self.instance.organization_type = generic_org_type
else:
# This can only happen with manual data tinkering, which causes these to be out of sync.
if self.instance.is_election_board is None:
logger.warning(
"create_or_update_organization_type() -> is_election_board is out of sync. Updating value."
)
self.instance.is_election_board = False
if self.instance.is_election_board:
@ -218,10 +211,6 @@ class CreateOrUpdateOrganizationTypeHelper:
# There is no avenue for this to occur in the UI,
# as such - this can only occur if the object is initialized in this way.
# Or if there are pre-existing data.
logger.warning(
"create_or_update_organization_type() -> is_election_board "
f"cannot exist for {current_org_type}. Setting to None."
)
self.instance.is_election_board = None
else:
# if self.instance.organization_type is set to None, then this means

View file

@ -6,7 +6,6 @@ import logging
from urllib.parse import parse_qs
from django.urls import reverse
from django.http import HttpResponseRedirect
from registrar.models.portfolio import Portfolio
from registrar.models.user import User
from waffle.decorators import flag_is_active
@ -141,14 +140,20 @@ class CheckPortfolioMiddleware:
def process_view(self, request, view_func, view_args, view_kwargs):
current_path = request.path
has_organization_feature_flag = flag_is_active(request, "organization_feature")
if current_path == self.home and request.user.is_authenticated and request.user.is_org_user(request):
if request.user.has_base_portfolio_permission():
portfolio = request.user.portfolio
# Add the portfolio to the request object
request.portfolio = portfolio
if request.user.has_domains_portfolio_permission():
portfolio_redirect = reverse("portfolio-domains", kwargs={"portfolio_id": portfolio.id})
else:
# View organization is the lowest access
portfolio_redirect = reverse("portfolio-organization", kwargs={"portfolio_id": portfolio.id})
return HttpResponseRedirect(portfolio_redirect)
if current_path == self.home:
if has_organization_feature_flag:
if request.user.is_authenticated:
user_portfolios = Portfolio.objects.filter(creator=request.user)
if user_portfolios.exists():
first_portfolio = user_portfolios.first()
home_with_portfolio = reverse("portfolio-domains", kwargs={"portfolio_id": first_portfolio.id})
return HttpResponseRedirect(home_with_portfolio)
return None

View file

@ -133,48 +133,10 @@
</section>
{% block usa_overlay %}<div class="usa-overlay"></div>{% endblock %}
{% block banner %}
<header class="usa-header usa-header--basic">
<div class="usa-nav-container">
<div class="usa-navbar">
{% block logo %}
{% include "includes/gov_extended_logo.html" with logo_clickable=True %}
{% endblock %}
<button type="button" class="usa-menu-btn">Menu</button>
</div>
{% block usa_nav %}
<nav class="usa-nav" aria-label="Primary navigation">
<button type="button" class="usa-nav__close">
<img src="/public/img/usa-icons/close.svg" role="img" alt="Close" />
</button>
<ul class="usa-nav__primary usa-accordion">
<li class="usa-nav__primary-item">
{% if user.is_authenticated %}
<span class="usa-nav__primary-username">{{ user.email }}</span>
</li>
{% if has_profile_feature_flag %}
<li class="usa-nav__primary-item">
{% url 'user-profile' as user_profile_url %}
{% url 'finish-user-profile-setup' as finish_setup_url %}
<a class="usa-nav-link {% if request.path == user_profile_url or request.path == finish_setup_url %}usa-current{% endif %}" href="{{ user_profile_url }}">
<span class="text-primary">Your profile</span>
</a>
</li>
{% endif %}
<li class="usa-nav__primary-item">
<a href="{% url 'logout' %}"><span class="text-primary">Sign out</span></a>
{% else %}
<a href="{% url 'login' %}"><span>Sign in</span></a>
{% endif %}
</li>
</ul>
</nav>
{% block usa_nav_secondary %}{% endblock %}
{% endblock %}
</div>
</header>
{% endblock banner %}
<div class="usa-overlay"></div>
{% block header %}
{% include "includes/header_selector.html" with logo_clickable=True %}
{% endblock header %}
{% block wrapper %}
<div id="wrapper">

View file

@ -0,0 +1,34 @@
{% extends 'django/admin/email_clipboard_change_form.html' %}
{% load i18n static %}
{% block after_related_objects %}
<div class="module aligned padding-3">
<h2>Associated groups and suborganizations</h2>
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
<h3>Domain groups</h3>
<ul class="margin-0 padding-0">
{% for domain_group in domain_groups %}
<li>
<a href="{% url 'admin:registrar_domaingroup_change' domain_group.pk %}">
{{ domain_group.name }}
</a>
</li>
{% endfor %}
</ul>
</div>
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
<h3>Suborganizations</h3>
<ul class="margin-0 padding-0">
{% for suborg in suborganizations %}
<li>
<a href="{% url 'admin:registrar_suborganization_change' suborg.pk %}">
{{ suborg.name }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends 'domain_request_form.html' %}
{% load field_helpers url_helpers %}
{% load field_helpers url_helpers static %}
{% block form_instructions %}
<p>If your domain request is approved, the name of your organization and your city/state will be listed in <a href="{% public_site_url 'about/data/' %}" target="_blank">.govs public data.</a></p>
@ -37,7 +37,12 @@
{% input_with_errors forms.0.zipcode %}
{% endwith %}
<div id="urbanization-field" style="display: none;">
{% input_with_errors forms.0.urbanization %}
</div>
</fieldset>
{% endblock %}
<script src="{% static 'js/get-gov.js' %}" defer></script>

View file

@ -42,13 +42,11 @@ Your domain request was rejected because we determined that {{ domain_request.or
eligible for a .gov domain. .Gov domains are only available to official U.S.-based
government organizations.
Learn more about eligibility for .gov domains
<https://get.gov/domains/eligibility/>.
DEMONSTRATE ELIGIBILITY
If you can provide documentation that demonstrates your eligibility, reply to this email.
This can include links to (or copies of) your authorizing legislation, your founding
charter or bylaws, or other similar documentation. Without this, we cant approve a
.gov domain for your organization. Learn more about eligibility for .gov domains
<https://get.gov/domains/eligibility/>.{% elif domain_request.rejection_reason == 'naming_not_met' %}
If you have questions or comments, reply to this email.
{% elif domain_request.rejection_reason == 'naming_not_met' %}
Your domain request was rejected because it does not meet our naming requirements.
Domains should uniquely identify a government organization and be clear to the
general public. Learn more about naming requirements for your type of organization

View file

@ -4,8 +4,8 @@
{% block title %} Finish setting up your profile | {% endblock %}
{# Disable the redirect #}
{% block logo %}
{% include "includes/gov_extended_logo.html" with logo_clickable=user_finished_setup %}
{% block header %}
{% include "includes/header_selector.html" with logo_clickable=user_finished_setup %}
{% endblock %}
{# Add the new form #}

View file

@ -10,11 +10,11 @@
{# the entire logged in page goes here #}
{% block homepage_content %}
<div class="tablet:grid-col-11 desktop:grid-col-10 tablet:grid-offset-1">
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
<h1>Manage your domains</h1>
{% comment %}
@ -32,26 +32,8 @@
{% include "includes/domains_table.html" %}
{% include "includes/domain_requests_table.html" %}
{# Note: Reimplement this after MVP #}
<!--
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
<h2>Archived domains</h2>
<p>You don't have any archived domains</p>
</section>
-->
<!-- Note: Uncomment below when this is being implemented post-MVP -->
<!-- <section class="tablet:grid-col-11 desktop:grid-col-10">
<h2 class="padding-top-1 mobile-lg:padding-top-3"> Export domains</h2>
<p>Download a list of your domains and their statuses as a csv file.</p>
<a href="{% url 'todo' %}" class="usa-button usa-button--outline">
Export domains as csv
</a>
</section>
-->
{% endblock %}
</div>
{% endblock %}
{% else %} {# not user.is_authenticated #}
{# the entire logged out page goes here #}

View file

@ -2,6 +2,7 @@
<section class="section--outlined domain-requests" id="domain-requests">
<div class="grid-row">
<!-- Use portfolio_base_permission when merging into 2366 and then delete this comment -->
{% if portfolio is None %}
<div class="mobile:grid-col-12 desktop:grid-col-6">
<h2 id="domain-requests-header" class="flex-6">Domain requests</h2>
@ -12,6 +13,9 @@
<form class="usa-search usa-search--small" method="POST" role="search">
{% csrf_token %}
<button class="usa-button usa-button--unstyled margin-right-2 domain-requests__reset-search display-none" type="button">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
</svg>
Reset
</button>
<label class="usa-sr-only" for="domain-requests__search-field">Search by domain name</label>

View file

@ -1,17 +1,20 @@
{% load static %}
<section class="section--outlined domains{% if portfolio is not None %} margin-top-0{% endif %}" id="domains">
<div class="grid-row">
<div class="section--outlined__header margin-bottom-3 {% if portfolio is None %} section--outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
<!-- Use portfolio_base_permission when merging into 2366 then delete this comment -->
{% if portfolio is None %}
<div class="mobile:grid-col-12 desktop:grid-col-6">
<h2 id="domains-header" class="flex-6">Domains</h2>
</div>
<h2 id="domains-header" class="display-inline-block">Domains</h2>
<span class="display-none" id="no-portfolio-js-flag"></span>
{% endif %}
<div class="mobile:grid-col-12 desktop:grid-col-6">
<section aria-label="Domains search component" class="flex-6 margin-y-2">
<div class="section--outlined__search {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6{% endif %}">
<section aria-label="Domains search component" class="margin-top-2">
<form class="usa-search usa-search--small" method="POST" role="search">
{% csrf_token %}
<button class="usa-button usa-button--unstyled margin-right-2 domains__reset-search display-none" type="button">
<button class="usa-button usa-button--unstyled margin-right-3 domains__reset-search display-none" type="button">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
</svg>
Reset
</button>
<label class="usa-sr-only" for="domains__search-field">Search by domain name</label>
@ -32,10 +35,20 @@
</form>
</section>
</div>
<div class="section--outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}">
<section aria-label="Domains report component" class="mobile-lg:margin-top-205">
<a href="{% url 'export_data_type_user' %}" class="usa-button usa-button--unstyled" role="button">
<svg class="usa-icon usa-icon--big margin-right-neg-4px" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
</svg>Export as CSV
</a>
</section>
</div>
</div>
<!-- Use portfolio_base_permission when merging into 2366 then delete this comment -->
{% if portfolio %}
<div class="display-flex flex-align-center margin-top-1">
<span class="margin-right-2 margin-top-neg-1 text-base-darker">Filter by</span>
<div class="display-flex flex-align-center">
<span class="margin-right-2 margin-top-neg-1 usa-prose text-base-darker">Filter by</span>
<div class="usa-accordion usa-accordion--select margin-right-2">
<div class="usa-accordion__heading">
<button
@ -136,6 +149,10 @@
<th data-sortable="name" scope="col" role="columnheader">Domain name</th>
<th data-sortable="expiration_date" scope="col" role="columnheader">Expires</th>
<th data-sortable="state_display" scope="col" role="columnheader">Status</th>
<!-- Use portfolio_base_permission when merging into 2366 then delete this comment -->
{% if portfolio %}
<th data-sortable="suborganization" scope="col" role="columnheader">Suborganization</th>
{% endif %}
<th
scope="col"
role="columnheader"
@ -156,7 +173,7 @@
<div class="domains__no-data display-none">
<p>You don't have any registered domains.</p>
<p class="maxw-none clearfix">
<a href="https://get.gov/help/faq/#do-not-see-my-domain" class="float-right-tablet display-flex flex-align-start usa-link" target="_blank">
<a href="https://get.gov/help/faq/#do-not-see-my-domain" class="float-right-tablet usa-link usa-link--icon" target="_blank">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="{%static 'img/sprite.svg'%}#help_outline"></use>
</svg>

View file

@ -0,0 +1,39 @@
{% load static %}
<header class="usa-header usa-header--basic">
<div class="usa-nav-container">
<div class="usa-navbar">
{% include "includes/gov_extended_logo.html" with logo_clickable=logo_clickable %}
<button type="button" class="usa-menu-btn">Menu</button>
</div>
{% block usa_nav %}
<nav class="usa-nav" aria-label="Primary navigation">
<button type="button" class="usa-nav__close">
<img src="{%static 'img/usa-icons/close.svg'%}" role="img" alt="Close" />
</button>
<ul class="usa-nav__primary usa-accordion">
<li class="usa-nav__primary-item">
{% if user.is_authenticated %}
<span class="usa-nav__username ellipsis">{{ user.email }}</span>
</li>
{% if has_profile_feature_flag %}
<li class="usa-nav__primary-item">
{% url 'user-profile' as user_profile_url %}
{% url 'finish-user-profile-setup' as finish_setup_url %}
<a class="usa-nav-link {% if request.path == user_profile_url or request.path == finish_setup_url %}usa-current{% endif %}" href="{{ user_profile_url }}">
<span class="text-primary">Your profile</span>
</a>
</li>
{% endif %}
<li class="usa-nav__primary-item">
<a href="{% url 'logout' %}"><span class="text-primary">Sign out</span></a>
{% else %}
<a href="{% url 'login' %}"><span>Sign in</span></a>
{% endif %}
</li>
</ul>
</nav>
{% block usa_nav_secondary %}{% endblock %}
{% endblock %}
</div>
</header>

View file

@ -0,0 +1,77 @@
{% load static %}
<header class="usa-header usa-header--extended">
<div class="usa-navbar">
{% include "includes/gov_extended_logo.html" with logo_clickable=logo_clickable %}
<button type="button" class="usa-menu-btn">Menu</button>
</div>
{% block usa_nav %}
<nav class="usa-nav" aria-label="Primary navigation">
<div class="usa-nav__inner">
<button type="button" class="usa-nav__close">
<img src="{%static 'img/usa-icons/close.svg'%}" role="img" alt="Close" />
</button>
<ul class="usa-nav__primary usa-accordion">
{% if has_domains_portfolio_permission %}
<li class="usa-nav__primary-item">
{% url 'portfolio-domains' portfolio.id as url %}
<a href="{{ url }}" class="usa-nav-link{% if request.path == url %} usa-current{% endif %}">
Domains
</a>
</li>
{% endif %}
<li class="usa-nav__primary-item">
<a href="#" class="usa-nav-link">
Domain groups
</a>
</li>
{% if has_domain_requests_portfolio_permission %}
<li class="usa-nav__primary-item">
{% url 'portfolio-domain-requests' portfolio.id as url %}
<a href="{{ url }}" class="usa-nav-link{% if request.path == url %} usa-current{% endif %}">
Domain requests
</a>
</li>
{% endif %}
<li class="usa-nav__primary-item">
<a href="#" class="usa-nav-link">
Members
</a>
</li>
<li class="usa-nav__primary-item">
{% url 'portfolio-organization' portfolio.id as url %}
<!-- Move the padding from the a to the span so that the descenders do not get cut off -->
<a href="{{ url }}" class="usa-nav-link padding-y-0">
<span class="ellipsis ellipsis--23 ellipsis--desktop-50 padding-y-1 desktop:padding-y-2">
{{ portfolio.organization_name }}
</span>
</a>
</li>
</ul>
<div class="usa-nav__secondary">
<ul class="usa-nav__secondary-links">
<li class="usa-nav__secondary-item">
{% if user.is_authenticated %}
<span class="ellipsis usa-nav__username">{{ user.email }}</span>
</li>
{% if has_profile_feature_flag %}
<li class="usa-nav__secondary-item">
{% url 'user-profile' as user_profile_url %}
{% url 'finish-user-profile-setup' as finish_setup_url %}
<a class="usa-nav-link {% if path == user_profile_url or path == finish_setup_url %}usa-current{% endif %}" href="{{ user_profile_url }}">
Your profile
</a>
</li>
{% endif %}
<li class="usa-nav__secondary-item">
<a class="usa-nav-link" href="{% url 'logout' %}">Sign out</a>
{% else %}
<a class="usa-nav-link" href="{% url 'login' %}">Sign in</a>
{% endif %}
</li>
</ul>
</div>
</div>
</nav>
{% endblock %}
</header>

View file

@ -0,0 +1,5 @@
{% if not is_org_user %}
{% include "includes/header_basic.html" with logo_clickable=logo_clickable %}
{% else %}
{% include "includes/header_extended.html" with logo_clickable=logo_clickable %}
{% endif %}

View file

@ -8,7 +8,7 @@
<use xlink:href="{%static 'img/sprite.svg'%}#error"></use>
{%endif %}
</svg>
<div class="display-inline padding-left-05 margin-left-3 readonly-field {% if not field.field.required %}text-base{% endif %}">
<div class="display-inline padding-left-05 margin-left-3 input-with-edit-button__readonly-field {% if not field.field.required %}text-base{% endif %}">
{% if field.name != "phone" %}
{{ field.value }}
{% else %}

View file

@ -112,8 +112,11 @@
<div class="text-right">
<a
href="{{ edit_link }}"
class="usa-link font-sans-sm"
class="usa-link usa-link--icon font-sans-sm line-height-sans-5"
>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="{% static 'img/sprite.svg' %}#edit"></use>
</svg>
Edit<span class="sr-only"> {{ title }}</span>
</a>
</div>

View file

@ -1,24 +0,0 @@
{% extends 'home.html' %}
{% load static %}
{% block homepage_content %}
<div class="tablet:grid-col-12">
<div class="grid-row grid-gap">
<div class="tablet:grid-col-3">
{% include "portfolio_sidebar.html" with portfolio=portfolio %}
</div>
<div class="tablet:grid-col-9">
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
{# Note: Reimplement commented out functionality #}
{% block portfolio_content %}
{% endblock %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block wrapper %}
<div id="wrapper" class="dashboard--portfolio">
{% block content %}
<main id="main-content" class="grid-container">
{% if user.is_authenticated %}
{# the entire logged in page goes here #}
<div class="tablet:grid-col-12">
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
{% block portfolio_content %}{% endblock %}
</div>
{% else %} {# not user.is_authenticated #}
{# the entire logged out page goes here #}
<p><a class="usa-button" href="{% url 'login' %}">
Sign in
</a></p>
{% endif %}
</main>
{% endblock %}
<div role="complementary">{% block complementary %}{% endblock %}</div>
{% block content_bottom %}{% endblock %}
</div>
{% endblock wrapper %}

View file

@ -1,7 +1,9 @@
{% extends 'portfolio.html' %}
{% extends 'portfolio_base.html' %}
{% load static %}
{% block title %} Domains | {% endblock %}
{% block portfolio_content %}
<h1 id="domains-header">Domains</h1>
{% include "includes/domains_table.html" with portfolio=portfolio %}

View file

@ -0,0 +1,64 @@
{% extends 'portfolio_base.html' %}
{% load static field_helpers%}
{% block title %}Organization mailing address | {{ portfolio.name }} | {% endblock %}
{% load static %}
{% block portfolio_content %}
<div class="grid-row grid-gap">
<div class="tablet:grid-col-3">
<p class="font-body-md margin-top-0 margin-bottom-2
text-primary-darker text-semibold"
>
<span class="usa-sr-only"> Portfolio name:</span> {{ portfolio }}
</p>
{% include 'portfolio_organization_sidebar.html' %}
</div>
<div class="tablet:grid-col-9">
<h1>Organization</h1>
<p>The name of your federal agency will be publicly listed as the domain registrant.</p>
<p>
The federal agency for your organization cant be updated here.
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
</p>
{% include "includes/form_errors.html" with form=form %}
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
{% csrf_token %}
<p>
<strong class="text-primary display-block margin-bottom-1">Federal agency</strong>
{{ portfolio }}
</p>
{% input_with_errors form.address_line1 %}
{% input_with_errors form.address_line2 %}
{% input_with_errors form.city %}
{% input_with_errors form.state_territory %}
{% with add_class="usa-input--small" %}
{% input_with_errors form.zipcode %}
{% endwith %}
<button
type="submit"
class="usa-button"
>
Save
</button>
</form>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,23 @@
{% load static url_helpers %}
<div class="margin-bottom-4 tablet:margin-bottom-0">
<nav aria-label="Domain sections">
<ul class="usa-sidenav">
<li class="usa-sidenav__item">
{% url 'portfolio-organization' portfolio_id=portfolio.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
Organization
</a>
</li>
<li class="usa-sidenav__item">
<a href="#"
>
Senior official
</a>
</li>
</ul>
</nav>
</div>

View file

@ -1,7 +1,9 @@
{% extends 'portfolio.html' %}
{% extends 'portfolio_base.html' %}
{% load static %}
{% block title %} Domain requests | {% endblock %}
{% block portfolio_content %}
<h1 id="domain-requests-header">Domain requests</h1>

View file

@ -1,37 +0,0 @@
{% load static url_helpers %}
<div class="margin-bottom-4 tablet:margin-bottom-0">
<nav aria-label="">
<h2 class="margin-top-0 text-semibold">{{ portfolio.organization_name }}</h2>
<ul class="usa-sidenav usa-sidenav--portfolio">
<li class="usa-sidenav__item">
{% url 'portfolio-domains' portfolio.id as url %}
<a href="{{ url }}" {% if request.path == url %}class="usa-current"{% endif %}>
Domains
</a>
</li>
<li class="usa-sidenav__item">
{% url 'portfolio-domain-requests' portfolio.id as url %}
<a href="{{ url }}" {% if request.path == url %}class="usa-current"{% endif %}>
Domain requests
</a>
</li>
<li class="usa-sidenav__item">
<a href="#">
Members
</a>
</li>
<li class="usa-sidenav__item">
<a href="#">
Organization
</a>
</li>
<li class="usa-sidenav__item">
<a href="#">
Senior official
</a>
</li>
</ul>
</nav>
</div>

View file

@ -6,8 +6,8 @@ Edit your User Profile |
{% load static url_helpers %}
{# Disable the redirect #}
{% block logo %}
{% include "includes/gov_extended_logo.html" with logo_clickable=user_finished_setup %}
{% block header %}
{% include "includes/header_selector.html" with logo_clickable=user_finished_setup %}
{% endblock %}
{% block content %}

View file

@ -41,6 +41,8 @@ from registrar.models.user_domain_role import UserDomainRole
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
from api.tests.common import less_console_noise_decorator
logger = logging.getLogger(__name__)
@ -525,230 +527,230 @@ class AuditedAdminMockData:
class MockDb(TestCase):
"""Hardcoded mocks make test case assertions straightforward."""
def setUp(self):
super().setUp()
@classmethod
@less_console_noise_decorator
def sharedSetUp(cls):
username = "test_user"
first_name = "First"
last_name = "Last"
email = "info@example.com"
self.user = get_user_model().objects.create(
cls.user = get_user_model().objects.create(
username=username, first_name=first_name, last_name=last_name, email=email
)
current_date = get_time_aware_date(datetime(2024, 4, 2))
# Create start and end dates using timedelta
self.end_date = current_date + timedelta(days=2)
self.start_date = current_date - timedelta(days=2)
cls.end_date = current_date + timedelta(days=2)
cls.start_date = current_date - timedelta(days=2)
self.federal_agency_1, _ = FederalAgency.objects.get_or_create(agency="World War I Centennial Commission")
self.federal_agency_2, _ = FederalAgency.objects.get_or_create(agency="Armed Forces Retirement Home")
cls.federal_agency_1, _ = FederalAgency.objects.get_or_create(agency="World War I Centennial Commission")
cls.federal_agency_2, _ = FederalAgency.objects.get_or_create(agency="Armed Forces Retirement Home")
self.domain_1, _ = Domain.objects.get_or_create(
cls.domain_1, _ = Domain.objects.get_or_create(
name="cdomain1.gov", state=Domain.State.READY, first_ready=get_time_aware_date(datetime(2024, 4, 2))
)
self.domain_2, _ = Domain.objects.get_or_create(name="adomain2.gov", state=Domain.State.DNS_NEEDED)
self.domain_3, _ = Domain.objects.get_or_create(name="ddomain3.gov", state=Domain.State.ON_HOLD)
self.domain_4, _ = Domain.objects.get_or_create(name="bdomain4.gov", state=Domain.State.UNKNOWN)
self.domain_5, _ = Domain.objects.get_or_create(
cls.domain_2, _ = Domain.objects.get_or_create(name="adomain2.gov", state=Domain.State.DNS_NEEDED)
cls.domain_3, _ = Domain.objects.get_or_create(name="ddomain3.gov", state=Domain.State.ON_HOLD)
cls.domain_4, _ = Domain.objects.get_or_create(name="bdomain4.gov", state=Domain.State.UNKNOWN)
cls.domain_5, _ = Domain.objects.get_or_create(
name="bdomain5.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(2023, 11, 1))
)
self.domain_6, _ = Domain.objects.get_or_create(
cls.domain_6, _ = Domain.objects.get_or_create(
name="bdomain6.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(1980, 10, 16))
)
self.domain_7, _ = Domain.objects.get_or_create(
cls.domain_7, _ = Domain.objects.get_or_create(
name="xdomain7.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(2024, 4, 2))
)
self.domain_8, _ = Domain.objects.get_or_create(
cls.domain_8, _ = Domain.objects.get_or_create(
name="sdomain8.gov", state=Domain.State.DELETED, deleted=get_time_aware_date(datetime(2024, 4, 2))
)
# We use timezone.make_aware to sync to server time a datetime object with the current date (using date.today())
# and a specific time (using datetime.min.time()).
# Deleted yesterday
self.domain_9, _ = Domain.objects.get_or_create(
cls.domain_9, _ = Domain.objects.get_or_create(
name="zdomain9.gov",
state=Domain.State.DELETED,
deleted=get_time_aware_date(datetime(2024, 4, 1)),
)
# ready tomorrow
self.domain_10, _ = Domain.objects.get_or_create(
cls.domain_10, _ = Domain.objects.get_or_create(
name="adomain10.gov",
state=Domain.State.READY,
first_ready=get_time_aware_date(datetime(2024, 4, 3)),
)
self.domain_11, _ = Domain.objects.get_or_create(
cls.domain_11, _ = Domain.objects.get_or_create(
name="cdomain11.gov", state=Domain.State.READY, first_ready=get_time_aware_date(datetime(2024, 4, 2))
)
self.domain_12, _ = Domain.objects.get_or_create(
cls.domain_12, _ = Domain.objects.get_or_create(
name="zdomain12.gov", state=Domain.State.READY, first_ready=get_time_aware_date(datetime(2024, 4, 2))
)
self.domain_information_1, _ = DomainInformation.objects.get_or_create(
creator=self.user,
domain=self.domain_1,
cls.domain_information_1, _ = DomainInformation.objects.get_or_create(
creator=cls.user,
domain=cls.domain_1,
generic_org_type="federal",
federal_agency=self.federal_agency_1,
federal_agency=cls.federal_agency_1,
federal_type="executive",
is_election_board=False,
)
self.domain_information_2, _ = DomainInformation.objects.get_or_create(
creator=self.user, domain=self.domain_2, generic_org_type="interstate", is_election_board=True
cls.domain_information_2, _ = DomainInformation.objects.get_or_create(
creator=cls.user, domain=cls.domain_2, generic_org_type="interstate", is_election_board=True
)
self.domain_information_3, _ = DomainInformation.objects.get_or_create(
creator=self.user,
domain=self.domain_3,
cls.domain_information_3, _ = DomainInformation.objects.get_or_create(
creator=cls.user,
domain=cls.domain_3,
generic_org_type="federal",
federal_agency=self.federal_agency_2,
federal_agency=cls.federal_agency_2,
is_election_board=False,
)
self.domain_information_4, _ = DomainInformation.objects.get_or_create(
creator=self.user,
domain=self.domain_4,
cls.domain_information_4, _ = DomainInformation.objects.get_or_create(
creator=cls.user,
domain=cls.domain_4,
generic_org_type="federal",
federal_agency=self.federal_agency_2,
federal_agency=cls.federal_agency_2,
is_election_board=False,
)
self.domain_information_5, _ = DomainInformation.objects.get_or_create(
creator=self.user,
domain=self.domain_5,
cls.domain_information_5, _ = DomainInformation.objects.get_or_create(
creator=cls.user,
domain=cls.domain_5,
generic_org_type="federal",
federal_agency=self.federal_agency_2,
federal_agency=cls.federal_agency_2,
is_election_board=False,
)
self.domain_information_6, _ = DomainInformation.objects.get_or_create(
creator=self.user,
domain=self.domain_6,
cls.domain_information_6, _ = DomainInformation.objects.get_or_create(
creator=cls.user,
domain=cls.domain_6,
generic_org_type="federal",
federal_agency=self.federal_agency_2,
federal_agency=cls.federal_agency_2,
is_election_board=False,
)
self.domain_information_7, _ = DomainInformation.objects.get_or_create(
creator=self.user,
domain=self.domain_7,
cls.domain_information_7, _ = DomainInformation.objects.get_or_create(
creator=cls.user,
domain=cls.domain_7,
generic_org_type="federal",
federal_agency=self.federal_agency_2,
federal_agency=cls.federal_agency_2,
is_election_board=False,
)
self.domain_information_8, _ = DomainInformation.objects.get_or_create(
creator=self.user,
domain=self.domain_8,
cls.domain_information_8, _ = DomainInformation.objects.get_or_create(
creator=cls.user,
domain=cls.domain_8,
generic_org_type="federal",
federal_agency=self.federal_agency_2,
federal_agency=cls.federal_agency_2,
is_election_board=False,
)
self.domain_information_9, _ = DomainInformation.objects.get_or_create(
creator=self.user,
domain=self.domain_9,
cls.domain_information_9, _ = DomainInformation.objects.get_or_create(
creator=cls.user,
domain=cls.domain_9,
generic_org_type="federal",
federal_agency=self.federal_agency_2,
federal_agency=cls.federal_agency_2,
is_election_board=False,
)
self.domain_information_10, _ = DomainInformation.objects.get_or_create(
creator=self.user,
domain=self.domain_10,
cls.domain_information_10, _ = DomainInformation.objects.get_or_create(
creator=cls.user,
domain=cls.domain_10,
generic_org_type="federal",
federal_agency=self.federal_agency_2,
federal_agency=cls.federal_agency_2,
is_election_board=False,
)
self.domain_information_11, _ = DomainInformation.objects.get_or_create(
creator=self.user,
domain=self.domain_11,
cls.domain_information_11, _ = DomainInformation.objects.get_or_create(
creator=cls.user,
domain=cls.domain_11,
generic_org_type="federal",
federal_agency=self.federal_agency_1,
federal_agency=cls.federal_agency_1,
federal_type="executive",
is_election_board=False,
)
self.domain_information_12, _ = DomainInformation.objects.get_or_create(
creator=self.user,
domain=self.domain_12,
cls.domain_information_12, _ = DomainInformation.objects.get_or_create(
creator=cls.user,
domain=cls.domain_12,
generic_org_type="interstate",
is_election_board=False,
)
self.meoward_user = get_user_model().objects.create(
cls.meoward_user = get_user_model().objects.create(
username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com"
)
lebowski_user = get_user_model().objects.create(
cls.lebowski_user = get_user_model().objects.create(
username="big_lebowski", first_name="big", last_name="lebowski", email="big_lebowski@dude.co"
)
_, created = UserDomainRole.objects.get_or_create(
user=self.meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
user=cls.meoward_user, domain=cls.domain_1, role=UserDomainRole.Roles.MANAGER
)
_, created = UserDomainRole.objects.get_or_create(
user=self.user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
user=cls.user, domain=cls.domain_1, role=UserDomainRole.Roles.MANAGER
)
_, created = UserDomainRole.objects.get_or_create(
user=lebowski_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
user=cls.lebowski_user, domain=cls.domain_1, role=UserDomainRole.Roles.MANAGER
)
_, created = UserDomainRole.objects.get_or_create(
user=self.meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER
user=cls.meoward_user, domain=cls.domain_2, role=UserDomainRole.Roles.MANAGER
)
_, created = UserDomainRole.objects.get_or_create(
user=self.meoward_user, domain=self.domain_11, role=UserDomainRole.Roles.MANAGER
user=cls.meoward_user, domain=cls.domain_11, role=UserDomainRole.Roles.MANAGER
)
_, created = UserDomainRole.objects.get_or_create(
user=self.meoward_user, domain=self.domain_12, role=UserDomainRole.Roles.MANAGER
user=cls.meoward_user, domain=cls.domain_12, role=UserDomainRole.Roles.MANAGER
)
_, created = DomainInvitation.objects.get_or_create(
email=self.meoward_user.email,
domain=self.domain_1,
email=cls.meoward_user.email,
domain=cls.domain_1,
status=DomainInvitation.DomainInvitationStatus.RETRIEVED,
)
_, created = DomainInvitation.objects.get_or_create(
email="woofwardthethird@rocks.com",
domain=self.domain_1,
domain=cls.domain_1,
status=DomainInvitation.DomainInvitationStatus.INVITED,
)
_, created = DomainInvitation.objects.get_or_create(
email="squeaker@rocks.com", domain=self.domain_2, status=DomainInvitation.DomainInvitationStatus.INVITED
email="squeaker@rocks.com", domain=cls.domain_2, status=DomainInvitation.DomainInvitationStatus.INVITED
)
_, created = DomainInvitation.objects.get_or_create(
email="squeaker@rocks.com", domain=self.domain_10, status=DomainInvitation.DomainInvitationStatus.INVITED
email="squeaker@rocks.com", domain=cls.domain_10, status=DomainInvitation.DomainInvitationStatus.INVITED
)
with less_console_noise():
self.domain_request_1 = completed_domain_request(
cls.domain_request_1 = completed_domain_request(
status=DomainRequest.DomainRequestStatus.STARTED,
name="city1.gov",
)
self.domain_request_2 = completed_domain_request(
cls.domain_request_2 = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
name="city2.gov",
)
self.domain_request_3 = completed_domain_request(
cls.domain_request_3 = completed_domain_request(
status=DomainRequest.DomainRequestStatus.STARTED,
name="city3.gov",
)
self.domain_request_4 = completed_domain_request(
cls.domain_request_4 = completed_domain_request(
status=DomainRequest.DomainRequestStatus.STARTED,
name="city4.gov",
is_election_board=True,
generic_org_type="city",
)
self.domain_request_5 = completed_domain_request(
cls.domain_request_5 = completed_domain_request(
status=DomainRequest.DomainRequestStatus.APPROVED,
name="city5.gov",
)
self.domain_request_6 = completed_domain_request(
cls.domain_request_6 = completed_domain_request(
status=DomainRequest.DomainRequestStatus.STARTED,
name="city6.gov",
)
self.domain_request_3.submit()
self.domain_request_4.submit()
self.domain_request_6.submit()
cls.domain_request_3.submit()
cls.domain_request_4.submit()
cls.domain_request_6.submit()
other, _ = Contact.objects.get_or_create(
first_name="Testy1232",
@ -769,29 +771,56 @@ class MockDb(TestCase):
website_3, _ = Website.objects.get_or_create(website="https://www.example.com")
website_4, _ = Website.objects.get_or_create(website="https://www.example2.com")
self.domain_request_3.other_contacts.add(other, other_2)
self.domain_request_3.alternative_domains.add(website, website_2)
self.domain_request_3.current_websites.add(website_3, website_4)
self.domain_request_3.cisa_representative_email = "test@igorville.com"
self.domain_request_3.submission_date = get_time_aware_date(datetime(2024, 4, 2))
self.domain_request_3.save()
cls.domain_request_3.other_contacts.add(other, other_2)
cls.domain_request_3.alternative_domains.add(website, website_2)
cls.domain_request_3.current_websites.add(website_3, website_4)
cls.domain_request_3.cisa_representative_email = "test@igorville.com"
cls.domain_request_3.submission_date = get_time_aware_date(datetime(2024, 4, 2))
cls.domain_request_3.save()
self.domain_request_4.submission_date = get_time_aware_date(datetime(2024, 4, 2))
self.domain_request_4.save()
cls.domain_request_4.submission_date = get_time_aware_date(datetime(2024, 4, 2))
cls.domain_request_4.save()
self.domain_request_6.submission_date = get_time_aware_date(datetime(2024, 4, 2))
self.domain_request_6.save()
cls.domain_request_6.submission_date = get_time_aware_date(datetime(2024, 4, 2))
cls.domain_request_6.save()
def tearDown(self):
super().tearDown()
@classmethod
def sharedTearDown(cls):
PublicContact.objects.all().delete()
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
User.objects.all().delete()
UserDomainRole.objects.all().delete()
User.objects.all().delete()
DomainInvitation.objects.all().delete()
FederalAgency.objects.all().delete()
cls.federal_agency_1.delete()
cls.federal_agency_2.delete()
class MockDbForSharedTests(MockDb):
"""Set up and tear down test data that is shared across all tests in a class"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.sharedSetUp()
@classmethod
def tearDownClass(cls):
super().tearDownClass()
cls.sharedTearDown()
class MockDbForIndividualTests(MockDb):
"""Set up and tear down test data for each test in a class"""
def setUp(self):
super().setUp()
self.sharedSetUp()
def tearDown(self):
super().tearDown()
self.sharedTearDown()
def mock_user():
@ -840,6 +869,19 @@ def create_user():
return user
def create_test_user():
username = "test_user"
first_name = "First"
last_name = "Last"
email = "info@example.com"
phone = "8003111234"
title = "test title"
user = get_user_model().objects.create(
username=username, first_name=first_name, last_name=last_name, email=email, phone=phone, title=title
)
return user
def create_ready_domain():
domain, _ = Domain.objects.get_or_create(name="city.gov", state=Domain.State.READY)
return domain

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,824 @@
from datetime import date
from django.test import TestCase, RequestFactory, Client, override_settings
from django.contrib.admin.sites import AdminSite
from api.tests.common import less_console_noise_decorator
from django_webtest import WebTest # type: ignore
from django.contrib import messages
from django.urls import reverse
from registrar.admin import (
DomainAdmin,
)
from registrar.models import (
Domain,
DomainRequest,
DomainInformation,
User,
Host,
)
from .common import (
MockSESClient,
completed_domain_request,
less_console_noise,
create_superuser,
create_user,
create_ready_domain,
MockEppLib,
GenericTestHelper,
)
from unittest.mock import ANY, call, patch
import boto3_mocking # type: ignore
import logging
logger = logging.getLogger(__name__)
class TestDomainAdminAsStaff(MockEppLib):
"""Test DomainAdmin class as staff user.
Notes:
all tests share staffuser; do not change staffuser model in tests
tests have available staffuser, client, and admin
"""
@classmethod
def setUpClass(self):
super().setUpClass()
self.staffuser = create_user()
self.site = AdminSite()
self.admin = DomainAdmin(model=Domain, admin_site=self.site)
self.factory = RequestFactory()
def setUp(self):
self.client = Client(HTTP_HOST="localhost:8080")
self.client.force_login(self.staffuser)
super().setUp()
def tearDown(self):
super().tearDown()
Host.objects.all().delete()
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
@classmethod
def tearDownClass(self):
User.objects.all().delete()
super().tearDownClass()
@less_console_noise_decorator
def test_staff_can_see_cisa_region_federal(self):
"""Tests if staff can see CISA Region: N/A"""
# Create a fake domain request
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
_domain_request.approve()
domain = _domain_request.approved_domain
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
# Test if the page has the right CISA region
expected_html = '<div class="flex-container margin-top-2"><span>CISA region: N/A</span></div>'
# Remove whitespace from expected_html
expected_html = "".join(expected_html.split())
# Remove whitespace from response content
response_content = "".join(response.content.decode().split())
# Check if response contains expected_html
self.assertIn(expected_html, response_content)
@less_console_noise_decorator
def test_staff_can_see_cisa_region_non_federal(self):
"""Tests if staff can see the correct CISA region"""
# Create a fake domain request. State will be NY (2).
_domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type="interstate"
)
_domain_request.approve()
domain = _domain_request.approved_domain
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
# Test if the page has the right CISA region
expected_html = '<div class="flex-container margin-top-2"><span>CISA region: 2</span></div>'
# Remove whitespace from expected_html
expected_html = "".join(expected_html.split())
# Remove whitespace from response content
response_content = "".join(response.content.decode().split())
# Check if response contains expected_html
self.assertIn(expected_html, response_content)
@less_console_noise_decorator
def test_analyst_can_see_inline_domain_information_in_domain_change_form(self):
"""Tests if an analyst can still see the inline domain information form"""
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
)
# Create a fake domain request
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
# Creates a Domain and DomainInformation object
_domain_request.approve()
domain_information = DomainInformation.objects.filter(domain_request=_domain_request).get()
domain_information.organization_name = "MonkeySeeMonkeyDo"
domain_information.save()
# We use filter here rather than just domain_information.domain just to get the latest data.
domain = Domain.objects.filter(domain_info=domain_information).get()
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
# Test for data. We only need to test one since its all interconnected.
expected_organization_name = "MonkeySeeMonkeyDo"
self.assertContains(response, expected_organization_name)
# clean up this test's data
domain.delete()
domain_information.delete()
_domain_request.delete()
_creator.delete()
@less_console_noise_decorator
def test_deletion_is_successful(self):
"""
Scenario: Domain deletion is unsuccessful
When the domain is deleted
Then a user-friendly success message is returned for displaying on the web
And `state` is set to `DELETED`
"""
domain = create_ready_domain()
# Put in client hold
domain.place_client_hold()
# Ensure everything is displaying correctly
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk),
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Remove from registry")
# The contents of the modal should exist before and after the post.
# Check for the header
self.assertContains(response, "Are you sure you want to remove this domain from the registry?")
# Check for some of its body
self.assertContains(response, "When a domain is removed from the registry:")
# Check for some of the button content
self.assertContains(response, "Yes, remove from registry")
# Test the info dialog
request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_delete_domain": "Remove from registry", "name": domain.name},
follow=True,
)
request.user = self.client
with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with(
request,
messages.INFO,
"Domain city.gov has been deleted. Thanks!",
extra_tags="",
fail_silently=False,
)
# The modal should still exist
self.assertContains(response, "Are you sure you want to remove this domain from the registry?")
self.assertContains(response, "When a domain is removed from the registry:")
self.assertContains(response, "Yes, remove from registry")
self.assertEqual(domain.state, Domain.State.DELETED)
# clean up data within this test
domain.delete()
@less_console_noise_decorator
def test_deletion_ready_fsm_failure(self):
"""
Scenario: Domain deletion is unsuccessful
When an error is returned from epplibwrapper
Then a user-friendly error message is returned for displaying on the web
And `state` is not set to `DELETED`
"""
domain = create_ready_domain()
# Ensure everything is displaying correctly
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk),
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Remove from registry")
# Test the error
request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_delete_domain": "Remove from registry", "name": domain.name},
follow=True,
)
request.user = self.client
with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with(
request,
messages.ERROR,
"Error deleting this Domain: "
"Can't switch from state 'ready' to 'deleted'"
", must be either 'dns_needed' or 'on_hold'",
extra_tags="",
fail_silently=False,
)
self.assertEqual(domain.state, Domain.State.READY)
# delete data created in this test
domain.delete()
@less_console_noise_decorator
def test_analyst_deletes_domain_idempotent(self):
"""
Scenario: Analyst tries to delete an already deleted domain
Given `state` is already `DELETED`
When `domain.deletedInEpp()` is called
Then `commands.DeleteDomain` is sent to the registry
And Domain returns normally without an error dialog
"""
domain = create_ready_domain()
# Put in client hold
domain.place_client_hold()
# Ensure everything is displaying correctly
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk),
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Remove from registry")
# Test the info dialog
request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_delete_domain": "Remove from registry", "name": domain.name},
follow=True,
)
request.user = self.client
# Delete it once
with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with(
request,
messages.INFO,
"Domain city.gov has been deleted. Thanks!",
extra_tags="",
fail_silently=False,
)
self.assertEqual(domain.state, Domain.State.DELETED)
# Try to delete it again
# Test the info dialog
request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_delete_domain": "Remove from registry", "name": domain.name},
follow=True,
)
request.user = self.client
with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with(
request,
messages.INFO,
"This domain is already deleted",
extra_tags="",
fail_silently=False,
)
self.assertEqual(domain.state, Domain.State.DELETED)
# delete data created in this test
domain.delete()
class TestDomainAdminWithClient(TestCase):
"""Test DomainAdmin class as super user.
Notes:
all tests share superuser; tests must not update superuser
tests have available superuser, client, and admin
"""
@classmethod
def setUpClass(self):
super().setUpClass()
self.site = AdminSite()
self.admin = DomainAdmin(model=Domain, admin_site=self.site)
self.factory = RequestFactory()
self.superuser = create_superuser()
def setUp(self):
self.client = Client(HTTP_HOST="localhost:8080")
self.client.force_login(self.superuser)
super().setUp()
def tearDown(self):
super().tearDown()
Host.objects.all().delete()
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
@classmethod
def tearDownClass(self):
User.objects.all().delete()
super().tearDownClass()
@less_console_noise_decorator
def test_has_model_description(self):
"""Tests if this model has a model description on the table view"""
response = self.client.get(
"/admin/registrar/domain/",
follow=True,
)
# Make sure that the page is loaded correctly
self.assertEqual(response.status_code, 200)
# Test for a description snippet
self.assertContains(response, "This table contains all approved domains in the .gov registrar.")
self.assertContains(response, "Show more")
@less_console_noise_decorator
def test_contact_fields_on_domain_change_form_have_detail_table(self):
"""Tests if the contact fields in the inlined Domain information have the detail table
which displays title, email, and phone"""
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
email="meoward.jones@igorville.gov",
phone="(555) 123 12345",
title="Treat inspector",
)
# Create a fake domain request
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
domain_request.approve()
_domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get()
domain = Domain.objects.filter(domain_info=_domain_info).get()
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
# Check that the fields have the right values.
# == Check for the creator == #
# Check for the right title, email, and phone number in the response.
# We only need to check for the end tag
# (Otherwise this test will fail if we change classes, etc)
self.assertContains(response, "Treat inspector")
self.assertContains(response, "meoward.jones@igorville.gov")
self.assertContains(response, "(555) 123 12345")
# Check for the field itself
self.assertContains(response, "Meoward Jones")
# == Check for the submitter == #
self.assertContains(response, "mayor@igorville.gov")
self.assertContains(response, "Admin Tester")
self.assertContains(response, "(555) 555 5556")
self.assertContains(response, "Testy2 Tester2")
# == Check for the senior_official == #
self.assertContains(response, "testy@town.com")
self.assertContains(response, "Chief Tester")
self.assertContains(response, "(555) 555 5555")
# Includes things like readonly fields
self.assertContains(response, "Testy Tester")
# == Test the other_employees field == #
self.assertContains(response, "testy2@town.com")
self.assertContains(response, "Another Tester")
self.assertContains(response, "(555) 555 5557")
# Test for the copy link
self.assertContains(response, "usa-button__clipboard")
# cleanup from this test
domain.delete()
_domain_info.delete()
domain_request.delete()
_creator.delete()
@less_console_noise_decorator
def test_helper_text(self):
"""
Tests for the correct helper text on this page
"""
# Create a ready domain with a preset expiration date
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
# Contains some test tools
test_helper = GenericTestHelper(
factory=self.factory,
user=self.superuser,
admin=self.admin,
url=reverse("admin:registrar_domain_changelist"),
model=Domain,
client=self.client,
)
# These should exist in the response
expected_values = [
("expiration_date", "Date the domain expires in the registry"),
("first_ready_at", 'Date when this domain first moved into "ready" state; date will never change'),
("deleted_at", 'Will appear blank unless the domain is in "deleted" state'),
]
test_helper.assert_response_contains_distinct_values(response, expected_values)
@less_console_noise_decorator
def test_helper_text_state(self):
"""
Tests for the correct state helper text on this page
"""
# Add domain data
ready_domain, _ = Domain.objects.get_or_create(name="fakeready.gov", state=Domain.State.READY)
unknown_domain, _ = Domain.objects.get_or_create(name="fakeunknown.gov", state=Domain.State.UNKNOWN)
dns_domain, _ = Domain.objects.get_or_create(name="fakedns.gov", state=Domain.State.DNS_NEEDED)
hold_domain, _ = Domain.objects.get_or_create(name="fakehold.gov", state=Domain.State.ON_HOLD)
deleted_domain, _ = Domain.objects.get_or_create(name="fakedeleted.gov", state=Domain.State.DELETED)
# We don't need to check for all text content, just a portion of it
expected_unknown_domain_message = "The creator of the associated domain request has not logged in to"
expected_dns_message = "Before this domain can be used, name server addresses need"
expected_hold_message = "While on hold, this domain"
expected_deleted_message = "This domain was permanently removed from the registry."
expected_messages = [
(ready_domain, "This domain has name servers and is ready for use."),
(unknown_domain, expected_unknown_domain_message),
(dns_domain, expected_dns_message),
(hold_domain, expected_hold_message),
(deleted_domain, expected_deleted_message),
]
for domain, message in expected_messages:
with self.subTest(domain_state=domain.state):
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.id),
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
# Check that the right help text exists
self.assertContains(response, message)
@less_console_noise_decorator
def test_admin_can_see_inline_domain_information_in_domain_change_form(self):
"""Tests if an admin can still see the inline domain information form"""
# Create fake creator
_creator = User.objects.create(
username="MrMeoward",
first_name="Meoward",
last_name="Jones",
)
# Create a fake domain request
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
# Creates a Domain and DomainInformation object
_domain_request.approve()
domain_information = DomainInformation.objects.filter(domain_request=_domain_request).get()
domain_information.organization_name = "MonkeySeeMonkeyDo"
domain_information.save()
# We use filter here rather than just domain_information.domain just to get the latest data.
domain = Domain.objects.filter(domain_info=domain_information).get()
response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
# Test for data. We only need to test one since its all interconnected.
expected_organization_name = "MonkeySeeMonkeyDo"
self.assertContains(response, expected_organization_name)
# cleanup from this test
domain.delete()
domain_information.delete()
_domain_request.delete()
_creator.delete()
@less_console_noise_decorator
def test_custom_delete_confirmation_page_table(self):
"""Tests if we override the delete confirmation page for custom content on the table"""
# Create a ready domain
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
# Get the index. The post expects the index to be encoded as a string
index = f"{domain.id}"
# Contains some test tools
test_helper = GenericTestHelper(
factory=self.factory,
user=self.superuser,
admin=self.admin,
url=reverse("admin:registrar_domain_changelist"),
model=Domain,
client=self.client,
)
# Simulate selecting a single record, then clicking "Delete selected domains"
response = test_helper.get_table_delete_confirmation_page("0", index)
# Check that our content exists
content_slice = "When a domain is deleted:"
self.assertContains(response, content_slice)
@less_console_noise_decorator
def test_short_org_name_in_domains_list(self):
"""
Make sure the short name is displaying in admin on the list page
"""
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
mock_client = MockSESClient()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
domain_request.approve()
response = self.client.get("/admin/registrar/domain/")
# There are 4 template references to Federal (4) plus four references in the table
# for our actual domain_request
self.assertContains(response, "Federal", count=56)
# This may be a bit more robust
self.assertContains(response, '<td class="field-generic_org_type">Federal</td>', count=1)
# Now let's make sure the long description does not exist
self.assertNotContains(response, "Federal: an agency of the U.S. government")
@override_settings(IS_PRODUCTION=True)
@less_console_noise_decorator
def test_prod_only_shows_export(self):
"""Test that production environment only displays export"""
response = self.client.get("/admin/registrar/domain/")
self.assertContains(response, ">Export<")
self.assertNotContains(response, ">Import<")
class TestDomainAdminWebTest(MockEppLib, WebTest):
"""Test DomainAdmin class as super user, using WebTest.
WebTest allows for easier handling of forms and html responses.
Notes:
all tests share superuser; tests must not update superuser
tests have available superuser, app, and admin
"""
# csrf checks do not work with WebTest.
# We disable them here. TODO for another ticket.
csrf_checks = False
@classmethod
def setUpClass(self):
super().setUpClass()
self.site = AdminSite()
self.admin = DomainAdmin(model=Domain, admin_site=self.site)
self.superuser = create_superuser()
self.factory = RequestFactory()
def setUp(self):
super().setUp()
self.app.set_user(self.superuser.username)
def tearDown(self):
super().tearDown()
Host.objects.all().delete()
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
@classmethod
def tearDownClass(self):
User.objects.all().delete()
super().tearDownClass()
@less_console_noise_decorator
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
def test_extend_expiration_date_button(self, mock_date_today):
"""
Tests if extend_expiration_date modal gives an accurate date
"""
# Create a ready domain with a preset expiration date
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
# load expiration date into cache and registrar with below command
domain.registry_expiration_date
# Make sure the ex date is what we expect it to be
domain_ex_date = Domain.objects.get(id=domain.id).expiration_date
self.assertEqual(domain_ex_date, date(2023, 5, 25))
# Make sure that the page is loading as expected
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Extend expiration date")
# Grab the form to submit
form = response.forms["domain_form"]
with patch("django.contrib.messages.add_message") as mock_add_message:
# Submit the form
response = form.submit("_extend_expiration_date")
# Follow the response
response = response.follow()
# Assert that everything on the page looks correct
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Extend expiration date")
# Ensure the message we recieve is in line with what we expect
expected_message = "Successfully extended the expiration date."
expected_call = call(
# The WGSI request doesn't need to be tested
ANY,
messages.INFO,
expected_message,
extra_tags="",
fail_silently=False,
)
mock_add_message.assert_has_calls([expected_call], 1)
@less_console_noise_decorator
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
def test_extend_expiration_date_button_epp(self, mock_date_today):
"""
Tests if extend_expiration_date button sends the right epp command
"""
# Create a ready domain with a preset expiration date
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
# Make sure that the page is loading as expected
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Extend expiration date")
# Grab the form to submit
form = response.forms["domain_form"]
with patch("django.contrib.messages.add_message") as mock_add_message:
with patch("registrar.models.Domain.renew_domain") as renew_mock:
# Submit the form
response = form.submit("_extend_expiration_date")
# Follow the response
response = response.follow()
# Assert that it is calling the function with the default extension length.
# We only need to test the value that EPP sends, as we can assume the other
# test cases cover the "renew" function.
renew_mock.assert_has_calls([call()], any_order=False)
# We should not make duplicate calls
self.assertEqual(renew_mock.call_count, 1)
# Assert that everything on the page looks correct
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Extend expiration date")
# Ensure the message we recieve is in line with what we expect
expected_message = "Successfully extended the expiration date."
expected_call = call(
# The WGSI request doesn't need to be tested
ANY,
messages.INFO,
expected_message,
extra_tags="",
fail_silently=False,
)
mock_add_message.assert_has_calls([expected_call], 1)
@less_console_noise_decorator
def test_custom_delete_confirmation_page(self):
"""Tests if we override the delete confirmation page for custom content"""
# Create a ready domain with a preset expiration date
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
domain_change_page = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
self.assertContains(domain_change_page, "fake.gov")
# click the "Delete" link
confirmation_page = domain_change_page.click("Delete", index=0)
content_slice = "When a domain is deleted:"
self.assertContains(confirmation_page, content_slice)
@less_console_noise_decorator
def test_on_hold_is_successful_web_test(self):
"""
Scenario: Domain on_hold is successful through webtest
"""
with less_console_noise():
domain = create_ready_domain()
response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
# Check the contents of the modal
# Check for the header
self.assertContains(response, "Are you sure you want to place this domain on hold?")
# Check for some of its body
self.assertContains(response, "When a domain is on hold:")
# Check for some of the button content
self.assertContains(response, "Yes, place hold")
# Grab the form to submit
form = response.forms["domain_form"]
# Submit the form
response = form.submit("_place_client_hold")
# Follow the response
response = response.follow()
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Remove hold")
# The modal should still exist
# Check for the header
self.assertContains(response, "Are you sure you want to place this domain on hold?")
# Check for some of its body
self.assertContains(response, "When a domain is on hold:")
# Check for some of the button content
self.assertContains(response, "Yes, place hold")
# Web test has issues grabbing up to date data from the db, so we can test
# the returned view instead
self.assertContains(response, '<div class="readonly">On hold</div>')

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
from django.test import TestCase, Client
from django.urls import reverse
from registrar.tests.common import create_superuser
from api.tests.common import less_console_noise_decorator
class TestAdminViews(TestCase):
@ -8,6 +9,7 @@ class TestAdminViews(TestCase):
self.client = Client(HTTP_HOST="localhost:8080")
self.superuser = create_superuser()
@less_console_noise_decorator
def test_export_data_view(self):
self.client.force_login(self.superuser)

View file

@ -6,8 +6,9 @@ from django.test import TestCase
from waffle.testutils import override_flag
from registrar.utility import email
from registrar.utility.email import send_templated_email
from .common import completed_domain_request, less_console_noise
from .common import completed_domain_request
from api.tests.common import less_console_noise_decorator
from datetime import datetime
import boto3_mocking # type: ignore
@ -19,6 +20,7 @@ class TestEmails(TestCase):
@boto3_mocking.patching
@override_flag("disable_email_sending", active=True)
@less_console_noise_decorator
def test_disable_email_flag(self):
"""Test if the 'disable_email_sending' stops emails from being sent"""
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
@ -36,12 +38,12 @@ class TestEmails(TestCase):
self.assertFalse(self.mock_client.send_email.called)
@boto3_mocking.patching
@less_console_noise_decorator
def test_submission_confirmation(self):
"""Submission confirmation email works."""
domain_request = completed_domain_request()
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
with less_console_noise():
domain_request.submit()
# check that an email was sent
@ -74,11 +76,11 @@ class TestEmails(TestCase):
self.assertIn("Anything else", body)
@boto3_mocking.patching
@less_console_noise_decorator
def test_submission_confirmation_no_current_website_spacing(self):
"""Test line spacing without current_website."""
domain_request = completed_domain_request(has_current_website=False)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
with less_console_noise():
domain_request.submit()
_, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
@ -87,11 +89,11 @@ class TestEmails(TestCase):
self.assertRegex(body, r"5555\n\n.gov domain:")
@boto3_mocking.patching
@less_console_noise_decorator
def test_submission_confirmation_current_website_spacing(self):
"""Test line spacing with current_website."""
domain_request = completed_domain_request(has_current_website=True)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
with less_console_noise():
domain_request.submit()
_, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
@ -101,11 +103,11 @@ class TestEmails(TestCase):
self.assertRegex(body, r"city.com\n\n.gov domain:")
@boto3_mocking.patching
@less_console_noise_decorator
def test_submission_confirmation_other_contacts_spacing(self):
"""Test line spacing with other contacts."""
domain_request = completed_domain_request(has_other_contacts=True)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
with less_console_noise():
domain_request.submit()
_, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
@ -115,11 +117,11 @@ class TestEmails(TestCase):
self.assertRegex(body, r"5557\n\nAnything else")
@boto3_mocking.patching
@less_console_noise_decorator
def test_submission_confirmation_no_other_contacts_spacing(self):
"""Test line spacing without other contacts."""
domain_request = completed_domain_request(has_other_contacts=False)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
with less_console_noise():
domain_request.submit()
_, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
@ -128,11 +130,11 @@ class TestEmails(TestCase):
self.assertRegex(body, r"None\n\nAnything else")
@boto3_mocking.patching
@less_console_noise_decorator
def test_submission_confirmation_alternative_govdomain_spacing(self):
"""Test line spacing with alternative .gov domain."""
domain_request = completed_domain_request(has_alternative_gov_domain=True)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
with less_console_noise():
domain_request.submit()
_, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
@ -141,11 +143,11 @@ class TestEmails(TestCase):
self.assertRegex(body, r"city.gov\n\nAlternative domains:\ncity1.gov\n\nPurpose of your domain:")
@boto3_mocking.patching
@less_console_noise_decorator
def test_submission_confirmation_no_alternative_govdomain_spacing(self):
"""Test line spacing without alternative .gov domain."""
domain_request = completed_domain_request(has_alternative_gov_domain=False)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
with less_console_noise():
domain_request.submit()
_, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
@ -154,11 +156,11 @@ class TestEmails(TestCase):
self.assertRegex(body, r"city.gov\n\nPurpose of your domain:")
@boto3_mocking.patching
@less_console_noise_decorator
def test_submission_confirmation_about_your_organization_spacing(self):
"""Test line spacing with about your organization."""
domain_request = completed_domain_request(has_about_your_organization=True)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
with less_console_noise():
domain_request.submit()
_, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
@ -167,11 +169,11 @@ class TestEmails(TestCase):
self.assertRegex(body, r"10002\n\nAbout your organization:")
@boto3_mocking.patching
@less_console_noise_decorator
def test_submission_confirmation_no_about_your_organization_spacing(self):
"""Test line spacing without about your organization."""
domain_request = completed_domain_request(has_about_your_organization=False)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
with less_console_noise():
domain_request.submit()
_, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
@ -180,11 +182,11 @@ class TestEmails(TestCase):
self.assertRegex(body, r"10002\n\nSenior official:")
@boto3_mocking.patching
@less_console_noise_decorator
def test_submission_confirmation_anything_else_spacing(self):
"""Test line spacing with anything else."""
domain_request = completed_domain_request(has_anything_else=True)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
with less_console_noise():
domain_request.submit()
_, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
@ -192,11 +194,11 @@ class TestEmails(TestCase):
self.assertRegex(body, r"5557\n\nAnything else?")
@boto3_mocking.patching
@less_console_noise_decorator
def test_submission_confirmation_no_anything_else_spacing(self):
"""Test line spacing without anything else."""
domain_request = completed_domain_request(has_anything_else=False)
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
with less_console_noise():
domain_request.submit()
_, kwargs = self.mock_client.send_email.call_args
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
@ -205,6 +207,7 @@ class TestEmails(TestCase):
self.assertRegex(body, r"5557\n\n----")
@boto3_mocking.patching
@less_console_noise_decorator
def test_send_email_with_attachment(self):
with boto3_mocking.clients.handler_for("ses", self.mock_client_class):
sender_email = "sender@example.com"

View file

@ -36,6 +36,7 @@ logger = logging.getLogger(__name__)
class TestPopulateVerificationType(MockEppLib):
"""Tests for the populate_organization_type script"""
@less_console_noise_decorator
def setUp(self):
"""Creates a fake domain object"""
super().setUp()
@ -133,6 +134,7 @@ class TestPopulateVerificationType(MockEppLib):
class TestPopulateOrganizationType(MockEppLib):
"""Tests for the populate_organization_type script"""
@less_console_noise_decorator
def setUp(self):
"""Creates a fake domain object"""
super().setUp()
@ -205,6 +207,7 @@ class TestPopulateOrganizationType(MockEppLib):
):
call_command("populate_organization_type", "registrar/tests/data/fake_election_domains.csv")
@less_console_noise_decorator
def assert_expected_org_values_on_request_and_info(
self,
domain_request: DomainRequest,
@ -247,6 +250,7 @@ class TestPopulateOrganizationType(MockEppLib):
"""Does nothing for mocking purposes"""
pass
@less_console_noise_decorator
def test_request_and_info_city_not_in_csv(self):
"""
Tests what happens to a city domain that is not defined in the CSV.
@ -282,6 +286,7 @@ class TestPopulateOrganizationType(MockEppLib):
# All values should be the same
self.assert_expected_org_values_on_request_and_info(city_request, city_info, expected_values)
@less_console_noise_decorator
def test_request_and_info_federal(self):
"""
Tests what happens to a federal domain after the script is run (should be unchanged).
@ -316,6 +321,7 @@ class TestPopulateOrganizationType(MockEppLib):
# All values should be the same
self.assert_expected_org_values_on_request_and_info(federal_request, federal_info, expected_values)
@less_console_noise_decorator
def test_request_and_info_tribal_add_election_office(self):
"""
Tests if a tribal domain in the election csv changes organization_type to TRIBAL - ELECTION
@ -356,6 +362,7 @@ class TestPopulateOrganizationType(MockEppLib):
self.assert_expected_org_values_on_request_and_info(tribal_request, tribal_info, expected_values)
@less_console_noise_decorator
def test_request_and_info_tribal_doesnt_remove_election_office(self):
"""
Tests if a tribal domain in the election csv changes organization_type to TRIBAL_ELECTION
@ -409,6 +416,7 @@ class TestPopulateOrganizationType(MockEppLib):
class TestPopulateFirstReady(TestCase):
"""Tests for the populate_first_ready script"""
@less_console_noise_decorator
def setUp(self):
"""Creates a fake domain object"""
super().setUp()
@ -537,6 +545,7 @@ class TestPopulateFirstReady(TestCase):
class TestPatchAgencyInfo(TestCase):
@less_console_noise_decorator
def setUp(self):
self.user, _ = User.objects.get_or_create(username="testuser")
self.domain, _ = Domain.objects.get_or_create(name="testdomain.gov")
@ -560,6 +569,7 @@ class TestPatchAgencyInfo(TestCase):
class TestExtendExpirationDates(MockEppLib):
@less_console_noise_decorator
def setUp(self):
"""Defines the file name of migration_json and the folder its contained in"""
super().setUp()
@ -882,6 +892,7 @@ class TestExportTables(MockEppLib):
def tearDown(self):
self.logger_patcher.stop()
@less_console_noise_decorator
@patch("os.makedirs")
@patch("os.path.exists")
@patch("os.remove")
@ -1113,6 +1124,7 @@ class TestImportTables(TestCase):
class TestTransferFederalAgencyType(TestCase):
"""Tests for the transfer_federal_agency_type script"""
@less_console_noise_decorator
def setUp(self):
"""Creates a fake domain object"""
super().setUp()
@ -1172,7 +1184,9 @@ class TestTransferFederalAgencyType(TestCase):
User.objects.all().delete()
Contact.objects.all().delete()
Website.objects.all().delete()
FederalAgency.objects.all().delete()
FederalAgency.objects.filter(
id__in=[self.amtrak.id, self.legislative_branch.id, self.library_of_congress.id, self.gov_admin.id]
).delete()
def run_transfer_federal_agency_type(self):
"""

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,16 @@
import io
from django.test import Client, RequestFactory
from io import StringIO
from registrar.models.domain_request import DomainRequest
from registrar.models.domain import Domain
from registrar.models import (
DomainRequest,
Domain,
UserDomainRole,
)
from registrar.utility.csv_export import (
DomainDataFull,
DomainDataType,
DomainDataFederal,
DomainDataTypeUser,
DomainGrowth,
DomainManaged,
DomainUnmanaged,
@ -27,14 +31,14 @@ import boto3_mocking
from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore
from django.utils import timezone
from api.tests.common import less_console_noise_decorator
from .common import MockDb, MockEppLib, less_console_noise, get_time_aware_date
from .common import MockDbForSharedTests, MockDbForIndividualTests, MockEppLib, less_console_noise, get_time_aware_date
class CsvReportsTest(MockDb):
"""Tests to determine if we are uploading our reports correctly"""
class CsvReportsTest(MockDbForSharedTests):
"""Tests to determine if we are uploading our reports correctly."""
def setUp(self):
"""Create fake domain data"""
"""setup fake comain data"""
super().setUp()
self.client = Client(HTTP_HOST="localhost:8080")
self.factory = RequestFactory()
@ -47,10 +51,10 @@ class CsvReportsTest(MockDb):
fake_open = mock_open()
expected_file_content = [
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,\r\n"),
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\r\n"),
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,\r\n"),
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
]
# We don't actually want to write anything for a test case,
# we just want to verify what is being written.
@ -69,12 +73,12 @@ class CsvReportsTest(MockDb):
fake_open = mock_open()
expected_file_content = [
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,\r\n"),
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\r\n"),
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,\r\n"),
call("adomain2.gov,Interstate,,,,,\r\n"),
call("zdomain12.gov,Interstate,,,,,\r\n"),
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"),
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"),
call("adomain2.gov,Interstate,,,,,(blank)\r\n"),
call("zdomain12.gov,Interstate,,,,,(blank)\r\n"),
]
# We don't actually want to write anything for a test case,
# we just want to verify what is being written.
@ -198,16 +202,13 @@ class CsvReportsTest(MockDb):
self.assertEqual(expected_file_content, response.content)
class ExportDataTest(MockDb, MockEppLib):
def setUp(self):
super().setUp()
def tearDown(self):
super().tearDown()
class ExportDataTest(MockDbForIndividualTests, MockEppLib):
"""Test the ExportData class from csv_export."""
@less_console_noise_decorator
def test_domain_data_type(self):
"""Shows security contacts, domain managers, so"""
# Add security email information
self.domain_1.name = "defaultsecurity.gov"
self.domain_1.save()
@ -233,29 +234,81 @@ class ExportDataTest(MockDb, MockEppLib):
expected_content = (
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,City,State,SO,"
"SO email,Security contact email,Domain managers,Invited domain managers\n"
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,World War I Centennial Commission,,,, ,,,"
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,World War I Centennial Commission,,,,(blank),,,"
"meoward@rocks.com,\n"
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,,"
', ,,dotgov@cisa.dhs.gov,"meoward@rocks.com, info@example.com, big_lebowski@dude.co",'
',,,(blank),"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
"woofwardthethird@rocks.com\n"
"adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,, ,,,,"
"adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,"
"squeaker@rocks.com\n"
"bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
"bdomain5.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
"bdomain6.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
"bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
"bdomain5.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
"bdomain6.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
"ddomain3.gov,On hold,(blank),2023-11-15,Federal,Armed Forces Retirement Home,,,,,,"
"security@mail.gov,,\n"
"sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
"xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
"zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
"adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,, ,,registrar@dotgov.gov,"
"sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
"xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
"zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,,(blank),,,,\n"
"adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,,(blank),,,"
"meoward@rocks.com,squeaker@rocks.com\n"
"zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,, ,,,meoward@rocks.com,\n"
"zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,,(blank),,,meoward@rocks.com,\n"
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
def test_domain_data_type_user(self):
"""Shows security contacts, domain managers, so for the current user"""
# Add security email information
self.domain_1.name = "defaultsecurity.gov"
self.domain_1.save()
# Invoke setter
self.domain_1.security_contact
self.domain_2.security_contact
self.domain_3.security_contact
# Add a first ready date on the first domain. Leaving the others blank.
self.domain_1.first_ready = get_default_start_date()
self.domain_1.save()
# Create a user and associate it with some domains
UserDomainRole.objects.create(user=self.user, domain=self.domain_2)
# Create a request object
factory = RequestFactory()
request = factory.get("/")
request.user = self.user
# Create a CSV file in memory
csv_file = StringIO()
# Call the export functions
DomainDataTypeUser.export_data_to_csv(csv_file, request=request)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
# Read the content into a variable
csv_content = csv_file.read()
# We expect only domains associated with the user
expected_content = (
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,"
"City,State,SO,SO email,"
"Security contact email,Domain managers,Invited domain managers\n"
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,,, ,,"
'(blank),"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
"woofwardthethird@rocks.com\n"
"adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,, ,,(blank),"
'"info@example.com, meoward@rocks.com",squeaker@rocks.com\n'
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@ -285,17 +338,18 @@ class ExportDataTest(MockDb, MockEppLib):
# sorted alphabetially by domain name
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,\n"
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,dotgov@cisa.dhs.gov\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,\n"
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
"adomain2.gov,Interstate,,,,,registrar@dotgov.gov\n"
"zdomain12.gov,Interstate,,,,,\n"
"adomain2.gov,Interstate,,,,,(blank)\n"
"zdomain12.gov,Interstate,,,,,(blank)\n"
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@ -325,15 +379,16 @@ class ExportDataTest(MockDb, MockEppLib):
# sorted alphabetially by domain name
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,\n"
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,dotgov@cisa.dhs.gov\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,\n"
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n"
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@ -366,8 +421,8 @@ class ExportDataTest(MockDb, MockEppLib):
# Call the export functions
DomainGrowth.export_data_to_csv(
csv_file,
self.start_date.strftime("%Y-%m-%d"),
self.end_date.strftime("%Y-%m-%d"),
start_date=self.start_date.strftime("%Y-%m-%d"),
end_date=self.end_date.strftime("%Y-%m-%d"),
)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
@ -402,13 +457,14 @@ class ExportDataTest(MockDb, MockEppLib):
squeaker@rocks.com is invited to domain2 (DNS_NEEDED) and domain10 (No managers).
She should show twice in this report but not in test_DomainManaged."""
self.maxDiff = None
# Create a CSV file in memory
csv_file = StringIO()
# Call the export functions
DomainManaged.export_data_to_csv(
csv_file,
self.start_date.strftime("%Y-%m-%d"),
self.end_date.strftime("%Y-%m-%d"),
start_date=self.start_date.strftime("%Y-%m-%d"),
end_date=self.end_date.strftime("%Y-%m-%d"),
)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
@ -428,7 +484,7 @@ class ExportDataTest(MockDb, MockEppLib):
"\n"
"Domain name,Domain type,Domain managers,Invited domain managers\n"
"cdomain11.gov,Federal - Executive,meoward@rocks.com,\n"
'cdomain1.gov,Federal - Executive,"meoward@rocks.com, info@example.com, big_lebowski@dude.co",'
'cdomain1.gov,Federal - Executive,"big_lebowski@dude.co, info@example.com, meoward@rocks.com",'
"woofwardthethird@rocks.com\n"
"zdomain12.gov,Interstate,meoward@rocks.com,\n"
)
@ -436,6 +492,7 @@ class ExportDataTest(MockDb, MockEppLib):
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@ -444,7 +501,7 @@ class ExportDataTest(MockDb, MockEppLib):
# Create a CSV file in memory
csv_file = StringIO()
DomainUnmanaged.export_data_to_csv(
csv_file, self.start_date.strftime("%Y-%m-%d"), self.end_date.strftime("%Y-%m-%d")
csv_file, start_date=self.start_date.strftime("%Y-%m-%d"), end_date=self.end_date.strftime("%Y-%m-%d")
)
# Reset the CSV file's position to the beginning
@ -491,8 +548,8 @@ class ExportDataTest(MockDb, MockEppLib):
# Call the export functions
DomainRequestGrowth.export_data_to_csv(
csv_file,
self.start_date.strftime("%Y-%m-%d"),
self.end_date.strftime("%Y-%m-%d"),
start_date=self.start_date.strftime("%Y-%m-%d"),
end_date=self.end_date.strftime("%Y-%m-%d"),
)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
@ -554,7 +611,6 @@ class ExportDataTest(MockDb, MockEppLib):
csv_file.seek(0)
# Read the content into a variable
csv_content = csv_file.read()
print(csv_content)
expected_content = (
# Header
"Domain request,Status,Domain type,Federal type,"
@ -591,7 +647,7 @@ class ExportDataTest(MockDb, MockEppLib):
self.assertEqual(csv_content, expected_content)
class HelperFunctions(MockDb):
class HelperFunctions(MockDbForSharedTests):
"""This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy."""
def test_get_default_start_date(self):

View file

@ -43,7 +43,6 @@ class TestProcessedMigrations(TestCase):
DomainInformation.objects.all().delete()
DomainInvitation.objects.all().delete()
TransitionDomain.objects.all().delete()
FederalAgency.objects.all().delete()
# Delete users
User.objects.all().delete()
@ -185,6 +184,7 @@ class TestOrganizationMigration(TestCase):
"""Defines the file name of migration_json and the folder its contained in"""
self.test_data_file_location = "registrar/tests/data"
self.migration_json_filename = "test_migrationFilepaths.json"
self.federal_agency, _ = FederalAgency.objects.get_or_create(agency="Department of Commerce")
def tearDown(self):
"""Deletes all DB objects related to migrations"""
@ -197,6 +197,7 @@ class TestOrganizationMigration(TestCase):
# Delete users
User.objects.all().delete()
UserDomainRole.objects.all().delete()
self.federal_agency.delete()
def run_load_domains(self):
"""
@ -331,7 +332,6 @@ class TestOrganizationMigration(TestCase):
# Lets test the first one
transition = transition_domains.first()
federal_agency, _ = FederalAgency.objects.get_or_create(agency="Department of Commerce")
expected_transition_domain = TransitionDomain(
username="alexandra.bobbitt5@test.com",
domain_name="fakewebsite2.gov",
@ -340,7 +340,7 @@ class TestOrganizationMigration(TestCase):
generic_org_type="Federal",
organization_name="Fanoodle",
federal_type="Executive",
federal_agency=federal_agency,
federal_agency=self.federal_agency,
epp_creation_date=datetime.date(2004, 5, 7),
epp_expiration_date=datetime.date(2023, 9, 30),
first_name="Seline",
@ -395,7 +395,6 @@ class TestOrganizationMigration(TestCase):
# == Third, test that we've loaded data as we expect == #
_domain = Domain.objects.filter(name="fakewebsite2.gov").get()
domain_information = DomainInformation.objects.filter(domain=_domain).get()
federal_agency, _ = FederalAgency.objects.get_or_create(agency="Department of Commerce")
expected_creator = User.objects.filter(username="System").get()
expected_so = Contact.objects.filter(
@ -404,7 +403,7 @@ class TestOrganizationMigration(TestCase):
expected_domain_information = DomainInformation(
creator=expected_creator,
generic_org_type="federal",
federal_agency=federal_agency,
federal_agency=self.federal_agency,
federal_type="executive",
organization_name="Fanoodle",
address_line1="93001 Arizona Drive",
@ -451,7 +450,6 @@ class TestOrganizationMigration(TestCase):
# == Fourth, test that no data is overwritten as we expect == #
_domain = Domain.objects.filter(name="fakewebsite2.gov").get()
domain_information = DomainInformation.objects.filter(domain=_domain).get()
federal_agency, _ = FederalAgency.objects.get_or_create(agency="Department of Commerce")
expected_creator = User.objects.filter(username="System").get()
expected_so = Contact.objects.filter(
@ -460,7 +458,7 @@ class TestOrganizationMigration(TestCase):
expected_domain_information = DomainInformation(
creator=expected_creator,
generic_org_type="federal",
federal_agency=federal_agency,
federal_agency=self.federal_agency,
federal_type="executive",
organization_name="Fanoodle",
address_line1="93001 Galactic Way",

View file

@ -8,13 +8,13 @@ from api.tests.common import less_console_noise_decorator
from registrar.models.contact import Contact
from registrar.models.domain import Domain
from registrar.models.draft_domain import DraftDomain
from registrar.models.federal_agency import FederalAgency
from registrar.models.portfolio import Portfolio
from registrar.models.public_contact import PublicContact
from registrar.models.user import User
from registrar.models.user_domain_role import UserDomainRole
from registrar.views.domain import DomainNameserversView
from .common import MockEppLib, less_console_noise # type: ignore
from .common import MockEppLib, create_test_user, less_console_noise # type: ignore
from unittest.mock import patch
from django.urls import reverse
@ -30,18 +30,23 @@ logger = logging.getLogger(__name__)
class TestViews(TestCase):
def setUp(self):
super().setUp()
self.client = Client()
@less_console_noise_decorator
def test_health_check_endpoint(self):
response = self.client.get("/health")
self.assertContains(response, "OK", status_code=200)
@less_console_noise_decorator
def test_home_page(self):
"""Home page should NOT be available without a login."""
response = self.client.get("/")
self.assertEqual(response.status_code, 302)
@less_console_noise_decorator
def test_domain_request_form_not_logged_in(self):
"""Domain request form not accessible without a logged-in user."""
response = self.client.get("/request/")
@ -50,75 +55,61 @@ class TestViews(TestCase):
class TestWithUser(MockEppLib):
"""Class for executing tests with a test user.
Note that tests share the test user within their test class, so the user
cannot be changed within a test."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = create_test_user()
def setUp(self):
super().setUp()
username = "test_user"
first_name = "First"
last_name = "Last"
email = "info@example.com"
phone = "8003111234"
title = "test title"
self.user = get_user_model().objects.create(
username=username, first_name=first_name, last_name=last_name, title=title, email=email, phone=phone
)
self.client = Client()
username_regular_incomplete = "test_regular_user_incomplete"
username_other_incomplete = "test_other_user_incomplete"
first_name_2 = "Incomplete"
email_2 = "unicorn@igorville.com"
# in the case below, REGULAR user is 'Verified by Login.gov, ie. IAL2
self.incomplete_regular_user = get_user_model().objects.create(
username=username_regular_incomplete,
first_name=first_name_2,
email=email_2,
verification_type=User.VerificationTypeChoices.REGULAR,
)
# in the case below, other user is representative of GRANDFATHERED,
# VERIFIED_BY_STAFF, INVITED, FIXTURE_USER, ie. IAL1
self.incomplete_other_user = get_user_model().objects.create(
username=username_other_incomplete,
first_name=first_name_2,
email=email_2,
verification_type=User.VerificationTypeChoices.VERIFIED_BY_STAFF,
)
def tearDown(self):
# delete any domain requests too
super().tearDown()
DomainRequest.objects.all().delete()
DomainInformation.objects.all().delete()
@classmethod
def tearDownClass(cls):
super().tearDownClass()
User.objects.all().delete()
class TestEnvironmentVariablesEffects(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = create_test_user()
def setUp(self):
self.client = Client()
username = "test_user"
first_name = "First"
last_name = "Last"
email = "info@example.com"
self.user = get_user_model().objects.create(
username=username, first_name=first_name, last_name=last_name, email=email
)
self.client.force_login(self.user)
def tearDown(self):
super().tearDown()
UserDomainRole.objects.all().delete()
Domain.objects.all().delete()
self.user.delete()
@classmethod
def tearDownClass(cls):
super().tearDownClass()
User.objects.all().delete()
@less_console_noise_decorator
@override_settings(IS_PRODUCTION=True)
def test_production_environment(self):
"""No banner on prod."""
home_page = self.client.get("/")
self.assertNotContains(home_page, "You are on a test site.")
@less_console_noise_decorator
@override_settings(IS_PRODUCTION=False)
def test_non_production_environment(self):
"""Banner on non-prod."""
home_page = self.client.get("/")
self.assertContains(home_page, "You are on a test site.")
@less_console_noise_decorator
def side_effect_raise_value_error(self):
"""Side effect that raises a 500 error"""
raise ValueError("Some error")
@ -130,9 +121,7 @@ class TestEnvironmentVariablesEffects(TestCase):
fake_domain, _ = Domain.objects.get_or_create(name="igorville.gov")
# Add a role
fake_role, _ = UserDomainRole.objects.get_or_create(
user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER
)
UserDomainRole.objects.get_or_create(user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER)
with patch.object(DomainNameserversView, "get_initial", side_effect=self.side_effect_raise_value_error):
with self.assertRaises(ValueError):
@ -153,9 +142,7 @@ class TestEnvironmentVariablesEffects(TestCase):
fake_domain, _ = Domain.objects.get_or_create(name="igorville.gov")
# Add a role
fake_role, _ = UserDomainRole.objects.get_or_create(
user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER
)
UserDomainRole.objects.get_or_create(user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER)
with patch.object(DomainNameserversView, "get_initial", side_effect=self.side_effect_raise_value_error):
with self.assertRaises(ValueError):
@ -176,15 +163,13 @@ class HomeTests(TestWithUser):
super().setUp()
self.client.force_login(self.user)
def tearDown(self):
super().tearDown()
Contact.objects.all().delete()
@less_console_noise_decorator
def test_empty_domain_table(self):
response = self.client.get("/")
self.assertContains(response, "You don't have any registered domains.")
self.assertContains(response, "Why don't I see my domain when I sign in to the registrar?")
@less_console_noise_decorator
def test_state_help_text(self):
"""Tests if each domain state has help text"""
@ -226,6 +211,7 @@ class HomeTests(TestWithUser):
user_role.delete()
test_domain.delete()
@less_console_noise_decorator
def test_state_help_text_expired(self):
"""Tests if each domain state has help text when expired"""
expired_text = "This domain has expired, but it is still online. "
@ -233,7 +219,9 @@ class HomeTests(TestWithUser):
test_domain.expiration_date = date(2011, 10, 10)
test_domain.save()
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER)
test_role, _ = UserDomainRole.objects.get_or_create(
user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER
)
# Grab the json response of the domains list
response = self.client.get("/get-domains-json/")
@ -244,6 +232,10 @@ class HomeTests(TestWithUser):
# Check that we have the right text content.
self.assertContains(response, expired_text, count=1)
test_role.delete()
test_domain.delete()
@less_console_noise_decorator
def test_state_help_text_no_expiration_date(self):
"""Tests if each domain state has help text when expiration date is None"""
@ -287,6 +279,10 @@ class HomeTests(TestWithUser):
# Check that we have the right text content.
self.assertContains(response, unknown_text, count=1)
UserDomainRole.objects.all().delete()
Domain.objects.all().delete()
@less_console_noise_decorator
def test_home_deletes_withdrawn_domain_request(self):
"""Tests if the user can delete a DomainRequest in the 'withdrawn' status"""
@ -303,6 +299,7 @@ class HomeTests(TestWithUser):
# clean up
domain_request.delete()
@less_console_noise_decorator
def test_home_deletes_started_domain_request(self):
"""Tests if the user can delete a DomainRequest in the 'started' status"""
@ -352,6 +349,7 @@ class HomeTests(TestWithUser):
# clean up
domain_request.delete()
@less_console_noise_decorator
def test_home_deletes_domain_request_and_orphans(self):
"""Tests if delete for DomainRequest deletes orphaned Contact objects"""
@ -421,6 +419,10 @@ class HomeTests(TestWithUser):
self.assertEqual(edge_case, contact_2)
DomainRequest.objects.all().delete()
Contact.objects.all().delete()
@less_console_noise_decorator
def test_home_deletes_domain_request_and_shared_orphans(self):
"""Test the edge case for an object that will become orphaned after a delete
(but is not an orphan at the time of deletion)"""
@ -481,6 +483,10 @@ class HomeTests(TestWithUser):
orphan = Contact.objects.filter(id=contact_shared.id)
self.assertFalse(orphan.exists())
DomainRequest.objects.all().delete()
Contact.objects.all().delete()
@less_console_noise_decorator
def test_domain_request_form_view(self):
response = self.client.get("/request/", follow=True)
self.assertContains(
@ -488,16 +494,24 @@ class HomeTests(TestWithUser):
"Youre about to start your .gov domain request.",
)
@less_console_noise_decorator
def test_domain_request_form_with_ineligible_user(self):
"""Domain request form not accessible for an ineligible user.
This test should be solid enough since all domain request wizard
views share the same permissions class"""
self.user.status = User.RESTRICTED
self.user.save()
with less_console_noise():
username = "restricted_user"
first_name = "First"
last_name = "Last"
email = "restricted@example.com"
phone = "8003111234"
status = User.RESTRICTED
restricted_user = get_user_model().objects.create(
username=username, first_name=first_name, last_name=last_name, email=email, phone=phone, status=status
)
self.client.force_login(restricted_user)
response = self.client.get("/request/", follow=True)
self.assertEqual(response.status_code, 403)
restricted_user.delete()
class FinishUserProfileTests(TestWithUser, WebTest):
@ -509,6 +523,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
def setUp(self):
super().setUp()
self.initial_user_title = self.user.title
self.user.title = None
self.user.save()
self.client.force_login(self.user)
@ -519,6 +534,10 @@ class FinishUserProfileTests(TestWithUser, WebTest):
def tearDown(self):
super().tearDown()
DomainRequest.objects.all().delete()
DomainInformation.objects.all().delete()
self.user.title = self.initial_user_title
self.user.save()
PublicContact.objects.filter(domain=self.domain).delete()
self.role.delete()
self.domain.delete()
@ -538,10 +557,75 @@ class FinishUserProfileTests(TestWithUser, WebTest):
self._set_session_cookie()
return page.follow() if follow else page
@less_console_noise_decorator
@override_flag("profile_feature", active=True)
def test_full_name_initial_value(self):
"""Test that full_name initial value is empty when first_name or last_name is empty.
This will later be displayed as "unknown" using javascript."""
username_regular_incomplete = "test_regular_user_incomplete"
first_name_2 = "Incomplete"
email_2 = "unicorn@igorville.com"
incomplete_regular_user = get_user_model().objects.create(
username=username_regular_incomplete,
first_name=first_name_2,
email=email_2,
verification_type=User.VerificationTypeChoices.REGULAR,
)
self.app.set_user(incomplete_regular_user.username)
# Test when first_name is empty
incomplete_regular_user.first_name = ""
incomplete_regular_user.last_name = "Doe"
incomplete_regular_user.save()
finish_setup_page = self.app.get(reverse("home")).follow()
form = finish_setup_page.form
self.assertEqual(form["full_name"].value, "")
# Test when last_name is empty
incomplete_regular_user.first_name = "John"
incomplete_regular_user.last_name = ""
incomplete_regular_user.save()
finish_setup_page = self.app.get(reverse("home")).follow()
form = finish_setup_page.form
self.assertEqual(form["full_name"].value, "")
# Test when both first_name and last_name are empty
incomplete_regular_user.first_name = ""
incomplete_regular_user.last_name = ""
incomplete_regular_user.save()
finish_setup_page = self.app.get(reverse("home")).follow()
form = finish_setup_page.form
self.assertEqual(form["full_name"].value, "")
# Test when both first_name and last_name are present
incomplete_regular_user.first_name = "John"
incomplete_regular_user.last_name = "Doe"
incomplete_regular_user.save()
finish_setup_page = self.app.get(reverse("home")).follow()
form = finish_setup_page.form
self.assertEqual(form["full_name"].value, "John Doe")
incomplete_regular_user.delete()
@less_console_noise_decorator
def test_new_user_with_profile_feature_on(self):
"""Tests that a new user is redirected to the profile setup page when profile_feature is on"""
self.app.set_user(self.incomplete_regular_user.username)
username_regular_incomplete = "test_regular_user_incomplete"
first_name_2 = "Incomplete"
email_2 = "unicorn@igorville.com"
# in the case below, REGULAR user is 'Verified by Login.gov, ie. IAL2
incomplete_regular_user = get_user_model().objects.create(
username=username_regular_incomplete,
first_name=first_name_2,
email=email_2,
verification_type=User.VerificationTypeChoices.REGULAR,
)
self.app.set_user(incomplete_regular_user.username)
with override_flag("profile_feature", active=True):
# This will redirect the user to the setup page.
# Follow implicity checks if our redirect is working.
@ -575,12 +659,73 @@ class FinishUserProfileTests(TestWithUser, WebTest):
# This is the same as clicking the back button.
completed_setup_page = self.app.get(reverse("home"))
self.assertContains(completed_setup_page, "Manage your domain")
incomplete_regular_user.delete()
@less_console_noise_decorator
def test_new_user_with_empty_name_can_add_name(self):
"""Tests that a new user without a name can still enter this information accordingly"""
username_regular_incomplete = "test_regular_user_incomplete"
email = "unicorn@igorville.com"
# in the case below, REGULAR user is 'Verified by Login.gov, ie. IAL2
incomplete_regular_user = get_user_model().objects.create(
username=username_regular_incomplete,
first_name="",
last_name="",
email=email,
verification_type=User.VerificationTypeChoices.REGULAR,
)
self.app.set_user(incomplete_regular_user.username)
with override_flag("profile_feature", active=True):
# This will redirect the user to the setup page.
# Follow implicity checks if our redirect is working.
finish_setup_page = self.app.get(reverse("home")).follow()
self._set_session_cookie()
# Assert that we're on the right page
self.assertContains(finish_setup_page, "Finish setting up your profile")
finish_setup_page = self._submit_form_webtest(finish_setup_page.form)
self.assertEqual(finish_setup_page.status_code, 200)
# We're missing a phone number, so the page should tell us that
self.assertContains(finish_setup_page, "Enter your phone number.")
# Check for the name of the save button
self.assertContains(finish_setup_page, "user_setup_save_button")
# Add a phone number
finish_setup_form = finish_setup_page.form
finish_setup_form["first_name"] = "test"
finish_setup_form["last_name"] = "test2"
finish_setup_form["phone"] = "(201) 555-0123"
finish_setup_form["title"] = "CEO"
finish_setup_form["last_name"] = "example"
save_page = self._submit_form_webtest(finish_setup_form, follow=True)
self.assertEqual(save_page.status_code, 200)
self.assertContains(save_page, "Your profile has been updated.")
# Try to navigate back to the home page.
# This is the same as clicking the back button.
completed_setup_page = self.app.get(reverse("home"))
self.assertContains(completed_setup_page, "Manage your domain")
incomplete_regular_user.delete()
@less_console_noise_decorator
def test_new_user_goes_to_domain_request_with_profile_feature_on(self):
"""Tests that a new user is redirected to the domain request page when profile_feature is on"""
self.app.set_user(self.incomplete_regular_user.username)
username_regular_incomplete = "test_regular_user_incomplete"
first_name_2 = "Incomplete"
email_2 = "unicorn@igorville.com"
# in the case below, REGULAR user is 'Verified by Login.gov, ie. IAL2
incomplete_regular_user = get_user_model().objects.create(
username=username_regular_incomplete,
first_name=first_name_2,
email=email_2,
verification_type=User.VerificationTypeChoices.REGULAR,
)
self.app.set_user(incomplete_regular_user.username)
with override_flag("profile_feature", active=True):
# This will redirect the user to the setup page
finish_setup_page = self.app.get(reverse("domain-request:")).follow()
@ -623,6 +768,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
self.assertNotContains(completed_setup_page, "What contact information should we use to reach you?")
self.assertContains(completed_setup_page, "Youre about to start your .gov domain request")
incomplete_regular_user.delete()
@less_console_noise_decorator
def test_new_user_with_profile_feature_off(self):
@ -653,6 +799,7 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
def setUp(self):
super().setUp()
self.initial_user_title = self.user.title
self.user.title = None
self.user.save()
self.client.force_login(self.user)
@ -663,6 +810,8 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
def tearDown(self):
super().tearDown()
self.user.title = self.initial_user_title
self.user.save()
PublicContact.objects.filter(domain=self.domain).delete()
self.role.delete()
Domain.objects.all().delete()
@ -682,7 +831,18 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
def test_new_user_with_profile_feature_on(self):
"""Tests that a new user is redirected to the profile setup page when profile_feature is on,
and testing that the confirmation modal is present"""
self.app.set_user(self.incomplete_other_user.username)
username_other_incomplete = "test_other_user_incomplete"
first_name_2 = "Incomplete"
email_2 = "unicorn@igorville.com"
# in the case below, other user is representative of GRANDFATHERED,
# VERIFIED_BY_STAFF, INVITED, FIXTURE_USER, ie. IAL1
incomplete_other_user = get_user_model().objects.create(
username=username_other_incomplete,
first_name=first_name_2,
email=email_2,
verification_type=User.VerificationTypeChoices.VERIFIED_BY_STAFF,
)
self.app.set_user(incomplete_other_user.username)
with override_flag("profile_feature", active=True):
# This will redirect the user to the user profile page.
# Follow implicity checks if our redirect is working.
@ -761,9 +921,10 @@ class UserProfileTests(TestWithUser, WebTest):
PublicContact.objects.filter(domain=self.domain).delete()
self.role.delete()
self.domain.delete()
Contact.objects.all().delete()
DraftDomain.objects.all().delete()
DomainRequest.objects.all().delete()
DraftDomain.objects.all().delete()
Contact.objects.all().delete()
DomainInformation.objects.all().delete()
@less_console_noise_decorator
def error_500_main_nav_with_profile_feature_turned_on(self):
@ -937,16 +1098,19 @@ class PortfoliosTests(TestWithUser, WebTest):
def setUp(self):
super().setUp()
self.user.save()
self.client.force_login(self.user)
self.domain, _ = Domain.objects.get_or_create(name="sampledomain.gov", state=Domain.State.READY)
self.role, _ = UserDomainRole.objects.get_or_create(
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
)
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="xyz inc")
self.federal_agency = FederalAgency.objects.create()
self.portfolio, _ = Portfolio.objects.get_or_create(
creator=self.user, organization_name="xyz inc", federal_agency=self.federal_agency
)
def tearDown(self):
Portfolio.objects.all().delete()
self.federal_agency.delete()
super().tearDown()
PublicContact.objects.filter(domain=self.domain).delete()
UserDomainRole.objects.all().delete()
@ -958,23 +1122,6 @@ class PortfoliosTests(TestWithUser, WebTest):
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
@less_console_noise_decorator
def test_middleware_redirects_to_portfolio_homepage(self):
"""Tests that a user is redirected to the portfolio homepage when organization_feature is on and
a portfolio belongs to the user, test for the special h1s which only exist in that version
of the homepage"""
self.app.set_user(self.user.username)
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
portfolio_page = self.app.get(reverse("home")).follow()
self._set_session_cookie()
# Assert that we're on the right page
self.assertContains(portfolio_page, self.portfolio.organization_name)
self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
@less_console_noise_decorator
def test_no_redirect_when_org_flag_false(self):
"""No redirect so no follow,

View file

@ -5,6 +5,7 @@ from django.conf import settings
from django.urls import reverse
from django.contrib.auth import get_user_model
from api.tests.common import less_console_noise_decorator
from .common import MockEppLib, MockSESClient, create_user # type: ignore
from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore
@ -45,6 +46,7 @@ logger = logging.getLogger(__name__)
class TestWithDomainPermissions(TestWithUser):
@less_console_noise_decorator
def setUp(self):
super().setUp()
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
@ -142,6 +144,7 @@ class TestWithDomainPermissions(TestWithUser):
class TestDomainPermissions(TestWithDomainPermissions):
@less_console_noise_decorator
def test_not_logged_in(self):
"""Not logged in gets a redirect to Login."""
for view_name in [
@ -158,6 +161,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 302)
@less_console_noise_decorator
def test_no_domain_role(self):
"""Logged in but no role gets 403 Forbidden."""
self.client.force_login(self.user)
@ -174,10 +178,10 @@ class TestDomainPermissions(TestWithDomainPermissions):
"domain-security-email",
]:
with self.subTest(view_name=view_name):
with less_console_noise():
response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
def test_domain_pages_blocked_for_on_hold_and_deleted(self):
"""Test that the domain pages are blocked for on hold and deleted domains"""
@ -199,12 +203,12 @@ class TestDomainPermissions(TestWithDomainPermissions):
self.domain_deleted,
]:
with self.subTest(view_name=view_name, domain=domain):
with less_console_noise():
response = self.client.get(reverse(view_name, kwargs={"pk": domain.id}))
self.assertEqual(response.status_code, 403)
class TestDomainOverview(TestWithDomainPermissions, WebTest):
def setUp(self):
super().setUp()
self.app.set_user(self.user.username)
@ -312,21 +316,25 @@ class TestDomainManagers(TestDomainOverview):
"""Ensure that the user has its original permissions"""
super().tearDown()
@less_console_noise_decorator
def test_domain_managers(self):
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
self.assertContains(response, "Domain managers")
@less_console_noise_decorator
def test_domain_managers_add_link(self):
"""Button to get to user add page works."""
management_page = self.app.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
add_page = management_page.click("Add a domain manager")
self.assertContains(add_page, "Add a domain manager")
@less_console_noise_decorator
def test_domain_user_add(self):
response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
self.assertContains(response, "Add a domain manager")
@boto3_mocking.patching
@less_console_noise_decorator
def test_domain_user_add_form(self):
"""Adding an existing user works."""
other_user, _ = get_user_model().objects.get_or_create(email="mayor@igorville.gov")
@ -353,6 +361,7 @@ class TestDomainManagers(TestDomainOverview):
self.assertContains(success_page, "mayor@igorville.gov")
@boto3_mocking.patching
@less_console_noise_decorator
def test_domain_invitation_created(self):
"""Add user on a nonexistent email creates an invitation.
@ -383,6 +392,7 @@ class TestDomainManagers(TestDomainOverview):
self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists())
@boto3_mocking.patching
@less_console_noise_decorator
def test_domain_invitation_created_for_caps_email(self):
"""Add user on a nonexistent email with CAPS creates an invitation to lowercase email.
@ -403,7 +413,6 @@ class TestDomainManagers(TestDomainOverview):
mock_client = MockSESClient()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with less_console_noise():
success_result = add_page.form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
@ -414,6 +423,7 @@ class TestDomainManagers(TestDomainOverview):
self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists())
@boto3_mocking.patching
@less_console_noise_decorator
def test_domain_invitation_email_sent(self):
"""Inviting a non-existent user sends them an email."""
# make sure there is no user with this email
@ -425,7 +435,6 @@ class TestDomainManagers(TestDomainOverview):
mock_client = MagicMock()
mock_client_instance = mock_client.return_value
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with less_console_noise():
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
@ -440,6 +449,7 @@ class TestDomainManagers(TestDomainOverview):
)
@boto3_mocking.patching
@less_console_noise_decorator
def test_domain_invitation_email_has_email_as_requestor_non_existent(self):
"""Inviting a non existent user sends them an email, with email as the name."""
# make sure there is no user with this email
@ -452,7 +462,6 @@ class TestDomainManagers(TestDomainOverview):
mock_client_instance = mock_client.return_value
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with less_console_noise():
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
@ -479,6 +488,7 @@ class TestDomainManagers(TestDomainOverview):
self.assertNotIn("First Last", email_content)
@boto3_mocking.patching
@less_console_noise_decorator
def test_domain_invitation_email_has_email_as_requestor(self):
"""Inviting a user sends them an email, with email as the name."""
# Create a fake user object
@ -491,7 +501,6 @@ class TestDomainManagers(TestDomainOverview):
mock_client_instance = mock_client.return_value
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with less_console_noise():
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
@ -518,6 +527,7 @@ class TestDomainManagers(TestDomainOverview):
self.assertNotIn("First Last", email_content)
@boto3_mocking.patching
@less_console_noise_decorator
def test_domain_invitation_email_has_email_as_requestor_staff(self):
"""Inviting a user sends them an email, with email as the name."""
# Create a fake user object
@ -534,7 +544,6 @@ class TestDomainManagers(TestDomainOverview):
mock_client_instance = mock_client.return_value
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with less_console_noise():
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
@ -561,6 +570,7 @@ class TestDomainManagers(TestDomainOverview):
self.assertNotIn("First Last", email_content)
@boto3_mocking.patching
@less_console_noise_decorator
def test_domain_invitation_email_displays_error_non_existent(self):
"""Inviting a non existent user sends them an email, with email as the name."""
# make sure there is no user with this email
@ -577,7 +587,6 @@ class TestDomainManagers(TestDomainOverview):
mock_error_message = MagicMock()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with patch("django.contrib.messages.error") as mock_error_message:
with less_console_noise():
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
@ -593,6 +602,7 @@ class TestDomainManagers(TestDomainOverview):
self.assertEqual(expected_message_content, returned_error_message)
@boto3_mocking.patching
@less_console_noise_decorator
def test_domain_invitation_email_displays_error(self):
"""When the requesting user has no email, an error is displayed"""
# make sure there is no user with this email
@ -611,7 +621,6 @@ class TestDomainManagers(TestDomainOverview):
mock_error_message = MagicMock()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with patch("django.contrib.messages.error") as mock_error_message:
with less_console_noise():
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = email_address
@ -626,25 +635,25 @@ class TestDomainManagers(TestDomainOverview):
# Check that the message content is what we expect
self.assertEqual(expected_message_content, returned_error_message)
@less_console_noise_decorator
def test_domain_invitation_cancel(self):
"""Posting to the delete view deletes an invitation."""
email_address = "mayor@igorville.gov"
invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address)
mock_client = MockSESClient()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with less_console_noise():
self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id}))
mock_client.EMAILS_SENT.clear()
with self.assertRaises(DomainInvitation.DoesNotExist):
DomainInvitation.objects.get(id=invitation.id)
@less_console_noise_decorator
def test_domain_invitation_cancel_retrieved_invitation(self):
"""Posting to the delete view when invitation retrieved returns an error message"""
email_address = "mayor@igorville.gov"
invitation, _ = DomainInvitation.objects.get_or_create(
domain=self.domain, email=email_address, status=DomainInvitation.DomainInvitationStatus.RETRIEVED
)
with less_console_noise():
response = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id}), follow=True)
# Assert that an error message is displayed to the user
self.assertContains(response, f"Invitation to {email_address} has already been retrieved.")
@ -654,6 +663,7 @@ class TestDomainManagers(TestDomainOverview):
self.assertTrue(DomainInvitation.objects.filter(id=invitation.id).exists())
DomainInvitation.objects.filter(email=email_address).delete()
@less_console_noise_decorator
def test_domain_invitation_cancel_no_permissions(self):
"""Posting to the delete view as a different user should fail."""
email_address = "mayor@igorville.gov"
@ -664,12 +674,12 @@ class TestDomainManagers(TestDomainOverview):
self.client.force_login(other_user)
mock_client = MagicMock()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with less_console_noise(): # permission denied makes console errors
result = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id}))
self.assertEqual(result.status_code, 403)
@boto3_mocking.patching
@less_console_noise_decorator
def test_domain_invitation_flow(self):
"""Send an invitation to a new user, log in and load the dashboard."""
email_address = "mayor@igorville.gov"
@ -685,7 +695,6 @@ class TestDomainManagers(TestDomainOverview):
mock_client = MagicMock()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with less_console_noise():
add_page.form.submit()
# user was invited, create them
@ -701,11 +710,13 @@ class TestDomainManagers(TestDomainOverview):
class TestDomainNameservers(TestDomainOverview, MockEppLib):
@less_console_noise_decorator
def test_domain_nameservers(self):
"""Can load domain's nameservers page."""
page = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
self.assertContains(page, "DNS name servers")
@less_console_noise_decorator
def test_domain_nameservers_form_submit_one_nameserver(self):
"""Nameserver form submitted with one nameserver throws error.
@ -717,7 +728,6 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# attempt to submit the form with only one nameserver, should error
# regarding required fields
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around
@ -729,6 +739,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
status_code=200,
)
@less_console_noise_decorator
def test_domain_nameservers_form_submit_subdomain_missing_ip(self):
"""Nameserver form catches missing ip error on subdomain.
@ -742,7 +753,6 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
# only one has ips
nameservers_page.form["form-1-server"] = "ns2.igorville.gov"
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around
@ -754,6 +764,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
status_code=200,
)
@less_console_noise_decorator
def test_domain_nameservers_form_submit_missing_host(self):
"""Nameserver form catches error when host is missing.
@ -766,7 +777,6 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
# attempt to submit the form without two hosts, both subdomains,
# only one has ips
nameservers_page.form["form-1-ip"] = "127.0.0.1"
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around
@ -778,6 +788,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
status_code=200,
)
@less_console_noise_decorator
def test_domain_nameservers_form_submit_duplicate_host(self):
"""Nameserver form catches error when host is duplicated.
@ -790,7 +801,6 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
# attempt to submit the form with duplicate host names of fake.host.com
nameservers_page.form["form-0-ip"] = ""
nameservers_page.form["form-1-server"] = "fake.host.com"
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around
@ -802,6 +812,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
status_code=200,
)
@less_console_noise_decorator
def test_domain_nameservers_form_submit_whitespace(self):
"""Nameserver form removes whitespace from ip.
@ -820,7 +831,6 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
nameservers_page.form["form-0-ip"] = valid_ip
nameservers_page.form["form-1-ip"] = valid_ip_2
nameservers_page.form["form-1-server"] = nameserver2
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a post with an ip address which has been stripped of whitespace,
# response should be a 302 to success page
@ -835,6 +845,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
# with an error message displayed, so need to follow 302 and test for success message
self.assertContains(page, "The name servers for this domain have been updated")
@less_console_noise_decorator
def test_domain_nameservers_form_submit_glue_record_not_allowed(self):
"""Nameserver form catches error when IP is present
but host not subdomain.
@ -853,7 +864,6 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
nameservers_page.form["form-0-server"] = nameserver1
nameservers_page.form["form-1-server"] = nameserver2
nameservers_page.form["form-1-ip"] = valid_ip
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around
@ -865,6 +875,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
status_code=200,
)
@less_console_noise_decorator
def test_domain_nameservers_form_submit_invalid_ip(self):
"""Nameserver form catches invalid IP on submission.
@ -880,7 +891,6 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
# only one has ips
nameservers_page.form["form-1-server"] = nameserver
nameservers_page.form["form-1-ip"] = invalid_ip
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around
@ -892,6 +902,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
status_code=200,
)
@less_console_noise_decorator
def test_domain_nameservers_form_submit_invalid_host(self):
"""Nameserver form catches invalid host on submission.
@ -907,7 +918,6 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
# only one has ips
nameservers_page.form["form-1-server"] = nameserver
nameservers_page.form["form-1-ip"] = valid_ip
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around
@ -919,6 +929,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
status_code=200,
)
@less_console_noise_decorator
def test_domain_nameservers_form_submits_successfully(self):
"""Nameserver form submits successfully with valid input.
@ -935,7 +946,6 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
nameservers_page.form["form-0-ip"] = valid_ip
nameservers_page.form["form-1-server"] = nameserver2
nameservers_page.form["form-1-ip"] = valid_ip_2
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a successful post, response should be a 302
self.assertEqual(result.status_code, 302)
@ -947,6 +957,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
page = result.follow()
self.assertContains(page, "The name servers for this domain have been updated")
@less_console_noise_decorator
def test_domain_nameservers_can_blank_out_first_or_second_one_if_enough_entries(self):
"""Nameserver form submits successfully with 2 valid inputs, even if the first or
second entries are blanked out.
@ -969,7 +980,6 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
nameservers_page.form["form-1-ip"] = valid_ip_2
nameservers_page.form["form-2-server"] = nameserver3
nameservers_page.form["form-2-ip"] = valid_ip_3
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a successful post, response should be a 302
@ -996,7 +1006,6 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
nameservers_page.form["form-1-ip"] = valid_ip_2
nameservers_page.form["form-2-server"] = nameserver3
nameservers_page.form["form-2-ip"] = valid_ip_3
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a successful post, response should be a 302
@ -1009,6 +1018,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
nameservers_page = result.follow()
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
@less_console_noise_decorator
def test_domain_nameservers_can_blank_out_first_and_second_one_if_enough_entries(self):
"""Nameserver form submits successfully with 2 valid inputs, even if the first and
second entries are blanked out.
@ -1045,7 +1055,6 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
nameservers_page.form["form-2-ip"] = valid_ip_3
nameservers_page.form["form-3-server"] = nameserver4
nameservers_page.form["form-3-ip"] = valid_ip_4
with less_console_noise(): # swallow log warning message
result = nameservers_page.form.submit()
# form submission was a successful post, response should be a 302
@ -1058,6 +1067,7 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
nameservers_page = result.follow()
self.assertContains(nameservers_page, "The name servers for this domain have been updated")
@less_console_noise_decorator
def test_domain_nameservers_form_invalid(self):
"""Nameserver form does not submit with invalid data.
@ -1069,7 +1079,6 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
# first two nameservers are required, so if we empty one out we should
# get a form error
nameservers_page.form["form-0-server"] = ""
with less_console_noise(): # swallow logged warning message
result = nameservers_page.form.submit()
# form submission was a post with an error, response should be a 200
# error text appears four times, twice at the top of the page,
@ -1083,11 +1092,13 @@ class TestDomainNameservers(TestDomainOverview, MockEppLib):
class TestDomainSeniorOfficial(TestDomainOverview):
@less_console_noise_decorator
def test_domain_senior_official(self):
"""Can load domain's senior official page."""
page = self.client.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
self.assertContains(page, "Senior official", count=13)
@less_console_noise_decorator
def test_domain_senior_official_content(self):
"""Senior official information appears on the page."""
self.domain_information.senior_official = Contact(first_name="Testy")
@ -1096,6 +1107,7 @@ class TestDomainSeniorOfficial(TestDomainOverview):
page = self.app.get(reverse("domain-senior-official", kwargs={"pk": self.domain.id}))
self.assertContains(page, "Testy")
@less_console_noise_decorator
def test_domain_edit_senior_official_in_place(self):
"""When editing a senior official for domain information and SO is not
joined to any other objects"""
@ -1120,6 +1132,7 @@ class TestDomainSeniorOfficial(TestDomainOverview):
self.assertEqual("Testy2", self.domain_information.senior_official.first_name)
self.assertEqual(so_pk, self.domain_information.senior_official.id)
@less_console_noise_decorator
def assert_all_form_fields_have_expected_values(self, form, test_cases, test_for_disabled=False):
"""
Asserts that each specified form field has the expected value and, optionally, checks if the field is disabled.
@ -1146,6 +1159,7 @@ class TestDomainSeniorOfficial(TestDomainOverview):
# Test for disabled on each field
self.assertTrue("disabled" in form[field_name].attrs)
@less_console_noise_decorator
def test_domain_edit_senior_official_federal(self):
"""Tests that no edit can occur when the underlying domain is federal"""
@ -1202,6 +1216,7 @@ class TestDomainSeniorOfficial(TestDomainOverview):
self.assertEqual("CIO", self.domain_information.senior_official.title)
self.assertEqual("nobody@igorville.gov", self.domain_information.senior_official.email)
@less_console_noise_decorator
def test_domain_edit_senior_official_tribal(self):
"""Tests that no edit can occur when the underlying domain is tribal"""
@ -1258,6 +1273,7 @@ class TestDomainSeniorOfficial(TestDomainOverview):
self.assertEqual("CIO", self.domain_information.senior_official.title)
self.assertEqual("nobody@igorville.gov", self.domain_information.senior_official.email)
@less_console_noise_decorator
def test_domain_edit_senior_official_creates_new(self):
"""When editing a senior official for domain information and SO IS
joined to another object"""
@ -1295,12 +1311,14 @@ class TestDomainSeniorOfficial(TestDomainOverview):
class TestDomainOrganization(TestDomainOverview):
@less_console_noise_decorator
def test_domain_org_name_address(self):
"""Can load domain's org name and mailing address page."""
page = self.client.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
# once on the sidebar, once in the page title, once as H1
self.assertContains(page, "Organization name and mailing address", count=3)
@less_console_noise_decorator
def test_domain_org_name_address_content(self):
"""Org name and address information appears on the page."""
self.domain_information.organization_name = "Town of Igorville"
@ -1308,6 +1326,7 @@ class TestDomainOrganization(TestDomainOverview):
page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
self.assertContains(page, "Town of Igorville")
@less_console_noise_decorator
def test_domain_org_name_address_form(self):
"""Submitting changes works on the org name address page."""
self.domain_information.organization_name = "Town of Igorville"
@ -1325,6 +1344,7 @@ class TestDomainOrganization(TestDomainOverview):
self.assertContains(success_result_page, "Not igorville")
self.assertContains(success_result_page, "Faketown")
@less_console_noise_decorator
def test_domain_org_name_address_form_tribal(self):
"""
Submitting a change to organization_name is blocked for tribal domains
@ -1382,6 +1402,7 @@ class TestDomainOrganization(TestDomainOverview):
# Check for the value we want to update
self.assertContains(success_result_page, "Faketown")
@less_console_noise_decorator
def test_domain_org_name_address_form_federal(self):
"""
Submitting a change to federal_agency is blocked for federal domains
@ -1437,6 +1458,7 @@ class TestDomainOrganization(TestDomainOverview):
# Check for the value we want to update
self.assertContains(success_result_page, "Faketown")
@less_console_noise_decorator
def test_federal_agency_submit_blocked(self):
"""
Submitting a change to federal_agency is blocked for federal domains
@ -1470,11 +1492,13 @@ class TestDomainOrganization(TestDomainOverview):
class TestDomainContactInformation(TestDomainOverview):
@less_console_noise_decorator
def test_domain_your_contact_information(self):
"""Can load domain's your contact information page."""
page = self.client.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id}))
self.assertContains(page, "Your contact information")
@less_console_noise_decorator
def test_domain_your_contact_information_content(self):
"""Logged-in user's contact information appears on the page."""
self.user.first_name = "Testy"
@ -1602,13 +1626,13 @@ class TestDomainSecurityEmail(TestDomainOverview):
self.assertEqual(message.tags, message_tag)
self.assertEqual(message.message.strip(), expected_message.strip())
@less_console_noise_decorator
def test_domain_overview_blocked_for_ineligible_user(self):
"""We could easily duplicate this test for all domain management
views, but a single url test should be solid enough since all domain
management pages share the same permissions class"""
self.user.status = User.RESTRICTED
self.user.save()
with less_console_noise():
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 403)
@ -1616,6 +1640,7 @@ class TestDomainSecurityEmail(TestDomainOverview):
class TestDomainDNSSEC(TestDomainOverview):
"""MockEPPLib is already inherited."""
@less_console_noise_decorator
def test_dnssec_page_refreshes_enable_button(self):
"""DNSSEC overview page loads when domain has no DNSSEC data
and shows a 'Enable DNSSEC' button."""
@ -1623,6 +1648,7 @@ class TestDomainDNSSEC(TestDomainOverview):
page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}))
self.assertContains(page, "Enable DNSSEC")
@less_console_noise_decorator
def test_dnssec_page_loads_with_data_in_domain(self):
"""DNSSEC overview page loads when domain has DNSSEC data
and the template contains a button to disable DNSSEC."""
@ -1644,6 +1670,7 @@ class TestDomainDNSSEC(TestDomainOverview):
self.assertContains(updated_page, "Enable DNSSEC")
@less_console_noise_decorator
def test_ds_form_loads_with_no_domain_data(self):
"""DNSSEC Add DS data page loads when there is no
domain DNSSEC data and shows a button to Add new record"""
@ -1652,6 +1679,7 @@ class TestDomainDNSSEC(TestDomainOverview):
self.assertContains(page, "You have no DS data added")
self.assertContains(page, "Add new record")
@less_console_noise_decorator
def test_ds_form_loads_with_ds_data(self):
"""DNSSEC Add DS data page loads when there is
domain DNSSEC DS data and shows the data"""
@ -1659,6 +1687,7 @@ class TestDomainDNSSEC(TestDomainOverview):
page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}))
self.assertContains(page, "DS data record 1")
@less_console_noise_decorator
def test_ds_data_form_modal(self):
"""When user clicks on save, a modal pops up."""
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}))
@ -1677,6 +1706,7 @@ class TestDomainDNSSEC(TestDomainOverview):
# Now check to see whether the JS trigger for the modal is present on the page
self.assertContains(response, "Trigger Disable DNSSEC Modal")
@less_console_noise_decorator
def test_ds_data_form_submits(self):
"""DS data form submits successfully
@ -1685,7 +1715,6 @@ class TestDomainDNSSEC(TestDomainOverview):
add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
with less_console_noise(): # swallow log warning message
result = add_data_page.forms[0].submit()
# form submission was a post, response should be a redirect
self.assertEqual(result.status_code, 302)
@ -1697,6 +1726,7 @@ class TestDomainDNSSEC(TestDomainOverview):
page = result.follow()
self.assertContains(page, "The DS data records for this domain have been updated.")
@less_console_noise_decorator
def test_ds_data_form_invalid(self):
"""DS data form errors with invalid data (missing required fields)
@ -1710,7 +1740,6 @@ class TestDomainDNSSEC(TestDomainOverview):
add_data_page.forms[0]["form-0-algorithm"] = ""
add_data_page.forms[0]["form-0-digest_type"] = ""
add_data_page.forms[0]["form-0-digest"] = ""
with less_console_noise(): # swallow logged warning message
result = add_data_page.forms[0].submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around
@ -1720,6 +1749,7 @@ class TestDomainDNSSEC(TestDomainOverview):
self.assertContains(result, "Digest type is required", count=2, status_code=200)
self.assertContains(result, "Digest is required", count=2, status_code=200)
@less_console_noise_decorator
def test_ds_data_form_invalid_keytag(self):
"""DS data form errors with invalid data (key tag too large)
@ -1734,7 +1764,6 @@ class TestDomainDNSSEC(TestDomainOverview):
add_data_page.forms[0]["form-0-algorithm"] = ""
add_data_page.forms[0]["form-0-digest_type"] = ""
add_data_page.forms[0]["form-0-digest"] = ""
with less_console_noise(): # swallow logged warning message
result = add_data_page.forms[0].submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around
@ -1743,6 +1772,7 @@ class TestDomainDNSSEC(TestDomainOverview):
result, str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE)), count=2, status_code=200
)
@less_console_noise_decorator
def test_ds_data_form_invalid_digest_chars(self):
"""DS data form errors with invalid data (digest contains non hexadecimal chars)
@ -1757,7 +1787,6 @@ class TestDomainDNSSEC(TestDomainOverview):
add_data_page.forms[0]["form-0-algorithm"] = "3"
add_data_page.forms[0]["form-0-digest_type"] = "1"
add_data_page.forms[0]["form-0-digest"] = "GG1234"
with less_console_noise(): # swallow logged warning message
result = add_data_page.forms[0].submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around
@ -1766,6 +1795,7 @@ class TestDomainDNSSEC(TestDomainOverview):
result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_CHARS)), count=2, status_code=200
)
@less_console_noise_decorator
def test_ds_data_form_invalid_digest_sha1(self):
"""DS data form errors with invalid data (digest is invalid sha-1)
@ -1780,7 +1810,6 @@ class TestDomainDNSSEC(TestDomainOverview):
add_data_page.forms[0]["form-0-algorithm"] = "3"
add_data_page.forms[0]["form-0-digest_type"] = "1" # SHA-1
add_data_page.forms[0]["form-0-digest"] = "A123"
with less_console_noise(): # swallow logged warning message
result = add_data_page.forms[0].submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around
@ -1789,6 +1818,7 @@ class TestDomainDNSSEC(TestDomainOverview):
result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA1)), count=2, status_code=200
)
@less_console_noise_decorator
def test_ds_data_form_invalid_digest_sha256(self):
"""DS data form errors with invalid data (digest is invalid sha-256)
@ -1803,7 +1833,6 @@ class TestDomainDNSSEC(TestDomainOverview):
add_data_page.forms[0]["form-0-algorithm"] = "3"
add_data_page.forms[0]["form-0-digest_type"] = "2" # SHA-256
add_data_page.forms[0]["form-0-digest"] = "GG1234"
with less_console_noise(): # swallow logged warning message
result = add_data_page.forms[0].submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around

View file

@ -24,7 +24,6 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
def tearDown(self):
super().tearDown()
UserDomainRole.objects.all().delete()
UserDomainRole.objects.all().delete()
@less_console_noise_decorator
def test_get_domains_json_unauthenticated(self):

View file

@ -0,0 +1,262 @@
from django.urls import reverse
from api.tests.common import less_console_noise_decorator
from registrar.config import settings
from registrar.models.portfolio import Portfolio
from django_webtest import WebTest # type: ignore
from registrar.models import (
DomainRequest,
Domain,
DomainInformation,
UserDomainRole,
User,
)
from .common import create_test_user
from waffle.testutils import override_flag
import logging
logger = logging.getLogger(__name__)
class TestPortfolio(WebTest):
def setUp(self):
super().setUp()
self.user = create_test_user()
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
self.role, _ = UserDomainRole.objects.get_or_create(
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
)
def tearDown(self):
Portfolio.objects.all().delete()
UserDomainRole.objects.all().delete()
DomainRequest.objects.all().delete()
DomainInformation.objects.all().delete()
Domain.objects.all().delete()
User.objects.all().delete()
super().tearDown()
@less_console_noise_decorator
def test_middleware_does_not_redirect_if_no_permission(self):
"""Test that user with no portfolio permission is not redirected when attempting to access home"""
self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio
self.user.save()
self.user.refresh_from_db()
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
portfolio_page = self.app.get(reverse("home"))
# Assert that we're on the right page
self.assertNotContains(portfolio_page, self.portfolio.organization_name)
@less_console_noise_decorator
def test_middleware_does_not_redirect_if_no_portfolio(self):
"""Test that user with no assigned portfolio is not redirected when attempting to access home"""
self.app.set_user(self.user.username)
self.user.portfolio_additional_permissions = [User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO]
self.user.save()
self.user.refresh_from_db()
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
portfolio_page = self.app.get(reverse("home"))
# Assert that we're on the right page
self.assertNotContains(portfolio_page, self.portfolio.organization_name)
@less_console_noise_decorator
def test_middleware_redirects_to_portfolio_organization_page(self):
"""Test that user with VIEW_PORTFOLIO is redirected to portfolio organization page"""
self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio
self.user.portfolio_additional_permissions = [User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO]
self.user.save()
self.user.refresh_from_db()
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
portfolio_page = self.app.get(reverse("home")).follow()
# Assert that we're on the right page
self.assertContains(portfolio_page, self.portfolio.organization_name)
self.assertContains(portfolio_page, "<h1>Organization</h1>")
@less_console_noise_decorator
def test_middleware_redirects_to_portfolio_domains_page(self):
"""Test that user with VIEW_PORTFOLIO and VIEW_ALL_DOMAINS is redirected to portfolio domains page"""
self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio
self.user.portfolio_additional_permissions = [
User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
User.UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
]
self.user.save()
self.user.refresh_from_db()
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
portfolio_page = self.app.get(reverse("home")).follow()
# Assert that we're on the right page
self.assertContains(portfolio_page, self.portfolio.organization_name)
self.assertNotContains(portfolio_page, "<h1>Organization</h1>")
self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
@less_console_noise_decorator
def test_portfolio_domains_page_403_when_user_not_have_permission(self):
"""Test that user without proper permission is denied access to portfolio domain view"""
self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio
self.user.save()
self.user.refresh_from_db()
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
response = self.app.get(
reverse("portfolio-domains", kwargs={"portfolio_id": self.portfolio.pk}), status=403
)
# Assert the response is a 403 Forbidden
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
def test_portfolio_domain_requests_page_403_when_user_not_have_permission(self):
"""Test that user without proper permission is denied access to portfolio domain view"""
self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio
self.user.save()
self.user.refresh_from_db()
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
response = self.app.get(
reverse("portfolio-domain-requests", kwargs={"portfolio_id": self.portfolio.pk}), status=403
)
# Assert the response is a 403 Forbidden
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
def test_portfolio_organization_page_403_when_user_not_have_permission(self):
"""Test that user without proper permission is not allowed access to portfolio organization page"""
self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio
self.user.save()
self.user.refresh_from_db()
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
response = self.app.get(
reverse("portfolio-organization", kwargs={"portfolio_id": self.portfolio.pk}), status=403
)
# Assert the response is a 403 Forbidden
self.assertEqual(response.status_code, 403)
@less_console_noise_decorator
def test_navigation_links_hidden_when_user_not_have_permission(self):
"""Test that navigation links are hidden when user does not have portfolio permissions"""
self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio
self.user.portfolio_additional_permissions = [
User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
User.UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
User.UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
]
self.user.save()
self.user.refresh_from_db()
with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio page.
# Follow implicity checks if our redirect is working.
portfolio_page = self.app.get(reverse("home")).follow()
# Assert that we're on the right page
self.assertContains(portfolio_page, self.portfolio.organization_name)
self.assertNotContains(portfolio_page, "<h1>Organization</h1>")
self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
self.assertContains(
portfolio_page, reverse("portfolio-domains", kwargs={"portfolio_id": self.portfolio.pk})
)
self.assertContains(
portfolio_page, reverse("portfolio-domain-requests", kwargs={"portfolio_id": self.portfolio.pk})
)
# reducing portfolio permissions to just VIEW_PORTFOLIO, which should remove domains
# and domain requests from nav
self.user.portfolio_additional_permissions = [User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO]
self.user.save()
self.user.refresh_from_db()
portfolio_page = self.app.get(reverse("home")).follow()
self.assertContains(portfolio_page, self.portfolio.organization_name)
self.assertContains(portfolio_page, "<h1>Organization</h1>")
self.assertNotContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
self.assertNotContains(
portfolio_page, reverse("portfolio-domains", kwargs={"portfolio_id": self.portfolio.pk})
)
self.assertNotContains(
portfolio_page, reverse("portfolio-domain-requests", kwargs={"portfolio_id": self.portfolio.pk})
)
class TestPortfolioOrganization(TestPortfolio):
def test_portfolio_org_name(self):
"""Can load portfolio's org name page."""
with override_flag("organization_feature", active=True):
self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio
self.user.portfolio_additional_permissions = [
User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
]
self.user.save()
self.user.refresh_from_db()
page = self.app.get(reverse("portfolio-organization", kwargs={"portfolio_id": self.portfolio.pk}))
self.assertContains(
page, "The name of your federal agency will be publicly listed as the domain registrant."
)
def test_domain_org_name_address_content(self):
"""Org name and address information appears on the page."""
with override_flag("organization_feature", active=True):
self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio
self.user.portfolio_additional_permissions = [
User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
]
self.user.save()
self.user.refresh_from_db()
self.portfolio.organization_name = "Hotel California"
self.portfolio.save()
page = self.app.get(reverse("portfolio-organization", kwargs={"portfolio_id": self.portfolio.pk}))
# Once in the sidenav, once in the main nav, once in the form
self.assertContains(page, "Hotel California", count=3)
def test_domain_org_name_address_form(self):
"""Submitting changes works on the org name address page."""
with override_flag("organization_feature", active=True):
self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio
self.user.portfolio_additional_permissions = [
User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
]
self.user.save()
self.user.refresh_from_db()
self.portfolio.address_line1 = "1600 Penn Ave"
self.portfolio.save()
portfolio_org_name_page = self.app.get(
reverse("portfolio-organization", kwargs={"portfolio_id": self.portfolio.pk})
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
portfolio_org_name_page.form["address_line1"] = "6 Downing st"
portfolio_org_name_page.form["city"] = "London"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_result_page = portfolio_org_name_page.form.submit()
self.assertEqual(success_result_page.status_code, 200)
self.assertContains(success_result_page, "6 Downing st")
self.assertContains(success_result_page, "London")

View file

@ -3,7 +3,7 @@ from unittest.mock import Mock
from django.conf import settings
from django.urls import reverse
from api.tests.common import less_console_noise_decorator
from .common import MockSESClient, completed_domain_request # type: ignore
from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore
@ -37,14 +37,23 @@ class DomainRequestTests(TestWithUser, WebTest):
def setUp(self):
super().setUp()
self.federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration")
self.app.set_user(self.user.username)
self.TITLES = DomainRequestWizard.TITLES
def tearDown(self):
super().tearDown()
DomainRequest.objects.all().delete()
DomainInformation.objects.all().delete()
self.federal_agency.delete()
@less_console_noise_decorator
def test_domain_request_form_intro_acknowledgement(self):
"""Tests that user is presented with intro acknowledgement page"""
intro_page = self.app.get(reverse("domain-request:"))
self.assertContains(intro_page, "Youre about to start your .gov domain request")
@less_console_noise_decorator
def test_domain_request_form_intro_is_skipped_when_edit_access(self):
"""Tests that user is NOT presented with intro acknowledgement page when accessed through 'edit'"""
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED, user=self.user)
@ -55,6 +64,7 @@ class DomainRequestTests(TestWithUser, WebTest):
redirect_url = detail_page.url
self.assertEqual(redirect_url, "/request/generic_org_type/")
@less_console_noise_decorator
def test_domain_request_form_empty_submit(self):
"""Tests empty submit on the first page after the acknowledgement page"""
intro_page = self.app.get(reverse("domain-request:"))
@ -77,18 +87,17 @@ class DomainRequestTests(TestWithUser, WebTest):
result = type_page.forms[0].submit()
self.assertIn("What kind of U.S.-based government organization do you represent?", result)
@less_console_noise_decorator
def test_domain_request_multiple_domain_requests_exist(self):
"""Test that an info message appears when user has multiple domain requests already"""
# create and submit a domain request
domain_request = completed_domain_request(user=self.user)
mock_client = MockSESClient()
with boto3_mocking.clients.handler_for("sesv2", mock_client):
with less_console_noise():
domain_request.submit()
domain_request.save()
# now, attempt to create another one
with less_console_noise():
intro_page = self.app.get(reverse("domain-request:"))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
intro_form = intro_page.forms[0]
@ -102,6 +111,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(type_page, "You cannot submit this request yet")
@less_console_noise_decorator
def test_domain_request_into_acknowledgement_creates_new_request(self):
"""
We had to solve a bug where the wizard was creating 2 requests on first intro acknowledgement ('continue')
@ -155,6 +165,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertEqual(domain_request_count, 2)
@boto3_mocking.patching
@less_console_noise_decorator
def test_domain_request_form_submission(self):
"""
Can fill out the entire form and submit.
@ -227,9 +238,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
org_contact_page = federal_result.follow()
org_contact_form = org_contact_page.forms[0]
# federal agency so we have to fill in federal_agency
federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration")
org_contact_form["organization_contact-federal_agency"] = federal_agency.id
org_contact_form["organization_contact-federal_agency"] = self.federal_agency.id
org_contact_form["organization_contact-organization_name"] = "Testorg"
org_contact_form["organization_contact-address_line1"] = "address 1"
org_contact_form["organization_contact-address_line2"] = "address 2"
@ -524,6 +533,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertEqual(num_pages, num_pages_tested)
@boto3_mocking.patching
@less_console_noise_decorator
def test_domain_request_form_submission_incomplete(self):
num_pages_tested = 0
# skipping elections, type_of_work, tribal_government
@ -584,9 +594,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
org_contact_page = federal_result.follow()
org_contact_form = org_contact_page.forms[0]
# federal agency so we have to fill in federal_agency
federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration")
org_contact_form["organization_contact-federal_agency"] = federal_agency.id
org_contact_form["organization_contact-federal_agency"] = self.federal_agency.id
org_contact_form["organization_contact-organization_name"] = "Testorg"
org_contact_form["organization_contact-address_line1"] = "address 1"
org_contact_form["organization_contact-address_line2"] = "address 2"
@ -879,6 +887,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertEqual(num_pages, num_pages_tested)
@less_console_noise_decorator
def test_domain_request_form_conditional_federal(self):
"""Federal branch question is shown for federal organizations."""
intro_page = self.app.get(reverse("domain-request:"))
@ -934,6 +943,7 @@ class DomainRequestTests(TestWithUser, WebTest):
contact_page = federal_result.follow()
self.assertContains(contact_page, "Federal agency")
@less_console_noise_decorator
def test_domain_request_form_conditional_elections(self):
"""Election question is shown for other organizations."""
intro_page = self.app.get(reverse("domain-request:"))
@ -988,6 +998,7 @@ class DomainRequestTests(TestWithUser, WebTest):
contact_page = election_result.follow()
self.assertNotContains(contact_page, "Federal agency")
@less_console_noise_decorator
def test_domain_request_form_section_skipping(self):
"""Can skip forward and back in sections"""
intro_page = self.app.get(reverse("domain-request:"))
@ -1025,6 +1036,7 @@ class DomainRequestTests(TestWithUser, WebTest):
0,
)
@less_console_noise_decorator
def test_domain_request_form_nonfederal(self):
"""Non-federal organizations don't have to provide their federal agency."""
intro_page = self.app.get(reverse("domain-request:"))
@ -1069,6 +1081,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertEqual(contact_result.status_code, 302)
self.assertEqual(contact_result["Location"], "/request/about_your_organization/")
@less_console_noise_decorator
def test_domain_request_about_your_organization_special(self):
"""Special districts have to answer an additional question."""
intro_page = self.app.get(reverse("domain-request:"))
@ -1097,6 +1110,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION])
@less_console_noise_decorator
def test_federal_agency_dropdown_excludes_expected_values(self):
"""The Federal Agency dropdown on a domain request form should not
include options for gov Administration and Non-Federal Agency"""
@ -1144,6 +1158,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# make sure correct federal agency options still show up
self.assertContains(org_contact_page, "General Services Administration")
@less_console_noise_decorator
def test_yes_no_contact_form_inits_blank_for_new_domain_request(self):
"""On the Other Contacts page, the yes/no form gets initialized with nothing selected for
new domain requests"""
@ -1151,6 +1166,7 @@ class DomainRequestTests(TestWithUser, WebTest):
other_contacts_form = other_contacts_page.forms[0]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None)
@less_console_noise_decorator
def test_yes_no_additional_form_inits_blank_for_new_domain_request(self):
"""On the Additional Details page, the yes/no form gets initialized with nothing selected for
new domain requests"""
@ -1163,6 +1179,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# Check the anything else yes/no field
self.assertEquals(additional_form["additional_details-has_anything_else_text"].value, None)
@less_console_noise_decorator
def test_yes_no_form_inits_yes_for_domain_request_with_other_contacts(self):
"""On the Other Contacts page, the yes/no form gets initialized with YES selected if the
domain request has other contacts"""
@ -1183,6 +1200,7 @@ class DomainRequestTests(TestWithUser, WebTest):
other_contacts_form = other_contacts_page.forms[0]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True")
@less_console_noise_decorator
def test_yes_no_form_inits_yes_for_cisa_representative_and_anything_else(self):
"""On the Additional Details page, the yes/no form gets initialized with YES selected
for both yes/no radios if the domain request has a values for cisa_representative_first_name and
@ -1214,6 +1232,7 @@ class DomainRequestTests(TestWithUser, WebTest):
yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value
self.assertEquals(yes_no_anything_else, "True")
@less_console_noise_decorator
def test_yes_no_form_inits_no_for_domain_request_with_no_other_contacts_rationale(self):
"""On the Other Contacts page, the yes/no form gets initialized with NO selected if the
domain request has no other contacts"""
@ -1236,6 +1255,7 @@ class DomainRequestTests(TestWithUser, WebTest):
other_contacts_form = other_contacts_page.forms[0]
self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False")
@less_console_noise_decorator
def test_yes_no_form_for_domain_request_with_no_cisa_representative_and_anything_else(self):
"""On the Additional details page, the form preselects "no" when has_cisa_representative
and anything_else is no"""
@ -1271,6 +1291,7 @@ class DomainRequestTests(TestWithUser, WebTest):
yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value
self.assertEquals(yes_no_anything_else, "False")
@less_console_noise_decorator
def test_submitting_additional_details_deletes_cisa_representative_and_anything_else(self):
"""When a user submits the Additional Details form with no selected for all fields,
the domain request's data gets wiped when submitted"""
@ -1332,6 +1353,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertEqual(domain_request.cisa_representative_last_name, None)
self.assertEqual(domain_request.cisa_representative_email, None)
@less_console_noise_decorator
def test_submitting_additional_details_populates_cisa_representative_and_anything_else(self):
"""When a user submits the Additional Details form,
the domain request's data gets submitted"""
@ -1385,6 +1407,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertEqual(domain_request.has_cisa_representative, True)
self.assertEqual(domain_request.has_anything_else_text, True)
@less_console_noise_decorator
def test_if_cisa_representative_yes_no_form_is_yes_then_field_is_required(self):
"""Applicants with a cisa representative must provide a value"""
domain_request = completed_domain_request(
@ -1417,6 +1440,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(response, "Enter the first name / given name of the CISA regional representative.")
self.assertContains(response, "Enter the last name / family name of the CISA regional representative.")
@less_console_noise_decorator
def test_if_anything_else_yes_no_form_is_yes_then_field_is_required(self):
"""Applicants with a anything else must provide a value"""
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
@ -1447,6 +1471,7 @@ class DomainRequestTests(TestWithUser, WebTest):
expected_message = "Provide additional details youd like us to know. If you have nothing to add, select “No.”"
self.assertContains(response, expected_message)
@less_console_noise_decorator
def test_additional_details_form_fields_required(self):
"""When a user submits the Additional Details form without checking the
has_cisa_representative and has_anything_else_text fields, the form should deny this action"""
@ -1480,6 +1505,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# due to screen reader information / html.
self.assertContains(response, "This question is required.", count=4)
@less_console_noise_decorator
def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self):
"""When a user submits the Other Contacts form with other contacts selected, the domain request's
no other contacts rationale gets deleted"""
@ -1528,6 +1554,7 @@ class DomainRequestTests(TestWithUser, WebTest):
None,
)
@less_console_noise_decorator
def test_submitting_no_other_contacts_rationale_deletes_other_contacts(self):
"""When a user submits the Other Contacts form with no other contacts selected, the domain request's
other contacts get deleted for other contacts that exist and are not joined to other objects
@ -1570,6 +1597,7 @@ class DomainRequestTests(TestWithUser, WebTest):
"Hello again!",
)
@less_console_noise_decorator
def test_submitting_no_other_contacts_rationale_removes_reference_other_contacts_when_joined(self):
"""When a user submits the Other Contacts form with no other contacts selected, the domain request's
other contacts references get removed for other contacts that exist and are joined to other objects"""
@ -1665,6 +1693,7 @@ class DomainRequestTests(TestWithUser, WebTest):
"Hello again!",
)
@less_console_noise_decorator
def test_if_yes_no_form_is_no_then_no_other_contacts_required(self):
"""Applicants with no other contacts have to give a reason."""
other_contacts_page = self.app.get(reverse("domain-request:other_contacts"))
@ -1680,6 +1709,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# Assert that it is not returned, ie the contacts form is not required
self.assertNotContains(response, "Enter the first name / given name of this contact.")
@less_console_noise_decorator
def test_if_yes_no_form_is_yes_then_other_contacts_required(self):
"""Applicants with other contacts do not have to give a reason."""
other_contacts_page = self.app.get(reverse("domain-request:other_contacts"))
@ -1695,6 +1725,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# Assert that it is returned, ie the contacts form is required
self.assertContains(response, "Enter the first name / given name of this contact.")
@less_console_noise_decorator
def test_delete_other_contact(self):
"""Other contacts can be deleted after being saved to database.
@ -1779,6 +1810,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertEqual(domain_request.other_contacts.count(), 1)
self.assertEqual(domain_request.other_contacts.first().first_name, "Testy3")
@less_console_noise_decorator
def test_delete_other_contact_does_not_allow_zero_contacts(self):
"""Delete Other Contact does not allow submission with zero contacts."""
# Populate the database with a domain request that
@ -1851,6 +1883,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertEqual(domain_request.other_contacts.count(), 1)
self.assertEqual(domain_request.other_contacts.first().first_name, "Testy2")
@less_console_noise_decorator
def test_delete_other_contact_sets_visible_empty_form_as_required_after_failed_submit(self):
"""When you:
1. add an empty contact,
@ -1928,6 +1961,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# Enter the first name ...
self.assertContains(response, "Enter the first name / given name of this contact.")
@less_console_noise_decorator
def test_edit_other_contact_in_place(self):
"""When you:
1. edit an existing contact which is not joined to another model,
@ -2009,6 +2043,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertEquals(other_contact_pk, other_contact.id)
self.assertEquals("Testy3", other_contact.first_name)
@less_console_noise_decorator
def test_edit_other_contact_creates_new(self):
"""When you:
1. edit an existing contact which IS joined to another model,
@ -2089,6 +2124,7 @@ class DomainRequestTests(TestWithUser, WebTest):
senior_official = domain_request.senior_official
self.assertEquals("Testy", senior_official.first_name)
@less_console_noise_decorator
def test_edit_senior_official_in_place(self):
"""When you:
1. edit a senior official which is not joined to another model,
@ -2154,6 +2190,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertEquals(so_pk, updated_so.id)
self.assertEquals("Testy2", updated_so.first_name)
@less_console_noise_decorator
def test_edit_senior_official_creates_new(self):
"""When you:
1. edit an existing senior official which IS joined to another model,
@ -2226,6 +2263,7 @@ class DomainRequestTests(TestWithUser, WebTest):
senior_official = domain_request.senior_official
self.assertEquals("Testy2", senior_official.first_name)
@less_console_noise_decorator
def test_edit_submitter_in_place(self):
"""When you:
1. edit a submitter (your contact) which is not joined to another model,
@ -2290,6 +2328,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertEquals(submitter_pk, updated_submitter.id)
self.assertEquals("Testy2", updated_submitter.first_name)
@less_console_noise_decorator
def test_edit_submitter_creates_new(self):
"""When you:
1. edit an existing your contact which IS joined to another model,
@ -2362,6 +2401,7 @@ class DomainRequestTests(TestWithUser, WebTest):
submitter = domain_request.submitter
self.assertEquals("Testy2", submitter.first_name)
@less_console_noise_decorator
def test_domain_request_about_your_organiztion_interstate(self):
"""Special districts have to answer an additional question."""
intro_page = self.app.get(reverse("domain-request:"))
@ -2390,6 +2430,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION])
@less_console_noise_decorator
def test_domain_request_tribal_government(self):
"""Tribal organizations have to answer an additional question."""
intro_page = self.app.get(reverse("domain-request:"))
@ -2421,6 +2462,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# and the step is on the sidebar list.
self.assertContains(tribal_government_page, self.TITLES[Step.TRIBAL_GOVERNMENT])
@less_console_noise_decorator
def test_domain_request_so_dynamic_text(self):
intro_page = self.app.get(reverse("domain-request:"))
# django-webtest does not handle cookie-based sessions well because it keeps
@ -2460,9 +2502,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
org_contact_page = federal_result.follow()
org_contact_form = org_contact_page.forms[0]
# federal agency so we have to fill in federal_agency
federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration")
org_contact_form["organization_contact-federal_agency"] = federal_agency.id
org_contact_form["organization_contact-federal_agency"] = self.federal_agency.id
org_contact_form["organization_contact-organization_name"] = "Testorg"
org_contact_form["organization_contact-address_line1"] = "address 1"
org_contact_form["organization_contact-address_line2"] = "address 2"
@ -2493,6 +2533,7 @@ class DomainRequestTests(TestWithUser, WebTest):
so_page = election_page.click(str(self.TITLES["senior_official"]), index=0)
self.assertContains(so_page, "Domain requests from cities")
@less_console_noise_decorator
def test_domain_request_dotgov_domain_dynamic_text(self):
intro_page = self.app.get(reverse("domain-request:"))
# django-webtest does not handle cookie-based sessions well because it keeps
@ -2532,9 +2573,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
org_contact_page = federal_result.follow()
org_contact_form = org_contact_page.forms[0]
# federal agency so we have to fill in federal_agency
federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration")
org_contact_form["organization_contact-federal_agency"] = federal_agency.id
org_contact_form["organization_contact-federal_agency"] = self.federal_agency.id
org_contact_form["organization_contact-organization_name"] = "Testorg"
org_contact_form["organization_contact-address_line1"] = "address 1"
org_contact_form["organization_contact-address_line2"] = "address 2"
@ -2595,6 +2634,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(dotgov_page, "CityofEudoraKS.gov")
self.assertNotContains(dotgov_page, "medicare.gov")
@less_console_noise_decorator
def test_domain_request_formsets(self):
"""Users are able to add more than one of some fields."""
current_sites_page = self.app.get(reverse("domain-request:current_sites"))
@ -2749,6 +2789,7 @@ class DomainRequestTests(TestWithUser, WebTest):
# page = self.app.get(url)
# self.assertNotContains(page, "VALUE")
@less_console_noise_decorator
def test_long_org_name_in_domain_request(self):
"""
Make sure the long name is displaying in the domain request form,
@ -2771,6 +2812,7 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(type_page, "Federal: an agency of the U.S. government")
@less_console_noise_decorator
def test_submit_modal_no_domain_text_fallback(self):
"""When user clicks on submit your domain request and the requested domain
is null (possible through url direct access to the review page), present
@ -2790,6 +2832,12 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
self.app.set_user(self.user.username)
self.client.force_login(self.user)
def tearDown(self):
super().tearDown()
DomainRequest.objects.all().delete()
DomainInformation.objects.all().delete()
@less_console_noise_decorator
def test_domain_request_status(self):
"""Checking domain request status page"""
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
@ -2803,6 +2851,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
self.assertContains(detail_page, "Admin Tester")
self.assertContains(detail_page, "Status:")
@less_console_noise_decorator
def test_domain_request_status_with_ineligible_user(self):
"""Checking domain request status page whith a blocked user.
The user should still have access to view."""
@ -2819,6 +2868,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
self.assertContains(detail_page, "Admin Tester")
self.assertContains(detail_page, "Status:")
@less_console_noise_decorator
def test_domain_request_withdraw(self):
"""Checking domain request status page"""
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
@ -2849,6 +2899,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
response = self.client.get("/get-domain-requests-json/")
self.assertContains(response, "Withdrawn")
@less_console_noise_decorator
def test_domain_request_withdraw_no_permissions(self):
"""Can't withdraw domain requests as a restricted user."""
self.user.status = User.RESTRICTED
@ -2873,6 +2924,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
page = self.client.get(reverse(url_name, kwargs={"pk": domain_request.pk}))
self.assertEqual(page.status_code, 403)
@less_console_noise_decorator
def test_domain_request_status_no_permissions(self):
"""Can't access domain requests without being the creator."""
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
@ -2892,6 +2944,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
page = self.client.get(reverse(url_name, kwargs={"pk": domain_request.pk}))
self.assertEqual(page.status_code, 403)
@less_console_noise_decorator
def test_approved_domain_request_not_in_active_requests(self):
"""An approved domain request is not shown in the Active
Requests table on home.html."""
@ -2916,13 +2969,17 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
def tearDown(self):
super().tearDown()
DomainRequest.objects.all().delete()
DomainInformation.objects.all().delete()
@less_console_noise_decorator
def test_unlocked_steps_empty_domain_request(self):
"""Test when all fields in the domain request are empty."""
unlocked_steps = self.wizard.db_check_for_unlocking_steps()
expected_dict = []
self.assertEqual(unlocked_steps, expected_dict)
@less_console_noise_decorator
def test_unlocked_steps_full_domain_request(self):
"""Test when all fields in the domain request are filled."""
@ -2959,6 +3016,7 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
else:
self.fail(f"Expected a redirect, but got a different response: {response}")
@less_console_noise_decorator
def test_unlocked_steps_partial_domain_request(self):
"""Test when some fields in the domain request are filled."""

View file

@ -12,99 +12,102 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
super().setUp()
self.app.set_user(self.user.username)
@classmethod
def setUpClass(cls):
super().setUpClass()
lamb_chops, _ = DraftDomain.objects.get_or_create(name="lamb-chops.gov")
short_ribs, _ = DraftDomain.objects.get_or_create(name="short-ribs.gov")
beef_chuck, _ = DraftDomain.objects.get_or_create(name="beef-chuck.gov")
stew_beef, _ = DraftDomain.objects.get_or_create(name="stew-beef.gov")
# Create domain requests for the user
self.domain_requests = [
cls.domain_requests = [
DomainRequest.objects.create(
creator=self.user,
creator=cls.user,
requested_domain=lamb_chops,
submission_date="2024-01-01",
status=DomainRequest.DomainRequestStatus.STARTED,
created_at="2024-01-01",
),
DomainRequest.objects.create(
creator=self.user,
creator=cls.user,
requested_domain=short_ribs,
submission_date="2024-02-01",
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
created_at="2024-02-01",
),
DomainRequest.objects.create(
creator=self.user,
creator=cls.user,
requested_domain=beef_chuck,
submission_date="2024-03-01",
status=DomainRequest.DomainRequestStatus.REJECTED,
created_at="2024-03-01",
),
DomainRequest.objects.create(
creator=self.user,
creator=cls.user,
requested_domain=stew_beef,
submission_date="2024-04-01",
status=DomainRequest.DomainRequestStatus.STARTED,
created_at="2024-04-01",
),
DomainRequest.objects.create(
creator=self.user,
creator=cls.user,
requested_domain=None,
submission_date="2024-05-01",
status=DomainRequest.DomainRequestStatus.STARTED,
created_at="2024-05-01",
),
DomainRequest.objects.create(
creator=self.user,
creator=cls.user,
requested_domain=None,
submission_date="2024-06-01",
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
created_at="2024-06-01",
),
DomainRequest.objects.create(
creator=self.user,
creator=cls.user,
requested_domain=None,
submission_date="2024-07-01",
status=DomainRequest.DomainRequestStatus.REJECTED,
created_at="2024-07-01",
),
DomainRequest.objects.create(
creator=self.user,
creator=cls.user,
requested_domain=None,
submission_date="2024-08-01",
status=DomainRequest.DomainRequestStatus.STARTED,
created_at="2024-08-01",
),
DomainRequest.objects.create(
creator=self.user,
creator=cls.user,
requested_domain=None,
submission_date="2024-09-01",
status=DomainRequest.DomainRequestStatus.STARTED,
created_at="2024-09-01",
),
DomainRequest.objects.create(
creator=self.user,
creator=cls.user,
requested_domain=None,
submission_date="2024-10-01",
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
created_at="2024-10-01",
),
DomainRequest.objects.create(
creator=self.user,
creator=cls.user,
requested_domain=None,
submission_date="2024-11-01",
status=DomainRequest.DomainRequestStatus.REJECTED,
created_at="2024-11-01",
),
DomainRequest.objects.create(
creator=self.user,
creator=cls.user,
requested_domain=None,
submission_date="2024-11-02",
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
created_at="2024-11-02",
),
DomainRequest.objects.create(
creator=self.user,
creator=cls.user,
requested_domain=None,
submission_date="2024-12-01",
status=DomainRequest.DomainRequestStatus.APPROVED,
@ -112,9 +115,11 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
),
]
def tearDown(self):
super().tearDown()
@classmethod
def tearDownClass(cls):
super().tearDownClass()
DomainRequest.objects.all().delete()
DraftDomain.objects.all().delete()
def test_get_domain_requests_json_authenticated(self):
"""Test that domain requests are returned properly for an authenticated user."""

View file

@ -18,6 +18,7 @@ from django.contrib.postgres.aggregates import StringAgg
from registrar.models.utility.generic_helper import convert_queryset_to_dict
from registrar.templatetags.custom_filters import get_region
from registrar.utility.constants import BranchChoices
from registrar.utility.enums import DefaultEmail
logger = logging.getLogger(__name__)
@ -108,7 +109,7 @@ class BaseExport(ABC):
return Q()
@classmethod
def get_filter_conditions(cls, start_date=None, end_date=None):
def get_filter_conditions(cls, **export_kwargs):
"""
Get a Q object of filter conditions to filter when building queryset.
"""
@ -144,7 +145,7 @@ class BaseExport(ABC):
return queryset
@classmethod
def write_csv_before(cls, csv_writer, start_date=None, end_date=None):
def write_csv_before(cls, csv_writer, **export_kwargs):
"""
Write to csv file before the write_csv method.
Override in subclasses where needed.
@ -191,7 +192,7 @@ class BaseExport(ABC):
return cls.update_queryset(queryset, **kwargs)
@classmethod
def export_data_to_csv(cls, csv_file, start_date=None, end_date=None):
def export_data_to_csv(cls, csv_file, **export_kwargs):
"""
All domain metadata:
Exports domains of all statuses plus domain managers.
@ -204,7 +205,7 @@ class BaseExport(ABC):
prefetch_related = cls.get_prefetch_related()
exclusions = cls.get_exclusions()
annotations_for_sort = cls.get_annotations_for_sort()
filter_conditions = cls.get_filter_conditions(start_date, end_date)
filter_conditions = cls.get_filter_conditions(**export_kwargs)
computed_fields = cls.get_computed_fields()
related_table_fields = cls.get_related_table_fields()
@ -226,10 +227,13 @@ class BaseExport(ABC):
models_dict = convert_queryset_to_dict(annotated_queryset, is_model=False)
# Write to csv file before the write_csv
cls.write_csv_before(writer, start_date, end_date)
cls.write_csv_before(writer, **export_kwargs)
# Write the csv file
cls.write_csv(writer, columns, models_dict)
rows = cls.write_csv(writer, columns, models_dict)
# Return rows that for easier parsing and testing
return rows
@classmethod
def write_csv(
@ -256,6 +260,9 @@ class BaseExport(ABC):
writer.writerows(rows)
# Return rows for easier parsing and testing
return rows
@classmethod
@abstractmethod
def parse_row(cls, columns, model):
@ -343,7 +350,11 @@ class DomainExport(BaseExport):
"""
Fetch all UserDomainRole entries and return a mapping of domain to user__email.
"""
user_domain_roles = UserDomainRole.objects.select_related("user").values_list("domain__name", "user__email")
user_domain_roles = (
UserDomainRole.objects.select_related("user")
.order_by("domain__name", "user__email")
.values_list("domain__name", "user__email")
)
return list(user_domain_roles)
@classmethod
@ -371,6 +382,15 @@ class DomainExport(BaseExport):
if domain_federal_type and domain_org_type == DomainRequest.OrgChoicesElectionOffice.FEDERAL:
domain_type = f"{human_readable_domain_org_type} - {human_readable_domain_federal_type}"
security_contact_email = model.get("security_contact_email")
invalid_emails = {DefaultEmail.LEGACY_DEFAULT.value, DefaultEmail.PUBLIC_CONTACT_DEFAULT.value}
if (
not security_contact_email
or not isinstance(security_contact_email, str)
or security_contact_email.lower().strip() in invalid_emails
):
security_contact_email = "(blank)"
# create a dictionary of fields which can be included in output.
# "extra_fields" are precomputed fields (generated in the DB or parsed).
FIELDS = {
@ -385,7 +405,7 @@ class DomainExport(BaseExport):
"State": model.get("state_territory"),
"SO": model.get("so_name"),
"SO email": model.get("senior_official__email"),
"Security contact email": model.get("security_contact_email"),
"Security contact email": security_contact_email,
"Created at": model.get("domain__created_at"),
"Deleted": model.get("domain__deleted"),
"Domain managers": model.get("managers"),
@ -544,6 +564,25 @@ class DomainDataType(DomainExport):
]
class DomainDataTypeUser(DomainDataType):
"""
The DomainDataType report, but sliced on the current request user
"""
@classmethod
def get_filter_conditions(cls, request=None):
"""
Get a Q object of filter conditions to filter when building queryset.
"""
if request is None or not hasattr(request, "user") or not request.user:
# Return nothing
return Q(id__in=[])
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
return Q(domain__id__in=domain_ids)
class DomainDataFull(DomainExport):
"""
Shows security contacts, filtered by state
@ -601,7 +640,7 @@ class DomainDataFull(DomainExport):
return ["domain"]
@classmethod
def get_filter_conditions(cls, start_date=None, end_date=None):
def get_filter_conditions(cls):
"""
Get a Q object of filter conditions to filter when building queryset.
"""
@ -696,7 +735,7 @@ class DomainDataFederal(DomainExport):
return ["domain"]
@classmethod
def get_filter_conditions(cls, start_date=None, end_date=None):
def get_filter_conditions(cls):
"""
Get a Q object of filter conditions to filter when building queryset.
"""

View file

@ -59,7 +59,7 @@ from epplibwrapper import (
from ..utility.email import send_templated_email, EmailSendingError
from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
from waffle.decorators import flag_is_active, waffle_flag
from waffle.decorators import waffle_flag
logger = logging.getLogger(__name__)
@ -102,13 +102,6 @@ class DomainBaseView(DomainPermissionView):
domain_pk = "domain:" + str(self.kwargs.get("pk"))
self.session[domain_pk] = self.object
def get_context_data(self, **kwargs):
"""Extend get_context_data to add has_profile_feature_flag to context"""
context = super().get_context_data(**kwargs)
# This is a django waffle flag which toggles features based off of the "flag" table
context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature")
return context
class DomainFormBaseView(DomainBaseView, FormMixin):
"""

View file

@ -228,10 +228,8 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
if request.path_info == self.NEW_URL_NAME:
# Clear context so the prop getter won't create a request here.
# Creating a request will be handled in the post method for the
# intro page. Only TEMPORARY context needed is has_profile_flag
has_profile_flag = flag_is_active(self.request, "profile_feature")
context_stuff = {"has_profile_feature_flag": has_profile_flag}
return render(request, "domain_request_intro.html", context=context_stuff)
# intro page.
return render(request, "domain_request_intro.html", {})
else:
return self.goto(self.steps.first)
@ -380,7 +378,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
def get_context_data(self):
"""Define context for access on all wizard pages."""
has_profile_flag = flag_is_active(self.request, "profile_feature")
context_stuff = {}
if DomainRequest._form_complete(self.domain_request, self.request):
@ -397,8 +394,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
"modal_description": "Once you submit this request, you wont be able to edit it until we review it.\
Youll only be able to withdraw your request.",
"review_form_is_complete": True,
# Use the profile waffle feature flag to toggle profile features throughout domain requests
"has_profile_feature_flag": has_profile_flag,
"user": self.request.user,
}
else: # form is not complete
@ -414,7 +409,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
"modal_description": 'This request cannot be submitted yet.\
Return to the request and visit the steps that are marked as "incomplete."',
"review_form_is_complete": False,
"has_profile_feature_flag": has_profile_flag,
"user": self.request.user,
}
return context_stuff
@ -740,13 +734,6 @@ class Finished(DomainRequestWizard):
class DomainRequestStatus(DomainRequestPermissionView):
template_name = "domain_request_status.html"
def get_context_data(self, **kwargs):
"""Extend get_context_data to add has_profile_feature_flag to context"""
context = super().get_context_data(**kwargs)
# This is a django waffle flag which toggles features based off of the "flag" table
context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature")
return context
class DomainRequestWithdrawConfirmation(DomainRequestPermissionWithdrawView):
"""This page will ask user to confirm if they want to withdraw
@ -757,13 +744,6 @@ class DomainRequestWithdrawConfirmation(DomainRequestPermissionWithdrawView):
template_name = "domain_request_withdraw_confirmation.html"
def get_context_data(self, **kwargs):
"""Extend get_context_data to add has_profile_feature_flag to context"""
context = super().get_context_data(**kwargs)
# This is a django waffle flag which toggles features based off of the "flag" table
context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature")
return context
class DomainRequestWithdrawn(DomainRequestPermissionWithdrawView):
# this view renders no template

View file

@ -1,3 +1,4 @@
import logging
from django.http import JsonResponse
from django.core.paginator import Paginator
from registrar.models import UserDomainRole, Domain
@ -5,89 +6,29 @@ from django.contrib.auth.decorators import login_required
from django.urls import reverse
from django.db.models import Q
logger = logging.getLogger(__name__)
@login_required
def get_domains_json(request):
"""Given the current request,
get all domains that are associated with the UserDomainRole object"""
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
user_domain_roles = UserDomainRole.objects.filter(user=request.user).select_related("domain_info__sub_organization")
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
objects = Domain.objects.filter(id__in=domain_ids)
unfiltered_total = objects.count()
# Handle sorting
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
order = request.GET.get("order", "asc") # Default to 'asc'
# Handle search term
search_term = request.GET.get("search_term")
if search_term:
objects = objects.filter(Q(name__icontains=search_term))
# Handle state
status_param = request.GET.get("status")
if status_param:
status_list = status_param.split(",")
# if unknown is in status_list, append 'dns needed' since both
# unknown and dns needed display as DNS Needed, and both are
# searchable via state parameter of 'unknown'
if "unknown" in status_list:
status_list.append("dns needed")
# Split the status list into normal states and custom states
normal_states = [state for state in status_list if state in Domain.State.values]
custom_states = [state for state in status_list if state == "expired"]
# Construct Q objects for normal states that can be queried through ORM
state_query = Q()
if normal_states:
state_query |= Q(state__in=normal_states)
# Handle custom states in Python, as expired can not be queried through ORM
if "expired" in custom_states:
expired_domain_ids = [domain.id for domain in objects if domain.state_display() == "Expired"]
state_query |= Q(id__in=expired_domain_ids)
# Apply the combined query
objects = objects.filter(state_query)
# If there are filtered states, and expired is not one of them, domains with
# state_display of 'Expired' must be removed
if "expired" not in custom_states:
expired_domain_ids = [domain.id for domain in objects if domain.state_display() == "Expired"]
objects = objects.exclude(id__in=expired_domain_ids)
if sort_by == "state_display":
# Fetch the objects and sort them in Python
objects = list(objects) # Evaluate queryset to a list
objects.sort(key=lambda domain: domain.state_display(), reverse=(order == "desc"))
else:
if order == "desc":
sort_by = f"-{sort_by}"
objects = objects.order_by(sort_by)
objects = apply_search(objects, request)
objects = apply_state_filter(objects, request)
objects = apply_sorting(objects, request)
paginator = Paginator(objects, 10)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
# Convert objects to JSON-serializable format
domains = [
{
"id": domain.id,
"name": domain.name,
"expiration_date": domain.expiration_date,
"state": domain.state,
"state_display": domain.state_display(),
"get_state_help_text": domain.get_state_help_text(),
"action_url": reverse("domain", kwargs={"pk": domain.id}),
"action_label": ("View" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "Manage"),
"svg_icon": ("visibility" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "settings"),
}
for domain in page_obj.object_list
]
domains = [serialize_domain(domain) for domain in page_obj.object_list]
return JsonResponse(
{
@ -100,3 +41,80 @@ def get_domains_json(request):
"unfiltered_total": unfiltered_total,
}
)
def apply_search(queryset, request):
search_term = request.GET.get("search_term")
if search_term:
queryset = queryset.filter(Q(name__icontains=search_term))
return queryset
def apply_state_filter(queryset, request):
status_param = request.GET.get("status")
if status_param:
status_list = status_param.split(",")
# if unknown is in status_list, append 'dns needed' since both
# unknown and dns needed display as DNS Needed, and both are
# searchable via state parameter of 'unknown'
if "unknown" in status_list:
status_list.append("dns needed")
# Split the status list into normal states and custom states
normal_states = [state for state in status_list if state in Domain.State.values]
custom_states = [state for state in status_list if state == "expired"]
# Construct Q objects for normal states that can be queried through ORM
state_query = Q()
if normal_states:
state_query |= Q(state__in=normal_states)
# Handle custom states in Python, as expired can not be queried through ORM
if "expired" in custom_states:
expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"]
state_query |= Q(id__in=expired_domain_ids)
# Apply the combined query
queryset = queryset.filter(state_query)
# If there are filtered states, and expired is not one of them, domains with
# state_display of 'Expired' must be removed
if "expired" not in custom_states:
expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"]
queryset = queryset.exclude(id__in=expired_domain_ids)
return queryset
def apply_sorting(queryset, request):
sort_by = request.GET.get("sort_by", "id")
order = request.GET.get("order", "asc")
if sort_by == "state_display":
objects = list(queryset)
objects.sort(key=lambda domain: domain.state_display(), reverse=(order == "desc"))
return objects
else:
if order == "desc":
sort_by = f"-{sort_by}"
return queryset.order_by(sort_by)
def serialize_domain(domain):
suborganization_name = None
try:
domain_info = domain.domain_info
if domain_info:
suborganization = domain_info.sub_organization
if suborganization:
suborganization_name = suborganization.name
except Domain.domain_info.RelatedObjectDoesNotExist:
domain_info = None
logger.debug(f"Issue in domains_json: We could not find domain_info for {domain}")
return {
"id": domain.id,
"name": domain.name,
"expiration_date": domain.expiration_date,
"state": domain.state,
"state_display": domain.state_display(),
"get_state_help_text": domain.get_state_help_text(),
"action_url": reverse("domain", kwargs={"pk": domain.id}),
"action_label": ("View" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "Manage"),
"svg_icon": ("visibility" if domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] else "settings"),
"suborganization": suborganization_name,
}

View file

@ -1,5 +1,4 @@
from django.shortcuts import render
from waffle.decorators import flag_is_active
def index(request):
@ -7,10 +6,6 @@ def index(request):
context = {}
if request.user.is_authenticated:
# This is a django waffle flag which toggles features based off of the "flag" table
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature")
# This controls the creation of a new domain request in the wizard
request.session["new_request"] = True

View file

@ -1,39 +1,110 @@
import logging
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.contrib import messages
from registrar.forms.portfolio import PortfolioOrgAddressForm
from registrar.models.portfolio import Portfolio
from registrar.views.utility.permission_views import (
PortfolioDomainRequestsPermissionView,
PortfolioDomainsPermissionView,
PortfolioBasePermissionView,
)
from waffle.decorators import flag_is_active
from django.contrib.auth.decorators import login_required
from django.views.generic import View
from django.views.generic.edit import FormMixin
@login_required
def portfolio_domains(request, portfolio_id):
logger = logging.getLogger(__name__)
class PortfolioDomainsView(PortfolioDomainsPermissionView, View):
template_name = "portfolio_domains.html"
def get(self, request, portfolio_id):
context = {}
if request.user.is_authenticated:
# This is a django waffle flag which toggles features based off of the "flag" table
if self.request.user.is_authenticated:
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature")
# Retrieve the portfolio object based on the provided portfolio_id
portfolio = get_object_or_404(Portfolio, id=portfolio_id)
context["portfolio"] = portfolio
return render(request, "portfolio_domains.html", context)
@login_required
def portfolio_domain_requests(request, portfolio_id):
class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):
template_name = "portfolio_requests.html"
def get(self, request, portfolio_id):
context = {}
if request.user.is_authenticated:
# This is a django waffle flag which toggles features based off of the "flag" table
if self.request.user.is_authenticated:
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature")
# Retrieve the portfolio object based on the provided portfolio_id
portfolio = get_object_or_404(Portfolio, id=portfolio_id)
context["portfolio"] = portfolio
# This controls the creation of a new domain request in the wizard
request.session["new_request"] = True
return render(request, "portfolio_requests.html", context)
class PortfolioOrganizationView(PortfolioBasePermissionView, FormMixin):
"""
View to handle displaying and updating the portfolio's organization details.
"""
model = Portfolio
template_name = "portfolio_organization.html"
form_class = PortfolioOrgAddressForm
context_object_name = "portfolio"
def get_context_data(self, **kwargs):
"""Add additional context data to the template."""
context = super().get_context_data(**kwargs)
# no need to add portfolio to request context here
context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature")
context["has_organization_feature_flag"] = flag_is_active(self.request, "organization_feature")
return context
def get_object(self, queryset=None):
"""Get the portfolio object based on the URL parameter."""
return get_object_or_404(Portfolio, id=self.kwargs.get("portfolio_id"))
def get_form_kwargs(self):
"""Include the instance in the form kwargs."""
kwargs = super().get_form_kwargs()
kwargs["instance"] = self.get_object()
return kwargs
def get(self, request, *args, **kwargs):
"""Handle GET requests to display the form."""
self.object = self.get_object()
form = self.get_form()
return self.render_to_response(self.get_context_data(form=form))
def post(self, request, *args, **kwargs):
"""Handle POST requests to process form submission."""
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
"""Handle the case when the form is valid."""
self.object = form.save(commit=False)
self.object.creator = self.request.user
self.object.save()
messages.success(self.request, "The organization information for this portfolio has been updated.")
return super().form_valid(form)
def form_invalid(self, form):
"""Handle the case when the form is invalid."""
return self.render_to_response(self.get_context_data(form=form))
def get_success_url(self):
"""Redirect to the overview page for the portfolio."""
return reverse("portfolio-organization", kwargs={"portfolio_id": self.object.pk})

View file

@ -158,6 +158,17 @@ class ExportDataType(View):
return response
class ExportDataTypeUser(View):
"""Returns a domain report for a given user on the request"""
def get(self, request, *args, **kwargs):
# match the CSV example with all the fields
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = 'attachment; filename="your-domains.csv"'
csv_export.DomainDataTypeUser.export_data_to_csv(response, request=request)
return response
class ExportDataFull(View):
def get(self, request, *args, **kwargs):
# Smaller export based on 1
@ -194,7 +205,7 @@ class ExportDataDomainsGrowth(View):
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="domain-growth-report-{start_date}-to-{end_date}.csv"'
csv_export.DomainGrowth.export_data_to_csv(response, start_date, end_date)
csv_export.DomainGrowth.export_data_to_csv(response, start_date=start_date, end_date=end_date)
return response
@ -206,7 +217,7 @@ class ExportDataRequestsGrowth(View):
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="requests-{start_date}-to-{end_date}.csv"'
csv_export.DomainRequestGrowth.export_data_to_csv(response, start_date, end_date)
csv_export.DomainRequestGrowth.export_data_to_csv(response, start_date=start_date, end_date=end_date)
return response
@ -217,7 +228,7 @@ class ExportDataManagedDomains(View):
end_date = request.GET.get("end_date", "")
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="managed-domains-{start_date}-to-{end_date}.csv"'
csv_export.DomainManaged.export_data_to_csv(response, start_date, end_date)
csv_export.DomainManaged.export_data_to_csv(response, start_date=start_date, end_date=end_date)
return response
@ -228,6 +239,6 @@ class ExportDataUnmanagedDomains(View):
end_date = request.GET.get("end_date", "")
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="unmanaged-domains-{start_date}-to-{end_date}.csv"'
csv_export.DomainUnmanaged.export_data_to_csv(response, start_date, end_date)
csv_export.DomainUnmanaged.export_data_to_csv(response, start_date=start_date, end_date=end_date)
return response

View file

@ -11,7 +11,7 @@ from django.urls import NoReverseMatch, reverse
from registrar.models.user import User
from registrar.models.utility.generic_helper import replace_url_queryparams
from registrar.views.utility.permission_views import UserProfilePermissionView
from waffle.decorators import flag_is_active, waffle_flag
from waffle.decorators import waffle_flag
logger = logging.getLogger(__name__)
@ -51,10 +51,8 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""Extend get_context_data to include has_profile_feature_flag"""
"""Extend get_context_data"""
context = super().get_context_data(**kwargs)
# This is a django waffle flag which toggles features based off of the "flag" table
context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature")
# Set the profile_back_button_text based on the redirect parameter
if kwargs.get("redirect") == "domain-request:":
@ -134,7 +132,7 @@ class FinishProfileSetupView(UserProfileView):
base_view_name = "finish-user-profile-setup"
def get_context_data(self, **kwargs):
"""Extend get_context_data to include has_profile_feature_flag"""
"""Extend get_context_data"""
context = super().get_context_data(**kwargs)
# Show back button conditional on user having finished setup

View file

@ -14,14 +14,12 @@ Rather than dealing with that, we keep everything centralized in one location.
"""
from django.shortcuts import render
from waffle.decorators import flag_is_active
def custom_500_error_view(request, context=None):
"""Used to redirect 500 errors to a custom view"""
if context is None:
context = {}
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
return render(request, "500.html", context=context, status=500)
@ -29,7 +27,6 @@ def custom_401_error_view(request, context=None):
"""Used to redirect 401 errors to a custom view"""
if context is None:
context = {}
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
return render(request, "401.html", context=context, status=401)
@ -37,5 +34,4 @@ def custom_403_error_view(request, exception=None, context=None):
"""Used to redirect 403 errors to a custom view"""
if context is None:
context = {}
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
return render(request, "403.html", context=context, status=403)

View file

@ -398,3 +398,49 @@ class UserProfilePermission(PermissionsLoginMixin):
return False
return True
class PortfolioBasePermission(PermissionsLoginMixin):
"""Permission mixin that redirects to portfolio pages if user
has access, otherwise 403"""
def has_permission(self):
"""Check if this user has access to this portfolio.
The user is in self.request.user and the portfolio can be looked
up from the portfolio's primary key in self.kwargs["pk"]
"""
if not self.request.user.is_authenticated:
return False
return self.request.user.has_base_portfolio_permission()
class PortfolioDomainsPermission(PortfolioBasePermission):
"""Permission mixin that allows access to portfolio domain pages if user
has access, otherwise 403"""
def has_permission(self):
"""Check if this user has access to domains for this portfolio.
The user is in self.request.user and the portfolio can be looked
up from the portfolio's primary key in self.kwargs["pk"]"""
if not self.request.user.is_authenticated:
return False
return self.request.user.has_domains_portfolio_permission()
class PortfolioDomainRequestsPermission(PortfolioBasePermission):
"""Permission mixin that allows access to portfolio domain request pages if user
has access, otherwise 403"""
def has_permission(self):
"""Check if this user has access to domain requests for this portfolio.
The user is in self.request.user and the portfolio can be looked
up from the portfolio's primary key in self.kwargs["pk"]"""
if not self.request.user.is_authenticated:
return False
return self.request.user.has_domain_requests_portfolio_permission()

View file

@ -3,7 +3,7 @@
import abc # abstract base class
from django.views.generic import DetailView, DeleteView, TemplateView
from registrar.models import Domain, DomainRequest, DomainInvitation
from registrar.models import Domain, DomainRequest, DomainInvitation, Portfolio
from registrar.models.user import User
from registrar.models.user_domain_role import UserDomainRole
@ -13,8 +13,11 @@ from .mixins import (
DomainRequestPermissionWithdraw,
DomainInvitationPermission,
DomainRequestWizardPermission,
PortfolioDomainRequestsPermission,
PortfolioDomainsPermission,
UserDeleteDomainRolePermission,
UserProfilePermission,
PortfolioBasePermission,
)
import logging
@ -163,3 +166,38 @@ class UserProfilePermissionView(UserProfilePermission, DetailView, abc.ABC):
@abc.abstractmethod
def template_name(self):
raise NotImplementedError
class PortfolioBasePermissionView(PortfolioBasePermission, DetailView, abc.ABC):
"""Abstract base view for portfolio views that enforces permissions.
This abstract view cannot be instantiated. Actual views must specify
`template_name`.
"""
# DetailView property for what model this is viewing
model = Portfolio
# variable name in template context for the model object
context_object_name = "portfolio"
# Abstract property enforces NotImplementedError on an attribute.
@property
@abc.abstractmethod
def template_name(self):
raise NotImplementedError
class PortfolioDomainsPermissionView(PortfolioDomainsPermission, PortfolioBasePermissionView, abc.ABC):
"""Abstract base view for portfolio domains views that enforces permissions.
This abstract view cannot be instantiated. Actual views must specify
`template_name`.
"""
class PortfolioDomainRequestsPermissionView(PortfolioDomainRequestsPermission, PortfolioBasePermissionView, abc.ABC):
"""Abstract base view for portfolio domain request views that enforces permissions.
This abstract view cannot be instantiated. Actual views must specify
`template_name`.
"""

View file

@ -70,6 +70,7 @@
10038 OUTOFSCOPE http://app:8080/org-name-address
10038 OUTOFSCOPE http://app:8080/domain_requests/
10038 OUTOFSCOPE http://app:8080/domains/
10038 OUTOFSCOPE http://app:8080/organization/
# This URL always returns 404, so include it as well.
10038 OUTOFSCOPE http://app:8080/todo
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers