mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-18 18:39:21 +02:00
Merge remote-tracking branch 'origin/main' into ms/2451-additional-domain-request-dates
This commit is contained in:
commit
eac757a6a6
23 changed files with 626 additions and 96 deletions
|
@ -30,8 +30,9 @@ Welcome to the .gov team! We're excited to have you here. Please follow the step
|
|||
- [ ] Review our [design tools](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?pli=1#heading=h.aprurp3z4gmv).
|
||||
- [ ] Accept invitation to our [Figma workspace](https://www.figma.com/files/1287135731043703282/team/1299882813146449644).
|
||||
- [ ] Follow the steps in [preparing for your sandbox](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?pli=1#heading=h.au66hq5e0l8s) section on the onboarding doc.
|
||||
- [ ] Schedule coffee chats with Design Lead, other designers, scrum master, and product manager ([team directory](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?pli=1#heading=h.1vq6r8e52e9f)).
|
||||
- [ ] Schedule coffee chats with Design Leads and other teammates ([team directory](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?pli=1#heading=h.1vq6r8e52e9f)).
|
||||
- [ ] Look over [recommended reading](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?pli=1#heading=h.7ox9ee7v5q5n) and [relevant links](https://docs.google.com/document/d/1ukbpW4LSqkb_CCt8LWfpehP03qqfyYfvK3Fl21NaEq8/edit?pli=1#heading=h.d9pac1gc751t).
|
||||
- [ ] Fill out the [Emails](https://docs.google.com/spreadsheets/d/1a6wj8I7FzWGP1AyIhAwP7yL84mXzORao8D_Q3c1xs-g/edit?gid=1637270167#gid=1637270167) tab of the Team OOO spreadsheet. To create a scheduling link, follow these instructions: [Outlook](https://learn.microsoft.com/en-us/microsoft-365/bookings/create-new-meeting-type?view=o365-worldwide) or [Google Calendar](https://support.google.com/calendar/answer/10729749?hl=en)
|
||||
- [ ] Fill out your own Personal Operating Manual (POM) on the [team norming board](https://miro.com/app/board/uXjVMxMu1SA=/). OPTIONAL: Present it on the next team coffee meeting.
|
||||
- [ ] FOR FEDERAL EMPLOYEES: Check in with your manager on the CISA onboarding process and getting your PIV card.
|
||||
- [ ] FOR CONTRACTORS: Check with your manager on your EOD clearance process.
|
||||
|
|
|
@ -9,8 +9,7 @@ from django.db.models.functions import Concat, Coalesce
|
|||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import redirect
|
||||
from django_fsm import get_available_FIELD_transitions, FSMField
|
||||
from registrar.models.domain_group import DomainGroup
|
||||
from registrar.models.suborganization import Suborganization
|
||||
from registrar.models.domain_information import DomainInformation
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from waffle.decorators import flag_is_active
|
||||
from django.contrib import admin, messages
|
||||
|
@ -23,6 +22,7 @@ from registrar.models.user_domain_role import UserDomainRole
|
|||
from waffle.admin import FlagAdmin
|
||||
from waffle.models import Sample, Switch
|
||||
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
|
||||
from registrar.utility.constants import BranchChoices
|
||||
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
||||
from registrar.views.utility.mixins import OrderableFieldsMixin
|
||||
from django.contrib.admin.views.main import ORDER_VAR
|
||||
|
@ -39,7 +39,7 @@ from import_export import resources
|
|||
from import_export.admin import ImportExportModelAdmin
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -2866,16 +2866,112 @@ class VerifiedByStaffAdmin(ListHeaderAdmin):
|
|||
|
||||
|
||||
class PortfolioAdmin(ListHeaderAdmin):
|
||||
|
||||
change_form_template = "django/admin/portfolio_change_form.html"
|
||||
fieldsets = [
|
||||
# created_on is the created_at field, and portfolio_type is f"{organization_type} - {federal_type}"
|
||||
(None, {"fields": ["portfolio_type", "organization_name", "creator", "created_on", "notes"]}),
|
||||
# TODO - uncomment in #2521
|
||||
# ("Portfolio members", {
|
||||
# "classes": ("collapse", "closed"),
|
||||
# "fields": ["administrators", "members"]}
|
||||
# ),
|
||||
("Portfolio domains", {"classes": ("collapse", "closed"), "fields": ["domains", "domain_requests"]}),
|
||||
("Type of organization", {"fields": ["organization_type", "federal_type"]}),
|
||||
(
|
||||
"Organization name and mailing address",
|
||||
{
|
||||
"fields": [
|
||||
"federal_agency",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
]
|
||||
},
|
||||
),
|
||||
("Suborganizations", {"fields": ["suborganizations"]}),
|
||||
("Senior official", {"fields": ["senior_official"]}),
|
||||
]
|
||||
|
||||
# This is the fieldset display when adding a new model
|
||||
add_fieldsets = [
|
||||
(None, {"fields": ["organization_name", "creator", "notes"]}),
|
||||
("Type of organization", {"fields": ["organization_type"]}),
|
||||
(
|
||||
"Organization name and mailing address",
|
||||
{
|
||||
"fields": [
|
||||
"federal_agency",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
]
|
||||
},
|
||||
),
|
||||
("Senior official", {"fields": ["senior_official"]}),
|
||||
]
|
||||
|
||||
list_display = ("organization_name", "federal_agency", "creator")
|
||||
search_fields = ["organization_name"]
|
||||
search_help_text = "Search by organization name."
|
||||
readonly_fields = [
|
||||
"creator",
|
||||
# This is the created_at field
|
||||
"created_on",
|
||||
# Custom fields such as these must be defined as readonly.
|
||||
"federal_type",
|
||||
"domains",
|
||||
"domain_requests",
|
||||
"suborganizations",
|
||||
"portfolio_type",
|
||||
]
|
||||
|
||||
def federal_type(self, obj: models.Portfolio):
|
||||
"""Returns the federal_type field"""
|
||||
return BranchChoices.get_branch_label(obj.federal_type) if obj.federal_type else "-"
|
||||
|
||||
federal_type.short_description = "Federal type" # type: ignore
|
||||
|
||||
def created_on(self, obj: models.Portfolio):
|
||||
"""Returns the created_at field, with a different short description"""
|
||||
# Format: Dec 12, 2024
|
||||
return obj.created_at.strftime("%b %d, %Y") if obj.created_at else "-"
|
||||
|
||||
created_on.short_description = "Created on" # type: ignore
|
||||
|
||||
def portfolio_type(self, obj: models.Portfolio):
|
||||
"""Returns the portfolio type, or "-" if the result is empty"""
|
||||
return obj.portfolio_type if obj.portfolio_type else "-"
|
||||
|
||||
portfolio_type.short_description = "Portfolio type" # type: ignore
|
||||
|
||||
def suborganizations(self, obj: models.Portfolio):
|
||||
"""Returns a list of links for each related suborg"""
|
||||
queryset = obj.get_suborganizations()
|
||||
return self.get_field_links_as_list(queryset, "suborganization")
|
||||
|
||||
suborganizations.short_description = "Suborganizations" # type: ignore
|
||||
|
||||
def domains(self, obj: models.Portfolio):
|
||||
"""Returns a list of links for each related domain"""
|
||||
queryset = obj.get_domains()
|
||||
return self.get_field_links_as_list(
|
||||
queryset, "domaininformation", link_info_attribute="get_state_display_of_domain"
|
||||
)
|
||||
|
||||
domains.short_description = "Domains" # type: ignore
|
||||
|
||||
def domain_requests(self, obj: models.Portfolio):
|
||||
"""Returns a list of links for each related domain request"""
|
||||
queryset = obj.get_domain_requests()
|
||||
return self.get_field_links_as_list(queryset, "domainrequest", link_info_attribute="get_status_display")
|
||||
|
||||
domain_requests.short_description = "Domain requests" # type: ignore
|
||||
|
||||
# Creates select2 fields (with search bars)
|
||||
autocomplete_fields = [
|
||||
"creator",
|
||||
|
@ -2883,17 +2979,91 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
"senior_official",
|
||||
]
|
||||
|
||||
def get_field_links_as_list(
|
||||
self, queryset, model_name, attribute_name=None, link_info_attribute=None, seperator=None
|
||||
):
|
||||
"""
|
||||
Generate HTML links for items in a queryset, using a specified attribute for link text.
|
||||
|
||||
Args:
|
||||
queryset: The queryset of items to generate links for.
|
||||
model_name: The model name used to construct the admin change URL.
|
||||
attribute_name: The attribute or method name to use for link text. If None, the item itself is used.
|
||||
link_info_attribute: Appends f"({value_of_attribute})" to the end of the link.
|
||||
separator: The separator to use between links in the resulting HTML.
|
||||
If none, an unordered list is returned.
|
||||
|
||||
Returns:
|
||||
A formatted HTML string with links to the admin change pages for each item.
|
||||
"""
|
||||
links = []
|
||||
for item in queryset:
|
||||
|
||||
# This allows you to pass in attribute_name="get_full_name" for instance.
|
||||
if attribute_name:
|
||||
item_display_value = self.value_of_attribute(item, attribute_name)
|
||||
else:
|
||||
item_display_value = item
|
||||
|
||||
if item_display_value:
|
||||
change_url = reverse(f"admin:registrar_{model_name}_change", args=[item.pk])
|
||||
|
||||
link = f'<a href="{change_url}">{escape(item_display_value)}</a>'
|
||||
if link_info_attribute:
|
||||
link += f" ({self.value_of_attribute(item, link_info_attribute)})"
|
||||
|
||||
if seperator:
|
||||
links.append(link)
|
||||
else:
|
||||
links.append(f"<li>{link}</li>")
|
||||
|
||||
# If no seperator is specified, just return an unordered list.
|
||||
if seperator:
|
||||
return format_html(seperator.join(links)) if links else "-"
|
||||
else:
|
||||
links = "".join(links)
|
||||
return format_html(f'<ul class="add-list-reset">{links}</ul>') if links else "-"
|
||||
|
||||
def value_of_attribute(self, obj, attribute_name: str):
|
||||
"""Returns the value of getattr if the attribute isn't callable.
|
||||
If it is, execute the underlying function and return that result instead."""
|
||||
value = getattr(obj, attribute_name)
|
||||
if callable(value):
|
||||
value = value()
|
||||
return value
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
"""Override of the default get_fieldsets definition to add an add_fieldsets view"""
|
||||
# This is the add view if no obj exists
|
||||
if not obj:
|
||||
return self.add_fieldsets
|
||||
return super().get_fieldsets(request, obj)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Set the read-only state on form elements.
|
||||
We have 2 conditions that determine which fields are read-only:
|
||||
admin user permissions and the creator's status, so
|
||||
we'll use the baseline readonly_fields and extend it as needed.
|
||||
"""
|
||||
readonly_fields = list(self.readonly_fields)
|
||||
|
||||
# Check if the creator is restricted
|
||||
if obj and obj.creator.status == models.User.RESTRICTED:
|
||||
# For fields like CharField, IntegerField, etc., the widget used is
|
||||
# straightforward and the readonly_fields list can control their behavior
|
||||
readonly_fields.extend([field.name for field in self.model._meta.fields])
|
||||
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
"""Add related suborganizations and domain groups"""
|
||||
obj = self.get_object(request, object_id)
|
||||
|
||||
# ---- Domain Groups
|
||||
domain_groups = DomainGroup.objects.filter(portfolio=obj)
|
||||
|
||||
# ---- Suborganizations
|
||||
suborganizations = Suborganization.objects.filter(portfolio=obj)
|
||||
|
||||
extra_context = {"domain_groups": domain_groups, "suborganizations": suborganizations}
|
||||
extra_context = {"skip_additional_contact_info": True}
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
|
@ -2975,12 +3145,32 @@ class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
|
||||
class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
|
||||
list_display = ["name", "portfolio"]
|
||||
autocomplete_fields = [
|
||||
"portfolio",
|
||||
]
|
||||
search_fields = ["name"]
|
||||
|
||||
change_form_template = "django/admin/suborg_change_form.html"
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
"""Add suborg's related domains and requests to context"""
|
||||
obj = self.get_object(request, object_id)
|
||||
|
||||
# ---- Domain Requests
|
||||
domain_requests = DomainRequest.objects.filter(sub_organization=obj)
|
||||
sort_by = request.GET.get("sort_by", "requested_domain__name")
|
||||
domain_requests = domain_requests.order_by(sort_by)
|
||||
|
||||
# ---- Domains
|
||||
domain_infos = DomainInformation.objects.filter(sub_organization=obj)
|
||||
domain_ids = domain_infos.values_list("domain", flat=True)
|
||||
domains = Domain.objects.filter(id__in=domain_ids).exclude(state=Domain.State.DELETED)
|
||||
|
||||
extra_context = {"domain_requests": domain_requests, "domains": domains}
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
|
||||
admin.site.unregister(LogEntry) # Unregister the default registration
|
||||
|
||||
|
|
|
@ -765,6 +765,11 @@ function initializeWidgetOnList(list, parentId) {
|
|||
*/
|
||||
(function dynamicPortfolioFields(){
|
||||
|
||||
// the federal agency change listener fires on page load, which we don't want.
|
||||
var isInitialPageLoad = true
|
||||
|
||||
// This is the additional information that exists beneath the SO element.
|
||||
var contactList = document.querySelector(".field-senior_official .dja-address-contact-list");
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
let isPortfolioPage = document.getElementById("portfolio_form");
|
||||
|
@ -776,9 +781,6 @@ function initializeWidgetOnList(list, parentId) {
|
|||
let $federalAgency = django.jQuery("#id_federal_agency");
|
||||
let organizationType = document.getElementById("id_organization_type");
|
||||
if ($federalAgency && organizationType) {
|
||||
// Execute this function once on load
|
||||
handleFederalAgencyChange($federalAgency, organizationType);
|
||||
|
||||
// Attach the change event listener
|
||||
$federalAgency.on("change", function() {
|
||||
handleFederalAgencyChange($federalAgency, organizationType);
|
||||
|
@ -800,6 +802,12 @@ function initializeWidgetOnList(list, parentId) {
|
|||
});
|
||||
|
||||
function handleFederalAgencyChange(federalAgency, organizationType) {
|
||||
// Don't do anything on page load
|
||||
if (isInitialPageLoad) {
|
||||
isInitialPageLoad = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the org type to federal if an agency is selected
|
||||
let selectedText = federalAgency.find("option:selected").text();
|
||||
|
||||
|
@ -825,6 +833,10 @@ function initializeWidgetOnList(list, parentId) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Hide the contactList initially.
|
||||
// If we can update the contact information, it'll be shown again.
|
||||
hideElement(contactList.parentElement);
|
||||
|
||||
let seniorOfficialApi = document.getElementById("senior_official_from_agency_json_url").value;
|
||||
fetch(`${seniorOfficialApi}?agency_name=${selectedText}`)
|
||||
.then(response => {
|
||||
|
@ -843,6 +855,10 @@ function initializeWidgetOnList(list, parentId) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Update the "contact details" blurb beneath senior official
|
||||
updateContactInfo(data);
|
||||
showElement(contactList.parentElement);
|
||||
|
||||
let seniorOfficialId = data.id;
|
||||
let seniorOfficialName = [data.first_name, data.last_name].join(" ");
|
||||
if (!seniorOfficialId || !seniorOfficialName || !seniorOfficialName.trim()){
|
||||
|
@ -873,4 +889,35 @@ function initializeWidgetOnList(list, parentId) {
|
|||
hideElement(urbanizationField)
|
||||
}
|
||||
}
|
||||
|
||||
function updateContactInfo(data) {
|
||||
if (!contactList) return;
|
||||
|
||||
const titleSpan = contactList.querySelector("#contact_info_title");
|
||||
const emailSpan = contactList.querySelector("#contact_info_email");
|
||||
const phoneSpan = contactList.querySelector("#contact_info_phone");
|
||||
|
||||
if (titleSpan) {
|
||||
titleSpan.textContent = data.title || "None";
|
||||
};
|
||||
|
||||
// Update the email field and the content for the clipboard
|
||||
if (emailSpan) {
|
||||
let copyButton = contactList.querySelector(".admin-icon-group");
|
||||
emailSpan.textContent = data.email || "None";
|
||||
if (data.email) {
|
||||
const clipboardInput = contactList.querySelector(".admin-icon-group input");
|
||||
if (clipboardInput) {
|
||||
clipboardInput.value = data.email;
|
||||
};
|
||||
showElement(copyButton);
|
||||
}else {
|
||||
hideElement(copyButton);
|
||||
}
|
||||
}
|
||||
|
||||
if (phoneSpan) {
|
||||
phoneSpan.textContent = data.phone || "None";
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -847,3 +847,8 @@ div.dja__model-description{
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul.add-list-reset {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
|
|
@ -356,9 +356,18 @@ CSP_FORM_ACTION = allowed_sources
|
|||
# strict CSP by allowing scripts to run from their domain
|
||||
# and inline with a nonce, as well as allowing connections back to their domain.
|
||||
# Note: If needed, we can embed chart.js instead of using the CDN
|
||||
CSP_SCRIPT_SRC_ELEM = ["'self'", "https://www.googletagmanager.com/", "https://cdn.jsdelivr.net/npm/chart.js"]
|
||||
CSP_CONNECT_SRC = ["'self'", "https://www.google-analytics.com/"]
|
||||
CSP_INCLUDE_NONCE_IN = ["script-src-elem"]
|
||||
CSP_DEFAULT_SRC = ("'self'",)
|
||||
CSP_STYLE_SRC = ["'self'", "https://www.ssa.gov/accessibility/andi/andi.css"]
|
||||
CSP_SCRIPT_SRC_ELEM = [
|
||||
"'self'",
|
||||
"https://www.googletagmanager.com/",
|
||||
"https://cdn.jsdelivr.net/npm/chart.js",
|
||||
"https://www.ssa.gov",
|
||||
"https://ajax.googleapis.com",
|
||||
]
|
||||
CSP_CONNECT_SRC = ["'self'", "https://www.google-analytics.com/", "https://www.ssa.gov/accessibility/andi/andi.js"]
|
||||
CSP_INCLUDE_NONCE_IN = ["script-src-elem", "style-src"]
|
||||
CSP_IMG_SRC = ["'self'", "https://www.ssa.gov/accessibility/andi/icons/"]
|
||||
|
||||
# Cross-Origin Resource Sharing (CORS) configuration
|
||||
# Sets clients that allow access control to manage.get.gov
|
||||
|
|
|
@ -22,6 +22,12 @@ class UserFixture:
|
|||
"""
|
||||
|
||||
ADMINS = [
|
||||
{
|
||||
"username": "43a7fa8d-0550-4494-a6fe-81500324d590",
|
||||
"first_name": "Jyoti",
|
||||
"last_name": "Bock",
|
||||
"email": "jyotibock@truss.works",
|
||||
},
|
||||
{
|
||||
"username": "aad084c3-66cc-4632-80eb-41cdf5c5bcbf",
|
||||
"first_name": "Aditi",
|
||||
|
@ -125,6 +131,12 @@ class UserFixture:
|
|||
]
|
||||
|
||||
STAFF = [
|
||||
{
|
||||
"username": "a5906815-dd80-4c64-aebe-2da6a4c9d7a4",
|
||||
"first_name": "Jyoti-Analyst",
|
||||
"last_name": "Bock-Analyst",
|
||||
"email": "jyotibock+1@truss.works",
|
||||
},
|
||||
{
|
||||
"username": "ffec5987-aa84-411b-a05a-a7ee5cbcde54",
|
||||
"first_name": "Aditi-Analyst",
|
||||
|
|
|
@ -679,7 +679,7 @@ class CisaRepresentativeYesNoForm(BaseYesNoForm):
|
|||
field_name = "has_cisa_representative"
|
||||
|
||||
|
||||
class AdditionalDetailsForm(BaseDeletableRegistrarForm):
|
||||
class AnythingElseForm(BaseDeletableRegistrarForm):
|
||||
anything_else = forms.CharField(
|
||||
required=True,
|
||||
label="Anything else?",
|
||||
|
@ -698,7 +698,7 @@ class AdditionalDetailsForm(BaseDeletableRegistrarForm):
|
|||
)
|
||||
|
||||
|
||||
class AdditionalDetailsYesNoForm(BaseYesNoForm):
|
||||
class AnythingElseYesNoForm(BaseYesNoForm):
|
||||
"""Yes/no toggle for the anything else question on additional details"""
|
||||
|
||||
# Note that these can be set as functions/init if you need more fine-grained control.
|
||||
|
|
|
@ -58,17 +58,17 @@ class Command(BaseCommand):
|
|||
reports = {
|
||||
"Domain report": {
|
||||
"report_filename": f"domain-metadata-{self.current_date}.csv",
|
||||
"report_function": csv_export.export_data_type_to_csv,
|
||||
"report_function": csv_export.DomainDataType.export_data_to_csv,
|
||||
},
|
||||
"Domain request report": {
|
||||
"report_filename": f"domain-request-metadata-{self.current_date}.csv",
|
||||
"report_function": csv_export.DomainRequestExport.export_full_domain_request_report,
|
||||
"report_function": csv_export.DomainRequestDataFull.export_data_to_csv,
|
||||
},
|
||||
}
|
||||
|
||||
# Set the password equal to our content in SECRET_ENCRYPT_METADATA.
|
||||
# For local development, this will be "devpwd" unless otherwise set.
|
||||
# Uncomment these lines if you want to use this:
|
||||
# Uncomment these lines (and comment out the line after) if you want to use this:
|
||||
# override = settings.SECRET_ENCRYPT_METADATA is None and not settings.IS_PRODUCTION
|
||||
# password = "devpwd" if override else settings.SECRET_ENCRYPT_METADATA
|
||||
password = settings.SECRET_ENCRYPT_METADATA
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
# Generated by Django 4.2.10 on 2024-08-15 15:32
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0117_alter_portfolioinvitation_portfolio_additional_permissions_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="portfolio",
|
||||
options={"ordering": ["organization_name"]},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="portfolio",
|
||||
name="creator",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="created_portfolios",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Portfolio creator",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="portfolio",
|
||||
name="organization_name",
|
||||
field=models.CharField(blank=True, null=True, verbose_name="Portfolio organization"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="portfolio",
|
||||
name="senior_official",
|
||||
field=models.ForeignKey(
|
||||
blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to="registrar.seniorofficial"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="suborganization",
|
||||
name="portfolio",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="portfolio_suborganizations",
|
||||
to="registrar.portfolio",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -296,23 +296,29 @@ class DomainInformation(TimeStampedModel):
|
|||
"""Some yes/no forms use a db field to track whether it was checked or not.
|
||||
We handle that here for def save().
|
||||
"""
|
||||
# Check if the firstname or lastname of cisa representative has any data.
|
||||
# Then set the has_cisa_representative flag accordingly (so that it isn't
|
||||
# "none", which indicates an incomplete form).
|
||||
# This ensures that if we have prefilled data, the form is prepopulated
|
||||
if self.cisa_representative_first_name is not None or self.cisa_representative_last_name is not None:
|
||||
self.has_cisa_representative = (
|
||||
self.cisa_representative_first_name != "" and self.cisa_representative_last_name != ""
|
||||
)
|
||||
|
||||
# This check is required to ensure that the form doesn't start out checked
|
||||
# Check for blank data and update has_cisa_representative accordingly (if it isn't None)
|
||||
if self.has_cisa_representative is not None:
|
||||
self.has_cisa_representative = (
|
||||
self.cisa_representative_first_name != "" and self.cisa_representative_first_name is not None
|
||||
) and (self.cisa_representative_last_name != "" and self.cisa_representative_last_name is not None)
|
||||
|
||||
# Check if anything_else has any data.
|
||||
# Then set the has_anything_else_text flag accordingly (so that it isn't
|
||||
# "none", which indicates an incomplete form).
|
||||
# This ensures that if we have prefilled data, the form is prepopulated
|
||||
if self.anything_else is not None:
|
||||
self.has_anything_else_text = self.anything_else != ""
|
||||
|
||||
# This check is required to ensure that the form doesn't start out checked.
|
||||
# Check for blank data and update has_anything_else_text accordingly (if it isn't None)
|
||||
if self.has_anything_else_text is not None:
|
||||
self.has_anything_else_text = self.anything_else != "" and self.anything_else is not None
|
||||
|
||||
|
@ -424,3 +430,10 @@ class DomainInformation(TimeStampedModel):
|
|||
def _get_many_to_many_fields():
|
||||
"""Returns a set of each field.name that has the many to many relation"""
|
||||
return {field.name for field in DomainInformation._meta.many_to_many} # type: ignore
|
||||
|
||||
def get_state_display_of_domain(self):
|
||||
"""Returns the state display of the underlying domain record"""
|
||||
if self.domain:
|
||||
return self.domain.get_state_display()
|
||||
else:
|
||||
return None
|
||||
|
|
|
@ -665,23 +665,29 @@ class DomainRequest(TimeStampedModel):
|
|||
"""Some yes/no forms use a db field to track whether it was checked or not.
|
||||
We handle that here for def save().
|
||||
"""
|
||||
# Check if the firstname or lastname of cisa representative has any data.
|
||||
# Then set the has_cisa_representative flag accordingly (so that it isn't
|
||||
# "none", which indicates an incomplete form).
|
||||
# This ensures that if we have prefilled data, the form is prepopulated
|
||||
if self.cisa_representative_first_name is not None or self.cisa_representative_last_name is not None:
|
||||
self.has_cisa_representative = (
|
||||
self.cisa_representative_first_name != "" and self.cisa_representative_last_name != ""
|
||||
)
|
||||
|
||||
# This check is required to ensure that the form doesn't start out checked
|
||||
# Check for blank data and update has_cisa_representative accordingly (if it isn't None)
|
||||
if self.has_cisa_representative is not None:
|
||||
self.has_cisa_representative = (
|
||||
self.cisa_representative_first_name != "" and self.cisa_representative_first_name is not None
|
||||
) and (self.cisa_representative_last_name != "" and self.cisa_representative_last_name is not None)
|
||||
|
||||
# Check if anything_else has any data.
|
||||
# Then set the has_anything_else_text flag accordingly (so that it isn't
|
||||
# "none", which indicates an incomplete form).
|
||||
# This ensures that if we have prefilled data, the form is prepopulated
|
||||
if self.anything_else is not None:
|
||||
self.has_anything_else_text = self.anything_else != ""
|
||||
|
||||
# This check is required to ensure that the form doesn't start out checked.
|
||||
# Check for blank data and update has_anything_else_text accordingly (if it isn't None)
|
||||
if self.has_anything_else_text is not None:
|
||||
self.has_anything_else_text = self.anything_else != "" and self.anything_else is not None
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ from django.db import models
|
|||
|
||||
from registrar.models.domain_request import DomainRequest
|
||||
from registrar.models.federal_agency import FederalAgency
|
||||
from registrar.utility.constants import BranchChoices
|
||||
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
|
@ -12,6 +13,10 @@ class Portfolio(TimeStampedModel):
|
|||
manageable groups.
|
||||
"""
|
||||
|
||||
# Addresses the UnorderedObjectListWarning
|
||||
class Meta:
|
||||
ordering = ["organization_name"]
|
||||
|
||||
# use the short names in Django admin
|
||||
OrganizationChoices = DomainRequest.OrganizationChoices
|
||||
StateTerritoryChoices = DomainRequest.StateTerritoryChoices
|
||||
|
@ -21,11 +26,25 @@ class Portfolio(TimeStampedModel):
|
|||
creator = models.ForeignKey(
|
||||
"registrar.User",
|
||||
on_delete=models.PROTECT,
|
||||
help_text="Associated user",
|
||||
verbose_name="Portfolio creator",
|
||||
related_name="created_portfolios",
|
||||
unique=False,
|
||||
)
|
||||
|
||||
organization_name = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Portfolio organization",
|
||||
)
|
||||
|
||||
organization_type = models.CharField(
|
||||
max_length=255,
|
||||
choices=OrganizationChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Type of organization",
|
||||
)
|
||||
|
||||
notes = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
|
@ -42,25 +61,11 @@ class Portfolio(TimeStampedModel):
|
|||
senior_official = models.ForeignKey(
|
||||
"registrar.SeniorOfficial",
|
||||
on_delete=models.PROTECT,
|
||||
help_text="Associated senior official",
|
||||
unique=False,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
organization_type = models.CharField(
|
||||
max_length=255,
|
||||
choices=OrganizationChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Type of organization",
|
||||
)
|
||||
|
||||
organization_name = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
address_line1 = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
|
@ -109,7 +114,7 @@ class Portfolio(TimeStampedModel):
|
|||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.organization_name}"
|
||||
return str(self.organization_name)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save override for custom properties"""
|
||||
|
@ -119,3 +124,35 @@ class Portfolio(TimeStampedModel):
|
|||
self.urbanization = None
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def portfolio_type(self):
|
||||
"""
|
||||
Returns a combination of organization_type / federal_type, seperated by ' - '.
|
||||
If no federal_type is found, we just return the org type.
|
||||
"""
|
||||
org_type_label = self.OrganizationChoices.get_org_label(self.organization_type)
|
||||
agency_type_label = BranchChoices.get_branch_label(self.federal_type)
|
||||
if self.organization_type == self.OrganizationChoices.FEDERAL and agency_type_label:
|
||||
return " - ".join([org_type_label, agency_type_label])
|
||||
else:
|
||||
return org_type_label
|
||||
|
||||
@property
|
||||
def federal_type(self):
|
||||
"""Returns the federal_type value on the underlying federal_agency field"""
|
||||
return self.federal_agency.federal_type if self.federal_agency else None
|
||||
|
||||
# == Getters for domains == #
|
||||
def get_domains(self):
|
||||
"""Returns all DomainInformations associated with this portfolio"""
|
||||
return self.information_portfolio.all()
|
||||
|
||||
def get_domain_requests(self):
|
||||
"""Returns all DomainRequests associated with this portfolio"""
|
||||
return self.DomainRequest_portfolio.all()
|
||||
|
||||
# == Getters for suborganization == #
|
||||
def get_suborganizations(self):
|
||||
"""Returns all suborganizations associated with this portfolio"""
|
||||
return self.portfolio_suborganizations.all()
|
||||
|
|
|
@ -16,6 +16,7 @@ class Suborganization(TimeStampedModel):
|
|||
portfolio = models.ForeignKey(
|
||||
"registrar.Portfolio",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="portfolio_suborganizations",
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{% load i18n static %}
|
||||
{% load custom_filters %}
|
||||
|
||||
<address class="{% if no_title_top_padding %}margin-top-neg-1__detail-list{% endif %} {% if user.has_contact_info %}margin-bottom-1{% endif %} dja-address-contact-list">
|
||||
<address class="{% if no_title_top_padding %}margin-top-neg-1__detail-list{% endif %} {% if user|has_contact_info %}margin-bottom-1{% endif %} dja-address-contact-list">
|
||||
|
||||
|
||||
{% if show_formatted_name %}
|
||||
|
@ -9,10 +10,10 @@
|
|||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</br>
|
||||
{% endif %}
|
||||
|
||||
{% if user.has_contact_info %}
|
||||
{% if user|has_contact_info %}
|
||||
{# Title #}
|
||||
{% if user.title %}
|
||||
<span id="contact_info_title">{{ user.title }}</span>
|
||||
|
@ -42,7 +43,7 @@
|
|||
No additional contact information found.<br>
|
||||
{% endif %}
|
||||
|
||||
{% if user_verification_type %}
|
||||
{% if user_verification_type and not skip_additional_contact_info %}
|
||||
<span id="contact_info_phone">{{ user_verification_type }}</span>
|
||||
{% endif %}
|
||||
</address>
|
||||
|
|
|
@ -184,7 +184,9 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
<label aria-label="Creator contact details"></label>
|
||||
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly user_verification_type=original_object.creator.get_verification_type_display%}
|
||||
</div>
|
||||
{% if not skip_additional_contact_info %}
|
||||
{% include "django/admin/includes/user_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
|
||||
{% endif%}
|
||||
{% elif field.field.name == "submitter" %}
|
||||
<div class="flex-container tablet:margin-top-2">
|
||||
<label aria-label="Submitter contact details"></label>
|
||||
|
|
|
@ -8,34 +8,16 @@
|
|||
{{ block.super }}
|
||||
{% endblock content %}
|
||||
|
||||
{% block after_related_objects %}
|
||||
<div class="module aligned padding-3">
|
||||
<h2>Associated groups and suborganizations</h2>
|
||||
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
|
||||
<h3>Domain groups</h3>
|
||||
<ul class="margin-0 padding-0">
|
||||
{% for domain_group in domain_groups %}
|
||||
<li>
|
||||
<a href="{% url 'admin:registrar_domaingroup_change' domain_group.pk %}">
|
||||
{{ domain_group.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% block field_sets %}
|
||||
{% for fieldset in adminform %}
|
||||
{% comment %}
|
||||
This is a placeholder for now.
|
||||
|
||||
Disclaimer:
|
||||
When extending the fieldset view - *make a new one* that extends from detail_table_fieldset.
|
||||
For instance, "portfolio_fieldset.html".
|
||||
detail_table_fieldset is used on multiple admin pages, so a change there can have unintended consequences.
|
||||
{% endcomment %}
|
||||
{% include "django/admin/includes/detail_table_fieldset.html" with original_object=original %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
|
||||
<h3>Suborganizations</h3>
|
||||
<ul class="margin-0 padding-0">
|
||||
{% for suborg in suborganizations %}
|
||||
<li>
|
||||
<a href="{% url 'admin:registrar_suborganization_change' suborg.pk %}">
|
||||
{{ suborg.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
36
src/registrar/templates/django/admin/suborg_change_form.html
Normal file
36
src/registrar/templates/django/admin/suborg_change_form.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
{% extends 'django/admin/email_clipboard_change_form.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block after_related_objects %}
|
||||
<div class="module aligned padding-3">
|
||||
<h2>Associated requests and domains</h2>
|
||||
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
|
||||
<h3>Domain requests</h3>
|
||||
<ul class="margin-0 padding-0">
|
||||
{% for domain_request in domain_requests %}
|
||||
<li>
|
||||
<a href="{% url 'admin:registrar_domainrequest_change' domain_request.pk %}">
|
||||
{{ domain_request.requested_domain }}
|
||||
</a>
|
||||
({{ domain_request.status }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
|
||||
<h3>Domains</h3>
|
||||
<ul class="margin-0 padding-0">
|
||||
{% for domain in domains %}
|
||||
<li>
|
||||
<a href="{% url 'admin:registrar_domain_change' domain.pk %}">
|
||||
{{ domain.name }}
|
||||
</a>
|
||||
({{ domain.state }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -159,3 +159,13 @@ def and_filter(value, arg):
|
|||
Usage: {{ value|and:arg }}
|
||||
"""
|
||||
return bool(value and arg)
|
||||
|
||||
|
||||
@register.filter(name="has_contact_info")
|
||||
def has_contact_info(user):
|
||||
"""Checks if the given object has the attributes: title, email, phone
|
||||
and checks if at least one of those is not null."""
|
||||
if not hasattr(user, "title") or not hasattr(user, "email") or not hasattr(user, "phone"):
|
||||
return False
|
||||
else:
|
||||
return bool(user.title or user.email or user.phone)
|
||||
|
|
|
@ -906,6 +906,7 @@ def completed_domain_request( # noqa
|
|||
federal_agency=None,
|
||||
federal_type=None,
|
||||
action_needed_reason=None,
|
||||
portfolio=None,
|
||||
):
|
||||
"""A completed domain request."""
|
||||
if not user:
|
||||
|
@ -976,6 +977,9 @@ def completed_domain_request( # noqa
|
|||
if action_needed_reason:
|
||||
domain_request_kwargs["action_needed_reason"] = action_needed_reason
|
||||
|
||||
if portfolio:
|
||||
domain_request_kwargs["portfolio"] = portfolio
|
||||
|
||||
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
|
||||
|
||||
if has_other_contacts:
|
||||
|
|
|
@ -23,6 +23,7 @@ from registrar.admin import (
|
|||
PublicContactAdmin,
|
||||
TransitionDomainAdmin,
|
||||
UserGroupAdmin,
|
||||
PortfolioAdmin,
|
||||
)
|
||||
from registrar.models import (
|
||||
Domain,
|
||||
|
@ -38,6 +39,8 @@ from registrar.models import (
|
|||
FederalAgency,
|
||||
UserGroup,
|
||||
TransitionDomain,
|
||||
Portfolio,
|
||||
Suborganization,
|
||||
)
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.senior_official import SeniorOfficial
|
||||
|
@ -2042,3 +2045,79 @@ class TestUserGroup(TestCase):
|
|||
response, "Groups are a way to bundle admin permissions so they can be easily assigned to multiple users."
|
||||
)
|
||||
self.assertContains(response, "Show more")
|
||||
|
||||
|
||||
class TestPortfolioAdmin(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.site = AdminSite()
|
||||
cls.superuser = create_superuser()
|
||||
cls.admin = PortfolioAdmin(model=Portfolio, admin_site=cls.site)
|
||||
cls.factory = RequestFactory()
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
|
||||
|
||||
def tearDown(self):
|
||||
Suborganization.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_created_on_display(self):
|
||||
"""Tests the custom created on which is a reskin of the created_at field"""
|
||||
created_on = self.admin.created_on(self.portfolio)
|
||||
expected_date = self.portfolio.created_at.strftime("%b %d, %Y")
|
||||
self.assertEqual(created_on, expected_date)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_suborganizations_display(self):
|
||||
"""Tests the custom suborg field which displays all related suborgs"""
|
||||
Suborganization.objects.create(name="Sub1", portfolio=self.portfolio)
|
||||
Suborganization.objects.create(name="Sub2", portfolio=self.portfolio)
|
||||
|
||||
suborganizations = self.admin.suborganizations(self.portfolio)
|
||||
self.assertIn("Sub1", suborganizations)
|
||||
self.assertIn("Sub2", suborganizations)
|
||||
self.assertIn('<ul class="add-list-reset">', suborganizations)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domains_display(self):
|
||||
"""Tests the custom domains field which displays all related domains"""
|
||||
request_1 = completed_domain_request(
|
||||
name="request1.gov", portfolio=self.portfolio, status=DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||
)
|
||||
request_2 = completed_domain_request(
|
||||
name="request2.gov", portfolio=self.portfolio, status=DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||
)
|
||||
|
||||
# Create some domain objects
|
||||
request_1.approve()
|
||||
request_2.approve()
|
||||
|
||||
domain_1 = DomainInformation.objects.get(domain_request=request_1).domain
|
||||
domain_1.name = "domain1.gov"
|
||||
domain_1.save()
|
||||
domain_2 = DomainInformation.objects.get(domain_request=request_2).domain
|
||||
domain_2.name = "domain2.gov"
|
||||
domain_2.save()
|
||||
|
||||
domains = self.admin.domains(self.portfolio)
|
||||
self.assertIn("domain1.gov", domains)
|
||||
self.assertIn("domain2.gov", domains)
|
||||
self.assertIn('<ul class="add-list-reset">', domains)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_requests_display(self):
|
||||
"""Tests the custom domains requests field which displays all related requests"""
|
||||
completed_domain_request(name="request1.gov", portfolio=self.portfolio)
|
||||
completed_domain_request(name="request2.gov", portfolio=self.portfolio)
|
||||
|
||||
domain_requests = self.admin.domain_requests(self.portfolio)
|
||||
self.assertIn("request1.gov", domain_requests)
|
||||
self.assertIn("request2.gov", domain_requests)
|
||||
self.assertIn('<ul class="add-list-reset">', domain_requests)
|
||||
|
|
|
@ -15,7 +15,7 @@ from registrar.forms.domain_request_wizard import (
|
|||
RequirementsForm,
|
||||
TribalGovernmentForm,
|
||||
PurposeForm,
|
||||
AdditionalDetailsForm,
|
||||
AnythingElseForm,
|
||||
AboutYourOrganizationForm,
|
||||
)
|
||||
from registrar.forms.domain import ContactForm
|
||||
|
@ -274,7 +274,7 @@ class TestFormValidation(MockEppLib):
|
|||
|
||||
def test_anything_else_form_about_your_organization_character_count_invalid(self):
|
||||
"""Response must be less than 2000 characters."""
|
||||
form = AdditionalDetailsForm(
|
||||
form = AnythingElseForm(
|
||||
data={
|
||||
"anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
||||
"shankle, drumstick doner chicken landjaeger turkey andouille."
|
||||
|
|
|
@ -1017,20 +1017,27 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
type_page = intro_result.follow()
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
# fill out the organization type section then submit
|
||||
type_form = type_page.forms[0]
|
||||
type_form["generic_org_type-generic_org_type"] = "federal"
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
type_result = type_form.submit()
|
||||
|
||||
# follow first redirect
|
||||
# follow first redirect to the next section
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
federal_page = type_result.follow()
|
||||
|
||||
# Now on federal type page, click back to the organization type
|
||||
# we need to fill out the federal section so it stays unlocked
|
||||
fed_branch_form = federal_page.forms[0]
|
||||
fed_branch_form["organization_federal-federal_type"] = "executive"
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
fed_branch_form.submit()
|
||||
|
||||
# Now click back to the organization type
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
new_page = federal_page.click(str(self.TITLES["generic_org_type"]), index=0)
|
||||
|
||||
# Should be a link to the organization_federal page
|
||||
# Should be a link to the organization_federal page since it is now unlocked
|
||||
self.assertGreater(
|
||||
len(new_page.html.find_all("a", href="/request/organization_federal/")),
|
||||
0,
|
||||
|
@ -2528,9 +2535,22 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
election_page = type_result.follow()
|
||||
|
||||
# Go back to SO page and test the dynamic text changed
|
||||
# Navigate to the org page as that is the step right before senior_official
|
||||
org_page = election_page.click(str(self.TITLES["organization_contact"]), index=0)
|
||||
org_contact_form = org_page.forms[0]
|
||||
org_contact_form["organization_contact-organization_name"] = "Testorg"
|
||||
org_contact_form["organization_contact-address_line1"] = "address 1"
|
||||
org_contact_form["organization_contact-address_line2"] = "address 2"
|
||||
org_contact_form["organization_contact-city"] = "NYC"
|
||||
org_contact_form["organization_contact-state_territory"] = "NY"
|
||||
org_contact_form["organization_contact-zipcode"] = "10002"
|
||||
org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks"
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
so_page = election_page.click(str(self.TITLES["senior_official"]), index=0)
|
||||
org_contact_result = org_contact_form.submit()
|
||||
|
||||
# Navigate back to the so page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
so_page = org_contact_result.follow()
|
||||
self.assertContains(so_page, "Domain requests from cities")
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -2628,9 +2648,15 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
election_page = type_result.follow()
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
current_websites = election_page.click(str(self.TITLES["current_sites"]), index=0)
|
||||
current_sites_form = current_websites.forms[0]
|
||||
current_sites_form["current_sites-0-website"] = "www.city.com"
|
||||
current_sites_result = current_sites_form.submit().follow()
|
||||
|
||||
# Go back to dotgov domain page to test the dynamic text changed
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
dotgov_page = election_page.click(str(self.TITLES["dotgov_domain"]), index=0)
|
||||
dotgov_page = current_sites_result.click(str(self.TITLES["dotgov_domain"]), index=0)
|
||||
self.assertContains(dotgov_page, "CityofEudoraKS.gov")
|
||||
self.assertNotContains(dotgov_page, "medicare.gov")
|
||||
|
||||
|
@ -2984,6 +3010,9 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
|
|||
"""Test when all fields in the domain request are filled."""
|
||||
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED, user=self.user)
|
||||
domain_request.anything_else = False
|
||||
domain_request.has_anything_else_text = False
|
||||
domain_request.save()
|
||||
|
||||
response = self.app.get(f"/domain-request/{domain_request.id}/edit/")
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
|
|
|
@ -217,7 +217,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
if current_url == self.EDIT_URL_NAME and "id" in kwargs:
|
||||
del self.storage
|
||||
self.storage["domain_request_id"] = kwargs["id"]
|
||||
self.storage["step_history"] = self.db_check_for_unlocking_steps()
|
||||
|
||||
# if accessing this class directly, redirect to either to an acknowledgement
|
||||
# page or to the first step in the processes (if an edit rather than a new request);
|
||||
|
@ -233,6 +232,9 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
else:
|
||||
return self.goto(self.steps.first)
|
||||
|
||||
# refresh step_history to ensure we don't erroneously unlock unfinished
|
||||
# steps just because we visited it
|
||||
self.storage["step_history"] = self.db_check_for_unlocking_steps()
|
||||
context = self.get_context_data()
|
||||
self.steps.current = current_url
|
||||
context["forms"] = self.get_forms()
|
||||
|
@ -341,6 +343,17 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
"""Helper for get_context_data
|
||||
|
||||
Queries the DB for a domain request and returns a list of unlocked steps."""
|
||||
|
||||
# The way this works is as follows:
|
||||
# Each step is assigned a true/false value to determine if it is
|
||||
# "unlocked" or not. This dictionary of values is looped through
|
||||
# at the end of this function and any step with a "true" value is
|
||||
# added to a simple array that is returned at the end of this function.
|
||||
# This array is eventually passed to the frontend context (eg. domain_request_sidebar.html),
|
||||
# and is used to determine how steps appear in the side nav.
|
||||
# It is worth noting that any step assigned "false" here will be EXCLUDED
|
||||
# from the list of "unlocked" steps.
|
||||
|
||||
history_dict = {
|
||||
"generic_org_type": self.domain_request.generic_org_type is not None,
|
||||
"tribal_government": self.domain_request.tribe_name is not None,
|
||||
|
@ -368,8 +381,11 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
or self.domain_request.no_other_contacts_rationale is not None
|
||||
),
|
||||
"additional_details": (
|
||||
(self.domain_request.anything_else is not None and self.domain_request.has_cisa_representative)
|
||||
or self.domain_request.is_policy_acknowledged is not None
|
||||
# Additional details is complete as long as "has anything else" and "has cisa rep" are not None
|
||||
(
|
||||
self.domain_request.has_anything_else_text is not None
|
||||
and self.domain_request.has_cisa_representative is not None
|
||||
)
|
||||
),
|
||||
"requirements": self.domain_request.is_policy_acknowledged is not None,
|
||||
"review": self.domain_request.is_policy_acknowledged is not None,
|
||||
|
@ -626,8 +642,8 @@ class AdditionalDetails(DomainRequestWizard):
|
|||
forms = [
|
||||
forms.CisaRepresentativeYesNoForm,
|
||||
forms.CisaRepresentativeForm,
|
||||
forms.AdditionalDetailsYesNoForm,
|
||||
forms.AdditionalDetailsForm,
|
||||
forms.AnythingElseYesNoForm,
|
||||
forms.AnythingElseForm,
|
||||
]
|
||||
|
||||
def is_valid(self, forms: list) -> bool:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue