mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-12 04:29:44 +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();
|
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(),
|
views.DomainOrgNameAddressView.as_view(),
|
||||||
name="domain-org-name-address",
|
name="domain-org-name-address",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"domain/<int:pk>/suborganization",
|
||||||
|
views.DomainSubOrganizationView.as_view(),
|
||||||
|
name="domain-suborganization",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"domain/<int:pk>/senior-official",
|
"domain/<int:pk>/senior-official",
|
||||||
views.DomainSeniorOfficialView.as_view(),
|
views.DomainSeniorOfficialView.as_view(),
|
||||||
|
|
|
@ -9,6 +9,7 @@ from .domain import (
|
||||||
DomainDnssecForm,
|
DomainDnssecForm,
|
||||||
DomainDsdataFormset,
|
DomainDsdataFormset,
|
||||||
DomainDsdataForm,
|
DomainDsdataForm,
|
||||||
|
DomainSuborganizationForm,
|
||||||
)
|
)
|
||||||
from .portfolio import (
|
from .portfolio import (
|
||||||
PortfolioOrgAddressForm,
|
PortfolioOrgAddressForm,
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.core.validators import MinValueValidator, MaxValueValidator, RegexVa
|
||||||
from django.forms import formset_factory
|
from django.forms import formset_factory
|
||||||
from registrar.models import DomainRequest
|
from registrar.models import DomainRequest
|
||||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||||
|
from registrar.models.suborganization import Suborganization
|
||||||
from registrar.models.utility.domain_helper import DomainHelper
|
from registrar.models.utility.domain_helper import DomainHelper
|
||||||
from registrar.utility.errors import (
|
from registrar.utility.errors import (
|
||||||
NameserverError,
|
NameserverError,
|
||||||
|
@ -153,6 +154,42 @@ class DomainNameserverForm(forms.Form):
|
||||||
self.add_error("ip", str(e))
|
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):
|
class BaseNameserverFormset(forms.BaseFormSet):
|
||||||
def clean(self):
|
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.EDIT_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
|
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
|
||||||
|
# Domain: field specific permissions
|
||||||
|
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
|
||||||
],
|
],
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||||
UserPortfolioPermissionChoices.VIEW_MEMBER,
|
UserPortfolioPermissionChoices.VIEW_MEMBER,
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
# Domain: field specific permissions
|
||||||
|
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
|
||||||
],
|
],
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
@ -270,6 +275,13 @@ class User(AbstractUser):
|
||||||
"""Determines if the current user can view all available domains in a given portfolio"""
|
"""Determines if the current user can view all available domains in a given portfolio"""
|
||||||
return self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
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
|
@classmethod
|
||||||
def needs_identity_verification(cls, email, uuid):
|
def needs_identity_verification(cls, email, uuid):
|
||||||
"""A method used by our oidc classes to test whether a user needs email/uuid verification
|
"""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"
|
VIEW_PORTFOLIO = "view_portfolio", "View organization"
|
||||||
EDIT_PORTFOLIO = "edit_portfolio", "Edit 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" %}
|
{% extends "domain_base.html" %}
|
||||||
{% load static url_helpers %}
|
{% load static url_helpers %}
|
||||||
|
{% load custom_filters %}
|
||||||
|
|
||||||
{% block domain_content %}
|
{% block domain_content %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
@ -64,11 +65,9 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if portfolio %}
|
{% if portfolio and has_domains_portfolio_permission and request.user.has_view_suborganization %}
|
||||||
{% comment %} TODO - uncomment in #2352 and add to edit_link
|
|
||||||
{% url 'domain-suborganization' pk=domain.id as url %}
|
{% 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=url editable=is_editable|and:request.user.has_edit_suborganization %}
|
||||||
{% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link="#" editable=is_editable %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% url 'domain-org-name-address' pk=domain.id as url %}
|
{% 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 %}
|
{% 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 %}
|
{% if portfolio %}
|
||||||
{% with url="#" %}
|
{% 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" %}
|
{% include "includes/domain_sidenav_item.html" with item_text="Suborganization" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% with url_name="domain-senior-official" %}
|
{% with url_name="domain-senior-official" %}
|
||||||
{% include "includes/domain_sidenav_item.html" with item_text="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="name" scope="col" role="columnheader">Domain name</th>
|
||||||
<th data-sortable="expiration_date" scope="col" role="columnheader">Expires</th>
|
<th data-sortable="expiration_date" scope="col" role="columnheader">Expires</th>
|
||||||
<th data-sortable="state_display" scope="col" role="columnheader">Status</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>
|
<th data-sortable="suborganization" scope="col" role="columnheader">Suborganization</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th
|
<th
|
||||||
|
|
|
@ -4,4 +4,11 @@ Template include for read-only form fields
|
||||||
|
|
||||||
|
|
||||||
<h4 class="read-only-label">{{ field.label }}</h4>
|
<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
|
@register.filter
|
||||||
def in_path(url, path):
|
def in_path(url, path):
|
||||||
return url in 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):
|
class TestDomainSuborganization(TestDomainOverview):
|
||||||
"""Tests the Suborganization page for portfolio users"""
|
"""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
|
@less_console_noise_decorator
|
||||||
@override_flag("organization_feature", active=True)
|
@override_flag("organization_feature", active=True)
|
||||||
def test_has_suborganization_field_on_overview_with_flag(self):
|
def test_has_suborganization_field_on_overview_with_flag(self):
|
||||||
|
|
|
@ -3,6 +3,7 @@ from .domain import (
|
||||||
DomainView,
|
DomainView,
|
||||||
DomainSeniorOfficialView,
|
DomainSeniorOfficialView,
|
||||||
DomainOrgNameAddressView,
|
DomainOrgNameAddressView,
|
||||||
|
DomainSubOrganizationView,
|
||||||
DomainDNSView,
|
DomainDNSView,
|
||||||
DomainNameserversView,
|
DomainNameserversView,
|
||||||
DomainDNSSECView,
|
DomainDNSSECView,
|
||||||
|
|
|
@ -15,7 +15,7 @@ from django.shortcuts import redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from registrar.forms.domain import DomainSuborganizationForm
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
Domain,
|
Domain,
|
||||||
DomainRequest,
|
DomainRequest,
|
||||||
|
@ -242,6 +242,51 @@ class DomainOrgNameAddressView(DomainFormBaseView):
|
||||||
return super().has_permission()
|
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):
|
class DomainSeniorOfficialView(DomainFormBaseView):
|
||||||
"""Domain senior official editing view."""
|
"""Domain senior official editing view."""
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue