mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-01 15:34:53 +02:00
Merge pull request #2536 from cisagov/za/portfolio-user-can-view-and-update-suborgs
Ticket #2352: Edit suborganization on portfolio domains
This commit is contained in:
commit
df4a02f8ce
17 changed files with 465 additions and 10 deletions
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -74,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,
|
||||
|
@ -270,6 +275,13 @@ class User(AbstractUser):
|
|||
"""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
|
||||
|
|
|
@ -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,9 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if portfolio %}
|
||||
{% comment %} TODO - uncomment in #2352 and add to edit_link
|
||||
{% if portfolio and has_domains_portfolio_permission and request.user.has_view_suborganization %}
|
||||
{% url 'domain-suborganization' pk=domain.id as url %}
|
||||
{% endcomment %}
|
||||
{% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link="#" editable=is_editable %}
|
||||
{% 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 %}
|
||||
|
|
|
@ -60,9 +60,12 @@
|
|||
|
||||
|
||||
{% if portfolio %}
|
||||
{% with url="#" %}
|
||||
{% include "includes/domain_sidenav_item.html" with item_text="Suborganization" %}
|
||||
{% endwith %}
|
||||
{% 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" %}
|
||||
|
|
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 %}
|
|
@ -152,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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1526,6 +1526,110 @@ class TestDomainOrganization(TestDomainOverview):
|
|||
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):
|
||||
|
|
|
@ -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,
|
||||
|
@ -242,6 +242,51 @@ class DomainOrgNameAddressView(DomainFormBaseView):
|
|||
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."""
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue