mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-26 04:28:39 +02:00
merge
This commit is contained in:
commit
ff37b3bc9b
19 changed files with 316 additions and 92 deletions
|
@ -1233,7 +1233,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
search_help_text = "Search by domain."
|
search_help_text = "Search by domain."
|
||||||
|
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(None, {"fields": ["creator", "submitter", "domain_request", "notes"]}),
|
(None, {"fields": ["portfolio", "creator", "submitter", "domain_request", "notes"]}),
|
||||||
(".gov domain", {"fields": ["domain"]}),
|
(".gov domain", {"fields": ["domain"]}),
|
||||||
("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}),
|
("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}),
|
||||||
("Background info", {"fields": ["anything_else"]}),
|
("Background info", {"fields": ["anything_else"]}),
|
||||||
|
@ -1319,6 +1319,32 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
change_form_template = "django/admin/domain_information_change_form.html"
|
change_form_template = "django/admin/domain_information_change_form.html"
|
||||||
|
|
||||||
|
superuser_only_fields = [
|
||||||
|
"portfolio",
|
||||||
|
]
|
||||||
|
|
||||||
|
# DEVELOPER's NOTE:
|
||||||
|
# Normally, to exclude a field from an Admin form, we could simply utilize
|
||||||
|
# Django's "exclude" feature. However, it causes a "missing key" error if we
|
||||||
|
# go that route for this particular form. The error gets thrown by our
|
||||||
|
# custom fieldset.html code and is due to the fact that "exclude" removes
|
||||||
|
# fields from base_fields but not fieldsets. Rather than reworking our
|
||||||
|
# custom frontend, it seems more straightforward (and easier to read) to simply
|
||||||
|
# modify the fieldsets list so that it excludes any fields we want to remove
|
||||||
|
# based on permissions (eg. superuser_only_fields) or other conditions.
|
||||||
|
def get_fieldsets(self, request, obj=None):
|
||||||
|
fieldsets = self.fieldsets
|
||||||
|
|
||||||
|
# Create a modified version of fieldsets to exclude certain fields
|
||||||
|
if not request.user.has_perm("registrar.full_access_permission"):
|
||||||
|
modified_fieldsets = []
|
||||||
|
for name, data in fieldsets:
|
||||||
|
fields = data.get("fields", [])
|
||||||
|
fields = tuple(field for field in fields if field not in DomainInformationAdmin.superuser_only_fields)
|
||||||
|
modified_fieldsets.append((name, {"fields": fields}))
|
||||||
|
return modified_fieldsets
|
||||||
|
return fieldsets
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
"""Set the read-only state on form elements.
|
"""Set the read-only state on form elements.
|
||||||
We have 1 conditions that determine which fields are read-only:
|
We have 1 conditions that determine which fields are read-only:
|
||||||
|
@ -1482,6 +1508,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
None,
|
None,
|
||||||
{
|
{
|
||||||
"fields": [
|
"fields": [
|
||||||
|
"portfolio",
|
||||||
"status",
|
"status",
|
||||||
"rejection_reason",
|
"rejection_reason",
|
||||||
"action_needed_reason",
|
"action_needed_reason",
|
||||||
|
@ -1592,6 +1619,32 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
]
|
]
|
||||||
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
|
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
|
||||||
|
|
||||||
|
superuser_only_fields = [
|
||||||
|
"portfolio",
|
||||||
|
]
|
||||||
|
|
||||||
|
# DEVELOPER's NOTE:
|
||||||
|
# Normally, to exclude a field from an Admin form, we could simply utilize
|
||||||
|
# Django's "exclude" feature. However, it causes a "missing key" error if we
|
||||||
|
# go that route for this particular form. The error gets thrown by our
|
||||||
|
# custom fieldset.html code and is due to the fact that "exclude" removes
|
||||||
|
# fields from base_fields but not fieldsets. Rather than reworking our
|
||||||
|
# custom frontend, it seems more straightforward (and easier to read) to simply
|
||||||
|
# modify the fieldsets list so that it excludes any fields we want to remove
|
||||||
|
# based on permissions (eg. superuser_only_fields) or other conditions.
|
||||||
|
def get_fieldsets(self, request, obj=None):
|
||||||
|
fieldsets = super().get_fieldsets(request, obj)
|
||||||
|
|
||||||
|
# Create a modified version of fieldsets to exclude certain fields
|
||||||
|
if not request.user.has_perm("registrar.full_access_permission"):
|
||||||
|
modified_fieldsets = []
|
||||||
|
for name, data in fieldsets:
|
||||||
|
fields = data.get("fields", [])
|
||||||
|
fields = tuple(field for field in fields if field not in self.superuser_only_fields)
|
||||||
|
modified_fieldsets.append((name, {"fields": fields}))
|
||||||
|
return modified_fieldsets
|
||||||
|
return fieldsets
|
||||||
|
|
||||||
# Table ordering
|
# Table ordering
|
||||||
# NOTE: This impacts the select2 dropdowns (combobox)
|
# NOTE: This impacts the select2 dropdowns (combobox)
|
||||||
# Currentl, there's only one for requests on DomainInfo
|
# Currentl, there's only one for requests on DomainInfo
|
||||||
|
@ -1937,13 +1990,7 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
template = "django/admin/includes/domain_info_inline_stacked.html"
|
template = "django/admin/includes/domain_info_inline_stacked.html"
|
||||||
model = models.DomainInformation
|
model = models.DomainInformation
|
||||||
|
|
||||||
fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets)
|
fieldsets = DomainInformationAdmin.fieldsets
|
||||||
# remove .gov domain from fieldset
|
|
||||||
for index, (title, f) in enumerate(fieldsets):
|
|
||||||
if title == ".gov domain":
|
|
||||||
del fieldsets[index]
|
|
||||||
break
|
|
||||||
|
|
||||||
readonly_fields = DomainInformationAdmin.readonly_fields
|
readonly_fields = DomainInformationAdmin.readonly_fields
|
||||||
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
|
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
|
||||||
|
|
||||||
|
@ -1990,6 +2037,23 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
return DomainInformationAdmin.get_readonly_fields(self, request, obj=None)
|
return DomainInformationAdmin.get_readonly_fields(self, request, obj=None)
|
||||||
|
|
||||||
|
# Re-route the get_fieldsets method to utilize DomainInformationAdmin.get_fieldsets
|
||||||
|
# since that has all the logic for excluding certain fields according to user permissions.
|
||||||
|
# Then modify the remaining fields to further trim out any we don't want for this inline
|
||||||
|
# form
|
||||||
|
def get_fieldsets(self, request, obj=None):
|
||||||
|
# Grab fieldsets from DomainInformationAdmin so that it handles all logic
|
||||||
|
# for permission-based field visibility.
|
||||||
|
modified_fieldsets = DomainInformationAdmin.get_fieldsets(self, request, obj=None)
|
||||||
|
|
||||||
|
# remove .gov domain from fieldset
|
||||||
|
for index, (title, f) in enumerate(modified_fieldsets):
|
||||||
|
if title == ".gov domain":
|
||||||
|
del modified_fieldsets[index]
|
||||||
|
break
|
||||||
|
|
||||||
|
return modified_fieldsets
|
||||||
|
|
||||||
|
|
||||||
class DomainResource(FsmModelResource):
|
class DomainResource(FsmModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
@ -2656,6 +2720,14 @@ class WaffleFlagAdmin(FlagAdmin):
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class DomainGroupAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
list_display = ["name", "portfolio"]
|
||||||
|
|
||||||
|
|
||||||
|
class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
list_display = ["name", "portfolio"]
|
||||||
|
|
||||||
|
|
||||||
admin.site.unregister(LogEntry) # Unregister the default registration
|
admin.site.unregister(LogEntry) # Unregister the default registration
|
||||||
|
|
||||||
admin.site.register(LogEntry, CustomLogEntryAdmin)
|
admin.site.register(LogEntry, CustomLogEntryAdmin)
|
||||||
|
@ -2679,6 +2751,8 @@ admin.site.register(models.DomainRequest, DomainRequestAdmin)
|
||||||
admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
|
admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
|
||||||
admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
|
admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
|
||||||
admin.site.register(models.Portfolio, PortfolioAdmin)
|
admin.site.register(models.Portfolio, PortfolioAdmin)
|
||||||
|
admin.site.register(models.DomainGroup, DomainGroupAdmin)
|
||||||
|
admin.site.register(models.Suborganization, SuborganizationAdmin)
|
||||||
|
|
||||||
# Register our custom waffle implementations
|
# Register our custom waffle implementations
|
||||||
admin.site.register(models.WaffleFlag, WaffleFlagAdmin)
|
admin.site.register(models.WaffleFlag, WaffleFlagAdmin)
|
||||||
|
|
|
@ -33,6 +33,33 @@ const showElement = (element) => {
|
||||||
element.classList.remove('display-none');
|
element.classList.remove('display-none');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that scrolls to an element
|
||||||
|
* @param {string} attributeName - The string "class" or "id"
|
||||||
|
* @param {string} attributeValue - The class or id name
|
||||||
|
*/
|
||||||
|
function ScrollToElement(attributeName, attributeValue) {
|
||||||
|
let targetEl = null;
|
||||||
|
|
||||||
|
if (attributeName === 'class') {
|
||||||
|
targetEl = document.getElementsByClassName(attributeValue)[0];
|
||||||
|
} else if (attributeName === 'id') {
|
||||||
|
targetEl = document.getElementById(attributeValue);
|
||||||
|
} else {
|
||||||
|
console.log('Error: unknown attribute name provided.');
|
||||||
|
return; // Exit the function if an invalid attributeName is provided
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetEl) {
|
||||||
|
const rect = targetEl.getBoundingClientRect();
|
||||||
|
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||||
|
window.scrollTo({
|
||||||
|
top: rect.top + scrollTop,
|
||||||
|
behavior: 'smooth' // Optional: for smooth scrolling
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Makes an element invisible. */
|
/** Makes an element invisible. */
|
||||||
function makeHidden(el) {
|
function makeHidden(el) {
|
||||||
el.style.position = "absolute";
|
el.style.position = "absolute";
|
||||||
|
@ -1407,6 +1434,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
* @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page
|
* @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page
|
||||||
*/
|
*/
|
||||||
function deleteDomainRequest(domainRequestPk,pageToDisplay) {
|
function deleteDomainRequest(domainRequestPk,pageToDisplay) {
|
||||||
|
|
||||||
|
// Use to debug uswds modal issues
|
||||||
|
//console.log('deleteDomainRequest')
|
||||||
|
|
||||||
// Get csrf token
|
// Get csrf token
|
||||||
const csrfToken = getCsrfToken();
|
const csrfToken = getCsrfToken();
|
||||||
// Create FormData object and append the CSRF token
|
// Create FormData object and append the CSRF token
|
||||||
|
@ -1461,6 +1492,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
const tbody = document.querySelector('.domain-requests__table tbody');
|
const tbody = document.querySelector('.domain-requests__table tbody');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
// Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases
|
||||||
|
// We do NOT want that as it will cause multiple placeholders and therefore multiple inits on delete,
|
||||||
|
// which will cause bad delete requests to be sent.
|
||||||
|
const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]');
|
||||||
|
preExistingModalPlaceholders.forEach(element => {
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
|
||||||
// remove any existing modal elements from the DOM so they can be properly re-initialized
|
// remove any existing modal elements from the DOM so they can be properly re-initialized
|
||||||
// after the DOM content changes and there are new delete modal buttons added
|
// after the DOM content changes and there are new delete modal buttons added
|
||||||
unloadModals();
|
unloadModals();
|
||||||
|
@ -1582,7 +1621,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
domainRequestsSectionWrapper.appendChild(modal);
|
domainRequestsSectionWrapper.appendChild(modal);
|
||||||
}
|
}
|
||||||
|
|
|
@ -189,7 +189,7 @@ MIDDLEWARE = [
|
||||||
# Used for waffle feature flags
|
# Used for waffle feature flags
|
||||||
"waffle.middleware.WaffleMiddleware",
|
"waffle.middleware.WaffleMiddleware",
|
||||||
"registrar.registrar_middleware.CheckUserProfileMiddleware",
|
"registrar.registrar_middleware.CheckUserProfileMiddleware",
|
||||||
"registrar.registrar_middleware.CheckOrganizationMiddleware",
|
"registrar.registrar_middleware.CheckPortfolioMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
# application object used by Django’s built-in servers (e.g. `runserver`)
|
# application object used by Django’s built-in servers (e.g. `runserver`)
|
||||||
|
|
|
@ -25,7 +25,7 @@ from registrar.views.domain_request import Step
|
||||||
from registrar.views.domain_requests_json import get_domain_requests_json
|
from registrar.views.domain_requests_json import get_domain_requests_json
|
||||||
from registrar.views.domains_json import get_domains_json
|
from registrar.views.domains_json import get_domains_json
|
||||||
from registrar.views.utility import always_404
|
from registrar.views.utility import always_404
|
||||||
from registrar.views.organizations import organization_domains, organization_domain_requests
|
from registrar.views.portfolios import portfolio_domains, portfolio_domain_requests
|
||||||
from api.views import available, get_current_federal, get_current_full
|
from api.views import available, get_current_federal, get_current_full
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,14 +60,14 @@ for step, view in [
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.index, name="home"),
|
path("", views.index, name="home"),
|
||||||
path(
|
path(
|
||||||
"organization/<int:portfolio_id>/domains/",
|
"portfolio/<int:portfolio_id>/domains/",
|
||||||
organization_domains,
|
portfolio_domains,
|
||||||
name="organization-domains",
|
name="portfolio-domains",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"organization/<int:portfolio_id>/domain_requests/",
|
"portfolio/<int:portfolio_id>/domain_requests/",
|
||||||
organization_domain_requests,
|
portfolio_domain_requests,
|
||||||
name="organization-domain-requests",
|
name="portfolio-domain-requests",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"admin/logout/",
|
"admin/logout/",
|
||||||
|
|
41
src/registrar/migrations/0105_suborganization_domaingroup.py
Normal file
41
src/registrar/migrations/0105_suborganization_domaingroup.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-06-21 18:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0104_create_groups_v13"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Suborganization",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
("name", models.CharField(help_text="Suborganization", max_length=1000, unique=True)),
|
||||||
|
("portfolio", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="registrar.portfolio")),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="DomainGroup",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
("name", models.CharField(help_text="Domain group", unique=True)),
|
||||||
|
("domains", models.ManyToManyField(blank=True, to="registrar.domaininformation")),
|
||||||
|
("portfolio", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="registrar.portfolio")),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"unique_together": {("name", "portfolio")},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
37
src/registrar/migrations/0106_create_groups_v14.py
Normal file
37
src/registrar/migrations/0106_create_groups_v14.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
|
||||||
|
# It is dependent on 0079 (which populates federal agencies)
|
||||||
|
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
|
||||||
|
# in the user_group model then:
|
||||||
|
# [NOT RECOMMENDED]
|
||||||
|
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
|
||||||
|
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
|
||||||
|
# step 3: fake run the latest migration in the migrations list
|
||||||
|
# [RECOMMENDED]
|
||||||
|
# Alternatively:
|
||||||
|
# step 1: duplicate the migration that loads data
|
||||||
|
# step 2: docker-compose exec app ./manage.py migrate
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from registrar.models import UserGroup
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
# For linting: RunPython expects a function reference,
|
||||||
|
# so let's give it one
|
||||||
|
def create_groups(apps, schema_editor) -> Any:
|
||||||
|
UserGroup.create_cisa_analyst_group(apps, schema_editor)
|
||||||
|
UserGroup.create_full_access_group(apps, schema_editor)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0105_suborganization_domaingroup"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
create_groups,
|
||||||
|
reverse_code=migrations.RunPython.noop,
|
||||||
|
atomic=True,
|
||||||
|
),
|
||||||
|
]
|
|
@ -17,6 +17,8 @@ from .transition_domain import TransitionDomain
|
||||||
from .verified_by_staff import VerifiedByStaff
|
from .verified_by_staff import VerifiedByStaff
|
||||||
from .waffle_flag import WaffleFlag
|
from .waffle_flag import WaffleFlag
|
||||||
from .portfolio import Portfolio
|
from .portfolio import Portfolio
|
||||||
|
from .domain_group import DomainGroup
|
||||||
|
from .suborganization import Suborganization
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -38,6 +40,8 @@ __all__ = [
|
||||||
"VerifiedByStaff",
|
"VerifiedByStaff",
|
||||||
"WaffleFlag",
|
"WaffleFlag",
|
||||||
"Portfolio",
|
"Portfolio",
|
||||||
|
"DomainGroup",
|
||||||
|
"Suborganization",
|
||||||
]
|
]
|
||||||
|
|
||||||
auditlog.register(Contact)
|
auditlog.register(Contact)
|
||||||
|
@ -58,3 +62,5 @@ auditlog.register(TransitionDomain)
|
||||||
auditlog.register(VerifiedByStaff)
|
auditlog.register(VerifiedByStaff)
|
||||||
auditlog.register(WaffleFlag)
|
auditlog.register(WaffleFlag)
|
||||||
auditlog.register(Portfolio)
|
auditlog.register(Portfolio)
|
||||||
|
auditlog.register(DomainGroup)
|
||||||
|
auditlog.register(Suborganization)
|
||||||
|
|
23
src/registrar/models/domain_group.py
Normal file
23
src/registrar/models/domain_group.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from django.db import models
|
||||||
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
|
||||||
|
|
||||||
|
class DomainGroup(TimeStampedModel):
|
||||||
|
"""
|
||||||
|
Organized group of domains.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = [("name", "portfolio")]
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
unique=True,
|
||||||
|
help_text="Domain group",
|
||||||
|
)
|
||||||
|
|
||||||
|
portfolio = models.ForeignKey("registrar.Portfolio", on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
domains = models.ManyToManyField("registrar.DomainInformation", blank=True)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.name}"
|
|
@ -97,3 +97,6 @@ class Portfolio(TimeStampedModel):
|
||||||
verbose_name="security contact e-mail",
|
verbose_name="security contact e-mail",
|
||||||
max_length=320,
|
max_length=320,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.organization_name}"
|
||||||
|
|
22
src/registrar/models/suborganization.py
Normal file
22
src/registrar/models/suborganization.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from django.db import models
|
||||||
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
|
||||||
|
|
||||||
|
class Suborganization(TimeStampedModel):
|
||||||
|
"""
|
||||||
|
Suborganization under an organization (portfolio)
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
unique=True,
|
||||||
|
max_length=1000,
|
||||||
|
help_text="Suborganization",
|
||||||
|
)
|
||||||
|
|
||||||
|
portfolio = models.ForeignKey(
|
||||||
|
"registrar.Portfolio",
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.name}"
|
|
@ -125,10 +125,10 @@ class CheckUserProfileMiddleware:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class CheckOrganizationMiddleware:
|
class CheckPortfolioMiddleware:
|
||||||
"""
|
"""
|
||||||
Checks if the current user has a portfolio
|
Checks if the current user has a portfolio
|
||||||
If they do, redirect them to the org homepage when they navigate to home.
|
If they do, redirect them to the portfolio homepage when they navigate to home.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, get_response):
|
def __init__(self, get_response):
|
||||||
|
@ -150,8 +150,6 @@ class CheckOrganizationMiddleware:
|
||||||
user_portfolios = Portfolio.objects.filter(creator=request.user)
|
user_portfolios = Portfolio.objects.filter(creator=request.user)
|
||||||
if user_portfolios.exists():
|
if user_portfolios.exists():
|
||||||
first_portfolio = user_portfolios.first()
|
first_portfolio = user_portfolios.first()
|
||||||
home_organization_with_portfolio = reverse(
|
home_with_portfolio = reverse("portfolio-domains", kwargs={"portfolio_id": first_portfolio.id})
|
||||||
"organization-domains", kwargs={"portfolio_id": first_portfolio.id}
|
return HttpResponseRedirect(home_with_portfolio)
|
||||||
)
|
|
||||||
return HttpResponseRedirect(home_organization_with_portfolio)
|
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</legend>
|
</legend>
|
||||||
|
|
||||||
<!-- Toggle -->
|
<!-- Toggle -->
|
||||||
<em>Select one (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).</em>
|
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||||
{% input_with_errors forms.0.has_cisa_representative %}
|
{% input_with_errors forms.0.has_cisa_representative %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
</legend>
|
</legend>
|
||||||
|
|
||||||
<!-- Toggle -->
|
<!-- Toggle -->
|
||||||
<em>Select one (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).</em>
|
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||||
{% input_with_errors forms.2.has_anything_else_text %}
|
{% input_with_errors forms.2.has_anything_else_text %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="margin-top-3" id="anything-else">
|
<div class="margin-top-3" id="anything-else">
|
||||||
<p>Provide details below (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).</p>
|
<p>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></p>
|
||||||
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||||
{% input_with_errors forms.3.anything_else %}
|
{% input_with_errors forms.3.anything_else %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
{% extends 'home.html' %}
|
|
||||||
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block homepage_content %}
|
|
||||||
|
|
||||||
<div class="tablet:grid-col-12">
|
|
||||||
<div class="grid-row grid-gap">
|
|
||||||
<div class="tablet:grid-col-3">
|
|
||||||
{% include "organization_sidebar.html" with portfolio=portfolio %}
|
|
||||||
</div>
|
|
||||||
<div class="tablet:grid-col-9">
|
|
||||||
{% block messages %}
|
|
||||||
{% include "includes/form_messages.html" %}
|
|
||||||
{% endblock %}
|
|
||||||
{# Note: Reimplement commented out functionality #}
|
|
||||||
|
|
||||||
{% block organization_content %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{# Note: Reimplement this after MVP #}
|
|
||||||
<!--
|
|
||||||
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
|
||||||
<h2>Archived domains</h2>
|
|
||||||
<p>You don't have any archived domains</p>
|
|
||||||
</section>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- Note: Uncomment below when this is being implemented post-MVP -->
|
|
||||||
<!-- <section class="tablet:grid-col-11 desktop:grid-col-10">
|
|
||||||
<h2 class="padding-top-1 mobile-lg:padding-top-3"> Export domains</h2>
|
|
||||||
<p>Download a list of your domains and their statuses as a csv file.</p>
|
|
||||||
<a href="{% url 'todo' %}" class="usa-button usa-button--outline">
|
|
||||||
Export domains as csv
|
|
||||||
</a>
|
|
||||||
</section>
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
24
src/registrar/templates/portfolio.html
Normal file
24
src/registrar/templates/portfolio.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends 'home.html' %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block homepage_content %}
|
||||||
|
|
||||||
|
<div class="tablet:grid-col-12">
|
||||||
|
<div class="grid-row grid-gap">
|
||||||
|
<div class="tablet:grid-col-3">
|
||||||
|
{% include "portfolio_sidebar.html" with portfolio=portfolio %}
|
||||||
|
</div>
|
||||||
|
<div class="tablet:grid-col-9">
|
||||||
|
{% block messages %}
|
||||||
|
{% include "includes/form_messages.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
{# Note: Reimplement commented out functionality #}
|
||||||
|
|
||||||
|
{% block portfolio_content %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends 'organization.html' %}
|
{% extends 'portfolio.html' %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block organization_content %}
|
{% block portfolio_content %}
|
||||||
<h1>Domains</h1>
|
<h1>Domains</h1>
|
||||||
{% include "includes/domains_table.html" with portfolio=portfolio %}
|
{% include "includes/domains_table.html" with portfolio=portfolio %}
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends 'organization.html' %}
|
{% extends 'portfolio.html' %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block organization_content %}
|
{% block portfolio_content %}
|
||||||
<h1>Domain requests</h1>
|
<h1>Domain requests</h1>
|
||||||
|
|
||||||
{% comment %}
|
{% comment %}
|
|
@ -5,14 +5,14 @@
|
||||||
<h2 class="margin-top-0 text-semibold">{{ portfolio.organization_name }}</h2>
|
<h2 class="margin-top-0 text-semibold">{{ portfolio.organization_name }}</h2>
|
||||||
<ul class="usa-sidenav usa-sidenav--portfolio">
|
<ul class="usa-sidenav usa-sidenav--portfolio">
|
||||||
<li class="usa-sidenav__item">
|
<li class="usa-sidenav__item">
|
||||||
{% url 'organization-domains' portfolio.id as url %}
|
{% url 'portfolio-domains' portfolio.id as url %}
|
||||||
<a href="{{ url }}" {% if request.path == url %}class="usa-current"{% endif %}>
|
<a href="{{ url }}" {% if request.path == url %}class="usa-current"{% endif %}>
|
||||||
Domains
|
Domains
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-sidenav__item">
|
<li class="usa-sidenav__item">
|
||||||
{% url 'organization-domain-requests' portfolio.id as url %}
|
{% url 'portfolio-domain-requests' portfolio.id as url %}
|
||||||
<a href="{{ url }}" {% if request.path == url %}class="usa-current"{% endif %}>
|
<a href="{{ url }}" {% if request.path == url %}class="usa-current"{% endif %}>
|
||||||
Domain requests
|
Domain requests
|
||||||
</a>
|
</a>
|
|
@ -908,7 +908,7 @@ class UserProfileTests(TestWithUser, WebTest):
|
||||||
self.assertContains(profile_page, "Your profile has been updated")
|
self.assertContains(profile_page, "Your profile has been updated")
|
||||||
|
|
||||||
|
|
||||||
class OrganizationsTests(TestWithUser, WebTest):
|
class PortfoliosTests(TestWithUser, WebTest):
|
||||||
"""A series of tests that target the organizations"""
|
"""A series of tests that target the organizations"""
|
||||||
|
|
||||||
# csrf checks do not work well with WebTest.
|
# csrf checks do not work well with WebTest.
|
||||||
|
@ -939,33 +939,33 @@ class OrganizationsTests(TestWithUser, WebTest):
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_middleware_redirects_to_organization_homepage(self):
|
def test_middleware_redirects_to_portfolio_homepage(self):
|
||||||
"""Tests that a user is redirected to the org homepage when organization_feature is on and
|
"""Tests that a user is redirected to the portfolio homepage when organization_feature is on and
|
||||||
a portfolio belongs to the user, test for the special h1s which only exist in that version
|
a portfolio belongs to the user, test for the special h1s which only exist in that version
|
||||||
of the homepage"""
|
of the homepage"""
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
with override_flag("organization_feature", active=True):
|
with override_flag("organization_feature", active=True):
|
||||||
# This will redirect the user to the org page.
|
# This will redirect the user to the portfolio page.
|
||||||
# Follow implicity checks if our redirect is working.
|
# Follow implicity checks if our redirect is working.
|
||||||
org_page = self.app.get(reverse("home")).follow()
|
portfolio_page = self.app.get(reverse("home")).follow()
|
||||||
self._set_session_cookie()
|
self._set_session_cookie()
|
||||||
|
|
||||||
# Assert that we're on the right page
|
# Assert that we're on the right page
|
||||||
self.assertContains(org_page, self.portfolio.organization_name)
|
self.assertContains(portfolio_page, self.portfolio.organization_name)
|
||||||
|
|
||||||
self.assertContains(org_page, "<h1>Domains</h1>")
|
self.assertContains(portfolio_page, "<h1>Domains</h1>")
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_no_redirect_when_org_flag_false(self):
|
def test_no_redirect_when_org_flag_false(self):
|
||||||
"""No redirect so no follow,
|
"""No redirect so no follow,
|
||||||
implicitely test for the presense of the h2 by looking up its id"""
|
implicitely test for the presense of the h2 by looking up its id"""
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
org_page = self.app.get(reverse("home"))
|
home_page = self.app.get(reverse("home"))
|
||||||
self._set_session_cookie()
|
self._set_session_cookie()
|
||||||
|
|
||||||
self.assertNotContains(org_page, self.portfolio.organization_name)
|
self.assertNotContains(home_page, self.portfolio.organization_name)
|
||||||
|
|
||||||
self.assertContains(org_page, 'id="domain-requests-header"')
|
self.assertContains(home_page, 'id="domain-requests-header"')
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_no_redirect_when_user_has_no_portfolios(self):
|
def test_no_redirect_when_user_has_no_portfolios(self):
|
||||||
|
@ -974,9 +974,9 @@ class OrganizationsTests(TestWithUser, WebTest):
|
||||||
self.portfolio.delete()
|
self.portfolio.delete()
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
with override_flag("organization_feature", active=True):
|
with override_flag("organization_feature", active=True):
|
||||||
org_page = self.app.get(reverse("home"))
|
home_page = self.app.get(reverse("home"))
|
||||||
self._set_session_cookie()
|
self._set_session_cookie()
|
||||||
|
|
||||||
self.assertNotContains(org_page, self.portfolio.organization_name)
|
self.assertNotContains(home_page, self.portfolio.organization_name)
|
||||||
|
|
||||||
self.assertContains(org_page, 'id="domain-requests-header"')
|
self.assertContains(home_page, 'id="domain-requests-header"')
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def organization_domains(request, portfolio_id):
|
def portfolio_domains(request, portfolio_id):
|
||||||
context = {}
|
context = {}
|
||||||
|
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
|
@ -17,11 +17,11 @@ def organization_domains(request, portfolio_id):
|
||||||
portfolio = get_object_or_404(Portfolio, id=portfolio_id)
|
portfolio = get_object_or_404(Portfolio, id=portfolio_id)
|
||||||
context["portfolio"] = portfolio
|
context["portfolio"] = portfolio
|
||||||
|
|
||||||
return render(request, "organization_domains.html", context)
|
return render(request, "portfolio_domains.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def organization_domain_requests(request, portfolio_id):
|
def portfolio_domain_requests(request, portfolio_id):
|
||||||
context = {}
|
context = {}
|
||||||
|
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
|
@ -36,4 +36,4 @@ def organization_domain_requests(request, portfolio_id):
|
||||||
# This controls the creation of a new domain request in the wizard
|
# This controls the creation of a new domain request in the wizard
|
||||||
request.session["new_request"] = True
|
request.session["new_request"] = True
|
||||||
|
|
||||||
return render(request, "organization_requests.html", context)
|
return render(request, "portfolio_requests.html", context)
|
Loading…
Add table
Add a link
Reference in a new issue