mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-02 07:52:15 +02:00
Merge remote-tracking branch 'origin/main' into nl/2240-additional-details-completion-bug
This commit is contained in:
commit
5083298aca
44 changed files with 1291 additions and 141 deletions
45
.github/ISSUE_TEMPLATE/developer-onboarding.md
vendored
45
.github/ISSUE_TEMPLATE/developer-onboarding.md
vendored
|
@ -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
2
.gitignore
vendored
|
@ -7,8 +7,10 @@ docs/research/data/**
|
|||
public/
|
||||
credentials*
|
||||
|
||||
src/certs/
|
||||
*.pem
|
||||
*.crt
|
||||
*.cer
|
||||
|
||||
*.bk
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -9,6 +9,7 @@ from .domain import (
|
|||
DomainDnssecForm,
|
||||
DomainDsdataFormset,
|
||||
DomainDsdataForm,
|
||||
DomainSuborganizationForm,
|
||||
)
|
||||
from .portfolio import (
|
||||
PortfolioOrgAddressForm,
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
123
src/registrar/management/commands/load_senior_official_table.py
Normal file
123
src/registrar/management/commands/load_senior_official_table.py
Normal 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)
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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}"
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
14
src/registrar/templates/django/forms/widgets/combobox.html
Normal file
14
src/registrar/templates/django/forms/widgets/combobox.html
Normal 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>
|
|
@ -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 %}
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
<ul class="usa-list">
|
||||
<li>Be available </li>
|
||||
<li>Relate to your organization’s 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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
29
src/registrar/templates/domain_suborganization.html
Normal file
29
src/registrar/templates/domain_suborganization.html
Normal 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 %}
|
|
@ -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>
|
||||
|
|
10
src/registrar/templates/includes/domain_sidenav_item.html
Normal file
10
src/registrar/templates/includes/domain_sidenav_item.html
Normal 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>
|
|
@ -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
|
||||
|
|
|
@ -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, you’ll 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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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, you’ll 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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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)
|
||||
|
|
5
src/registrar/tests/data/fake_federal_cio.csv
Normal file
5
src/registrar/tests/data/fake_federal_cio.csv
Normal 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,,,,,
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -3,6 +3,7 @@ from .domain import (
|
|||
DomainView,
|
||||
DomainSeniorOfficialView,
|
||||
DomainOrgNameAddressView,
|
||||
DomainSubOrganizationView,
|
||||
DomainDNSView,
|
||||
DomainNameserversView,
|
||||
DomainDNSSECView,
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue