Merge remote-tracking branch 'origin/main' into nl/2240-additional-details-completion-bug

This commit is contained in:
CocoByte 2024-08-12 14:35:43 -06:00
commit 5083298aca
No known key found for this signature in database
GPG key ID: BBFAA2526384C97F
44 changed files with 1291 additions and 141 deletions

View file

@ -16,10 +16,11 @@ assignees: abroddrick
There are several tools we use locally that you will need to have.
- [ ] [Cloudfoundry CLI](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html#pkg-mac) Note: If you are on Windows the cli will be under `cf8` or `cf7` depending on which version you install.
- [ ] [Cloudfoundry CLI](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html#pkg-mac)
- If you are using Windows the cli will be under `cf8` or `cf7` depending on which version you install.
- 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)
- [ ] [GPG](https://gnupg.org/download/)
- [ ] [GPG](https://gnupg.org/download/) if you are using GPG to sign commits.
- Make sure you have `gpg` >2.1.7. Run `gpg --version` to check. If not, [install gnupg](https://formulae.brew.sh/formula/gnupg)
- This may not work on DHS devices. Alternatively, you can [use ssh keys](#setting-up-commit-signing-with-ssh) instead.
- [ ] Docker Community Edition*
@ -34,7 +35,7 @@ The following tools are optional but recommended. For DHS devices, these can be
- [ ] Putty*
- [ ] Windows Subsystem for Linux*
* Must be requested through DHS IT portal on DHS devices
\* Must be requested through DHS IT portal on DHS devices
** Downloadable via DHS Software Center
@ -75,7 +76,7 @@ Follow the [.gov onboarding dev setup instructions](https://docs.google.com/docu
## 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.
Follow GitHub's instructions to [generate a new GPG key](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key) (default configurations are okay) and [add it to your GitHub GPG keys](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account).
Configure your key locally:
@ -94,23 +95,7 @@ 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
### Troubleshooting GPG on MacOS
**Note:** if you are on a mac and not able to successfully create a signed commit, getting the following error:
```zsh
error: gpg failed to sign the data
@ -131,7 +116,7 @@ or
source ~/.zshrc
```
### Windows
### Troubleshooting GPG on Windows
If GPG doesn't work out of the box with git for you:
- You can [download the GPG binary directly](https://gnupg.org/download/).
- It may be helpful to use [gpg4win](https://www.gpg4win.org/get-gpg4win.html).
@ -140,6 +125,22 @@ From there, you should be able to access gpg through the terminal.
Additionally, consider a gpg key manager like Kleopatra if you run into issues with environment variables or with the gpg service not running on startup.
## Setting up commit signing with SSH
Follow GitHub's instructions to [generate a new SSH key](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) and [add it to your GitHub SSH keys](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account) 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_PATH>
```
Where `<YOUR_KEY_PATH>` is the path of your public key file. GitHub defaults this to `~/.ssh/id_ed25519.pub`. If you named SSH public key a different name from the default, you may need to replace `id_ed25519.pub` with the name you gave your key.
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`.
## Setting up developer sandbox
We have three types of environments: stable, staging, and sandbox. Stable (production)and staging (pre-prod) get deployed via tagged release, and developer sandboxes are given to get.gov developers to mess around in a production-like environment without disrupting stable or staging. Each sandbox is namespaced and will automatically be deployed too when the appropriate branch syntax is used for that space in an open pull request. There are several things you need to setup to make the sandbox work for a developer.

2
.gitignore vendored
View file

@ -7,8 +7,10 @@ docs/research/data/**
public/
credentials*
src/certs/
*.pem
*.crt
*.cer
*.bk

View file

@ -357,4 +357,8 @@ Then, copy the variables under the section labled `s3`.
1. On the app, navigate to `\admin`.
2. Under models, click `Waffle flags`.
3. Click the `disable_email_sending` record. This should exist by default, if not - create one with that name.
4. (Important) Set the field `everyone` to `Yes`. This field overrides all other settings
4. (Important) Set the field `everyone` to `Yes`. This field overrides all other settings
## Request Flow FSM Diagram
The [.gov Domain Request & Domain Status Digram](https://miro.com/app/board/uXjVMuqbLOk=/?moveToWidget=3458764594819017396&cot=14) visualizes the domain request flow and resulting domain objects.

View file

@ -30,7 +30,19 @@ You should end up with `40_some_migration_from_main`, `41_local_migration`
Alternatively, assuming that the conflicting migrations are not dependent on each other, you can manually edit the migration file such that your new migration is incremented by one (file name, and definition inside the file) but this approach is not recommended.
### Scenario 2: Conflicting migrations on sandbox
### Scenario 2: Conflicting migrations on sandbox (can be fixed with GH workflow)
A 500 error on a sanbox after a fresh push usually indicates a migration issue.
Most of the time, these migration issues can easily be fixed by simply running the
"reset-db" workflow in Github.
For the workflow, select the following inputs before running it;
"Use workflow from": Branch-main
"Which environment should we flush and re-load data for?" <YOUR_TARGET_SANDBOX>
This is not a cure-all since it simply flushes and re-runs migrations against your sandbox.
If running this workflow does not solve your issue, proceed examining the scenarios below.
### Scenario 3: Conflicting migrations on sandbox (cannot be fixed with GH workflow)
This occurs when the logs return the following:
>Conflicting migrations detected; multiple leaf nodes in the migration graph: (0040_example, 0041_example in base).

View file

@ -751,4 +751,68 @@ Example: `cf ssh getgov-za`
##### Parameters
| | Parameter | Description |
|:-:|:-------------------------- |:-----------------------------------------------------------------------------------|
| 1 | **emailTo** | Specifies where the email will be emailed. Defaults to help@get.gov on production. |
| 1 | **emailTo** | Specifies where the email will be emailed. Defaults to help@get.gov on production. |
## Populate federal agency initials and FCEB
This script adds to the "is_fceb" and "initials" fields on the FederalAgency model. This script expects a CSV of federal CIOs to pull from, which can be sourced from [here](https://docs.google.com/spreadsheets/d/14oXHFpKyUXS5_mDWARPusghGdHCrP67jCleOknaSx38/edit?gid=479328070#gid=479328070).
### Running on sandboxes
#### Step 1: Login to CloudFoundry
```cf login -a api.fr.cloud.gov --sso```
#### Step 2: SSH into your environment
```cf ssh getgov-{space}```
Example: `cf ssh getgov-za`
#### Step 3: Create a shell instance
```/tmp/lifecycle/shell```
#### Step 4: Upload your csv to the desired sandbox
[Follow these steps](#use-scp-to-transfer-data-to-sandboxes) to upload the federal_cio csv to a sandbox of your choice.
#### Step 5: Running the script
```./manage.py populate_federal_agency_initials_and_fceb {path_to_CIO_csv}```
### Running locally
#### Step 1: Running the script
```docker-compose exec app ./manage.py populate_federal_agency_initials_and_fceb {path_to_CIO_csv}```
##### Parameters
| | Parameter | Description |
|:-:|:-------------------------- |:-----------------------------------------------------------------------------------|
| 1 | **federal_cio_csv_path** | Specifies where the federal CIO csv is |
## Load senior official table
This script adds SeniorOfficial records to the related table based off of a CSV. This script expects a CSV of federal CIOs to pull from, which can be sourced from [here](https://docs.google.com/spreadsheets/d/14oXHFpKyUXS5_mDWARPusghGdHCrP67jCleOknaSx38/edit?gid=479328070#gid=479328070).
### Running on sandboxes
#### Step 1: Login to CloudFoundry
```cf login -a api.fr.cloud.gov --sso```
#### Step 2: SSH into your environment
```cf ssh getgov-{space}```
Example: `cf ssh getgov-za`
#### Step 3: Create a shell instance
```/tmp/lifecycle/shell```
#### Step 4: Upload your csv to the desired sandbox
[Follow these steps](#use-scp-to-transfer-data-to-sandboxes) to upload the federal_cio csv to a sandbox of your choice.
#### Step 5: Running the script
```./manage.py load_senior_official_table {path_to_CIO_csv}```
### Running locally
#### Step 1: Running the script
```docker-compose exec app ./manage.py load_senior_official_table {path_to_CIO_csv}```
##### Parameters
| | Parameter | Description |
|:-:|:-------------------------- |:-----------------------------------------------------------------------------------|
| 1 | **federal_cio_csv_path** | Specifies where the federal CIO csv is |

View file

@ -1130,10 +1130,9 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
class SeniorOfficialAdmin(ListHeaderAdmin):
"""Custom Senior Official Admin class."""
# NOTE: these are just placeholders. Not part of ACs (haven't been defined yet). Update in future tickets.
search_fields = ["first_name", "last_name", "email"]
search_help_text = "Search by first name, last name or email."
list_display = ["first_name", "last_name", "email"]
list_display = ["first_name", "last_name", "email", "federal_agency"]
# this ordering effects the ordering of results
# in autocomplete_fields for Senior Official
@ -2841,6 +2840,9 @@ class PortfolioAdmin(ListHeaderAdmin):
list_display = ("organization_name", "federal_agency", "creator")
search_fields = ["organization_name"]
search_help_text = "Search by organization name."
readonly_fields = [
"creator",
]
# Creates select2 fields (with search bars)
autocomplete_fields = [
@ -2863,7 +2865,7 @@ class PortfolioAdmin(ListHeaderAdmin):
def save_model(self, request, obj, form, change):
if obj.creator is not None:
if hasattr(obj, "creator") is False:
# ---- update creator ----
# Set the creator field to the current admin user
obj.creator = request.user if request.user.is_authenticated else None

View file

@ -1985,3 +1985,122 @@ document.addEventListener('DOMContentLoaded', function() {
showInputOnErrorFields();
})();
/**
* An IIFE that changes the default clear behavior on comboboxes to the input field.
* We want the search bar to act soley as a search bar.
*/
(function loadInitialValuesForComboBoxes() {
var overrideDefaultClearButton = true;
var isTyping = false;
document.addEventListener('DOMContentLoaded', (event) => {
handleAllComboBoxElements();
});
function handleAllComboBoxElements() {
const comboBoxElements = document.querySelectorAll(".usa-combo-box");
comboBoxElements.forEach(comboBox => {
const input = comboBox.querySelector("input");
const select = comboBox.querySelector("select");
if (!input || !select) {
console.warn("No combobox element found");
return;
}
// Set the initial value of the combobox
let initialValue = select.getAttribute("data-default-value");
let clearInputButton = comboBox.querySelector(".usa-combo-box__clear-input");
if (!clearInputButton) {
console.warn("No clear element found");
return;
}
// Override the default clear button behavior such that it no longer clears the input,
// it just resets to the data-initial-value.
// Due to the nature of how uswds works, this is slightly hacky.
// Use a MutationObserver to watch for changes in the dropdown list
const dropdownList = document.querySelector(`#${input.id}--list`);
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === "childList") {
addBlankOption(clearInputButton, dropdownList, initialValue);
}
});
});
// Configure the observer to watch for changes in the dropdown list
const config = { childList: true, subtree: true };
observer.observe(dropdownList, config);
// Input event listener to detect typing
input.addEventListener("input", () => {
isTyping = true;
});
// Blur event listener to reset typing state
input.addEventListener("blur", () => {
isTyping = false;
});
// Hide the reset button when there is nothing to reset.
// Do this once on init, then everytime a change occurs.
updateClearButtonVisibility(select, initialValue, clearInputButton)
select.addEventListener("change", () => {
updateClearButtonVisibility(select, initialValue, clearInputButton)
});
// Change the default input behaviour - have it reset to the data default instead
clearInputButton.addEventListener("click", (e) => {
if (overrideDefaultClearButton && initialValue) {
e.preventDefault();
e.stopPropagation();
input.click();
// Find the dropdown option with the desired value
const dropdownOptions = document.querySelectorAll(".usa-combo-box__list-option");
if (dropdownOptions) {
dropdownOptions.forEach(option => {
if (option.getAttribute("data-value") === initialValue) {
// Simulate a click event on the dropdown option
option.click();
}
});
}
}
});
});
}
function updateClearButtonVisibility(select, initialValue, clearInputButton) {
if (select.value === initialValue) {
hideElement(clearInputButton);
}else {
showElement(clearInputButton)
}
}
function addBlankOption(clearInputButton, dropdownList, initialValue) {
if (dropdownList && !dropdownList.querySelector('[data-value=""]') && !isTyping) {
const blankOption = document.createElement("li");
blankOption.setAttribute("role", "option");
blankOption.setAttribute("data-value", "");
blankOption.classList.add("usa-combo-box__list-option");
if (!initialValue){
blankOption.classList.add("usa-combo-box__list-option--selected")
}
blankOption.textContent = "---------";
dropdownList.insertBefore(blankOption, dropdownList.firstChild);
blankOption.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
overrideDefaultClearButton = false;
// Trigger the default clear behavior
clearInputButton.click();
overrideDefaultClearButton = true;
});
}
}
})();

View file

@ -192,6 +192,11 @@ urlpatterns = [
views.DomainOrgNameAddressView.as_view(),
name="domain-org-name-address",
),
path(
"domain/<int:pk>/suborganization",
views.DomainSubOrganizationView.as_view(),
name="domain-suborganization",
),
path(
"domain/<int:pk>/senior-official",
views.DomainSeniorOfficialView.as_view(),

View file

@ -9,6 +9,7 @@ from .domain import (
DomainDnssecForm,
DomainDsdataFormset,
DomainDsdataForm,
DomainSuborganizationForm,
)
from .portfolio import (
PortfolioOrgAddressForm,

View file

@ -6,6 +6,7 @@ from django.core.validators import MinValueValidator, MaxValueValidator, RegexVa
from django.forms import formset_factory
from registrar.models import DomainRequest
from phonenumber_field.widgets import RegionalPhoneNumberWidget
from registrar.models.suborganization import Suborganization
from registrar.models.utility.domain_helper import DomainHelper
from registrar.utility.errors import (
NameserverError,
@ -153,6 +154,42 @@ class DomainNameserverForm(forms.Form):
self.add_error("ip", str(e))
class DomainSuborganizationForm(forms.ModelForm):
"""Form for updating the suborganization"""
sub_organization = forms.ModelChoiceField(
queryset=Suborganization.objects.none(),
required=False,
widget=forms.Select(),
)
class Meta:
model = DomainInformation
fields = [
"sub_organization",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
portfolio = self.instance.portfolio if self.instance else None
self.fields["sub_organization"].queryset = Suborganization.objects.filter(portfolio=portfolio)
# Set initial value
if self.instance and self.instance.sub_organization:
self.fields["sub_organization"].initial = self.instance.sub_organization
# Set custom form label
self.fields["sub_organization"].label = "Suborganization name"
# Use the combobox rather than the regular select widget
self.fields["sub_organization"].widget.template_name = "django/forms/widgets/combobox.html"
# Set data-default-value attribute
if self.instance and self.instance.sub_organization:
self.fields["sub_organization"].widget.attrs["data-default-value"] = self.instance.sub_organization.pk
class BaseNameserverFormset(forms.BaseFormSet):
def clean(self):
"""

View file

@ -0,0 +1,123 @@
import argparse
import csv
import logging
import os
from django.core.management import BaseCommand
from registrar.management.commands.utility.terminal_helper import TerminalHelper, TerminalColors
from registrar.models import SeniorOfficial, FederalAgency
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = """Populates the SeniorOfficial table based off of a given csv"""
def add_arguments(self, parser):
"""Add command line arguments."""
parser.add_argument("federal_cio_csv_path", help="A csv containing information about federal CIOs")
def handle(self, federal_cio_csv_path, **kwargs):
"""Populates the SeniorOfficial table with data given to it through a CSV"""
# Check if the provided file path is valid.
if not os.path.isfile(federal_cio_csv_path):
raise argparse.ArgumentTypeError(f"Invalid file path '{federal_cio_csv_path}'")
TerminalHelper.prompt_for_execution(
system_exit_on_terminate=True,
info_to_inspect=f"""
==Proposed Changes==
CSV: {federal_cio_csv_path}
For each item in this CSV, a SeniorOffical record will be added.
Note:
- If the row is missing SO data - it will not be added.
- Given we can add the row, any blank first_name will be replaced with "-".
""", # noqa: W291
prompt_title="Do you wish to load records into the SeniorOfficial table?",
)
logger.info("Updating...")
# Get all existing data.
self.existing_senior_officials = SeniorOfficial.objects.all().prefetch_related("federal_agency")
self.existing_agencies = FederalAgency.objects.all()
# Read the CSV
self.added_senior_officials = []
self.skipped_rows = []
with open(federal_cio_csv_path, "r") as requested_file:
for row in csv.DictReader(requested_file):
# Note: the csv files we have received do not currently have a phone field.
# However, we will include it in our kwargs because that is the data we are mapping to
# and it seems best to check for the data even if it ends up not being there.
so_kwargs = {
"first_name": row.get("First Name"),
"last_name": row.get("Last Name"),
"title": row.get("Role/Position"),
"email": row.get("Email"),
"phone": row.get("Phone"),
}
# Clean the returned data
for key, value in so_kwargs.items():
if isinstance(value, str):
so_kwargs[key] = value.strip()
# Handle the federal_agency record seperately (db call)
agency_name = row.get("Agency").strip() if row.get("Agency") else None
if agency_name:
so_kwargs["federal_agency"] = self.existing_agencies.filter(agency=agency_name).first()
# Check if at least one field has a non-empty value
if row and any(so_kwargs.values()):
# Split into a function: C901 'Command.handle' is too complex.
# Doesn't add it to the DB, but just inits a class of SeniorOfficial.
self.create_senior_official(so_kwargs)
else:
self.skipped_rows.append(row)
message = f"Skipping row (no data was found): {row}"
TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message)
# Bulk create the SO fields
if len(self.added_senior_officials) > 0:
SeniorOfficial.objects.bulk_create(self.added_senior_officials)
added_message = f"Added {len(self.added_senior_officials)} records"
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKBLUE, added_message)
if len(self.skipped_rows) > 0:
skipped_message = f"Skipped {len(self.skipped_rows)} records"
TerminalHelper.colorful_logger(logger.warning, TerminalColors.MAGENTA, skipped_message)
def create_senior_official(self, so_kwargs):
"""Creates a senior official object from kwargs but does not add it to the DB"""
# WORKAROUND: Placeholder value for first name,
# as not having these makes it impossible to access through DJA.
old_first_name = so_kwargs["first_name"]
if not so_kwargs["first_name"]:
so_kwargs["first_name"] = "-"
# Create a new SeniorOfficial object
new_so = SeniorOfficial(**so_kwargs)
# Store a variable for the console logger
if all([old_first_name, new_so.last_name]):
record_display = new_so
else:
record_display = so_kwargs
# Before adding this record, check to make sure we aren't adding a duplicate.
duplicate_field = self.existing_senior_officials.filter(**so_kwargs).exists()
if not duplicate_field:
self.added_senior_officials.append(new_so)
message = f"Creating record: {record_display}"
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, message)
else:
# if this field is a duplicate, don't do anything
self.skipped_rows.append(new_so)
message = f"Skipping add on duplicate record: {record_display}"
TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message)

View file

@ -0,0 +1,56 @@
import argparse
import csv
import logging
import os
from django.core.management import BaseCommand
from registrar.management.commands.utility.terminal_helper import TerminalHelper, PopulateScriptTemplate, TerminalColors
from registrar.models import FederalAgency
logger = logging.getLogger(__name__)
class Command(BaseCommand, PopulateScriptTemplate):
help = """Populates the initials and fceb fields for FederalAgencies"""
def add_arguments(self, parser):
"""Add command line arguments."""
parser.add_argument("federal_cio_csv_path", help="A csv containing information about federal CIOs")
def handle(self, federal_cio_csv_path, **kwargs):
"""Loops through each FederalAgency object and attempts to update is_fceb and initials"""
# Check if the provided file path is valid.
if not os.path.isfile(federal_cio_csv_path):
raise argparse.ArgumentTypeError(f"Invalid file path '{federal_cio_csv_path}'")
# Returns a dictionary keyed by the agency name containing initials and agency status
self.federal_agency_dict = {}
with open(federal_cio_csv_path, "r") as requested_file:
for row in csv.DictReader(requested_file):
agency_name = row.get("Agency")
if agency_name:
initials = row.get("Initials")
agency_status = row.get("Agency Status")
self.federal_agency_dict[agency_name.strip()] = (initials, agency_status)
# Update every federal agency record
self.mass_update_records(FederalAgency, {"agency__isnull": False}, ["initials", "is_fceb"])
def update_record(self, record: FederalAgency):
"""For each record, update the initials and is_fceb field if data exists for it"""
initials, agency_status = self.federal_agency_dict.get(record.agency)
record.initials = initials
if agency_status and isinstance(agency_status, str) and agency_status.strip().upper() == "FCEB":
record.is_fceb = True
else:
record.is_fceb = False
message = f"Updating {record} => initials: {initials} | is_fceb: {record.is_fceb}"
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKCYAN, message)
def should_skip_record(self, record) -> bool:
"""Skip record update if there is no data for that particular agency"""
return record.agency not in self.federal_agency_dict

View file

@ -317,6 +317,7 @@ class TerminalHelper:
case _:
logger.info(print_statement)
# TODO - "info_to_inspect" should be refactored to "prompt_message"
@staticmethod
def prompt_for_execution(system_exit_on_terminate: bool, info_to_inspect: str, prompt_title: str) -> bool:
"""Create to reduce code complexity.
@ -373,3 +374,26 @@ class TerminalHelper:
logger.info(f"{TerminalColors.MAGENTA}Writing to file " f" {filepath}..." f"{TerminalColors.ENDC}")
with open(f"{filepath}", "w+") as f:
f.write(file_contents)
@staticmethod
def colorful_logger(log_level, color, message):
"""Adds some color to your log output.
Args:
log_level: str | Logger.method -> Desired log level. ex: logger.info or "INFO"
color: str | TerminalColors -> Output color. ex: TerminalColors.YELLOW or "YELLOW"
message: str -> Message to display.
"""
if isinstance(log_level, str) and hasattr(logger, log_level.lower()):
log_method = getattr(logger, log_level.lower())
else:
log_method = log_level
if isinstance(color, str) and hasattr(TerminalColors, color.upper()):
terminal_color = getattr(TerminalColors, color.upper())
else:
terminal_color = color
colored_message = f"{terminal_color}{message}{TerminalColors.ENDC}"
log_method(colored_message)

View file

@ -0,0 +1,53 @@
# Generated by Django 4.2.10 on 2024-08-06 19:54
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("registrar", "0115_portfolioinvitation"),
]
operations = [
migrations.AddField(
model_name="federalagency",
name="initials",
field=models.CharField(blank=True, help_text="Agency initials", max_length=10, null=True),
),
migrations.AddField(
model_name="federalagency",
name="is_fceb",
field=models.BooleanField(
blank=True, help_text="Determines if this agency is FCEB", null=True, verbose_name="FCEB"
),
),
migrations.AddField(
model_name="seniorofficial",
name="federal_agency",
field=models.ForeignKey(
blank=True,
help_text="The federal agency this user is associated with",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="so_federal_agency",
to="registrar.federalagency",
),
),
migrations.AlterField(
model_name="seniorofficial",
name="first_name",
field=models.CharField(blank=True, null=True, verbose_name="first name"),
),
migrations.AlterField(
model_name="seniorofficial",
name="last_name",
field=models.CharField(blank=True, null=True, verbose_name="last name"),
),
migrations.AlterField(
model_name="seniorofficial",
name="title",
field=models.CharField(blank=True, null=True, verbose_name="title / role"),
),
]

View file

@ -0,0 +1,66 @@
# Generated by Django 4.2.10 on 2024-08-08 14:14
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0116_federalagency_initials_federalagency_is_fceb_and_more"),
]
operations = [
migrations.AlterField(
model_name="portfolioinvitation",
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"),
("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"),
("view_suborganization", "View suborganization"),
("edit_suborganization", "Edit suborganization"),
],
max_length=50,
),
blank=True,
help_text="Select one or more additional permissions.",
null=True,
size=None,
),
),
migrations.AlterField(
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"),
("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"),
("view_suborganization", "View suborganization"),
("edit_suborganization", "Edit suborganization"),
],
max_length=50,
),
blank=True,
help_text="Select one or more additional permissions.",
null=True,
size=None,
),
),
]

View file

@ -25,6 +25,20 @@ class FederalAgency(TimeStampedModel):
help_text="Federal agency type (executive, judicial, legislative, etc.)",
)
initials = models.CharField(
max_length=10,
null=True,
blank=True,
help_text="Agency initials",
)
is_fceb = models.BooleanField(
null=True,
blank=True,
verbose_name="FCEB",
help_text="Determines if this agency is FCEB",
)
def __str__(self) -> str:
return f"{self.agency}"

View file

@ -12,30 +12,43 @@ class SeniorOfficial(TimeStampedModel):
"""
first_name = models.CharField(
null=False,
blank=False,
null=True,
blank=True,
verbose_name="first name",
)
last_name = models.CharField(
null=False,
blank=False,
null=True,
blank=True,
verbose_name="last name",
)
title = models.CharField(
null=False,
blank=False,
null=True,
blank=True,
verbose_name="title / role",
)
phone = PhoneNumberField(
null=True,
blank=True,
)
email = models.EmailField(
null=True,
blank=True,
max_length=320,
)
federal_agency = models.ForeignKey(
"registrar.FederalAgency",
on_delete=models.PROTECT,
null=True,
blank=True,
related_name="so_federal_agency",
help_text="The federal agency this user is associated with",
)
def get_formatted_name(self):
"""Returns the contact's name in Western order."""
names = [n for n in [self.first_name, self.last_name] if n]

View file

@ -4,6 +4,7 @@ from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models import Q
from registrar.models.domain_information import DomainInformation
from registrar.models.user_domain_role import UserDomainRole
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
@ -73,12 +74,17 @@ class User(AbstractUser):
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
# Domain: field specific permissions
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
],
UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
UserPortfolioPermissionChoices.VIEW_MEMBER,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
# Domain: field specific permissions
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
],
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
@ -265,6 +271,17 @@ class User(AbstractUser):
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
) or self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
def has_view_all_domains_permission(self):
"""Determines if the current user can view all available domains in a given portfolio"""
return self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
# Field specific permission checks
def has_view_suborganization(self):
return self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION)
def has_edit_suborganization(self):
return self._has_portfolio_permission(UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION)
@classmethod
def needs_identity_verification(cls, email, uuid):
"""A method used by our oidc classes to test whether a user needs email/uuid verification
@ -406,3 +423,10 @@ class User(AbstractUser):
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()
def get_user_domain_ids(self, request):
"""Returns either the domains ids associated with this user on UserDomainRole or Portfolio"""
if self.is_org_user(request) and self.has_view_all_domains_permission():
return DomainInformation.objects.filter(portfolio=self.portfolio).values_list("domain_id", flat=True)
else:
return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True)

View file

@ -4,7 +4,6 @@ import time
import logging
from urllib.parse import urlparse, urlunparse, urlencode
logger = logging.getLogger(__name__)
@ -173,10 +172,6 @@ class CreateOrUpdateOrganizationTypeHelper:
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:
self.instance.is_election_board = False
if self.instance.is_election_board:
self.instance.organization_type = self.generic_org_to_org_map[generic_org_type]
else:
@ -219,12 +214,15 @@ class CreateOrUpdateOrganizationTypeHelper:
self.instance.is_election_board = None
self.instance.generic_org_type = None
def _validate_new_instance(self):
def _validate_new_instance(self) -> bool:
"""
Validates whether a new instance of DomainRequest or DomainInformation can proceed with the update
based on the consistency between organization_type, generic_org_type, and is_election_board.
Returns a boolean determining if execution should proceed or not.
Raises:
ValueError if there is a mismatch between organization_type, generic_org_type, and is_election_board
"""
# We conditionally accept both of these values to exist simultaneously, as long as
@ -242,13 +240,20 @@ class CreateOrUpdateOrganizationTypeHelper:
is_election_type = "_election" in organization_type
can_have_election_board = organization_type in self.generic_org_to_org_map
election_board_mismatch = (is_election_type != self.instance.is_election_board) and can_have_election_board
election_board_mismatch = (
is_election_type and not self.instance.is_election_board and can_have_election_board
)
org_type_mismatch = mapped_org_type is not None and (generic_org_type != mapped_org_type)
if election_board_mismatch or org_type_mismatch:
message = (
"Cannot add organization_type and generic_org_type simultaneously "
"when generic_org_type, is_election_board, and organization_type values do not match."
"Cannot add organization_type and generic_org_type simultaneously when"
"generic_org_type ({}), is_election_board ({}), and organization_type ({}) don't match.".format(
generic_org_type, self.instance.is_election_board, organization_type
)
)
message = "Mismatch on election board, {}".format(message) if election_board_mismatch else message
message = "Mistmatch on org type, {}".format(message) if org_type_mismatch else message
logger.error("_validate_new_instance: %s", message)
raise ValueError(message)
return True

View file

@ -26,3 +26,7 @@ class UserPortfolioPermissionChoices(models.TextChoices):
VIEW_PORTFOLIO = "view_portfolio", "View organization"
EDIT_PORTFOLIO = "edit_portfolio", "Edit organization"
# Domain: field specific permissions
VIEW_SUBORGANIZATION = "view_suborganization", "View suborganization"
EDIT_SUBORGANIZATION = "edit_suborganization", "Edit suborganization"

View file

@ -0,0 +1,14 @@
{% comment %}
This is a custom widget for USWDS's comboboxes.
USWDS comboboxes are basically just selects with a "usa-combo-box" div wrapper.
We can further customize these by applying attributes to this parent element,
for now we just carry the attribute to both the parent element and the select.
{% endcomment %}
<div class="usa-combo-box"
{% for name, value in widget.attrs.items %}
{{ name }}="{{ value }}"
{% endfor %}
>
{% include "django/forms/widgets/select.html" %}
</div>

View file

@ -1,5 +1,6 @@
{% extends "domain_base.html" %}
{% load static url_helpers %}
{% load custom_filters %}
{% block domain_content %}
{{ block.super }}
@ -64,11 +65,17 @@
{% endif %}
{% endif %}
{% url 'domain-org-name-address' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url editable=is_editable %}
{% if portfolio and has_domains_portfolio_permission and request.user.has_view_suborganization %}
{% url 'domain-suborganization' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:request.user.has_edit_suborganization %}
{% else %}
{% url 'domain-org-name-address' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url editable=is_editable %}
{% url 'domain-senior-official' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Senior official' value=domain.domain_info.senior_official contact='true' edit_link=url editable=is_editable %}
{% endif %}
{% url 'domain-senior-official' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Senior official' value=domain.domain_info.senior_official contact='true' edit_link=url editable=is_editable %}
{# Conditionally display profile #}
{% if not has_profile_feature_flag %}

View file

@ -6,8 +6,7 @@
<ul class="usa-list">
<li>Be available </li>
<li>Relate to your organizations name, location, and/or services </li>
<li>Be clear to the general public. Your domain name must not be easily confused
with other organizations.</li>
<li>Be unlikely to mislead or confuse the general public (even if your domain is only intended for a specific audience) </li>
</ul>
</p>

View file

@ -3,16 +3,19 @@
<div class="margin-bottom-4 tablet:margin-bottom-0">
<nav aria-label="Domain sections">
<ul class="usa-sidenav">
<li class="usa-sidenav__item">
{% url 'domain' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
Domain overview
</a>
</li>
{% with url_name="domain" %}
{% include "includes/domain_sidenav_item.html" with item_text="Domain overview" %}
{% endwith %}
{% if is_editable %}
{% if not portfolio %}
{% with url_name="domain-org-name-address" %}
{% include "includes/domain_sidenav_item.html" with item_text="Organization name and mailing address" %}
{% endwith %}
{% endif %}
<li class="usa-sidenav__item">
{% url 'domain-dns' pk=domain.id as url %}
<a href="{{ url }}" {% if request.path|startswith:url %}class="usa-current"{% endif %}>
@ -55,53 +58,35 @@
{% endif %}
</li>
<li class="usa-sidenav__item">
{% url 'domain-org-name-address' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
Organization name and mailing address
</a>
</li>
<li class="usa-sidenav__item">
{% url 'domain-senior-official' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
Senior official
</a>
</li>
{% if portfolio %}
{% comment %} Only show this menu option if the user has the perms to do so {% endcomment %}
{% if has_domains_portfolio_permission and request.user.has_view_suborganization %}
{% with url_name="domain-suborganization" %}
{% include "includes/domain_sidenav_item.html" with item_text="Suborganization" %}
{% endwith %}
{% endif %}
{% else %}
{% with url_name="domain-senior-official" %}
{% include "includes/domain_sidenav_item.html" with item_text="Senior official" %}
{% endwith %}
{% endif %}
{% if not has_profile_feature_flag %}
{# Conditionally display profile link in main nav #}
<li class="usa-sidenav__item">
{% url 'domain-your-contact-information' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
Your contact information
</a>
</li>
{% with url_name="domain-your-contact-information" %}
{% include "includes/domain_sidenav_item.html" with item_text="Your contact information" %}
{% endwith %}
{% endif %}
<li class="usa-sidenav__item">
{% url 'domain-security-email' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
Security email
</a>
</li>
{% with url_name="domain-security-email" %}
{% include "includes/domain_sidenav_item.html" with item_text="Security email" %}
{% endwith %}
{% with url_name="domain-users" %}
{% include "includes/domain_sidenav_item.html" with item_text="Domain managers" %}
{% endwith %}
<li class="usa-sidenav__item">
{% url 'domain-users' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path|startswith:url %}class="usa-current"{% endif %}
>
Domain managers
</a>
</li>
{% endif %}
</ul>
</nav>

View file

@ -0,0 +1,29 @@
{% extends "domain_base.html" %}
{% load static field_helpers%}
{% block title %}Suborganization{% endblock %}
{% block domain_content %}
{# this is right after the messages block in the parent template #}
{% include "includes/form_errors.html" with form=form %}
<h1>Suborganization</h1>
<p>
The name of your suborganization will be publicly listed as the domain registrant.
This list of suborganizations has been populated the .gov program.
If you believe there is an error please contact <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
</p>
{% if has_domains_portfolio_permission and request.user.has_edit_suborganization %}
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
{% csrf_token %}
{% input_with_errors form.sub_organization %}
<button type="submit" class="usa-button">Save</button>
</form>
{% else %}
{% with description="The suborganization for this domain can only be updated by a organization administrator."%}
{% include "includes/input_read_only.html" with field=form.sub_organization value=suborganization_name label_description=description%}
{% endwith %}
{% endif %}
{% endblock %}

View file

@ -29,7 +29,7 @@
</a>
</p>
{% include "includes/domains_table.html" %}
{% include "includes/domains_table.html" with user_domain_count=user_domain_count %}
{% include "includes/domain_requests_table.html" %}
</div>

View file

@ -0,0 +1,10 @@
<li class="usa-sidenav__item">
{% if url_name %}
{% url url_name pk=domain.id as url %}
{% endif %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
{{ item_text }}
</a>
</li>

View file

@ -37,6 +37,7 @@
</form>
</section>
</div>
{% if user_domain_count and user_domain_count > 0 %}
<div class="section--outlined__utility-button mobile-lg:padding-right-105 {% if has_domains_portfolio_permission %} 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">
@ -46,6 +47,7 @@
</a>
</section>
</div>
{% endif %}
</div>
{% if has_domains_portfolio_permission %}
<div class="display-flex flex-align-center">
@ -150,7 +152,7 @@
<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>
{% if has_domains_portfolio_permission %}
{% if has_domains_portfolio_permission and request.user.has_view_suborganization %}
<th data-sortable="suborganization" scope="col" role="columnheader">Suborganization</th>
{% endif %}
<th

View file

@ -56,12 +56,13 @@
{% with toggleable_input=True toggleable_label=True group_classes="usa-form-editable padding-top-2" %}
{% input_with_errors form.title %}
{% endwith %}
{% public_site_url "help/account-management/#get-help-with-login.gov" as login_help_url %}
{% public_site_url "help/account-management/#email-address" as login_help_url %}
{% with toggleable_input=True add_class="display-none" group_classes="usa-form-editable usa-form-editable padding-top-2 bold-usa-label" %}
{% with link_href=login_help_url %}
{% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, youll need to update your Login.gov account and log back in. Get help with your Login.gov account." %}
{% with link_text="Get help with your Login.gov account" target_blank=True do_not_show_max_chars=True %}
{% with sublabel_text="We recommend using a Login.gov account that's only connected to your work email address. If you need to change your email, you'll need to make a change to your Login.gov account. Get help with updating your email address." %}
{% with link_text="Get help with updating your email address" target_blank=True do_not_show_max_chars=True %}
{% input_with_errors form.email %}
{% endwith %}
{% endwith %}

View file

@ -4,4 +4,11 @@ Template include for read-only form fields
<h4 class="read-only-label">{{ field.label }}</h4>
<p class="read-only-value">{{ field.value }}</p>
{% if label_description %}
<p class="usa-hint margin-top-0 margin-bottom-05">{{ label_description }}</p>
{% endif %}
{% comment %}
This allows us to customize the displayed value.
For instance, Select fields will display the id by default.
{% endcomment %}
<p class="read-only-value">{{ value|default:field.value }}</p>

View file

@ -30,11 +30,11 @@
{% input_with_errors form.title %}
{% public_site_url "help/account-management/#get-help-with-login.gov" as login_help_url %}
{% public_site_url "help/account-management/#email-address" as login_help_url %}
{% with link_href=login_help_url %}
{% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, youll need to update your Login.gov account and log back in. Get help with your Login.gov account." %}
{% with link_text="Get help with your Login.gov account" %}
{% with sublabel_text="We recommend using a Login.gov account that's only connected to your work email address. If you need to change your email, you'll need to make a change to your Login.gov account. Get help with updating your email address." %}
{% with link_text="Get help with updating your email address" %}
{% with target_blank=True %}
{% with do_not_show_max_chars=True %}
{% input_with_errors form.email %}

View file

@ -6,5 +6,5 @@
{% block portfolio_content %}
<h1 id="domains-header">Domains</h1>
{% include "includes/domains_table.html" with portfolio=portfolio %}
{% include "includes/domains_table.html" with portfolio=portfolio user_domain_count=user_domain_count %}
{% endblock %}

View file

@ -150,3 +150,12 @@ def format_phone(value):
@register.filter
def in_path(url, path):
return url in path
@register.filter(name="and")
def and_filter(value, arg):
"""
Implements logical AND operation in templates.
Usage: {{ value|and:arg }}
"""
return bool(value and arg)

View file

@ -0,0 +1,5 @@
Contact Status,Email,First Name,Last Name,Role/Position,Initials,Agency,Agency Status,Notes,Active?,Modified,Created
Unconfirmed,fakemrfake@igorville.gov,Jan,Uary,CIO,ABMC,American Battle Monuments Commission,FCEB,,,,,
Import,reggie.ronald@igorville.gov,Reggie,Ronald,CIO,ACHP,Advisory Council on Historic Preservation,FCEB,Some notes field,,,
,,,,,AMTRAK,,,,,,
,,,,,KC,John F. Kennedy Center for Performing Arts,,,,,
1 Contact Status,Email,First Name,Last Name,Role/Position,Initials,Agency,Agency Status,Notes,Active?,Modified,Created
2 Unconfirmed,fakemrfake@igorville.gov,Jan,Uary,CIO,ABMC,American Battle Monuments Commission,FCEB,,,,,
3 Import,reggie.ronald@igorville.gov,Reggie,Ronald,CIO,ACHP,Advisory Council on Historic Preservation,FCEB,Some notes field,,,
4 ,,,,,AMTRAK,,,,,,
5 ,,,,,KC,John F. Kennedy Center for Performing Arts,,,,,

View file

@ -2,6 +2,7 @@ import copy
from datetime import date, datetime, time
from django.core.management import call_command
from django.test import TestCase, override_settings
from registrar.models.senior_official import SeniorOfficial
from registrar.utility.constants import BranchChoices
from django.utils import timezone
from django.utils.module_loading import import_string
@ -1285,3 +1286,125 @@ class TestTransferFederalAgencyType(TestCase):
# We don't expect this field to be updated (as it has duplicate data)
self.assertEqual(self.gov_admin.federal_type, None)
class TestLoadSeniorOfficialTable(TestCase):
def setUp(self):
super().setUp()
self.csv_path = "registrar/tests/data/fake_federal_cio.csv"
def tearDown(self):
super().tearDown()
SeniorOfficial.objects.all().delete()
FederalAgency.objects.all().delete()
@less_console_noise_decorator
def run_load_senior_official_table(self):
with patch(
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit",
return_value=True,
):
call_command("load_senior_official_table", self.csv_path)
@less_console_noise_decorator
def test_load_senior_official_table(self):
"""Ensures that running the senior official script creates the data we expect"""
# Get test FederalAgency objects
abmc, _ = FederalAgency.objects.get_or_create(agency="American Battle Monuments Commission")
achp, _ = FederalAgency.objects.get_or_create(agency="Advisory Council on Historic Preservation")
# run the script
self.run_load_senior_official_table()
# Check the data returned by the script
jan_uary = SeniorOfficial.objects.get(first_name="Jan", last_name="Uary")
self.assertEqual(jan_uary.title, "CIO")
self.assertEqual(jan_uary.email, "fakemrfake@igorville.gov")
self.assertEqual(jan_uary.federal_agency, abmc)
reggie_ronald = SeniorOfficial.objects.get(first_name="Reggie", last_name="Ronald")
self.assertEqual(reggie_ronald.title, "CIO")
self.assertEqual(reggie_ronald.email, "reggie.ronald@igorville.gov")
self.assertEqual(reggie_ronald.federal_agency, achp)
# Two should be created in total
self.assertEqual(SeniorOfficial.objects.count(), 2)
@less_console_noise_decorator
def test_load_senior_official_table_duplicate_entry(self):
"""Ensures that duplicate data won't be created"""
# Create a SeniorOfficial that matches one in the CSV
abmc, _ = FederalAgency.objects.get_or_create(agency="American Battle Monuments Commission")
SeniorOfficial.objects.create(
first_name="Jan", last_name="Uary", title="CIO", email="fakemrfake@igorville.gov", federal_agency=abmc
)
self.assertEqual(SeniorOfficial.objects.count(), 1)
# run the script
self.run_load_senior_official_table()
# Check if only one new SeniorOfficial object was created
self.assertEqual(SeniorOfficial.objects.count(), 2)
class TestPopulateFederalAgencyInitialsAndFceb(TestCase):
def setUp(self):
self.csv_path = "registrar/tests/data/fake_federal_cio.csv"
# Create test FederalAgency objects
self.agency1, _ = FederalAgency.objects.get_or_create(agency="American Battle Monuments Commission")
self.agency2, _ = FederalAgency.objects.get_or_create(agency="Advisory Council on Historic Preservation")
self.agency3, _ = FederalAgency.objects.get_or_create(agency="AMTRAK")
self.agency4, _ = FederalAgency.objects.get_or_create(agency="John F. Kennedy Center for Performing Arts")
def tearDown(self):
SeniorOfficial.objects.all().delete()
FederalAgency.objects.all().delete()
@less_console_noise_decorator
def run_populate_federal_agency_initials_and_fceb(self):
with patch(
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit",
return_value=True,
):
call_command("populate_federal_agency_initials_and_fceb", self.csv_path)
@less_console_noise_decorator
def test_populate_federal_agency_initials_and_fceb(self):
"""Ensures that the script generates the data we want"""
self.run_populate_federal_agency_initials_and_fceb()
# Refresh the objects from the database
self.agency1.refresh_from_db()
self.agency2.refresh_from_db()
self.agency3.refresh_from_db()
self.agency4.refresh_from_db()
# Check if FederalAgency objects were updated correctly
self.assertEqual(self.agency1.initials, "ABMC")
self.assertTrue(self.agency1.is_fceb)
self.assertEqual(self.agency2.initials, "ACHP")
self.assertTrue(self.agency2.is_fceb)
# We expect that this field doesn't have any data,
# as none is specified in the CSV
self.assertIsNone(self.agency3.initials)
self.assertIsNone(self.agency3.is_fceb)
self.assertEqual(self.agency4.initials, "KC")
self.assertFalse(self.agency4.is_fceb)
@less_console_noise_decorator
def test_populate_federal_agency_initials_and_fceb_missing_agency(self):
"""A test to ensure that the script doesn't modify unrelated fields"""
# Add a FederalAgency that's not in the CSV
missing_agency = FederalAgency.objects.create(agency="Missing Agency")
self.run_populate_federal_agency_initials_and_fceb()
# Verify that the missing agency was not updated
missing_agency.refresh_from_db()
self.assertIsNone(missing_agency.initials)
self.assertIsNone(missing_agency.is_fceb)

View file

@ -1495,11 +1495,28 @@ class TestDomainRequestCustomSave(TestCase):
self.assertEqual(domain_request.is_election_board, False)
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
# Try reverting setting an invalid value for election board (should revert to False)
@less_console_noise_decorator
def test_existing_instance_updates_election_board_to_none(self):
"""Test create_or_update_organization_type for an existing instance, first to True and then to None.
Start our with is_election_board as none to simulate a situation where the request was started, but
only completed to the point of filling out the generic_org_type."""
domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.STARTED,
name="started.gov",
generic_org_type=DomainRequest.OrganizationChoices.CITY,
is_election_board=None,
)
domain_request.is_election_board = True
domain_request.save()
self.assertEqual(domain_request.is_election_board, True)
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
# Try reverting the election board value.
domain_request.is_election_board = None
domain_request.save()
self.assertEqual(domain_request.is_election_board, False)
self.assertEqual(domain_request.is_election_board, None)
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
@less_console_noise_decorator
@ -1654,11 +1671,30 @@ class TestDomainInformationCustomSave(TestCase):
self.assertEqual(domain_information.is_election_board, False)
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
# Try reverting setting an invalid value for election board (should revert to False)
domain_information.is_election_board = None
@less_console_noise_decorator
def test_existing_instance_update_election_board_to_none(self):
"""Test create_or_update_organization_type for an existing instance, first to True and then to None.
Start our with is_election_board as none to simulate a situation where the request was started, but
only completed to the point of filling out the generic_org_type."""
domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.STARTED,
name="started.gov",
generic_org_type=DomainRequest.OrganizationChoices.CITY,
is_election_board=None,
)
domain_information = DomainInformation.create_from_da(domain_request)
domain_information.is_election_board = True
domain_information.save()
self.assertEqual(domain_information.is_election_board, False)
self.assertEqual(domain_information.is_election_board, True)
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
# Try reverting the election board value
domain_information.is_election_board = None
domain_information.save()
domain_information.refresh_from_db()
self.assertEqual(domain_information.is_election_board, None)
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
@less_console_noise_decorator
@ -1858,8 +1894,7 @@ class TestDomainRequestIncomplete(TestCase):
self.assertTrue(self.domain_request._is_state_or_territory_complete())
self.domain_request.is_election_board = None
self.domain_request.save()
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
self.assertTrue(self.domain_request._is_state_or_territory_complete())
self.assertFalse(self.domain_request._is_state_or_territory_complete())
@less_console_noise_decorator
def test_is_tribal_complete(self):
@ -1868,10 +1903,11 @@ class TestDomainRequestIncomplete(TestCase):
self.domain_request.is_election_board = False
self.domain_request.save()
self.assertTrue(self.domain_request._is_tribal_complete())
self.domain_request.tribe_name = None
self.domain_request.is_election_board = None
self.domain_request.save()
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
self.assertFalse(self.domain_request._is_tribal_complete())
self.domain_request.tribe_name = None
self.domain_request.save()
self.assertFalse(self.domain_request._is_tribal_complete())
@less_console_noise_decorator
@ -1882,8 +1918,7 @@ class TestDomainRequestIncomplete(TestCase):
self.assertTrue(self.domain_request._is_county_complete())
self.domain_request.is_election_board = None
self.domain_request.save()
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
self.assertTrue(self.domain_request._is_county_complete())
self.assertFalse(self.domain_request._is_county_complete())
@less_console_noise_decorator
def test_is_city_complete(self):
@ -1893,8 +1928,7 @@ class TestDomainRequestIncomplete(TestCase):
self.assertTrue(self.domain_request._is_city_complete())
self.domain_request.is_election_board = None
self.domain_request.save()
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
self.assertTrue(self.domain_request._is_city_complete())
self.assertFalse(self.domain_request._is_city_complete())
@less_console_noise_decorator
def test_is_special_district_complete(self):
@ -1903,10 +1937,11 @@ class TestDomainRequestIncomplete(TestCase):
self.domain_request.is_election_board = False
self.domain_request.save()
self.assertTrue(self.domain_request._is_special_district_complete())
self.domain_request.about_your_organization = None
self.domain_request.is_election_board = None
self.domain_request.save()
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
self.assertFalse(self.domain_request._is_special_district_complete())
self.domain_request.about_your_organization = None
self.domain_request.save()
self.assertFalse(self.domain_request._is_special_district_complete())
@less_console_noise_decorator

View file

@ -6,6 +6,8 @@ from registrar.models import (
Domain,
UserDomainRole,
)
from registrar.models import Portfolio
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from registrar.utility.csv_export import (
DomainDataFull,
DomainDataType,
@ -32,6 +34,7 @@ from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # typ
from django.utils import timezone
from api.tests.common import less_console_noise_decorator
from .common import MockDbForSharedTests, MockDbForIndividualTests, MockEppLib, less_console_noise, get_time_aware_date
from waffle.testutils import override_flag
class CsvReportsTest(MockDbForSharedTests):
@ -311,6 +314,80 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
self.maxDiff = None
self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_domain_data_type_user_with_portfolio(self):
"""Tests DomainDataTypeUser export with portfolio permissions"""
# Create a portfolio and assign it to the user
portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
self.user.portfolio = portfolio
self.user.save()
UserDomainRole.objects.create(user=self.user, domain=self.domain_2)
UserDomainRole.objects.filter(user=self.user, domain=self.domain_1).delete()
UserDomainRole.objects.filter(user=self.user, domain=self.domain_3).delete()
# Add portfolios to the first and third domains
self.domain_1.domain_info.portfolio = portfolio
self.domain_3.domain_info.portfolio = portfolio
self.domain_1.domain_info.save()
self.domain_3.domain_info.save()
# Set up user permissions
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
self.user.save()
self.user.refresh_from_db()
# Create a request object
factory = RequestFactory()
request = factory.get("/")
request.user = self.user
# Get the csv content
csv_content = self._run_domain_data_type_user_export(request)
# We expect only domains associated with the user's portfolio
self.assertIn(self.domain_1.name, csv_content)
self.assertIn(self.domain_3.name, csv_content)
self.assertNotIn(self.domain_2.name, csv_content)
# Test the output for readonly admin
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY]
self.user.save()
self.assertIn(self.domain_1.name, csv_content)
self.assertIn(self.domain_3.name, csv_content)
self.assertNotIn(self.domain_2.name, csv_content)
# Get the csv content
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
self.user.save()
csv_content = self._run_domain_data_type_user_export(request)
self.assertNotIn(self.domain_1.name, csv_content)
self.assertNotIn(self.domain_3.name, csv_content)
self.assertIn(self.domain_2.name, csv_content)
self.domain_1.delete()
self.domain_2.delete()
self.domain_3.delete()
portfolio.delete()
def _run_domain_data_type_user_export(self, request):
"""Helper function to run the export_data_to_csv function on DomainDataTypeUser"""
# 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()
return csv_content
@less_console_noise_decorator
def test_domain_data_full(self):
"""Shows security contacts, filtered by state"""

View file

@ -4,10 +4,9 @@ from unittest.mock import MagicMock, ANY, patch
from django.conf import settings
from django.urls import reverse
from django.contrib.auth import get_user_model
from waffle.testutils import override_flag
from api.tests.common import less_console_noise_decorator
from registrar.models.portfolio import Portfolio
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from .common import MockEppLib, MockSESClient, create_user # type: ignore
from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore
@ -35,6 +34,8 @@ from registrar.models import (
UserDomainRole,
User,
FederalAgency,
Portfolio,
Suborganization,
)
from datetime import date, datetime, timedelta
from django.utils import timezone
@ -140,6 +141,7 @@ class TestWithDomainPermissions(TestWithUser):
Host.objects.all().delete()
Domain.objects.all().delete()
UserDomainRole.objects.all().delete()
Suborganization.objects.all().delete()
Portfolio.objects.all().delete()
except ValueError: # pass if already deleted
pass
@ -1126,7 +1128,7 @@ class TestDomainSeniorOfficial(TestDomainOverview):
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)
self.assertContains(page, "Senior official", count=14)
@less_console_noise_decorator
def test_domain_senior_official_content(self):
@ -1346,7 +1348,7 @@ class TestDomainOrganization(TestDomainOverview):
"""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)
self.assertContains(page, "Organization name and mailing address", count=4)
@less_console_noise_decorator
def test_domain_org_name_address_content(self):
@ -1521,6 +1523,156 @@ class TestDomainOrganization(TestDomainOverview):
self.assertNotEqual(self.domain_information.federal_agency, new_value)
class TestDomainSuborganization(TestDomainOverview):
"""Tests the Suborganization page for portfolio users"""
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_edit_suborganization_field(self):
"""Ensure that org admins can edit the suborganization field"""
# Create a portfolio and two suborgs
portfolio = Portfolio.objects.create(creator=self.user, organization_name="Ice Cream")
suborg = Suborganization.objects.create(portfolio=portfolio, name="Vanilla")
suborg_2 = Suborganization.objects.create(portfolio=portfolio, name="Chocolate")
# Create an unrelated portfolio
unrelated_portfolio = Portfolio.objects.create(creator=self.user, organization_name="Fruit")
unrelated_suborg = Suborganization.objects.create(portfolio=unrelated_portfolio, name="Apple")
# Add the portfolio to the domain_information object
self.domain_information.portfolio = portfolio
self.domain_information.sub_organization = suborg
# Add a organization_name to test if the old value still displays
self.domain_information.organization_name = "Broccoli"
self.domain_information.save()
self.domain_information.refresh_from_db()
# Add portfolio perms to the user object
self.user.portfolio = portfolio
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
self.user.save()
self.user.refresh_from_db()
self.assertEqual(self.domain_information.sub_organization, suborg)
# Navigate to the suborganization page
page = self.app.get(reverse("domain-suborganization", kwargs={"pk": self.domain.id}))
# The page should contain the choices Vanilla and Chocolate
self.assertContains(page, "Vanilla")
self.assertContains(page, "Chocolate")
self.assertNotContains(page, unrelated_suborg.name)
# Assert that the right option is selected. This component uses data-default-value.
self.assertContains(page, f'data-default-value="{suborg.id}"')
# Try changing the suborg
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
page.form["sub_organization"] = suborg_2.id
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
page = page.form.submit().follow()
# The page should contain the choices Vanilla and Chocolate
self.assertContains(page, "Vanilla")
self.assertContains(page, "Chocolate")
self.assertNotContains(page, unrelated_suborg.name)
# Assert that the right option is selected
self.assertContains(page, f'data-default-value="{suborg_2.id}"')
self.domain_information.refresh_from_db()
self.assertEqual(self.domain_information.sub_organization, suborg_2)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_view_suborganization_field(self):
"""Only org admins can edit the suborg field, ensure that others cannot"""
# Create a portfolio and two suborgs
portfolio = Portfolio.objects.create(creator=self.user, organization_name="Ice Cream")
suborg = Suborganization.objects.create(portfolio=portfolio, name="Vanilla")
Suborganization.objects.create(portfolio=portfolio, name="Chocolate")
# Create an unrelated portfolio
unrelated_portfolio = Portfolio.objects.create(creator=self.user, organization_name="Fruit")
unrelated_suborg = Suborganization.objects.create(portfolio=unrelated_portfolio, name="Apple")
# Add the portfolio to the domain_information object
self.domain_information.portfolio = portfolio
self.domain_information.sub_organization = suborg
# Add a organization_name to test if the old value still displays
self.domain_information.organization_name = "Broccoli"
self.domain_information.save()
self.domain_information.refresh_from_db()
# Add portfolio perms to the user object
self.user.portfolio = portfolio
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY]
self.user.save()
self.user.refresh_from_db()
self.assertEqual(self.domain_information.sub_organization, suborg)
# Navigate to the suborganization page
page = self.app.get(reverse("domain-suborganization", kwargs={"pk": self.domain.id}))
# The page should display the readonly option
self.assertContains(page, "Vanilla")
# The page shouldn't contain these choices
self.assertNotContains(page, "Chocolate")
self.assertNotContains(page, unrelated_suborg.name)
self.assertNotContains(page, "Save")
self.assertContains(
page, "The suborganization for this domain can only be updated by a organization administrator."
)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_has_suborganization_field_on_overview_with_flag(self):
"""Ensures that the suborganization field is visible
and displays correctly on the domain overview page"""
# Create a portfolio
portfolio = Portfolio.objects.create(creator=self.user, organization_name="Ice Cream")
suborg = Suborganization.objects.create(portfolio=portfolio, name="Vanilla")
# Add the portfolio to the domain_information object
self.domain_information.portfolio = portfolio
# Add a organization_name to test if the old value still displays
self.domain_information.organization_name = "Broccoli"
self.domain_information.save()
self.domain_information.refresh_from_db()
# Add portfolio perms to the user object
self.user.portfolio = portfolio
self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_PORTFOLIO]
self.user.save()
self.user.refresh_from_db()
# Navigate to the domain overview page
page = self.app.get(reverse("domain", kwargs={"pk": self.domain.id}))
# Test for the title change
self.assertContains(page, "Suborganization")
self.assertNotContains(page, "Organization name")
# Test for the good value
self.assertContains(page, "Ice Cream")
# Test for the bad value
self.assertNotContains(page, "Broccoli")
# Cleanup
self.domain_information.delete()
suborg.delete()
portfolio.delete()
class TestDomainContactInformation(TestDomainOverview):
@less_console_noise_decorator
def test_domain_your_contact_information(self):

View file

@ -578,10 +578,9 @@ class DomainDataTypeUser(DomainDataType):
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)
else:
# Get all domains the user is associated with
return Q(domain__id__in=request.user.get_user_domain_ids(request))
class DomainDataFull(DomainExport):

View file

@ -3,6 +3,7 @@ from .domain import (
DomainView,
DomainSeniorOfficialView,
DomainOrgNameAddressView,
DomainSubOrganizationView,
DomainDNSView,
DomainNameserversView,
DomainDNSSECView,

View file

@ -15,7 +15,7 @@ from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic.edit import FormMixin
from django.conf import settings
from registrar.forms.domain import DomainSuborganizationForm
from registrar.models import (
Domain,
DomainRequest,
@ -23,8 +23,8 @@ from registrar.models import (
DomainInvitation,
User,
UserDomainRole,
PublicContact,
)
from registrar.models.public_contact import PublicContact
from registrar.utility.enums import DefaultEmail
from registrar.utility.errors import (
GenericError,
@ -231,6 +231,61 @@ class DomainOrgNameAddressView(DomainFormBaseView):
# superclass has the redirect
return super().form_valid(form)
def has_permission(self):
"""Override for the has_permission class to exclude portfolio users"""
# Org users shouldn't have access to this page
is_org_user = self.request.user.is_org_user(self.request)
if self.request.user.portfolio and is_org_user:
return False
else:
return super().has_permission()
class DomainSubOrganizationView(DomainFormBaseView):
"""Suborganization view"""
model = Domain
template_name = "domain_suborganization.html"
context_object_name = "domain"
form_class = DomainSuborganizationForm
def has_permission(self):
"""Override for the has_permission class to exclude non-portfolio users"""
# non-org users shouldn't have access to this page
is_org_user = self.request.user.is_org_user(self.request)
if self.request.user.portfolio and is_org_user:
return super().has_permission()
else:
return False
def get_context_data(self, **kwargs):
"""Adds custom context."""
context = super().get_context_data(**kwargs)
if self.object and self.object.domain_info and self.object.domain_info.sub_organization:
context["suborganization_name"] = self.object.domain_info.sub_organization.name
return context
def get_form_kwargs(self, *args, **kwargs):
"""Add domain_info.organization_name instance to make a bound form."""
form_kwargs = super().get_form_kwargs(*args, **kwargs)
form_kwargs["instance"] = self.object.domain_info
return form_kwargs
def get_success_url(self):
"""Redirect to the overview page for the domain."""
return reverse("domain-suborganization", kwargs={"pk": self.object.pk})
def form_valid(self, form):
"""The form is valid, save the organization name and mailing address."""
form.save()
messages.success(self.request, "The suborganization name for this domain has been updated.")
# superclass has the redirect
return super().form_valid(form)
class DomainSeniorOfficialView(DomainFormBaseView):
"""Domain senior official editing view."""
@ -248,7 +303,6 @@ class DomainSeniorOfficialView(DomainFormBaseView):
domain_info = self.get_domain_info_from_domain()
invalid_fields = [DomainRequest.OrganizationChoices.FEDERAL, DomainRequest.OrganizationChoices.TRIBAL]
is_federal_or_tribal = domain_info and (domain_info.generic_org_type in invalid_fields)
form_kwargs["disable_fields"] = is_federal_or_tribal
return form_kwargs
@ -276,6 +330,16 @@ class DomainSeniorOfficialView(DomainFormBaseView):
# superclass has the redirect
return super().form_valid(form)
def has_permission(self):
"""Override for the has_permission class to exclude portfolio users"""
# Org users shouldn't have access to this page
is_org_user = self.request.user.is_org_user(self.request)
if self.request.user.portfolio and is_org_user:
return False
else:
return super().has_permission()
class DomainDNSView(DomainBaseView):
"""DNS Information View."""

View file

@ -1,13 +1,11 @@
import logging
from django.http import JsonResponse
from django.core.paginator import Paginator
from registrar.models import UserDomainRole, Domain
from registrar.models import UserDomainRole, Domain, DomainInformation
from django.contrib.auth.decorators import login_required
from django.urls import reverse
from django.db.models import Q
from registrar.models.domain_information import DomainInformation
logger = logging.getLogger(__name__)

View file

@ -5,8 +5,9 @@ def index(request):
"""This page is available to anyone without logging in."""
context = {}
if request.user.is_authenticated:
if request and request.user and request.user.is_authenticated:
# This controls the creation of a new domain request in the wizard
request.session["new_request"] = True
context["user_domain_count"] = request.user.get_user_domain_ids(request).count()
return render(request, "home.html", context)

View file

@ -22,7 +22,10 @@ class PortfolioDomainsView(PortfolioDomainsPermissionView, View):
template_name = "portfolio_domains.html"
def get(self, request):
return render(request, "portfolio_domains.html")
context = {}
if self.request and self.request.user and self.request.user.is_authenticated:
context["user_domain_count"] = self.request.user.get_user_domain_ids(request).count()
return render(request, "portfolio_domains.html", context)
class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):