mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-17 18:09:25 +02:00
Merge branch 'main' of https://github.com/cisagov/manage.get.gov into rh/2258-update-ao-to-so
This commit is contained in:
commit
6e1669ddf8
21 changed files with 622 additions and 207 deletions
|
@ -2720,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)
|
||||||
|
@ -2743,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";
|
||||||
|
@ -895,33 +922,6 @@ function unloadModals() {
|
||||||
window.modal.off();
|
window.modal.off();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generalized function to update pagination for a list.
|
* Generalized function to update pagination for a list.
|
||||||
* @param {string} itemName - The name displayed in the counter
|
* @param {string} itemName - The name displayed in the counter
|
||||||
|
@ -1294,6 +1294,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
|
||||||
|
@ -1348,6 +1352,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();
|
||||||
|
@ -1469,7 +1481,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
domainRequestsSectionWrapper.appendChild(modal);
|
domainRequestsSectionWrapper.appendChild(modal);
|
||||||
}
|
}
|
||||||
|
|
|
@ -189,6 +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.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,6 +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.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
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,6 +59,16 @@ for step, view in [
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.index, name="home"),
|
path("", views.index, name="home"),
|
||||||
|
path(
|
||||||
|
"portfolio/<int:portfolio_id>/domains/",
|
||||||
|
portfolio_domains,
|
||||||
|
name="portfolio-domains",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"portfolio/<int:portfolio_id>/domain_requests/",
|
||||||
|
portfolio_domain_requests,
|
||||||
|
name="portfolio-domain-requests",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"admin/logout/",
|
"admin/logout/",
|
||||||
RedirectView.as_view(pattern_name="logout", permanent=False),
|
RedirectView.as_view(pattern_name="logout", permanent=False),
|
||||||
|
|
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}"
|
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}"
|
|
@ -2,14 +2,18 @@
|
||||||
Contains middleware used in settings.py
|
Contains middleware used in settings.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
|
from registrar.models.portfolio import Portfolio
|
||||||
from registrar.models.user import User
|
from registrar.models.user import User
|
||||||
from waffle.decorators import flag_is_active
|
from waffle.decorators import flag_is_active
|
||||||
|
|
||||||
from registrar.models.utility.generic_helper import replace_url_queryparams
|
from registrar.models.utility.generic_helper import replace_url_queryparams
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class NoCacheMiddleware:
|
class NoCacheMiddleware:
|
||||||
"""
|
"""
|
||||||
|
@ -119,3 +123,33 @@ class CheckUserProfileMiddleware:
|
||||||
else:
|
else:
|
||||||
# Process the view as normal
|
# Process the view as normal
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class CheckPortfolioMiddleware:
|
||||||
|
"""
|
||||||
|
Checks if the current user has a portfolio
|
||||||
|
If they do, redirect them to the portfolio homepage when they navigate to home.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
self.home = reverse("home")
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
response = self.get_response(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||||
|
current_path = request.path
|
||||||
|
|
||||||
|
has_organization_feature_flag = flag_is_active(request, "organization_feature")
|
||||||
|
|
||||||
|
if current_path == self.home:
|
||||||
|
if has_organization_feature_flag:
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
user_portfolios = Portfolio.objects.filter(creator=request.user)
|
||||||
|
if user_portfolios.exists():
|
||||||
|
first_portfolio = user_portfolios.first()
|
||||||
|
home_with_portfolio = reverse("portfolio-domains", kwargs={"portfolio_id": first_portfolio.id})
|
||||||
|
return HttpResponseRedirect(home_with_portfolio)
|
||||||
|
return None
|
||||||
|
|
|
@ -9,189 +9,48 @@
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
{# the entire logged in page goes here #}
|
{# the entire logged in page goes here #}
|
||||||
|
|
||||||
<div class="tablet:grid-col-11 desktop:grid-col-10 tablet:grid-offset-1">
|
{% block homepage_content %}
|
||||||
{% block messages %}
|
|
||||||
{% include "includes/form_messages.html" %}
|
|
||||||
{% endblock %}
|
|
||||||
<h1>Manage your domains</h2>
|
|
||||||
|
|
||||||
{% comment %}
|
<div class="tablet:grid-col-11 desktop:grid-col-10 tablet:grid-offset-1">
|
||||||
IMPORTANT:
|
{% block messages %}
|
||||||
If this button is added on any other page, make sure to update the
|
{% include "includes/form_messages.html" %}
|
||||||
relevant view to reset request.session["new_request"] = True
|
{% endblock %}
|
||||||
{% endcomment %}
|
<h1>Manage your domains</h1>
|
||||||
<p class="margin-top-4">
|
|
||||||
<a href="{% url 'domain-request:' %}" class="usa-button"
|
|
||||||
>
|
|
||||||
Start a new domain request
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<section class="section--outlined domains">
|
{% comment %}
|
||||||
<div class="grid-row">
|
IMPORTANT:
|
||||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
If this button is added on any other page, make sure to update the
|
||||||
<h2 id="domains-header" class="flex-6">Domains</h2>
|
relevant view to reset request.session["new_request"] = True
|
||||||
</div>
|
{% endcomment %}
|
||||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
<p class="margin-top-4">
|
||||||
<section aria-label="Domains search component" class="flex-6 margin-y-2">
|
<a href="{% url 'domain-request:' %}" class="usa-button"
|
||||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
>
|
||||||
{% csrf_token %}
|
Start a new domain request
|
||||||
<button class="usa-button usa-button--unstyled margin-right-2 domains__reset-button display-none" type="button">
|
</a>
|
||||||
Reset
|
</p>
|
||||||
</button>
|
|
||||||
<label class="usa-sr-only" for="domains__search-field">Search</label>
|
|
||||||
<input
|
|
||||||
class="usa-input"
|
|
||||||
id="domains__search-field"
|
|
||||||
type="search"
|
|
||||||
name="search"
|
|
||||||
placeholder="Search by domain name"
|
|
||||||
/>
|
|
||||||
<button class="usa-button" type="submit" id="domains__search-field-submit">
|
|
||||||
<img
|
|
||||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
|
||||||
class="usa-search__submit-icon"
|
|
||||||
alt="Search"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="domains__table-wrapper display-none">
|
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domains__table">
|
|
||||||
<caption class="sr-only">Your registered domains</caption>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<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>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
role="columnheader"
|
|
||||||
>
|
|
||||||
<span class="usa-sr-only">Action</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<!-- AJAX will populate this tbody -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div
|
|
||||||
class="usa-sr-only usa-table__announcement-region"
|
|
||||||
aria-live="polite"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="domains__no-data display-none">
|
|
||||||
<p>You don't have any registered domains.</p>
|
|
||||||
<p class="maxw-none clearfix">
|
|
||||||
<a href="https://get.gov/help/faq/#do-not-see-my-domain" class="float-right-tablet display-flex flex-align-start usa-link" target="_blank">
|
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#help_outline"></use>
|
|
||||||
</svg>
|
|
||||||
Why don't I see my domain when I sign in to the registrar?
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="domains__no-search-results display-none">
|
|
||||||
<p>No results found for "<span class="domains__search-term"></span>"</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domains-pagination">
|
|
||||||
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
|
|
||||||
<!-- Count will be dynamically populated by JS -->
|
|
||||||
</span>
|
|
||||||
<ul class="usa-pagination__list">
|
|
||||||
<!-- Pagination links will be dynamically populated by JS -->
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<section class="section--outlined domain-requests">
|
{% include "includes/domains_table.html" %}
|
||||||
<div class="grid-row">
|
{% include "includes/domain_requests_table.html" %}
|
||||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
|
||||||
<h2 id="domain-requests-header" class="flex-6">Domain requests</h2>
|
|
||||||
</div>
|
|
||||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
|
||||||
<section aria-label="Domain requests search component" class="flex-6 margin-y-2">
|
|
||||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button class="usa-button usa-button--unstyled margin-right-2 domain-requests__reset-button display-none" type="button">
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
<label class="usa-sr-only" for="domain-requests__search-field">Search</label>
|
|
||||||
<input
|
|
||||||
class="usa-input"
|
|
||||||
id="domain-requests__search-field"
|
|
||||||
type="search"
|
|
||||||
name="search"
|
|
||||||
placeholder="Search by domain name"
|
|
||||||
/>
|
|
||||||
<button class="usa-button" type="submit" id="domain-requests__search-field-submit">
|
|
||||||
<img
|
|
||||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
|
||||||
class="usa-search__submit-icon"
|
|
||||||
alt="Search"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="domain-requests__table-wrapper display-none">
|
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domain-requests__table">
|
|
||||||
<caption class="sr-only">Your domain requests</caption>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th data-sortable="requested_domain__name" scope="col" role="columnheader">Domain name</th>
|
|
||||||
<th data-sortable="submission_date" scope="col" role="columnheader">Date submitted</th>
|
|
||||||
<th data-sortable="status" scope="col" role="columnheader">Status</th>
|
|
||||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
|
||||||
<!-- AJAX will conditionally add a th for delete actions -->
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="domain-requests-tbody">
|
|
||||||
<!-- AJAX will populate this tbody -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div
|
|
||||||
class="usa-sr-only usa-table__announcement-region"
|
|
||||||
aria-live="polite"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="domain-requests__no-data display-none">
|
|
||||||
<p>You haven't requested any domains.</p>
|
|
||||||
</div>
|
|
||||||
<div class="domain-requests__no-search-results display-none">
|
|
||||||
<p>No results found for "<span class="domain-requests__search-term"></span>"</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domain-requests-pagination">
|
|
||||||
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
|
|
||||||
<!-- Count will be dynamically populated by JS -->
|
|
||||||
</span>
|
|
||||||
<ul class="usa-pagination__list">
|
|
||||||
<!-- Pagination links will be dynamically populated by JS -->
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{# Note: Reimplement this after MVP #}
|
{# Note: Reimplement this after MVP #}
|
||||||
<!--
|
<!--
|
||||||
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
||||||
<h2>Archived domains</h2>
|
<h2>Archived domains</h2>
|
||||||
<p>You don't have any archived domains</p>
|
<p>You don't have any archived domains</p>
|
||||||
</section>
|
</section>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- Note: Uncomment below when this is being implemented post-MVP -->
|
<!-- Note: Uncomment below when this is being implemented post-MVP -->
|
||||||
<!-- <section class="tablet:grid-col-11 desktop:grid-col-10">
|
<!-- <section class="tablet:grid-col-11 desktop:grid-col-10">
|
||||||
<h2 class="padding-top-1 mobile-lg:padding-top-3"> Export domains</h2>
|
<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>
|
<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">
|
<a href="{% url 'todo' %}" class="usa-button usa-button--outline">
|
||||||
Export domains as csv
|
Export domains as csv
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %} {# not user.is_authenticated #}
|
{% else %} {# not user.is_authenticated #}
|
||||||
|
|
71
src/registrar/templates/includes/domain_requests_table.html
Normal file
71
src/registrar/templates/includes/domain_requests_table.html
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<section class="section--outlined domain-requests">
|
||||||
|
<div class="grid-row">
|
||||||
|
{% if portfolio is None %}
|
||||||
|
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||||
|
<h2 id="domain-requests-header" class="flex-6">Domain requests</h2>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||||
|
<section aria-label="Domain requests search component" class="flex-6 margin-y-2">
|
||||||
|
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="usa-button usa-button--unstyled margin-right-2 domain-requests__reset-button display-none" type="button">
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<label class="usa-sr-only" for="domain-requests__search-field">Search</label>
|
||||||
|
<input
|
||||||
|
class="usa-input"
|
||||||
|
id="domain-requests__search-field"
|
||||||
|
type="search"
|
||||||
|
name="search"
|
||||||
|
placeholder="Search by domain name"
|
||||||
|
/>
|
||||||
|
<button class="usa-button" type="submit" id="domain-requests__search-field-submit">
|
||||||
|
<img
|
||||||
|
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||||
|
class="usa-search__submit-icon"
|
||||||
|
alt="Search"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="domain-requests__table-wrapper display-none">
|
||||||
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domain-requests__table">
|
||||||
|
<caption class="sr-only">Your domain requests</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sortable="requested_domain__name" scope="col" role="columnheader">Domain name</th>
|
||||||
|
<th data-sortable="submission_date" scope="col" role="columnheader">Date submitted</th>
|
||||||
|
<th data-sortable="status" scope="col" role="columnheader">Status</th>
|
||||||
|
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
||||||
|
<!-- AJAX will conditionally add a th for delete actions -->
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="domain-requests-tbody">
|
||||||
|
<!-- AJAX will populate this tbody -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div
|
||||||
|
class="usa-sr-only usa-table__announcement-region"
|
||||||
|
aria-live="polite"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="domain-requests__no-data display-none">
|
||||||
|
<p>You haven't requested any domains.</p>
|
||||||
|
</div>
|
||||||
|
<div class="domain-requests__no-search-results display-none">
|
||||||
|
<p>No results found for "<span class="domain-requests__search-term"></span>"</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domain-requests-pagination">
|
||||||
|
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
|
||||||
|
<!-- Count will be dynamically populated by JS -->
|
||||||
|
</span>
|
||||||
|
<ul class="usa-pagination__list">
|
||||||
|
<!-- Pagination links will be dynamically populated by JS -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
83
src/registrar/templates/includes/domains_table.html
Normal file
83
src/registrar/templates/includes/domains_table.html
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<section class="section--outlined domains">
|
||||||
|
<div class="grid-row">
|
||||||
|
{% if portfolio is None %}
|
||||||
|
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||||
|
<h2 id="domains-header" class="flex-6">Domains</h2>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||||
|
<section aria-label="Domains search component" class="flex-6 margin-y-2">
|
||||||
|
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="usa-button usa-button--unstyled margin-right-2 domains__reset-button display-none" type="button">
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<label class="usa-sr-only" for="domains__search-field">Search</label>
|
||||||
|
<input
|
||||||
|
class="usa-input"
|
||||||
|
id="domains__search-field"
|
||||||
|
type="search"
|
||||||
|
name="search"
|
||||||
|
placeholder="Search by domain name"
|
||||||
|
/>
|
||||||
|
<button class="usa-button" type="submit" id="domains__search-field-submit">
|
||||||
|
<img
|
||||||
|
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||||
|
class="usa-search__submit-icon"
|
||||||
|
alt="Search"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="domains__table-wrapper display-none">
|
||||||
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domains__table">
|
||||||
|
<caption class="sr-only">Your registered domains</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<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>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
role="columnheader"
|
||||||
|
>
|
||||||
|
<span class="usa-sr-only">Action</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- AJAX will populate this tbody -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div
|
||||||
|
class="usa-sr-only usa-table__announcement-region"
|
||||||
|
aria-live="polite"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="domains__no-data display-none">
|
||||||
|
<p>You don't have any registered domains.</p>
|
||||||
|
<p class="maxw-none clearfix">
|
||||||
|
<a href="https://get.gov/help/faq/#do-not-see-my-domain" class="float-right-tablet display-flex flex-align-start usa-link" target="_blank">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#help_outline"></use>
|
||||||
|
</svg>
|
||||||
|
Why don't I see my domain when I sign in to the registrar?
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="domains__no-search-results display-none">
|
||||||
|
<p>No results found for "<span class="domains__search-term"></span>"</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domains-pagination">
|
||||||
|
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
|
||||||
|
<!-- Count will be dynamically populated by JS -->
|
||||||
|
</span>
|
||||||
|
<ul class="usa-pagination__list">
|
||||||
|
<!-- Pagination links will be dynamically populated by JS -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
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 %}
|
8
src/registrar/templates/portfolio_domains.html
Normal file
8
src/registrar/templates/portfolio_domains.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends 'portfolio.html' %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block portfolio_content %}
|
||||||
|
<h1>Domains</h1>
|
||||||
|
{% include "includes/domains_table.html" with portfolio=portfolio %}
|
||||||
|
{% endblock %}
|
21
src/registrar/templates/portfolio_requests.html
Normal file
21
src/registrar/templates/portfolio_requests.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{% extends 'portfolio.html' %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block portfolio_content %}
|
||||||
|
<h1>Domain requests</h1>
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
IMPORTANT:
|
||||||
|
If this button is added on any other page, make sure to update the
|
||||||
|
relevant view to reset request.session["new_request"] = True
|
||||||
|
{% endcomment %}
|
||||||
|
<p class="margin-top-4">
|
||||||
|
<a href="{% url 'domain-request:' %}" class="usa-button"
|
||||||
|
>
|
||||||
|
Start a new domain request
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% include "includes/domain_requests_table.html" with portfolio=portfolio %}
|
||||||
|
{% endblock %}
|
37
src/registrar/templates/portfolio_sidebar.html
Normal file
37
src/registrar/templates/portfolio_sidebar.html
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{% load static url_helpers %}
|
||||||
|
|
||||||
|
<div class="margin-bottom-4 tablet:margin-bottom-0">
|
||||||
|
<nav aria-label="">
|
||||||
|
<h2 class="margin-top-0 text-semibold">{{ portfolio.organization_name }}</h2>
|
||||||
|
<ul class="usa-sidenav">
|
||||||
|
<li class="usa-sidenav__item">
|
||||||
|
{% url 'portfolio-domains' portfolio.id as url %}
|
||||||
|
<a href="{{ url }}" {% if request.path == url %}class="usa-current"{% endif %}>
|
||||||
|
Domains
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
<li class="usa-sidenav__item">
|
||||||
|
{% url 'portfolio-domain-requests' portfolio.id as url %}
|
||||||
|
<a href="{{ url }}" {% if request.path == url %}class="usa-current"{% endif %}>
|
||||||
|
Domain requests
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="usa-sidenav__item">
|
||||||
|
<a href="#">
|
||||||
|
Members
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="usa-sidenav__item">
|
||||||
|
<a href="#">
|
||||||
|
Organization
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="usa-sidenav__item">
|
||||||
|
<a href="#">
|
||||||
|
Senior official
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
|
@ -24,6 +24,7 @@ SAMPLE_KWARGS = {
|
||||||
"object_id": "3",
|
"object_id": "3",
|
||||||
"domain": "whitehouse.gov",
|
"domain": "whitehouse.gov",
|
||||||
"user_pk": "1",
|
"user_pk": "1",
|
||||||
|
"portfolio_id": "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Our test suite will ignore some namespaces.
|
# Our test suite will ignore some namespaces.
|
||||||
|
|
|
@ -8,6 +8,7 @@ from api.tests.common import less_console_noise_decorator
|
||||||
from registrar.models.contact import Contact
|
from registrar.models.contact import Contact
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
from registrar.models.draft_domain import DraftDomain
|
from registrar.models.draft_domain import DraftDomain
|
||||||
|
from registrar.models.portfolio import Portfolio
|
||||||
from registrar.models.public_contact import PublicContact
|
from registrar.models.public_contact import PublicContact
|
||||||
from registrar.models.user import User
|
from registrar.models.user import User
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
|
@ -652,7 +653,6 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
PublicContact.objects.filter(domain=self.domain).delete()
|
PublicContact.objects.filter(domain=self.domain).delete()
|
||||||
self.role.delete()
|
self.role.delete()
|
||||||
self.domain.delete()
|
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
Website.objects.all().delete()
|
Website.objects.all().delete()
|
||||||
Contact.objects.all().delete()
|
Contact.objects.all().delete()
|
||||||
|
@ -906,3 +906,77 @@ class UserProfileTests(TestWithUser, WebTest):
|
||||||
profile_page = profile_page.follow()
|
profile_page = profile_page.follow()
|
||||||
self.assertEqual(profile_page.status_code, 200)
|
self.assertEqual(profile_page.status_code, 200)
|
||||||
self.assertContains(profile_page, "Your profile has been updated")
|
self.assertContains(profile_page, "Your profile has been updated")
|
||||||
|
|
||||||
|
|
||||||
|
class PortfoliosTests(TestWithUser, WebTest):
|
||||||
|
"""A series of tests that target the organizations"""
|
||||||
|
|
||||||
|
# csrf checks do not work well with WebTest.
|
||||||
|
# We disable them here.
|
||||||
|
csrf_checks = False
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.user.save()
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
self.domain, _ = Domain.objects.get_or_create(name="sampledomain.gov", state=Domain.State.READY)
|
||||||
|
self.role, _ = UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="xyz inc")
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
|
super().tearDown()
|
||||||
|
PublicContact.objects.filter(domain=self.domain).delete()
|
||||||
|
UserDomainRole.objects.all().delete()
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
Website.objects.all().delete()
|
||||||
|
Contact.objects.all().delete()
|
||||||
|
|
||||||
|
def _set_session_cookie(self):
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_middleware_redirects_to_portfolio_homepage(self):
|
||||||
|
"""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
|
||||||
|
of the homepage"""
|
||||||
|
self.app.set_user(self.user.username)
|
||||||
|
with override_flag("organization_feature", active=True):
|
||||||
|
# This will redirect the user to the portfolio page.
|
||||||
|
# Follow implicity checks if our redirect is working.
|
||||||
|
portfolio_page = self.app.get(reverse("home")).follow()
|
||||||
|
self._set_session_cookie()
|
||||||
|
|
||||||
|
# Assert that we're on the right page
|
||||||
|
self.assertContains(portfolio_page, self.portfolio.organization_name)
|
||||||
|
|
||||||
|
self.assertContains(portfolio_page, "<h1>Domains</h1>")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_no_redirect_when_org_flag_false(self):
|
||||||
|
"""No redirect so no follow,
|
||||||
|
implicitely test for the presense of the h2 by looking up its id"""
|
||||||
|
self.app.set_user(self.user.username)
|
||||||
|
home_page = self.app.get(reverse("home"))
|
||||||
|
self._set_session_cookie()
|
||||||
|
|
||||||
|
self.assertNotContains(home_page, self.portfolio.organization_name)
|
||||||
|
|
||||||
|
self.assertContains(home_page, 'id="domain-requests-header"')
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_no_redirect_when_user_has_no_portfolios(self):
|
||||||
|
"""No redirect so no follow,
|
||||||
|
implicitely test for the presense of the h2 by looking up its id"""
|
||||||
|
self.portfolio.delete()
|
||||||
|
self.app.set_user(self.user.username)
|
||||||
|
with override_flag("organization_feature", active=True):
|
||||||
|
home_page = self.app.get(reverse("home"))
|
||||||
|
self._set_session_cookie()
|
||||||
|
|
||||||
|
self.assertNotContains(home_page, self.portfolio.organization_name)
|
||||||
|
|
||||||
|
self.assertContains(home_page, 'id="domain-requests-header"')
|
||||||
|
|
|
@ -9,6 +9,7 @@ def index(request):
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
# This is a django waffle flag which toggles features based off of the "flag" table
|
# This is a django waffle flag which toggles features based off of the "flag" table
|
||||||
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
|
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
|
||||||
|
context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature")
|
||||||
|
|
||||||
# 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
|
||||||
|
|
39
src/registrar/views/portfolios.py
Normal file
39
src/registrar/views/portfolios.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
from registrar.models.portfolio import Portfolio
|
||||||
|
from waffle.decorators import flag_is_active
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def portfolio_domains(request, portfolio_id):
|
||||||
|
context = {}
|
||||||
|
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
# This is a django waffle flag which toggles features based off of the "flag" table
|
||||||
|
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
|
||||||
|
context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature")
|
||||||
|
|
||||||
|
# Retrieve the portfolio object based on the provided portfolio_id
|
||||||
|
portfolio = get_object_or_404(Portfolio, id=portfolio_id)
|
||||||
|
context["portfolio"] = portfolio
|
||||||
|
|
||||||
|
return render(request, "portfolio_domains.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def portfolio_domain_requests(request, portfolio_id):
|
||||||
|
context = {}
|
||||||
|
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
# This is a django waffle flag which toggles features based off of the "flag" table
|
||||||
|
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
|
||||||
|
context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature")
|
||||||
|
|
||||||
|
# Retrieve the portfolio object based on the provided portfolio_id
|
||||||
|
portfolio = get_object_or_404(Portfolio, id=portfolio_id)
|
||||||
|
context["portfolio"] = portfolio
|
||||||
|
|
||||||
|
# This controls the creation of a new domain request in the wizard
|
||||||
|
request.session["new_request"] = True
|
||||||
|
|
||||||
|
return render(request, "portfolio_requests.html", context)
|
Loading…
Add table
Add a link
Reference in a new issue