Merge branch 'main' into za/2597-block-email-sending

This commit is contained in:
zandercymatics 2024-08-29 12:12:33 -06:00
commit d27829e8ab
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
47 changed files with 1148 additions and 388 deletions

View file

@ -816,3 +816,25 @@ Example: `cf ssh getgov-za`
| | Parameter | Description | | | Parameter | Description |
|:-:|:-------------------------- |:-----------------------------------------------------------------------------------| |:-:|:-------------------------- |:-----------------------------------------------------------------------------------|
| 1 | **federal_cio_csv_path** | Specifies where the federal CIO csv is | | 1 | **federal_cio_csv_path** | Specifies where the federal CIO csv is |
## Populate Domain Request Dates
This section outlines how to run the populate_domain_request_dates script
### Running on sandboxes
#### Step 1: Login to CloudFoundry
```cf login -a api.fr.cloud.gov --sso```
#### Step 2: SSH into your environment
```cf ssh getgov-{space}```
Example: `cf ssh getgov-za`
#### Step 3: Create a shell instance
```/tmp/lifecycle/shell```
#### Step 4: Running the script
```./manage.py populate_domain_request_dates```
### Running locally
```docker-compose exec app ./manage.py populate_domain_request_dates```

View file

@ -133,14 +133,6 @@ class MyUserAdminForm(UserChangeForm):
widgets = { widgets = {
"groups": NoAutocompleteFilteredSelectMultiple("groups", False), "groups": NoAutocompleteFilteredSelectMultiple("groups", False),
"user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False), "user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False),
"portfolio_roles": FilteredSelectMultipleArrayWidget(
"portfolio_roles", is_stacked=False, choices=UserPortfolioRoleChoices.choices
),
"portfolio_additional_permissions": FilteredSelectMultipleArrayWidget(
"portfolio_additional_permissions",
is_stacked=False,
choices=UserPortfolioPermissionChoices.choices,
),
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -172,6 +164,22 @@ class MyUserAdminForm(UserChangeForm):
) )
class UserPortfolioPermissionsForm(forms.ModelForm):
class Meta:
model = models.UserPortfolioPermission
fields = "__all__"
widgets = {
"roles": FilteredSelectMultipleArrayWidget(
"roles", is_stacked=False, choices=UserPortfolioRoleChoices.choices
),
"additional_permissions": FilteredSelectMultipleArrayWidget(
"additional_permissions",
is_stacked=False,
choices=UserPortfolioPermissionChoices.choices,
),
}
class PortfolioInvitationAdminForm(UserChangeForm): class PortfolioInvitationAdminForm(UserChangeForm):
"""This form utilizes the custom widget for its class's ManyToMany UIs.""" """This form utilizes the custom widget for its class's ManyToMany UIs."""
@ -745,19 +753,12 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
"is_superuser", "is_superuser",
"groups", "groups",
"user_permissions", "user_permissions",
"portfolio",
"portfolio_roles",
"portfolio_additional_permissions",
) )
}, },
), ),
("Important dates", {"fields": ("last_login", "date_joined")}), ("Important dates", {"fields": ("last_login", "date_joined")}),
) )
autocomplete_fields = [
"portfolio",
]
readonly_fields = ("verification_type",) readonly_fields = ("verification_type",)
analyst_fieldsets = ( analyst_fieldsets = (
@ -777,9 +778,6 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
"fields": ( "fields": (
"is_active", "is_active",
"groups", "groups",
"portfolio",
"portfolio_roles",
"portfolio_additional_permissions",
) )
}, },
), ),
@ -834,9 +832,6 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
"Important dates", "Important dates",
"last_login", "last_login",
"date_joined", "date_joined",
"portfolio",
"portfolio_roles",
"portfolio_additional_permissions",
] ]
# TODO: delete after we merge organization feature # TODO: delete after we merge organization feature
@ -1245,6 +1240,26 @@ class UserDomainRoleResource(resources.ModelResource):
model = models.UserDomainRole model = models.UserDomainRole
class UserPortfolioPermissionAdmin(ListHeaderAdmin):
form = UserPortfolioPermissionsForm
class Meta:
"""Contains meta information about this class"""
model = models.UserPortfolioPermission
fields = "__all__"
_meta = Meta()
# Columns
list_display = [
"user",
"portfolio",
]
autocomplete_fields = ["user", "portfolio"]
class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin): class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"""Custom user domain role admin class.""" """Custom user domain role admin class."""
@ -1684,7 +1699,9 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# Columns # Columns
list_display = [ list_display = [
"requested_domain", "requested_domain",
"submission_date", "first_submitted_date",
"last_submitted_date",
"last_status_update",
"status", "status",
"generic_org_type", "generic_org_type",
"federal_type", "federal_type",
@ -1887,7 +1904,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# 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
ordering = ["-submission_date", "requested_domain__name"] ordering = ["-last_submitted_date", "requested_domain__name"]
change_form_template = "django/admin/domain_request_change_form.html" change_form_template = "django/admin/domain_request_change_form.html"
@ -3009,6 +3026,7 @@ class PortfolioAdmin(ListHeaderAdmin):
"domain_requests", "domain_requests",
"suborganizations", "suborganizations",
"portfolio_type", "portfolio_type",
"creator",
] ]
def federal_type(self, obj: models.Portfolio): def federal_type(self, obj: models.Portfolio):
@ -3299,6 +3317,7 @@ admin.site.register(models.Portfolio, PortfolioAdmin)
admin.site.register(models.DomainGroup, DomainGroupAdmin) admin.site.register(models.DomainGroup, DomainGroupAdmin)
admin.site.register(models.Suborganization, SuborganizationAdmin) admin.site.register(models.Suborganization, SuborganizationAdmin)
admin.site.register(models.SeniorOfficial, SeniorOfficialAdmin) admin.site.register(models.SeniorOfficial, SeniorOfficialAdmin)
admin.site.register(models.UserPortfolioPermission, UserPortfolioPermissionAdmin)
admin.site.register(models.AllowedEmail, AllowedEmailAdmin) admin.site.register(models.AllowedEmail, AllowedEmailAdmin)
# Register our custom waffle implementations # Register our custom waffle implementations

View file

@ -908,10 +908,28 @@ function initializeWidgetOnList(list, parentId) {
return; return;
} }
// Determine if any changes are necessary to the display of portfolio type or federal type
// based on changes to the Federal Agency
let federalPortfolioApi = document.getElementById("federal_and_portfolio_types_from_agency_json_url").value;
fetch(`${federalPortfolioApi}?organization_type=${organizationType.value}&agency_name=${selectedText}`)
.then(response => {
const statusCode = response.status;
return response.json().then(data => ({ statusCode, data }));
})
.then(({ statusCode, data }) => {
if (data.error) {
console.error("Error in AJAX call: " + data.error);
return;
}
updateReadOnly(data.federal_type, '.field-federal_type');
updateReadOnly(data.portfolio_type, '.field-portfolio_type');
})
.catch(error => console.error("Error fetching federal and portfolio types: ", error));
// Hide the contactList initially. // Hide the contactList initially.
// If we can update the contact information, it'll be shown again. // If we can update the contact information, it'll be shown again.
hideElement(contactList.parentElement); hideElement(contactList.parentElement);
let seniorOfficialApi = document.getElementById("senior_official_from_agency_json_url").value; let seniorOfficialApi = document.getElementById("senior_official_from_agency_json_url").value;
fetch(`${seniorOfficialApi}?agency_name=${selectedText}`) fetch(`${seniorOfficialApi}?agency_name=${selectedText}`)
.then(response => { .then(response => {
@ -954,6 +972,7 @@ function initializeWidgetOnList(list, parentId) {
} }
}) })
.catch(error => console.error("Error fetching senior official: ", error)); .catch(error => console.error("Error fetching senior official: ", error));
} }
function handleStateTerritoryChange(stateTerritory, urbanizationField) { function handleStateTerritoryChange(stateTerritory, urbanizationField) {
@ -965,6 +984,26 @@ function initializeWidgetOnList(list, parentId) {
} }
} }
/**
* Utility that selects a div from the DOM using selectorString,
* and updates a div within that div which has class of 'readonly'
* so that the text of the div is updated to updateText
* @param {*} updateText
* @param {*} selectorString
*/
function updateReadOnly(updateText, selectorString) {
// find the div by selectorString
const selectedDiv = document.querySelector(selectorString);
if (selectedDiv) {
// find the nested div with class 'readonly' inside the selectorString div
const readonlyDiv = selectedDiv.querySelector('.readonly');
if (readonlyDiv) {
// Update the text content of the readonly div
readonlyDiv.textContent = updateText !== null ? updateText : '-';
}
}
}
function updateContactInfo(data) { function updateContactInfo(data) {
if (!contactList) return; if (!contactList) return;

View file

@ -1599,7 +1599,7 @@ document.addEventListener('DOMContentLoaded', function() {
const domainName = request.requested_domain ? request.requested_domain : `New domain request <br><span class="text-base font-body-xs">(${utcDateString(request.created_at)})</span>`; const domainName = request.requested_domain ? request.requested_domain : `New domain request <br><span class="text-base font-body-xs">(${utcDateString(request.created_at)})</span>`;
const actionUrl = request.action_url; const actionUrl = request.action_url;
const actionLabel = request.action_label; const actionLabel = request.action_label;
const submissionDate = request.submission_date ? new Date(request.submission_date).toLocaleDateString('en-US', options) : `<span class="text-base">Not submitted</span>`; const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `<span class="text-base">Not submitted</span>`;
// Even if the request is not deletable, we may need this empty string for the td if the deletable column is displayed // Even if the request is not deletable, we may need this empty string for the td if the deletable column is displayed
let modalTrigger = ''; let modalTrigger = '';
@ -1699,7 +1699,7 @@ document.addEventListener('DOMContentLoaded', function() {
<th scope="row" role="rowheader" data-label="Domain name"> <th scope="row" role="rowheader" data-label="Domain name">
${domainName} ${domainName}
</th> </th>
<td data-sort-value="${new Date(request.submission_date).getTime()}" data-label="Date submitted"> <td data-sort-value="${new Date(request.last_submitted_date).getTime()}" data-label="Date submitted">
${submissionDate} ${submissionDate}
</td> </td>
<td data-label="Status"> <td data-label="Status">

View file

@ -24,7 +24,10 @@ from registrar.views.report_views import (
from registrar.views.domain_request import Step 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.utility.api_views import get_senior_official_from_federal_agency_json from registrar.views.utility.api_views import (
get_senior_official_from_federal_agency_json,
get_federal_and_portfolio_types_from_federal_agency_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 api.views import available, get_current_federal, get_current_full from api.views import available, get_current_federal, get_current_full
@ -139,6 +142,11 @@ urlpatterns = [
get_senior_official_from_federal_agency_json, get_senior_official_from_federal_agency_json,
name="get-senior-official-from-federal-agency-json", name="get-senior-official-from-federal-agency-json",
), ),
path(
"admin/api/get-federal-and-portfolio-types-from-federal-agency-json/",
get_federal_and_portfolio_types_from_federal_agency_json,
name="get-federal-and-portfolio-types-from-federal-agency-json",
),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path( path(
"reports/export_data_type_user/", "reports/export_data_type_user/",

View file

@ -61,27 +61,37 @@ def add_has_profile_feature_flag_to_context(request):
def portfolio_permissions(request): def portfolio_permissions(request):
"""Make portfolio permissions for the request user available in global context""" """Make portfolio permissions for the request user available in global context"""
try: try:
if not request.user or not request.user.is_authenticated or not flag_is_active(request, "organization_feature"): portfolio = request.session.get("portfolio")
if portfolio:
return { return {
"has_base_portfolio_permission": False, "has_base_portfolio_permission": request.user.has_base_portfolio_permission(portfolio),
"has_domains_portfolio_permission": False, "has_domains_portfolio_permission": request.user.has_domains_portfolio_permission(portfolio),
"has_domain_requests_portfolio_permission": False, "has_domain_requests_portfolio_permission": request.user.has_domain_requests_portfolio_permission(
"portfolio": None, portfolio
"has_organization_feature_flag": False, ),
"has_view_suborganization": request.user.has_view_suborganization(portfolio),
"has_edit_suborganization": request.user.has_edit_suborganization(portfolio),
"portfolio": portfolio,
"has_organization_feature_flag": True,
} }
return { return {
"has_base_portfolio_permission": request.user.has_base_portfolio_permission(), "has_base_portfolio_permission": False,
"has_domains_portfolio_permission": request.user.has_domains_portfolio_permission(), "has_domains_portfolio_permission": False,
"has_domain_requests_portfolio_permission": request.user.has_domain_requests_portfolio_permission(), "has_domain_requests_portfolio_permission": False,
"portfolio": request.user.portfolio, "has_view_suborganization": False,
"has_organization_feature_flag": True, "has_edit_suborganization": False,
"portfolio": None,
"has_organization_feature_flag": False,
} }
except AttributeError: except AttributeError:
# Handles cases where request.user might not exist # Handles cases where request.user might not exist
return { return {
"has_base_portfolio_permission": False, "has_base_portfolio_permission": False,
"has_domains_portfolio_permission": False, "has_domains_portfolio_permission": False,
"has_domain_requests_portfolio_permission": False, "has_domain_requests_portfolio_permission": False,
"has_view_suborganization": False,
"has_edit_suborganization": False,
"portfolio": None, "portfolio": None,
"has_organization_feature_flag": False, "has_organization_feature_flag": False,
} }

View file

@ -95,7 +95,7 @@ class DomainRequestFixture:
# TODO for a future ticket: Allow for more than just "federal" here # TODO for a future ticket: Allow for more than just "federal" here
da.generic_org_type = app["generic_org_type"] if "generic_org_type" in app else "federal" da.generic_org_type = app["generic_org_type"] if "generic_org_type" in app else "federal"
da.submission_date = fake.date() da.last_submitted_date = fake.date()
da.federal_type = ( da.federal_type = (
app["federal_type"] app["federal_type"]
if "federal_type" in app if "federal_type" in app

View file

@ -0,0 +1,45 @@
import logging
from django.core.management import BaseCommand
from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors
from registrar.models import DomainRequest
from auditlog.models import LogEntry
logger = logging.getLogger(__name__)
class Command(BaseCommand, PopulateScriptTemplate):
help = "Loops through each domain request object and populates the last_status_update and first_submitted_date"
def handle(self, **kwargs):
"""Loops through each DomainRequest object and populates
its last_status_update and first_submitted_date values"""
self.mass_update_records(DomainRequest, None, ["last_status_update", "first_submitted_date"])
def update_record(self, record: DomainRequest):
"""Defines how we update the first_submitted_date and last_status_update fields"""
# Retrieve and order audit log entries by timestamp in descending order
audit_log_entries = LogEntry.objects.filter(object_pk=record.pk).order_by("-timestamp")
# Loop through logs in descending order to find most recent status change
for log_entry in audit_log_entries:
if "status" in log_entry.changes_dict:
record.last_status_update = log_entry.timestamp.date()
break
# Loop through logs in ascending order to find first submission
for log_entry in audit_log_entries.reverse():
status = log_entry.changes_dict.get("status")
if status and status[1] == "submitted":
record.first_submitted_date = log_entry.timestamp.date()
break
logger.info(
f"""{TerminalColors.OKCYAN}Updating {record} =>
first submitted date: {record.first_submitted_date},
last status update: {record.last_status_update}{TerminalColors.ENDC}
"""
)
def should_skip_record(self, record) -> bool:
# make sure the record had some kind of history
return not LogEntry.objects.filter(object_pk=record.pk).exists()

View file

@ -86,7 +86,7 @@ class PopulateScriptTemplate(ABC):
You must define update_record before you can use this function. You must define update_record before you can use this function.
""" """
records = object_class.objects.filter(**filter_conditions) records = object_class.objects.filter(**filter_conditions) if filter_conditions else object_class.objects.all()
readable_class_name = self.get_class_name(object_class) readable_class_name = self.get_class_name(object_class)
# Code execution will stop here if the user prompts "N" # Code execution will stop here if the user prompts "N"

View file

@ -0,0 +1,97 @@
# Generated by Django 4.2.10 on 2024-08-19 20:24
from django.conf import settings
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("registrar", "0118_alter_portfolio_options_alter_portfolio_creator_and_more"),
]
operations = [
migrations.RemoveField(
model_name="user",
name="portfolio",
),
migrations.RemoveField(
model_name="user",
name="portfolio_additional_permissions",
),
migrations.RemoveField(
model_name="user",
name="portfolio_roles",
),
migrations.CreateModel(
name="UserPortfolioPermission",
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)),
(
"roles",
django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(
choices=[
("organization_admin", "Admin"),
("organization_admin_read_only", "Admin read only"),
("organization_member", "Member"),
],
max_length=50,
),
blank=True,
help_text="Select one or more roles.",
null=True,
size=None,
),
),
(
"additional_permissions",
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,
),
),
(
"portfolio",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="portfolio_users",
to="registrar.portfolio",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="portfolio_permissions",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"unique_together": {("user", "portfolio")},
},
),
]

View file

@ -0,0 +1,47 @@
# Generated by Django 4.2.10 on 2024-08-16 15:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0119_remove_user_portfolio_and_more"),
]
operations = [
migrations.RenameField(
model_name="domainrequest",
old_name="submission_date",
new_name="last_submitted_date",
),
migrations.AlterField(
model_name="domainrequest",
name="last_submitted_date",
field=models.DateField(
blank=True, default=None, help_text="Date last submitted", null=True, verbose_name="last submitted on"
),
),
migrations.AddField(
model_name="domainrequest",
name="first_submitted_date",
field=models.DateField(
blank=True,
default=None,
help_text="Date initially submitted",
null=True,
verbose_name="first submitted on",
),
),
migrations.AddField(
model_name="domainrequest",
name="last_status_update",
field=models.DateField(
blank=True,
default=None,
help_text="Date of the last status update",
null=True,
verbose_name="last updated on",
),
),
]

View file

@ -21,6 +21,7 @@ from .portfolio import Portfolio
from .domain_group import DomainGroup from .domain_group import DomainGroup
from .suborganization import Suborganization from .suborganization import Suborganization
from .senior_official import SeniorOfficial from .senior_official import SeniorOfficial
from .user_portfolio_permission import UserPortfolioPermission
from .allowed_email import AllowedEmail from .allowed_email import AllowedEmail
@ -47,6 +48,7 @@ __all__ = [
"DomainGroup", "DomainGroup",
"Suborganization", "Suborganization",
"SeniorOfficial", "SeniorOfficial",
"UserPortfolioPermission",
"AllowedEmail", "AllowedEmail",
] ]
@ -72,4 +74,5 @@ auditlog.register(Portfolio)
auditlog.register(DomainGroup) auditlog.register(DomainGroup)
auditlog.register(Suborganization) auditlog.register(Suborganization)
auditlog.register(SeniorOfficial) auditlog.register(SeniorOfficial)
auditlog.register(UserPortfolioPermission)
auditlog.register(AllowedEmail) auditlog.register(AllowedEmail)

View file

@ -563,15 +563,32 @@ class DomainRequest(TimeStampedModel):
help_text="Acknowledged .gov acceptable use policy", help_text="Acknowledged .gov acceptable use policy",
) )
# submission date records when domain request is submitted # Records when the domain request was first submitted
submission_date = models.DateField( first_submitted_date = models.DateField(
null=True, null=True,
blank=True, blank=True,
default=None, default=None,
verbose_name="submitted at", verbose_name="first submitted on",
help_text="Date submitted", help_text="Date initially submitted",
) )
# Records when domain request was last submitted
last_submitted_date = models.DateField(
null=True,
blank=True,
default=None,
verbose_name="last submitted on",
help_text="Date last submitted",
)
# Records when domain request status was last updated by an admin or analyst
last_status_update = models.DateField(
null=True,
blank=True,
default=None,
verbose_name="last updated on",
help_text="Date of the last status update",
)
notes = models.TextField( notes = models.TextField(
null=True, null=True,
blank=True, blank=True,
@ -627,6 +644,9 @@ class DomainRequest(TimeStampedModel):
self.sync_organization_type() self.sync_organization_type()
self.sync_yes_no_form_fields() self.sync_yes_no_form_fields()
if self._cached_status != self.status:
self.last_status_update = timezone.now().date()
super().save(*args, **kwargs) super().save(*args, **kwargs)
# Handle the action needed email. # Handle the action needed email.
@ -809,8 +829,12 @@ class DomainRequest(TimeStampedModel):
if not DraftDomain.string_could_be_domain(self.requested_domain.name): if not DraftDomain.string_could_be_domain(self.requested_domain.name):
raise ValueError("Requested domain is not a valid domain name.") raise ValueError("Requested domain is not a valid domain name.")
# Update submission_date to today # if the domain has not been submitted before this must be the first time
self.submission_date = timezone.now().date() if not self.first_submitted_date:
self.first_submitted_date = timezone.now().date()
# Update last_submitted_date to today
self.last_submitted_date = timezone.now().date()
self.save() self.save()
# Limit email notifications to transitions from Started and Withdrawn # Limit email notifications to transitions from Started and Withdrawn

View file

@ -131,9 +131,13 @@ class Portfolio(TimeStampedModel):
Returns a combination of organization_type / federal_type, seperated by ' - '. Returns a combination of organization_type / federal_type, seperated by ' - '.
If no federal_type is found, we just return the org type. If no federal_type is found, we just return the org type.
""" """
org_type_label = self.OrganizationChoices.get_org_label(self.organization_type) return self.get_portfolio_type(self.organization_type, self.federal_type)
agency_type_label = BranchChoices.get_branch_label(self.federal_type)
if self.organization_type == self.OrganizationChoices.FEDERAL and agency_type_label: @classmethod
def get_portfolio_type(cls, organization_type, federal_type):
org_type_label = cls.OrganizationChoices.get_org_label(organization_type)
agency_type_label = BranchChoices.get_branch_label(federal_type)
if organization_type == cls.OrganizationChoices.FEDERAL and agency_type_label:
return " - ".join([org_type_label, agency_type_label]) return " - ".join([org_type_label, agency_type_label])
else: else:
return org_type_label return org_type_label
@ -141,7 +145,11 @@ class Portfolio(TimeStampedModel):
@property @property
def federal_type(self): def federal_type(self):
"""Returns the federal_type value on the underlying federal_agency field""" """Returns the federal_type value on the underlying federal_agency field"""
return self.federal_agency.federal_type if self.federal_agency else None return self.get_federal_type(self.federal_agency)
@classmethod
def get_federal_type(cls, federal_agency):
return federal_agency.federal_type if federal_agency else None
# == Getters for domains == # # == Getters for domains == #
def get_domains(self): def get_domains(self):

View file

@ -1,13 +1,11 @@
"""People are invited by email to administer domains.""" """People are invited by email to administer domains."""
import logging import logging
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from django_fsm import FSMField, transition from django_fsm import FSMField, transition
from registrar.models.user_portfolio_permission import UserPortfolioPermission
from .utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices # type: ignore from .utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices # type: ignore
from .utility.time_stamped_model import TimeStampedModel from .utility.time_stamped_model import TimeStampedModel
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
@ -87,9 +85,11 @@ class PortfolioInvitation(TimeStampedModel):
raise RuntimeError("Cannot find the user to retrieve this portfolio invitation.") raise RuntimeError("Cannot find the user to retrieve this portfolio invitation.")
# and create a role for that user on this portfolio # and create a role for that user on this portfolio
user.portfolio = self.portfolio user_portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
portfolio=self.portfolio, user=user
)
if self.portfolio_roles and len(self.portfolio_roles) > 0: if self.portfolio_roles and len(self.portfolio_roles) > 0:
user.portfolio_roles = self.portfolio_roles user_portfolio_permission.roles = self.portfolio_roles
if self.portfolio_additional_permissions and len(self.portfolio_additional_permissions) > 0: if self.portfolio_additional_permissions and len(self.portfolio_additional_permissions) > 0:
user.portfolio_additional_permissions = self.portfolio_additional_permissions user_portfolio_permission.additional_permissions = self.portfolio_additional_permissions
user.save() user_portfolio_permission.save()

View file

@ -3,10 +3,9 @@ import logging
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.forms import ValidationError from django.http import HttpRequest
from registrar.models.domain_information import DomainInformation from registrar.models import DomainInformation, UserDomainRole
from registrar.models.user_domain_role import UserDomainRole
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from .domain_invitation import DomainInvitation from .domain_invitation import DomainInvitation
@ -15,7 +14,6 @@ from .transition_domain import TransitionDomain
from .verified_by_staff import VerifiedByStaff from .verified_by_staff import VerifiedByStaff
from .domain import Domain from .domain import Domain
from .domain_request import DomainRequest from .domain_request import DomainRequest
from django.contrib.postgres.fields import ArrayField
from waffle.decorators import flag_is_active from waffle.decorators import flag_is_active
from phonenumber_field.modelfields import PhoneNumberField # type: ignore from phonenumber_field.modelfields import PhoneNumberField # type: ignore
@ -112,34 +110,6 @@ class User(AbstractUser):
related_name="users", related_name="users",
) )
portfolio = models.ForeignKey(
"registrar.Portfolio",
null=True,
blank=True,
related_name="user",
on_delete=models.SET_NULL,
)
portfolio_roles = ArrayField(
models.CharField(
max_length=50,
choices=UserPortfolioRoleChoices.choices,
),
null=True,
blank=True,
help_text="Select one or more roles.",
)
portfolio_additional_permissions = ArrayField(
models.CharField(
max_length=50,
choices=UserPortfolioPermissionChoices.choices,
),
null=True,
blank=True,
help_text="Select one or more additional permissions.",
)
phone = PhoneNumberField( phone = PhoneNumberField(
null=True, null=True,
blank=True, blank=True,
@ -230,68 +200,50 @@ class User(AbstractUser):
def has_contact_info(self): def has_contact_info(self):
return bool(self.title or self.email or self.phone) return bool(self.title or self.email or self.phone)
def clean(self): def _has_portfolio_permission(self, portfolio, portfolio_permission):
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
super().clean()
if self.portfolio is None and self._get_portfolio_permissions():
raise ValidationError("When portfolio roles or additional permissions are assigned, portfolio is required.")
if self.portfolio is not None and not self._get_portfolio_permissions():
raise ValidationError("When portfolio is assigned, portfolio roles or additional permissions are required.")
def _get_portfolio_permissions(self):
"""
Retrieve the permissions for the user's portfolio roles.
"""
portfolio_permissions = set() # Use a set to avoid duplicate permissions
if self.portfolio_roles:
for role in self.portfolio_roles:
if role in self.PORTFOLIO_ROLE_PERMISSIONS:
portfolio_permissions.update(self.PORTFOLIO_ROLE_PERMISSIONS[role])
if self.portfolio_additional_permissions:
portfolio_permissions.update(self.portfolio_additional_permissions)
return list(portfolio_permissions) # Convert back to list if necessary
def _has_portfolio_permission(self, portfolio_permission):
"""The views should only call this function when testing for perms and not rely on roles.""" """The views should only call this function when testing for perms and not rely on roles."""
if not self.portfolio: if not portfolio:
return False return False
portfolio_permissions = self._get_portfolio_permissions() user_portfolio_perms = self.portfolio_permissions.filter(portfolio=portfolio, user=self).first()
if not user_portfolio_perms:
return False
return portfolio_permission in portfolio_permissions return portfolio_permission in user_portfolio_perms._get_portfolio_permissions()
# the methods below are checks for individual portfolio permissions. They are defined here def has_base_portfolio_permission(self, portfolio):
# to make them easier to call elsewhere throughout the application return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_PORTFOLIO)
def has_base_portfolio_permission(self):
return self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_PORTFOLIO)
def has_edit_org_portfolio_permission(self): def has_edit_org_portfolio_permission(self, portfolio):
return self._has_portfolio_permission(UserPortfolioPermissionChoices.EDIT_PORTFOLIO) return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_PORTFOLIO)
def has_domains_portfolio_permission(self): def has_domains_portfolio_permission(self, portfolio):
return self._has_portfolio_permission( return self._has_portfolio_permission(
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS
) or self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS) ) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
def has_domain_requests_portfolio_permission(self): def has_domain_requests_portfolio_permission(self, portfolio):
return self._has_portfolio_permission( return self._has_portfolio_permission(
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
) or self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS) ) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
def has_view_all_domains_permission(self): def has_view_all_domains_permission(self, portfolio):
"""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(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
# Field specific permission checks # Field specific permission checks
def has_view_suborganization(self): def has_view_suborganization(self, portfolio):
return self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION) return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION)
def has_edit_suborganization(self): def has_edit_suborganization(self, portfolio):
return self._has_portfolio_permission(UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION) return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION)
def get_first_portfolio(self):
permission = self.portfolio_permissions.first()
if permission:
return permission.portfolio
return None
@classmethod @classmethod
def needs_identity_verification(cls, email, uuid): def needs_identity_verification(cls, email, uuid):
@ -406,7 +358,14 @@ class User(AbstractUser):
for invitation in PortfolioInvitation.objects.filter( for invitation in PortfolioInvitation.objects.filter(
email__iexact=self.email, status=PortfolioInvitation.PortfolioInvitationStatus.INVITED email__iexact=self.email, status=PortfolioInvitation.PortfolioInvitationStatus.INVITED
): ):
if self.portfolio is None: # need to create a bogus request and assign user to it, in order to pass request
# to flag_is_active
request = HttpRequest()
request.user = self
only_single_portfolio = (
not flag_is_active(request, "multiple_portfolios") and self.get_first_portfolio() is None
)
if only_single_portfolio or flag_is_active(None, "multiple_portfolios"):
try: try:
invitation.retrieve() invitation.retrieve()
invitation.save() invitation.save()
@ -431,13 +390,17 @@ class User(AbstractUser):
self.check_domain_invitations_on_login() self.check_domain_invitations_on_login()
self.check_portfolio_invitations_on_login() self.check_portfolio_invitations_on_login()
# NOTE TO DAVE: I'd simply suggest that we move these functions outside of the user object,
# and move them to some sort of utility file. That way we aren't calling request inside here.
def is_org_user(self, request): def is_org_user(self, request):
has_organization_feature_flag = flag_is_active(request, "organization_feature") has_organization_feature_flag = flag_is_active(request, "organization_feature")
return has_organization_feature_flag and self.has_base_portfolio_permission() portfolio = request.session.get("portfolio")
return has_organization_feature_flag and self.has_base_portfolio_permission(portfolio)
def get_user_domain_ids(self, request): def get_user_domain_ids(self, request):
"""Returns either the domains ids associated with this user on UserDomainRole or Portfolio""" """Returns either the domains ids associated with this user on UserDomainRole or Portfolio"""
if self.is_org_user(request) and self.has_view_all_domains_permission(): portfolio = request.session.get("portfolio")
return DomainInformation.objects.filter(portfolio=self.portfolio).values_list("domain_id", flat=True) if self.is_org_user(request) and self.has_view_all_domains_permission(portfolio):
return DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True)
else: else:
return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True) return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True)

View file

@ -0,0 +1,119 @@
from django.db import models
from django.forms import ValidationError
from django.http import HttpRequest
from waffle import flag_is_active
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from .utility.time_stamped_model import TimeStampedModel
from django.contrib.postgres.fields import ArrayField
class UserPortfolioPermission(TimeStampedModel):
"""This is a linking table that connects a user with a role on a portfolio."""
class Meta:
unique_together = ["user", "portfolio"]
PORTFOLIO_ROLE_PERMISSIONS = {
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
UserPortfolioPermissionChoices.VIEW_MEMBER,
UserPortfolioPermissionChoices.EDIT_MEMBER,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
# Domain: field specific permissions
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
],
UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
UserPortfolioPermissionChoices.VIEW_MEMBER,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
# Domain: field specific permissions
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
],
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
],
}
user = models.ForeignKey(
"registrar.User",
null=False,
# when a user is deleted, permissions are too
on_delete=models.CASCADE,
related_name="portfolio_permissions",
)
portfolio = models.ForeignKey(
"registrar.Portfolio",
null=False,
# when a portfolio is deleted, permissions are too
on_delete=models.CASCADE,
related_name="portfolio_users",
)
roles = ArrayField(
models.CharField(
max_length=50,
choices=UserPortfolioRoleChoices.choices,
),
null=True,
blank=True,
help_text="Select one or more roles.",
)
additional_permissions = ArrayField(
models.CharField(
max_length=50,
choices=UserPortfolioPermissionChoices.choices,
),
null=True,
blank=True,
help_text="Select one or more additional permissions.",
)
def __str__(self):
return f"User '{self.user}' on Portfolio '{self.portfolio}' " f"<Roles: {self.roles}>" if self.roles else ""
def _get_portfolio_permissions(self):
"""
Retrieve the permissions for the user's portfolio roles.
"""
# Use a set to avoid duplicate permissions
portfolio_permissions = set()
if self.roles:
for role in self.roles:
portfolio_permissions.update(self.PORTFOLIO_ROLE_PERMISSIONS.get(role, []))
if self.additional_permissions:
portfolio_permissions.update(self.additional_permissions)
return list(portfolio_permissions)
def clean(self):
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
super().clean()
# Check if a user is set without accessing the related object.
has_user = bool(self.user_id)
if self.pk is None and has_user:
# Have to create a bogus request to set the user and pass to flag_is_active
request = HttpRequest()
request.user = self.user
existing_permissions = UserPortfolioPermission.objects.filter(user=self.user)
if not flag_is_active(request, "multiple_portfolios") and existing_permissions.exists():
raise ValidationError(
"Only one portfolio permission is allowed per user when multiple portfolios are disabled."
)
# Check if portfolio is set without accessing the related object.
has_portfolio = bool(self.portfolio_id)
if not has_portfolio and self._get_portfolio_permissions():
raise ValidationError("When portfolio roles or additional permissions are assigned, portfolio is required.")
if has_portfolio and not self._get_portfolio_permissions():
raise ValidationError("When portfolio is assigned, portfolio roles or additional permissions are required.")

View file

@ -125,8 +125,9 @@ class CheckUserProfileMiddleware:
class CheckPortfolioMiddleware: class CheckPortfolioMiddleware:
""" """
Checks if the current user has a portfolio this middleware should serve two purposes:
If they do, redirect them to the portfolio homepage when they navigate to home. 1 - set the portfolio in session if appropriate # views will need the session portfolio
2 - if path is home and session portfolio is set, redirect based on permissions of user
""" """
def __init__(self, get_response): def __init__(self, get_response):
@ -140,15 +141,24 @@ class CheckPortfolioMiddleware:
def process_view(self, request, view_func, view_args, view_kwargs): def process_view(self, request, view_func, view_args, view_kwargs):
current_path = request.path current_path = request.path
if current_path == self.home and request.user.is_authenticated and request.user.is_org_user(request): if not request.user.is_authenticated:
return None
if request.user.has_base_portfolio_permission(): # set the portfolio in the session if it is not set
portfolio = request.user.portfolio if "portfolio" not in request.session or request.session["portfolio"] is None:
# if multiple portfolios are allowed for this user
if flag_is_active(request, "multiple_portfolios"):
# NOTE: we will want to change later to have a workflow for selecting
# portfolio and another for switching portfolio; for now, select first
request.session["portfolio"] = request.user.get_first_portfolio()
elif flag_is_active(request, "organization_feature"):
request.session["portfolio"] = request.user.get_first_portfolio()
else:
request.session["portfolio"] = None
# Add the portfolio to the request object if request.session["portfolio"] is not None and current_path == self.home:
request.portfolio = portfolio if request.user.is_org_user(request):
if request.user.has_domains_portfolio_permission(request.session["portfolio"]):
if request.user.has_domains_portfolio_permission():
portfolio_redirect = reverse("domains") portfolio_redirect = reverse("domains")
else: else:
portfolio_redirect = reverse("no-portfolio-domains") portfolio_redirect = reverse("no-portfolio-domains")

View file

@ -5,6 +5,8 @@
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %} {% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
{% url 'get-senior-official-from-federal-agency-json' as url %} {% url 'get-senior-official-from-federal-agency-json' as url %}
<input id="senior_official_from_agency_json_url" class="display-none" value="{{url}}" /> <input id="senior_official_from_agency_json_url" class="display-none" value="{{url}}" />
{% url 'get-federal-and-portfolio-types-from-federal-agency-json' as url %}
<input id="federal_and_portfolio_types_from_agency_json_url" class="display-none" value="{{url}}" />
{{ block.super }} {{ block.super }}
{% endblock content %} {% endblock content %}

View file

@ -72,9 +72,9 @@
{% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %} {% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %}
{% endif %} {% endif %}
{% if portfolio and has_domains_portfolio_permission and request.user.has_view_suborganization %} {% if portfolio and has_domains_portfolio_permission and has_view_suborganization %}
{% url 'domain-suborganization' pk=domain.id as url %} {% url 'domain-suborganization' pk=domain.id as url %}
{% 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=url editable=is_editable|and:has_edit_suborganization %}
{% 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 %}

View file

@ -61,7 +61,7 @@
{% if portfolio %} {% if portfolio %}
{% comment %} Only show this menu option if the user has the perms to do so {% endcomment %} {% 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 %} {% if has_domains_portfolio_permission and has_view_suborganization %}
{% with url_name="domain-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 %}

View file

@ -15,7 +15,7 @@
If you believe there is an error please contact <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>. If you believe there is an error please contact <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
</p> </p>
{% if has_domains_portfolio_permission and request.user.has_edit_suborganization %} {% if has_domains_portfolio_permission and has_edit_suborganization %}
<form class="usa-form usa-form--large" method="post" novalidate id="form-container"> <form class="usa-form usa-form--large" method="post" novalidate id="form-container">
{% csrf_token %} {% csrf_token %}
{% input_with_errors form.sub_organization %} {% input_with_errors form.sub_organization %}

View file

@ -4,7 +4,7 @@ Hi, {{ recipient.first_name }}.
We've identified an action that youll need to complete before we continue reviewing your .gov domain request. We've identified an action that youll need to complete before we continue reviewing your .gov domain request.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Action needed STATUS: Action needed
---------------------------------------------------------------- ----------------------------------------------------------------

View file

@ -4,7 +4,7 @@ Hi, {{ recipient.first_name }}.
We've identified an action that youll need to complete before we continue reviewing your .gov domain request. We've identified an action that youll need to complete before we continue reviewing your .gov domain request.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Action needed STATUS: Action needed
---------------------------------------------------------------- ----------------------------------------------------------------

View file

@ -4,7 +4,7 @@ Hi, {{ recipient.first_name }}.
We've identified an action that youll need to complete before we continue reviewing your .gov domain request. We've identified an action that youll need to complete before we continue reviewing your .gov domain request.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Action needed STATUS: Action needed
---------------------------------------------------------------- ----------------------------------------------------------------

View file

@ -4,7 +4,7 @@ Hi, {{ recipient.first_name }}.
We've identified an action that youll need to complete before we continue reviewing your .gov domain request. We've identified an action that youll need to complete before we continue reviewing your .gov domain request.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Action needed STATUS: Action needed
---------------------------------------------------------------- ----------------------------------------------------------------

View file

@ -4,7 +4,7 @@ Hi, {{ recipient.first_name }}.
Your .gov domain request has been withdrawn and will not be reviewed by our team. Your .gov domain request has been withdrawn and will not be reviewed by our team.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Withdrawn STATUS: Withdrawn
---------------------------------------------------------------- ----------------------------------------------------------------

View file

@ -4,7 +4,7 @@ Hi, {{ recipient.first_name }}.
Congratulations! Your .gov domain request has been approved. Congratulations! Your .gov domain request has been approved.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Approved STATUS: Approved
You can manage your approved domain on the .gov registrar <https://manage.get.gov>. You can manage your approved domain on the .gov registrar <https://manage.get.gov>.

View file

@ -4,7 +4,7 @@ Hi, {{ recipient.first_name }}.
Your .gov domain request has been rejected. Your .gov domain request has been rejected.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Rejected STATUS: Rejected
---------------------------------------------------------------- ----------------------------------------------------------------

View file

@ -4,7 +4,7 @@ Hi, {{ recipient.first_name }}.
We received your .gov domain request. We received your .gov domain request.
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }} DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }} REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
STATUS: Submitted STATUS: Submitted
---------------------------------------------------------------- ----------------------------------------------------------------

View file

@ -45,7 +45,7 @@
<thead> <thead>
<tr> <tr>
<th data-sortable="requested_domain__name" scope="col" role="columnheader">Domain name</th> <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="last_submitted_date" scope="col" role="columnheader">Date submitted</th>
<th data-sortable="status" scope="col" role="columnheader">Status</th> <th data-sortable="status" scope="col" role="columnheader">Status</th>
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th> <th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
<!-- AJAX will conditionally add a th for delete actions --> <!-- AJAX will conditionally add a th for delete actions -->

View file

@ -157,7 +157,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 portfolio and request.user.has_view_suborganization %} {% if portfolio and 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

View file

@ -27,6 +27,8 @@ from registrar.models import (
PublicContact, PublicContact,
Domain, Domain,
FederalAgency, FederalAgency,
UserPortfolioPermission,
Portfolio,
) )
from epplibwrapper import ( from epplibwrapper import (
commands, commands,
@ -775,13 +777,13 @@ class MockDb(TestCase):
cls.domain_request_3.alternative_domains.add(website, website_2) cls.domain_request_3.alternative_domains.add(website, website_2)
cls.domain_request_3.current_websites.add(website_3, website_4) cls.domain_request_3.current_websites.add(website_3, website_4)
cls.domain_request_3.cisa_representative_email = "test@igorville.com" cls.domain_request_3.cisa_representative_email = "test@igorville.com"
cls.domain_request_3.submission_date = get_time_aware_date(datetime(2024, 4, 2)) cls.domain_request_3.last_submitted_date = get_time_aware_date(datetime(2024, 4, 2))
cls.domain_request_3.save() cls.domain_request_3.save()
cls.domain_request_4.submission_date = get_time_aware_date(datetime(2024, 4, 2)) cls.domain_request_4.last_submitted_date = get_time_aware_date(datetime(2024, 4, 2))
cls.domain_request_4.save() cls.domain_request_4.save()
cls.domain_request_6.submission_date = get_time_aware_date(datetime(2024, 4, 2)) cls.domain_request_6.last_submitted_date = get_time_aware_date(datetime(2024, 4, 2))
cls.domain_request_6.save() cls.domain_request_6.save()
@classmethod @classmethod
@ -791,6 +793,8 @@ class MockDb(TestCase):
DomainInformation.objects.all().delete() DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete() DomainRequest.objects.all().delete()
UserDomainRole.objects.all().delete() UserDomainRole.objects.all().delete()
Portfolio.objects.all().delete()
UserPortfolioPermission.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
DomainInvitation.objects.all().delete() DomainInvitation.objects.all().delete()
cls.federal_agency_1.delete() cls.federal_agency_1.delete()
@ -1743,3 +1747,12 @@ class MockEppLib(TestCase):
def tearDown(self): def tearDown(self):
self.mockSendPatch.stop() self.mockSendPatch.stop()
def get_wsgi_request_object(client, user, url="/"):
"""Returns client.get(url).wsgi_request for testing functions or classes
that need a request object directly passed to them."""
client.force_login(user)
request = client.get(url).wsgi_request
request.user = user
return request

View file

@ -455,7 +455,7 @@ class TestDomainRequestAdmin(MockEppLib):
# Assert that our sort works correctly # Assert that our sort works correctly
self.test_helper.assert_table_sorted( self.test_helper.assert_table_sorted(
"11", "13",
( (
"submitter__first_name", "submitter__first_name",
"submitter__last_name", "submitter__last_name",
@ -464,7 +464,7 @@ class TestDomainRequestAdmin(MockEppLib):
# Assert that sorting in reverse works correctly # Assert that sorting in reverse works correctly
self.test_helper.assert_table_sorted( self.test_helper.assert_table_sorted(
"-11", "-13",
( (
"-submitter__first_name", "-submitter__first_name",
"-submitter__last_name", "-submitter__last_name",
@ -487,7 +487,7 @@ class TestDomainRequestAdmin(MockEppLib):
# Assert that our sort works correctly # Assert that our sort works correctly
self.test_helper.assert_table_sorted( self.test_helper.assert_table_sorted(
"12", "14",
( (
"investigator__first_name", "investigator__first_name",
"investigator__last_name", "investigator__last_name",
@ -496,7 +496,7 @@ class TestDomainRequestAdmin(MockEppLib):
# Assert that sorting in reverse works correctly # Assert that sorting in reverse works correctly
self.test_helper.assert_table_sorted( self.test_helper.assert_table_sorted(
"-12", "-14",
( (
"-investigator__first_name", "-investigator__first_name",
"-investigator__last_name", "-investigator__last_name",
@ -509,7 +509,7 @@ class TestDomainRequestAdmin(MockEppLib):
@less_console_noise_decorator @less_console_noise_decorator
def test_default_sorting_in_domain_requests_list(self): def test_default_sorting_in_domain_requests_list(self):
""" """
Make sure the default sortin in on the domain requests list page is reverse submission_date Make sure the default sortin in on the domain requests list page is reverse last_submitted_date
then alphabetical requested_domain then alphabetical requested_domain
""" """
@ -519,12 +519,12 @@ class TestDomainRequestAdmin(MockEppLib):
for name in ["ccc.gov", "bbb.gov", "eee.gov", "aaa.gov", "zzz.gov", "ddd.gov"] for name in ["ccc.gov", "bbb.gov", "eee.gov", "aaa.gov", "zzz.gov", "ddd.gov"]
] ]
domain_requests[0].submission_date = timezone.make_aware(datetime(2024, 10, 16)) domain_requests[0].last_submitted_date = timezone.make_aware(datetime(2024, 10, 16))
domain_requests[1].submission_date = timezone.make_aware(datetime(2001, 10, 16)) domain_requests[1].last_submitted_date = timezone.make_aware(datetime(2001, 10, 16))
domain_requests[2].submission_date = timezone.make_aware(datetime(1980, 10, 16)) domain_requests[2].last_submitted_date = timezone.make_aware(datetime(1980, 10, 16))
domain_requests[3].submission_date = timezone.make_aware(datetime(1998, 10, 16)) domain_requests[3].last_submitted_date = timezone.make_aware(datetime(1998, 10, 16))
domain_requests[4].submission_date = timezone.make_aware(datetime(2013, 10, 16)) domain_requests[4].last_submitted_date = timezone.make_aware(datetime(2013, 10, 16))
domain_requests[5].submission_date = timezone.make_aware(datetime(1980, 10, 16)) domain_requests[5].last_submitted_date = timezone.make_aware(datetime(1980, 10, 16))
# Save the modified domain requests to update their attributes in the database # Save the modified domain requests to update their attributes in the database
for domain_request in domain_requests: for domain_request in domain_requests:
@ -1595,7 +1595,9 @@ class TestDomainRequestAdmin(MockEppLib):
"cisa_representative_last_name", "cisa_representative_last_name",
"has_cisa_representative", "has_cisa_representative",
"is_policy_acknowledged", "is_policy_acknowledged",
"submission_date", "first_submitted_date",
"last_submitted_date",
"last_status_update",
"notes", "notes",
"alternative_domains", "alternative_domains",
] ]

View file

@ -5,6 +5,7 @@ from django.contrib.auth import get_user_model
from registrar.tests.common import create_superuser, create_user from registrar.tests.common import create_superuser, create_user
from api.tests.common import less_console_noise_decorator from api.tests.common import less_console_noise_decorator
from registrar.utility.constants import BranchChoices
class GetSeniorOfficialJsonTest(TestCase): class GetSeniorOfficialJsonTest(TestCase):
@ -71,3 +72,40 @@ class GetSeniorOfficialJsonTest(TestCase):
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
data = response.json() data = response.json()
self.assertEqual(data["error"], "Senior Official not found") self.assertEqual(data["error"], "Senior Official not found")
class GetFederalPortfolioTypeJsonTest(TestCase):
def setUp(self):
self.client = Client()
p = "password"
self.user = get_user_model().objects.create_user(username="testuser", password=p)
self.superuser = create_superuser()
self.analyst_user = create_user()
self.agency = FederalAgency.objects.create(agency="Test Agency", federal_type=BranchChoices.JUDICIAL)
self.api_url = reverse("get-federal-and-portfolio-types-from-federal-agency-json")
def tearDown(self):
User.objects.all().delete()
FederalAgency.objects.all().delete()
@less_console_noise_decorator
def test_get_federal_and_portfolio_types_json_authenticated_superuser(self):
"""Test that a superuser can fetch the federal and portfolio types."""
p = "adminpass"
self.client.login(username="superuser", password=p)
response = self.client.get(self.api_url, {"agency_name": "Test Agency", "organization_type": "federal"})
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data["federal_type"], "Judicial")
self.assertEqual(data["portfolio_type"], "Federal - Judicial")
@less_console_noise_decorator
def test_get_federal_and_portfolio_types_json_authenticated_regularuser(self):
"""Test that a regular user receives a 403 with an error message."""
p = "password"
self.client.login(username="testuser", password=p)
response = self.client.get(self.api_url, {"agency_name": "Test Agency", "organization_type": "federal"})
self.assertEqual(response.status_code, 302)

View file

@ -17,6 +17,7 @@ from registrar.models import (
DomainInvitation, DomainInvitation,
UserDomainRole, UserDomainRole,
FederalAgency, FederalAgency,
UserPortfolioPermission,
AllowedEmail, AllowedEmail,
) )
@ -1145,19 +1146,24 @@ class TestPortfolioInvitations(TestCase):
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete() Portfolio.objects.all().delete()
PortfolioInvitation.objects.all().delete() PortfolioInvitation.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
@less_console_noise_decorator @less_console_noise_decorator
def test_retrieval(self): def test_retrieval(self):
self.assertFalse(self.user.portfolio) portfolio_role_exists = UserPortfolioPermission.objects.filter(
user=self.user, portfolio=self.portfolio
).exists()
self.assertFalse(portfolio_role_exists)
self.invitation.retrieve() self.invitation.retrieve()
self.user.refresh_from_db() self.user.refresh_from_db()
self.assertEqual(self.user.portfolio.organization_name, "Hotel California") created_role = UserPortfolioPermission.objects.get(user=self.user, portfolio=self.portfolio)
self.assertEqual(self.user.portfolio_roles, [self.portfolio_role_base, self.portfolio_role_admin]) self.assertEqual(created_role.portfolio.organization_name, "Hotel California")
self.assertEqual(created_role.roles, [self.portfolio_role_base, self.portfolio_role_admin])
self.assertEqual( self.assertEqual(
self.user.portfolio_additional_permissions, [self.portfolio_permission_1, self.portfolio_permission_2] created_role.additional_permissions, [self.portfolio_permission_1, self.portfolio_permission_2]
) )
self.assertEqual(self.invitation.status, PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED) self.assertEqual(self.invitation.status, PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
@ -1170,16 +1176,129 @@ class TestPortfolioInvitations(TestCase):
@less_console_noise_decorator @less_console_noise_decorator
def test_retrieve_user_already_member_error(self): def test_retrieve_user_already_member_error(self):
self.assertFalse(self.user.portfolio) portfolio_role_exists = UserPortfolioPermission.objects.filter(
portfolio2, _ = Portfolio.objects.get_or_create(creator=self.user2, organization_name="Tokyo Hotel") user=self.user, portfolio=self.portfolio
self.user.portfolio = portfolio2 ).exists()
self.assertEqual(self.user.portfolio.organization_name, "Tokyo Hotel") self.assertFalse(portfolio_role_exists)
self.user.save() portfolio_role, _ = UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio)
self.assertEqual(portfolio_role.portfolio.organization_name, "Hotel California")
self.user.check_portfolio_invitations_on_login() self.user.check_portfolio_invitations_on_login()
self.user.refresh_from_db() self.user.refresh_from_db()
self.assertEqual(self.user.portfolio.organization_name, "Tokyo Hotel")
roles = UserPortfolioPermission.objects.filter(user=self.user)
self.assertEqual(len(roles), 1)
self.assertEqual(self.invitation.status, PortfolioInvitation.PortfolioInvitationStatus.INVITED) self.assertEqual(self.invitation.status, PortfolioInvitation.PortfolioInvitationStatus.INVITED)
@less_console_noise_decorator
def test_retrieve_user_multiple_invitations(self):
"""Retrieve user portfolio invitations when there are multiple and multiple_options flag true."""
# create a 2nd portfolio and a 2nd portfolio invitation to self.user
portfolio2, _ = Portfolio.objects.get_or_create(creator=self.user2, organization_name="Take It Easy")
PortfolioInvitation.objects.get_or_create(
email=self.email,
portfolio=portfolio2,
portfolio_roles=[self.portfolio_role_base, self.portfolio_role_admin],
portfolio_additional_permissions=[self.portfolio_permission_1, self.portfolio_permission_2],
)
with override_flag("multiple_portfolios", active=True):
self.user.check_portfolio_invitations_on_login()
self.user.refresh_from_db()
roles = UserPortfolioPermission.objects.filter(user=self.user)
self.assertEqual(len(roles), 2)
updated_invitation1, _ = PortfolioInvitation.objects.get_or_create(
email=self.email, portfolio=self.portfolio
)
self.assertEqual(updated_invitation1.status, PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
updated_invitation2, _ = PortfolioInvitation.objects.get_or_create(email=self.email, portfolio=portfolio2)
self.assertEqual(updated_invitation2.status, PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
@less_console_noise_decorator
def test_retrieve_user_multiple_invitations_when_multiple_portfolios_inactive(self):
"""Attempt to retrieve user portfolio invitations when there are multiple
but multiple_portfolios flag set to False"""
# create a 2nd portfolio and a 2nd portfolio invitation to self.user
portfolio2, _ = Portfolio.objects.get_or_create(creator=self.user2, organization_name="Take It Easy")
PortfolioInvitation.objects.get_or_create(
email=self.email,
portfolio=portfolio2,
portfolio_roles=[self.portfolio_role_base, self.portfolio_role_admin],
portfolio_additional_permissions=[self.portfolio_permission_1, self.portfolio_permission_2],
)
self.user.check_portfolio_invitations_on_login()
self.user.refresh_from_db()
roles = UserPortfolioPermission.objects.filter(user=self.user)
self.assertEqual(len(roles), 1)
updated_invitation1, _ = PortfolioInvitation.objects.get_or_create(email=self.email, portfolio=self.portfolio)
self.assertEqual(updated_invitation1.status, PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
updated_invitation2, _ = PortfolioInvitation.objects.get_or_create(email=self.email, portfolio=portfolio2)
self.assertEqual(updated_invitation2.status, PortfolioInvitation.PortfolioInvitationStatus.INVITED)
class TestUserPortfolioPermission(TestCase):
@less_console_noise_decorator
def setUp(self):
self.user, _ = User.objects.get_or_create(email="mayor@igorville.gov")
super().setUp()
def tearDown(self):
super().tearDown()
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete()
User.objects.all().delete()
UserDomainRole.objects.all().delete()
@less_console_noise_decorator
@override_flag("multiple_portfolios", active=True)
def test_clean_on_multiple_portfolios_when_flag_active(self):
"""Ensures that a user can create multiple portfolio permission objects when the flag is enabled"""
# Create an instance of User with a portfolio but no roles or additional permissions
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
portfolio_2, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Motel California")
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
portfolio=portfolio, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
portfolio_permission_2 = UserPortfolioPermission(
portfolio=portfolio_2, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
# Clean should pass on both of these objects
try:
portfolio_permission.clean()
portfolio_permission_2.clean()
except ValidationError as error:
self.fail(f"Raised ValidationError unexpectedly: {error}")
@less_console_noise_decorator
@override_flag("multiple_portfolios", active=False)
def test_clean_on_creates_multiple_portfolios(self):
"""Ensures that a user cannot create multiple portfolio permission objects when the flag is disabled"""
# Create an instance of User with a portfolio but no roles or additional permissions
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
portfolio_2, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Motel California")
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
portfolio=portfolio, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
portfolio_permission_2 = UserPortfolioPermission(
portfolio=portfolio_2, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
# This should work as intended
portfolio_permission.clean()
# Test if the ValidationError is raised with the correct message
with self.assertRaises(ValidationError) as cm:
portfolio_permission_2.clean()
portfolio_permission_2, _ = UserPortfolioPermission.objects.get_or_create(portfolio=portfolio, user=self.user)
self.assertEqual(
cm.exception.message,
"Only one portfolio permission is allowed per user when multiple portfolios are disabled.",
)
class TestUser(TestCase): class TestUser(TestCase):
"""Test actions that occur on user login, """Test actions that occur on user login,
@ -1191,6 +1310,7 @@ class TestUser(TestCase):
self.domain_name = "igorvilleInTransition.gov" self.domain_name = "igorvilleInTransition.gov"
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
self.user, _ = User.objects.get_or_create(email=self.email) self.user, _ = User.objects.get_or_create(email=self.email)
self.factory = RequestFactory()
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
@ -1200,6 +1320,7 @@ class TestUser(TestCase):
DomainRequest.objects.all().delete() DomainRequest.objects.all().delete()
DraftDomain.objects.all().delete() DraftDomain.objects.all().delete()
TransitionDomain.objects.all().delete() TransitionDomain.objects.all().delete()
UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete() Portfolio.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
UserDomainRole.objects.all().delete() UserDomainRole.objects.all().delete()
@ -1362,44 +1483,41 @@ class TestUser(TestCase):
Note: This tests _get_portfolio_permissions as a side effect Note: This tests _get_portfolio_permissions as a side effect
""" """
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California") portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS] user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
self.user.save() user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
self.user.refresh_from_db()
user_can_view_all_domains = self.user.has_domains_portfolio_permission()
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission()
self.assertFalse(user_can_view_all_domains) self.assertFalse(user_can_view_all_domains)
self.assertFalse(user_can_view_all_requests) self.assertFalse(user_can_view_all_requests)
self.user.portfolio = portfolio portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
self.user.save() portfolio=portfolio,
self.user.refresh_from_db() user=self.user,
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS],
)
user_can_view_all_domains = self.user.has_domains_portfolio_permission() user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission() user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
self.assertTrue(user_can_view_all_domains) self.assertTrue(user_can_view_all_domains)
self.assertFalse(user_can_view_all_requests) self.assertFalse(user_can_view_all_requests)
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN] portfolio_permission.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
self.user.save() portfolio_permission.save()
self.user.refresh_from_db() portfolio_permission.refresh_from_db()
user_can_view_all_domains = self.user.has_domains_portfolio_permission() user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission() user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
self.assertTrue(user_can_view_all_domains) self.assertTrue(user_can_view_all_domains)
self.assertTrue(user_can_view_all_requests) self.assertTrue(user_can_view_all_requests)
UserDomainRole.objects.all().get_or_create( UserDomainRole.objects.get_or_create(user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
)
user_can_view_all_domains = self.user.has_domains_portfolio_permission() user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission() user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
self.assertTrue(user_can_view_all_domains) self.assertTrue(user_can_view_all_domains)
self.assertTrue(user_can_view_all_requests) self.assertTrue(user_can_view_all_requests)
@ -1410,13 +1528,15 @@ class TestUser(TestCase):
def test_user_with_portfolio_but_no_roles(self): def test_user_with_portfolio_but_no_roles(self):
# Create an instance of User with a portfolio but no roles or additional permissions # Create an instance of User with a portfolio but no roles or additional permissions
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California") portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(portfolio=portfolio, user=self.user)
self.user.portfolio = portfolio # Try to remove the role
self.user.portfolio_roles = [] portfolio_permission.portfolio = portfolio
portfolio_permission.roles = []
# Test if the ValidationError is raised with the correct message # Test if the ValidationError is raised with the correct message
with self.assertRaises(ValidationError) as cm: with self.assertRaises(ValidationError) as cm:
self.user.clean() portfolio_permission.clean()
self.assertEqual( self.assertEqual(
cm.exception.message, "When portfolio is assigned, portfolio roles or additional permissions are required." cm.exception.message, "When portfolio is assigned, portfolio roles or additional permissions are required."
@ -1425,13 +1545,18 @@ class TestUser(TestCase):
@less_console_noise_decorator @less_console_noise_decorator
def test_user_with_portfolio_roles_but_no_portfolio(self): def test_user_with_portfolio_roles_but_no_portfolio(self):
# Create an instance of User with a portfolio role but no portfolio portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
self.user.portfolio = None portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN] portfolio=portfolio, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
# Try to remove the portfolio
portfolio_permission.portfolio = None
portfolio_permission.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
# Test if the ValidationError is raised with the correct message # Test if the ValidationError is raised with the correct message
with self.assertRaises(ValidationError) as cm: with self.assertRaises(ValidationError) as cm:
self.user.clean() portfolio_permission.clean()
self.assertEqual( self.assertEqual(
cm.exception.message, "When portfolio roles or additional permissions are assigned, portfolio is required." cm.exception.message, "When portfolio roles or additional permissions are assigned, portfolio is required."

View file

@ -7,6 +7,7 @@ from registrar.models import (
UserDomainRole, UserDomainRole,
) )
from registrar.models import Portfolio from registrar.models import Portfolio
from registrar.models.user_portfolio_permission import UserPortfolioPermission
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from registrar.utility.csv_export import ( from registrar.utility.csv_export import (
DomainDataFull, DomainDataFull,
@ -33,7 +34,14 @@ import boto3_mocking
from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore
from django.utils import timezone from django.utils import timezone
from api.tests.common import less_console_noise_decorator from api.tests.common import less_console_noise_decorator
from .common import MockDbForSharedTests, MockDbForIndividualTests, MockEppLib, less_console_noise, get_time_aware_date from .common import (
MockDbForSharedTests,
MockDbForIndividualTests,
MockEppLib,
get_wsgi_request_object,
less_console_noise,
get_time_aware_date,
)
from waffle.testutils import override_flag from waffle.testutils import override_flag
@ -281,10 +289,8 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
# Create a user and associate it with some domains # Create a user and associate it with some domains
UserDomainRole.objects.create(user=self.user, domain=self.domain_2) UserDomainRole.objects.create(user=self.user, domain=self.domain_2)
# Create a request object # Make a GET request using self.client to get a request object
factory = RequestFactory() request = get_wsgi_request_object(client=self.client, user=self.user)
request = factory.get("/")
request.user = self.user
# Create a CSV file in memory # Create a CSV file in memory
csv_file = StringIO() csv_file = StringIO()
@ -321,8 +327,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
# Create a portfolio and assign it to the user # Create a portfolio and assign it to the user
portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio") portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
self.user.portfolio = portfolio portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(portfolio=portfolio, user=self.user)
self.user.save()
UserDomainRole.objects.create(user=self.user, domain=self.domain_2) UserDomainRole.objects.create(user=self.user, domain=self.domain_2)
UserDomainRole.objects.filter(user=self.user, domain=self.domain_1).delete() UserDomainRole.objects.filter(user=self.user, domain=self.domain_1).delete()
@ -336,14 +341,12 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
self.domain_3.domain_info.save() self.domain_3.domain_info.save()
# Set up user permissions # Set up user permissions
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN] portfolio_permission.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
self.user.save() portfolio_permission.save()
self.user.refresh_from_db() portfolio_permission.refresh_from_db()
# Create a request object # Make a GET request using self.client to get a request object
factory = RequestFactory() request = get_wsgi_request_object(client=self.client, user=self.user)
request = factory.get("/")
request.user = self.user
# Get the csv content # Get the csv content
csv_content = self._run_domain_data_type_user_export(request) csv_content = self._run_domain_data_type_user_export(request)
@ -354,19 +357,22 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
self.assertNotIn(self.domain_2.name, csv_content) self.assertNotIn(self.domain_2.name, csv_content)
# Test the output for readonly admin # Test the output for readonly admin
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY] portfolio_permission.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY]
self.user.save() portfolio_permission.save()
portfolio_permission.refresh_from_db()
# Get the csv content
csv_content = self._run_domain_data_type_user_export(request)
self.assertIn(self.domain_1.name, csv_content) self.assertIn(self.domain_1.name, csv_content)
self.assertIn(self.domain_3.name, csv_content) self.assertIn(self.domain_3.name, csv_content)
self.assertNotIn(self.domain_2.name, csv_content) self.assertNotIn(self.domain_2.name, csv_content)
portfolio_permission.roles = [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
portfolio_permission.save()
portfolio_permission.refresh_from_db()
# Get the csv content # Get the csv content
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
self.user.save()
csv_content = self._run_domain_data_type_user_export(request) csv_content = self._run_domain_data_type_user_export(request)
self.assertNotIn(self.domain_1.name, csv_content) self.assertNotIn(self.domain_1.name, csv_content)
self.assertNotIn(self.domain_3.name, csv_content) self.assertNotIn(self.domain_3.name, csv_content)
self.assertIn(self.domain_2.name, csv_content) self.assertIn(self.domain_2.name, csv_content)
@ -762,7 +768,7 @@ class HelperFunctions(MockDbForSharedTests):
with less_console_noise(): with less_console_noise():
filter_condition = { filter_condition = {
"status": DomainRequest.DomainRequestStatus.SUBMITTED, "status": DomainRequest.DomainRequestStatus.SUBMITTED,
"submission_date__lte": self.end_date, "last_submitted_date__lte": self.end_date,
} }
submitted_requests_sliced_at_end_date = DomainRequestExport.get_sliced_requests(filter_condition) submitted_requests_sliced_at_end_date = DomainRequestExport.get_sliced_requests(filter_condition)
expected_content = [3, 2, 0, 0, 0, 0, 1, 0, 0, 1] expected_content = [3, 2, 0, 0, 0, 0, 1, 0, 0, 1]

View file

@ -6,7 +6,7 @@ from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from waffle.testutils import override_flag from waffle.testutils import override_flag
from api.tests.common import less_console_noise_decorator from api.tests.common import less_console_noise_decorator
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from .common import MockEppLib, MockSESClient, create_user # type: ignore from .common import MockEppLib, MockSESClient, create_user # type: ignore
from django_webtest import WebTest # type: ignore from django_webtest import WebTest # type: ignore
import boto3_mocking # type: ignore import boto3_mocking # type: ignore
@ -37,6 +37,7 @@ from registrar.models import (
FederalAgency, FederalAgency,
Portfolio, Portfolio,
Suborganization, Suborganization,
UserPortfolioPermission,
) )
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from django.utils import timezone from django.utils import timezone
@ -317,6 +318,7 @@ class TestDomainDetail(TestDomainOverview):
self.assertContains(detail_page, "Domain missing domain information") self.assertContains(detail_page, "Domain missing domain information")
@less_console_noise_decorator @less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_domain_readonly_on_detail_page(self): def test_domain_readonly_on_detail_page(self):
"""Test that a domain, which is part of a portfolio, but for which the user is not a domain manager, """Test that a domain, which is part of a portfolio, but for which the user is not a domain manager,
properly displays read only""" properly displays read only"""
@ -329,11 +331,14 @@ class TestDomainDetail(TestDomainOverview):
email="bogus@example.gov", email="bogus@example.gov",
phone="8003111234", phone="8003111234",
title="test title", title="test title",
portfolio=portfolio,
portfolio_roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
) )
domain, _ = Domain.objects.get_or_create(name="bogusdomain.gov") domain, _ = Domain.objects.get_or_create(name="bogusdomain.gov")
DomainInformation.objects.get_or_create(creator=user, domain=domain, portfolio=portfolio) DomainInformation.objects.get_or_create(creator=user, domain=domain, portfolio=portfolio)
UserPortfolioPermission.objects.get_or_create(
user=user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
user.refresh_from_db()
self.client.force_login(user) self.client.force_login(user)
detail_page = self.client.get(f"/domain/{domain.id}") detail_page = self.client.get(f"/domain/{domain.id}")
# Check that alert message displays properly # Check that alert message displays properly
@ -1497,10 +1502,9 @@ class TestDomainSuborganization(TestDomainOverview):
self.domain_information.refresh_from_db() self.domain_information.refresh_from_db()
# Add portfolio perms to the user object # Add portfolio perms to the user object
self.user.portfolio = portfolio portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN] user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
self.user.save() )
self.user.refresh_from_db()
self.assertEqual(self.domain_information.sub_organization, suborg) self.assertEqual(self.domain_information.sub_organization, suborg)
@ -1556,10 +1560,9 @@ class TestDomainSuborganization(TestDomainOverview):
self.domain_information.refresh_from_db() self.domain_information.refresh_from_db()
# Add portfolio perms to the user object # Add portfolio perms to the user object
self.user.portfolio = portfolio portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY] user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY]
self.user.save() )
self.user.refresh_from_db()
self.assertEqual(self.domain_information.sub_organization, suborg) self.assertEqual(self.domain_information.sub_organization, suborg)
@ -1597,9 +1600,9 @@ class TestDomainSuborganization(TestDomainOverview):
self.domain_information.refresh_from_db() self.domain_information.refresh_from_db()
# Add portfolio perms to the user object # Add portfolio perms to the user object
self.user.portfolio = portfolio UserPortfolioPermission.objects.get_or_create(
self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_PORTFOLIO] user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
self.user.save() )
self.user.refresh_from_db() self.user.refresh_from_db()
# Navigate to the domain overview page # Navigate to the domain overview page

View file

@ -10,9 +10,11 @@ from registrar.models import (
UserDomainRole, UserDomainRole,
User, User,
) )
from registrar.models.user_portfolio_permission import UserPortfolioPermission
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from .common import create_test_user from .common import create_test_user
from waffle.testutils import override_flag from waffle.testutils import override_flag
from django.contrib.sessions.middleware import SessionMiddleware
import logging import logging
@ -30,6 +32,7 @@ class TestPortfolio(WebTest):
) )
def tearDown(self): def tearDown(self):
UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete() Portfolio.objects.all().delete()
UserDomainRole.objects.all().delete() UserDomainRole.objects.all().delete()
DomainRequest.objects.all().delete() DomainRequest.objects.all().delete()
@ -52,10 +55,11 @@ class TestPortfolio(WebTest):
self.portfolio.save() self.portfolio.save()
self.portfolio.refresh_from_db() self.portfolio.refresh_from_db()
self.user.portfolio = self.portfolio portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_PORTFOLIO] user=self.user,
self.user.save() portfolio=self.portfolio,
self.user.refresh_from_db() additional_permissions=[UserPortfolioPermissionChoices.VIEW_PORTFOLIO],
)
so_portfolio_page = self.app.get(reverse("senior-official")) so_portfolio_page = self.app.get(reverse("senior-official"))
# Assert that we're on the right page # Assert that we're on the right page
@ -72,6 +76,9 @@ class TestPortfolio(WebTest):
def test_middleware_does_not_redirect_if_no_permission(self): def test_middleware_does_not_redirect_if_no_permission(self):
"""Test that user with no portfolio permission is not redirected when attempting to access home""" """Test that user with no portfolio permission is not redirected when attempting to access home"""
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, additional_permissions=[]
)
self.user.portfolio = self.portfolio self.user.portfolio = self.portfolio
self.user.save() self.user.save()
self.user.refresh_from_db() self.user.refresh_from_db()
@ -86,9 +93,6 @@ class TestPortfolio(WebTest):
def test_middleware_does_not_redirect_if_no_portfolio(self): def test_middleware_does_not_redirect_if_no_portfolio(self):
"""Test that user with no assigned portfolio is not redirected when attempting to access home""" """Test that user with no assigned portfolio is not redirected when attempting to access home"""
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_PORTFOLIO]
self.user.save()
self.user.refresh_from_db()
with override_flag("organization_feature", active=True): with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio 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.
@ -100,10 +104,11 @@ class TestPortfolio(WebTest):
def test_middleware_redirects_to_portfolio_no_domains_page(self): def test_middleware_redirects_to_portfolio_no_domains_page(self):
"""Test that user with a portfolio and VIEW_PORTFOLIO is redirected to the no domains page""" """Test that user with a portfolio and VIEW_PORTFOLIO is redirected to the no domains page"""
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio UserPortfolioPermission.objects.get_or_create(
self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_PORTFOLIO] user=self.user,
self.user.save() portfolio=self.portfolio,
self.user.refresh_from_db() additional_permissions=[UserPortfolioPermissionChoices.VIEW_PORTFOLIO],
)
with override_flag("organization_feature", active=True): with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio 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.
@ -118,13 +123,14 @@ class TestPortfolio(WebTest):
"""Test that user with a portfolio, VIEW_PORTFOLIO, VIEW_ALL_DOMAINS """Test that user with a portfolio, VIEW_PORTFOLIO, VIEW_ALL_DOMAINS
is redirected to portfolio domains page""" is redirected to portfolio domains page"""
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio UserPortfolioPermission.objects.get_or_create(
self.user.portfolio_additional_permissions = [ user=self.user,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO, portfolio=self.portfolio,
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, additional_permissions=[
] UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
self.user.save() UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
self.user.refresh_from_db() ],
)
with override_flag("organization_feature", active=True): with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio 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.
@ -138,9 +144,9 @@ class TestPortfolio(WebTest):
def test_portfolio_domains_page_403_when_user_not_have_permission(self): def test_portfolio_domains_page_403_when_user_not_have_permission(self):
"""Test that user without proper permission is denied access to portfolio domain view""" """Test that user without proper permission is denied access to portfolio domain view"""
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio UserPortfolioPermission.objects.get_or_create(
self.user.save() user=self.user, portfolio=self.portfolio, additional_permissions=[]
self.user.refresh_from_db() )
with override_flag("organization_feature", active=True): with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio 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.
@ -152,9 +158,9 @@ class TestPortfolio(WebTest):
def test_portfolio_domain_requests_page_403_when_user_not_have_permission(self): def test_portfolio_domain_requests_page_403_when_user_not_have_permission(self):
"""Test that user without proper permission is denied access to portfolio domain view""" """Test that user without proper permission is denied access to portfolio domain view"""
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio UserPortfolioPermission.objects.get_or_create(
self.user.save() user=self.user, portfolio=self.portfolio, additional_permissions=[]
self.user.refresh_from_db() )
with override_flag("organization_feature", active=True): with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio 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.
@ -166,9 +172,9 @@ class TestPortfolio(WebTest):
def test_portfolio_organization_page_403_when_user_not_have_permission(self): def test_portfolio_organization_page_403_when_user_not_have_permission(self):
"""Test that user without proper permission is not allowed access to portfolio organization page""" """Test that user without proper permission is not allowed access to portfolio organization page"""
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
self.user.save() user=self.user, portfolio=self.portfolio, additional_permissions=[]
self.user.refresh_from_db() )
with override_flag("organization_feature", active=True): with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio 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.
@ -180,12 +186,13 @@ class TestPortfolio(WebTest):
def test_portfolio_organization_page_read_only(self): def test_portfolio_organization_page_read_only(self):
"""Test that user with a portfolio can access the portfolio organization page, read only""" """Test that user with a portfolio can access the portfolio organization page, read only"""
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[UserPortfolioPermissionChoices.VIEW_PORTFOLIO],
)
self.portfolio.city = "Los Angeles" self.portfolio.city = "Los Angeles"
self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_PORTFOLIO]
self.portfolio.save() self.portfolio.save()
self.user.save()
self.user.refresh_from_db()
with override_flag("organization_feature", active=True): with override_flag("organization_feature", active=True):
response = self.app.get(reverse("organization")) response = self.app.get(reverse("organization"))
# Assert the response is a 200 # Assert the response is a 200
@ -201,15 +208,16 @@ class TestPortfolio(WebTest):
def test_portfolio_organization_page_edit_access(self): def test_portfolio_organization_page_edit_access(self):
"""Test that user with a portfolio can access the portfolio organization page, read only""" """Test that user with a portfolio can access the portfolio organization page, read only"""
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
self.user.portfolio_additional_permissions = [ user=self.user,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO, portfolio=self.portfolio,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO, additional_permissions=[
] UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
],
)
self.portfolio.city = "Los Angeles" self.portfolio.city = "Los Angeles"
self.portfolio.save() self.portfolio.save()
self.user.save()
self.user.refresh_from_db()
with override_flag("organization_feature", active=True): with override_flag("organization_feature", active=True):
response = self.app.get(reverse("organization")) response = self.app.get(reverse("organization"))
# Assert the response is a 200 # Assert the response is a 200
@ -225,14 +233,14 @@ class TestPortfolio(WebTest):
def test_accessible_pages_when_user_does_not_have_permission(self): def test_accessible_pages_when_user_does_not_have_permission(self):
"""Tests which pages are accessible when user does not have portfolio permissions""" """Tests which pages are accessible when user does not have portfolio permissions"""
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio portfolio_additional_permissions = [
self.user.portfolio_additional_permissions = [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO, UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
] ]
self.user.save() portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
self.user.refresh_from_db() user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions
)
with override_flag("organization_feature", active=True): with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio 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.
@ -246,9 +254,9 @@ class TestPortfolio(WebTest):
# removing non-basic portfolio perms, which should remove domains # removing non-basic portfolio perms, which should remove domains
# and domain requests from nav # and domain requests from nav
self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_PORTFOLIO] portfolio_permission.additional_permissions = [UserPortfolioPermissionChoices.VIEW_PORTFOLIO]
self.user.save() portfolio_permission.save()
self.user.refresh_from_db() portfolio_permission.refresh_from_db()
# Members should be redirected to the readonly domains page # Members should be redirected to the readonly domains page
portfolio_page = self.app.get(reverse("home")).follow() portfolio_page = self.app.get(reverse("home")).follow()
@ -275,10 +283,10 @@ class TestPortfolio(WebTest):
def test_accessible_pages_when_user_does_not_have_role(self): def test_accessible_pages_when_user_does_not_have_role(self):
"""Test that admin / memmber roles are associated with the right access""" """Test that admin / memmber roles are associated with the right access"""
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN] portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
self.user.save() user=self.user, portfolio=self.portfolio, roles=portfolio_roles
self.user.refresh_from_db() )
with override_flag("organization_feature", active=True): with override_flag("organization_feature", active=True):
# This will redirect the user to the portfolio 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.
@ -292,9 +300,9 @@ class TestPortfolio(WebTest):
# removing non-basic portfolio role, which should remove domains # removing non-basic portfolio role, which should remove domains
# and domain requests from nav # and domain requests from nav
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_MEMBER] portfolio_permission.roles = [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
self.user.save() portfolio_permission.save()
self.user.refresh_from_db() portfolio_permission.refresh_from_db()
# Members should be redirected to the readonly domains page # Members should be redirected to the readonly domains page
portfolio_page = self.app.get(reverse("home")).follow() portfolio_page = self.app.get(reverse("home")).follow()
@ -322,14 +330,13 @@ class TestPortfolio(WebTest):
"""Can load portfolio's org name page.""" """Can load portfolio's org name page."""
with override_flag("organization_feature", active=True): with override_flag("organization_feature", active=True):
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio portfolio_additional_permissions = [
self.user.portfolio_additional_permissions = [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO, UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO, UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
] ]
self.user.save() portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
self.user.refresh_from_db() user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions
)
page = self.app.get(reverse("organization")) page = self.app.get(reverse("organization"))
self.assertContains( self.assertContains(
page, "The name of your federal agency will be publicly listed as the domain registrant." page, "The name of your federal agency will be publicly listed as the domain registrant."
@ -340,13 +347,13 @@ class TestPortfolio(WebTest):
"""Org name and address information appears on the page.""" """Org name and address information appears on the page."""
with override_flag("organization_feature", active=True): with override_flag("organization_feature", active=True):
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio portfolio_additional_permissions = [
self.user.portfolio_additional_permissions = [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO, UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO, UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
] ]
self.user.save() portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
self.user.refresh_from_db() user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions
)
self.portfolio.organization_name = "Hotel California" self.portfolio.organization_name = "Hotel California"
self.portfolio.save() self.portfolio.save()
@ -360,13 +367,13 @@ class TestPortfolio(WebTest):
"""Submitting changes works on the org name address page.""" """Submitting changes works on the org name address page."""
with override_flag("organization_feature", active=True): with override_flag("organization_feature", active=True):
self.app.set_user(self.user.username) self.app.set_user(self.user.username)
self.user.portfolio = self.portfolio portfolio_additional_permissions = [
self.user.portfolio_additional_permissions = [
UserPortfolioPermissionChoices.VIEW_PORTFOLIO, UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.EDIT_PORTFOLIO, UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
] ]
self.user.save() portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
self.user.refresh_from_db() user=self.user, portfolio=self.portfolio, additional_permissions=portfolio_additional_permissions
)
self.portfolio.address_line1 = "1600 Penn Ave" self.portfolio.address_line1 = "1600 Penn Ave"
self.portfolio.save() self.portfolio.save()
@ -383,6 +390,103 @@ class TestPortfolio(WebTest):
self.assertContains(success_result_page, "6 Downing st") self.assertContains(success_result_page, "6 Downing st")
self.assertContains(success_result_page, "London") self.assertContains(success_result_page, "London")
@less_console_noise_decorator
def test_portfolio_in_session_when_organization_feature_active(self):
"""When organization_feature flag is true and user has a portfolio,
the portfolio should be set in session."""
self.client.force_login(self.user)
portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=portfolio_roles)
with override_flag("organization_feature", active=True):
response = self.client.get(reverse("home"))
# Ensure that middleware processes the session
session_middleware = SessionMiddleware(lambda request: None)
session_middleware.process_request(response.wsgi_request)
response.wsgi_request.session.save()
# Access the session via the request
session = response.wsgi_request.session
# Check if the 'portfolio' session variable exists
self.assertIn("portfolio", session, "Portfolio session variable should exist.")
# Check the value of the 'portfolio' session variable
self.assertEqual(session["portfolio"], self.portfolio, "Portfolio session variable has the wrong value.")
@less_console_noise_decorator
def test_portfolio_in_session_is_none_when_organization_feature_inactive(self):
"""When organization_feature flag is false and user has a portfolio,
the portfolio should be set to None in session.
This test also satisfies the condition when multiple_portfolios flag
is false and user has a portfolio, so won't add a redundant test for that."""
self.client.force_login(self.user)
portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=portfolio_roles)
response = self.client.get(reverse("home"))
# Ensure that middleware processes the session
session_middleware = SessionMiddleware(lambda request: None)
session_middleware.process_request(response.wsgi_request)
response.wsgi_request.session.save()
# Access the session via the request
session = response.wsgi_request.session
# Check if the 'portfolio' session variable exists
self.assertIn("portfolio", session, "Portfolio session variable should exist.")
# Check the value of the 'portfolio' session variable
self.assertIsNone(session["portfolio"])
@less_console_noise_decorator
def test_portfolio_in_session_is_none_when_organization_feature_active_and_no_portfolio(self):
"""When organization_feature flag is true and user does not have a portfolio,
the portfolio should be set to None in session."""
self.client.force_login(self.user)
with override_flag("organization_feature", active=True):
response = self.client.get(reverse("home"))
# Ensure that middleware processes the session
session_middleware = SessionMiddleware(lambda request: None)
session_middleware.process_request(response.wsgi_request)
response.wsgi_request.session.save()
# Access the session via the request
session = response.wsgi_request.session
# Check if the 'portfolio' session variable exists
self.assertIn("portfolio", session, "Portfolio session variable should exist.")
# Check the value of the 'portfolio' session variable
self.assertIsNone(session["portfolio"])
@less_console_noise_decorator
def test_portfolio_in_session_when_multiple_portfolios_active(self):
"""When multiple_portfolios flag is true and user has a portfolio,
the portfolio should be set in session."""
self.client.force_login(self.user)
portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=portfolio_roles)
with override_flag("organization_feature", active=True), override_flag("multiple_portfolios", active=True):
response = self.client.get(reverse("home"))
# Ensure that middleware processes the session
session_middleware = SessionMiddleware(lambda request: None)
session_middleware.process_request(response.wsgi_request)
response.wsgi_request.session.save()
# Access the session via the request
session = response.wsgi_request.session
# Check if the 'portfolio' session variable exists
self.assertIn("portfolio", session, "Portfolio session variable should exist.")
# Check the value of the 'portfolio' session variable
self.assertEqual(session["portfolio"], self.portfolio, "Portfolio session variable has the wrong value.")
@less_console_noise_decorator
def test_portfolio_in_session_is_none_when_multiple_portfolios_active_and_no_portfolio(self):
"""When multiple_portfolios flag is true and user does not have a portfolio,
the portfolio should be set to None in session."""
self.client.force_login(self.user)
with override_flag("multiple_portfolios", active=True):
response = self.client.get(reverse("home"))
# Ensure that middleware processes the session
session_middleware = SessionMiddleware(lambda request: None)
session_middleware.process_request(response.wsgi_request)
response.wsgi_request.session.save()
# Access the session via the request
session = response.wsgi_request.session
# Check if the 'portfolio' session variable exists
self.assertIn("portfolio", session, "Portfolio session variable should exist.")
# Check the value of the 'portfolio' session variable
self.assertIsNone(session["portfolio"])
@less_console_noise_decorator @less_console_noise_decorator
@override_flag("organization_feature", active=True) @override_flag("organization_feature", active=True)
def test_org_member_can_only_see_domains_with_appropriate_permissions(self): def test_org_member_can_only_see_domains_with_appropriate_permissions(self):
@ -390,43 +494,41 @@ class TestPortfolio(WebTest):
if they do not have the right permissions. if they do not have the right permissions.
""" """
permission, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
)
# A default organization member should not be able to see any domains # A default organization member should not be able to see any domains
self.app.set_user(self.user.username) self.client.force_login(self.user)
self.user.portfolio = self.portfolio response = self.client.get(reverse("home"), follow=True)
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
self.user.save()
self.user.refresh_from_db()
self.assertFalse(self.user.has_domains_portfolio_permission()) self.assertFalse(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
response = self.app.get(reverse("no-portfolio-domains"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "You arent managing any domains.") self.assertContains(response, "You aren")
# Test the domains page - this user should not have access # Test the domains page - this user should not have access
response = self.app.get(reverse("domains"), expect_errors=True) response = self.client.get(reverse("domains"))
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
# Ensure that this user can see domains with the right permissions # Ensure that this user can see domains with the right permissions
self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS] permission.additional_permissions = [UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS]
self.user.save() permission.save()
self.user.refresh_from_db() permission.refresh_from_db()
self.assertTrue(self.user.has_domains_portfolio_permission())
# Test the domains page - this user should have access # Test the domains page - this user should have access
response = self.app.get(reverse("domains")) response = self.client.get(reverse("domains"))
self.assertTrue(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Domain name") self.assertContains(response, "Domain name")
# Test the managed domains permission # Test the managed domains permission
self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS] permission.additional_permissions = [UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS]
self.user.save() permission.save()
self.user.refresh_from_db() permission.refresh_from_db()
self.assertTrue(self.user.has_domains_portfolio_permission())
# Test the domains page - this user should have access # Test the domains page - this user should have access
response = self.app.get(reverse("domains")) response = self.client.get(reverse("domains"))
self.assertTrue(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Domain name") self.assertContains(response, "Domain name")
permission.delete()

View file

@ -25,91 +25,91 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
DomainRequest.objects.create( DomainRequest.objects.create(
creator=cls.user, creator=cls.user,
requested_domain=lamb_chops, requested_domain=lamb_chops,
submission_date="2024-01-01", last_submitted_date="2024-01-01",
status=DomainRequest.DomainRequestStatus.STARTED, status=DomainRequest.DomainRequestStatus.STARTED,
created_at="2024-01-01", created_at="2024-01-01",
), ),
DomainRequest.objects.create( DomainRequest.objects.create(
creator=cls.user, creator=cls.user,
requested_domain=short_ribs, requested_domain=short_ribs,
submission_date="2024-02-01", last_submitted_date="2024-02-01",
status=DomainRequest.DomainRequestStatus.WITHDRAWN, status=DomainRequest.DomainRequestStatus.WITHDRAWN,
created_at="2024-02-01", created_at="2024-02-01",
), ),
DomainRequest.objects.create( DomainRequest.objects.create(
creator=cls.user, creator=cls.user,
requested_domain=beef_chuck, requested_domain=beef_chuck,
submission_date="2024-03-01", last_submitted_date="2024-03-01",
status=DomainRequest.DomainRequestStatus.REJECTED, status=DomainRequest.DomainRequestStatus.REJECTED,
created_at="2024-03-01", created_at="2024-03-01",
), ),
DomainRequest.objects.create( DomainRequest.objects.create(
creator=cls.user, creator=cls.user,
requested_domain=stew_beef, requested_domain=stew_beef,
submission_date="2024-04-01", last_submitted_date="2024-04-01",
status=DomainRequest.DomainRequestStatus.STARTED, status=DomainRequest.DomainRequestStatus.STARTED,
created_at="2024-04-01", created_at="2024-04-01",
), ),
DomainRequest.objects.create( DomainRequest.objects.create(
creator=cls.user, creator=cls.user,
requested_domain=None, requested_domain=None,
submission_date="2024-05-01", last_submitted_date="2024-05-01",
status=DomainRequest.DomainRequestStatus.STARTED, status=DomainRequest.DomainRequestStatus.STARTED,
created_at="2024-05-01", created_at="2024-05-01",
), ),
DomainRequest.objects.create( DomainRequest.objects.create(
creator=cls.user, creator=cls.user,
requested_domain=None, requested_domain=None,
submission_date="2024-06-01", last_submitted_date="2024-06-01",
status=DomainRequest.DomainRequestStatus.WITHDRAWN, status=DomainRequest.DomainRequestStatus.WITHDRAWN,
created_at="2024-06-01", created_at="2024-06-01",
), ),
DomainRequest.objects.create( DomainRequest.objects.create(
creator=cls.user, creator=cls.user,
requested_domain=None, requested_domain=None,
submission_date="2024-07-01", last_submitted_date="2024-07-01",
status=DomainRequest.DomainRequestStatus.REJECTED, status=DomainRequest.DomainRequestStatus.REJECTED,
created_at="2024-07-01", created_at="2024-07-01",
), ),
DomainRequest.objects.create( DomainRequest.objects.create(
creator=cls.user, creator=cls.user,
requested_domain=None, requested_domain=None,
submission_date="2024-08-01", last_submitted_date="2024-08-01",
status=DomainRequest.DomainRequestStatus.STARTED, status=DomainRequest.DomainRequestStatus.STARTED,
created_at="2024-08-01", created_at="2024-08-01",
), ),
DomainRequest.objects.create( DomainRequest.objects.create(
creator=cls.user, creator=cls.user,
requested_domain=None, requested_domain=None,
submission_date="2024-09-01", last_submitted_date="2024-09-01",
status=DomainRequest.DomainRequestStatus.STARTED, status=DomainRequest.DomainRequestStatus.STARTED,
created_at="2024-09-01", created_at="2024-09-01",
), ),
DomainRequest.objects.create( DomainRequest.objects.create(
creator=cls.user, creator=cls.user,
requested_domain=None, requested_domain=None,
submission_date="2024-10-01", last_submitted_date="2024-10-01",
status=DomainRequest.DomainRequestStatus.WITHDRAWN, status=DomainRequest.DomainRequestStatus.WITHDRAWN,
created_at="2024-10-01", created_at="2024-10-01",
), ),
DomainRequest.objects.create( DomainRequest.objects.create(
creator=cls.user, creator=cls.user,
requested_domain=None, requested_domain=None,
submission_date="2024-11-01", last_submitted_date="2024-11-01",
status=DomainRequest.DomainRequestStatus.REJECTED, status=DomainRequest.DomainRequestStatus.REJECTED,
created_at="2024-11-01", created_at="2024-11-01",
), ),
DomainRequest.objects.create( DomainRequest.objects.create(
creator=cls.user, creator=cls.user,
requested_domain=None, requested_domain=None,
submission_date="2024-11-02", last_submitted_date="2024-11-02",
status=DomainRequest.DomainRequestStatus.WITHDRAWN, status=DomainRequest.DomainRequestStatus.WITHDRAWN,
created_at="2024-11-02", created_at="2024-11-02",
), ),
DomainRequest.objects.create( DomainRequest.objects.create(
creator=cls.user, creator=cls.user,
requested_domain=None, requested_domain=None,
submission_date="2024-12-01", last_submitted_date="2024-12-01",
status=DomainRequest.DomainRequestStatus.APPROVED, status=DomainRequest.DomainRequestStatus.APPROVED,
created_at="2024-12-01", created_at="2024-12-01",
), ),
@ -138,7 +138,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
# Extract fields from response # Extract fields from response
requested_domains = [request["requested_domain"] for request in data["domain_requests"]] requested_domains = [request["requested_domain"] for request in data["domain_requests"]]
submission_dates = [request["submission_date"] for request in data["domain_requests"]] last_submitted_dates = [request["last_submitted_date"] for request in data["domain_requests"]]
statuses = [request["status"] for request in data["domain_requests"]] statuses = [request["status"] for request in data["domain_requests"]]
created_ats = [request["created_at"] for request in data["domain_requests"]] created_ats = [request["created_at"] for request in data["domain_requests"]]
ids = [request["id"] for request in data["domain_requests"]] ids = [request["id"] for request in data["domain_requests"]]
@ -154,7 +154,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
self.domain_requests[i].requested_domain.name if self.domain_requests[i].requested_domain else None, self.domain_requests[i].requested_domain.name if self.domain_requests[i].requested_domain else None,
requested_domains[i], requested_domains[i],
) )
self.assertEqual(self.domain_requests[i].submission_date, submission_dates[i]) self.assertEqual(self.domain_requests[i].last_submitted_date, last_submitted_dates[i])
self.assertEqual(self.domain_requests[i].get_status_display(), statuses[i]) self.assertEqual(self.domain_requests[i].get_status_display(), statuses[i])
self.assertEqual( self.assertEqual(
parse_datetime(self.domain_requests[i].created_at.isoformat()), parse_datetime(created_ats[i]) parse_datetime(self.domain_requests[i].created_at.isoformat()), parse_datetime(created_ats[i])
@ -287,26 +287,30 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
def test_sorting(self): def test_sorting(self):
"""test that sorting works properly on the result set""" """test that sorting works properly on the result set"""
response = self.app.get(reverse("get_domain_requests_json"), {"sort_by": "submission_date", "order": "desc"}) response = self.app.get(
reverse("get_domain_requests_json"), {"sort_by": "last_submitted_date", "order": "desc"}
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = response.json data = response.json
# Check if sorted by submission_date in descending order # Check if sorted by last_submitted_date in descending order
submission_dates = [req["submission_date"] for req in data["domain_requests"]] last_submitted_dates = [req["last_submitted_date"] for req in data["domain_requests"]]
self.assertEqual(submission_dates, sorted(submission_dates, reverse=True)) self.assertEqual(last_submitted_dates, sorted(last_submitted_dates, reverse=True))
response = self.app.get(reverse("get_domain_requests_json"), {"sort_by": "submission_date", "order": "asc"}) response = self.app.get(reverse("get_domain_requests_json"), {"sort_by": "last_submitted_date", "order": "asc"})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = response.json data = response.json
# Check if sorted by submission_date in ascending order # Check if sorted by last_submitted_date in ascending order
submission_dates = [req["submission_date"] for req in data["domain_requests"]] last_submitted_dates = [req["last_submitted_date"] for req in data["domain_requests"]]
self.assertEqual(submission_dates, sorted(submission_dates)) self.assertEqual(last_submitted_dates, sorted(last_submitted_dates))
def test_filter_approved_excluded(self): def test_filter_approved_excluded(self):
"""test that approved requests are excluded from result set.""" """test that approved requests are excluded from result set."""
# sort in reverse chronological order of submission date, since most recent request is approved # sort in reverse chronological order of submission date, since most recent request is approved
response = self.app.get(reverse("get_domain_requests_json"), {"sort_by": "submission_date", "order": "desc"}) response = self.app.get(
reverse("get_domain_requests_json"), {"sort_by": "last_submitted_date", "order": "desc"}
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = response.json data = response.json

View file

@ -1235,7 +1235,9 @@ class DomainRequestExport(BaseExport):
"State/territory": model.get("state_territory"), "State/territory": model.get("state_territory"),
"Request purpose": model.get("purpose"), "Request purpose": model.get("purpose"),
"CISA regional representative": model.get("cisa_representative_email"), "CISA regional representative": model.get("cisa_representative_email"),
"Submitted at": model.get("submission_date"), "Last submitted date": model.get("last_submitted_date"),
"First submitted date": model.get("first_submitted_date"),
"Last status update": model.get("last_status_update"),
} }
row = [FIELDS.get(column, "") for column in columns] row = [FIELDS.get(column, "") for column in columns]
@ -1279,8 +1281,8 @@ class DomainRequestGrowth(DomainRequestExport):
end_date_formatted = format_end_date(end_date) end_date_formatted = format_end_date(end_date)
return Q( return Q(
status=DomainRequest.DomainRequestStatus.SUBMITTED, status=DomainRequest.DomainRequestStatus.SUBMITTED,
submission_date__lte=end_date_formatted, last_submitted_date__lte=end_date_formatted,
submission_date__gte=start_date_formatted, last_submitted_date__gte=start_date_formatted,
) )
@classmethod @classmethod
@ -1304,7 +1306,9 @@ class DomainRequestDataFull(DomainRequestExport):
""" """
return [ return [
"Domain request", "Domain request",
"Submitted at", "Last submitted date",
"First submitted date",
"Last status update",
"Status", "Status",
"Domain type", "Domain type",
"Federal type", "Federal type",

View file

@ -174,10 +174,11 @@ class DomainView(DomainBaseView):
"""Most views should not allow permission to portfolio users. """Most views should not allow permission to portfolio users.
If particular views allow permissions, they will need to override If particular views allow permissions, they will need to override
this function.""" this function."""
if self.request.user.has_domains_portfolio_permission(): portfolio = self.request.session.get("portfolio")
if self.request.user.has_domains_portfolio_permission(portfolio):
if Domain.objects.filter(id=pk).exists(): if Domain.objects.filter(id=pk).exists():
domain = Domain.objects.get(id=pk) domain = Domain.objects.get(id=pk)
if domain.domain_info.portfolio == self.request.user.portfolio: if domain.domain_info.portfolio == portfolio:
return True return True
return False return False
@ -236,7 +237,8 @@ class DomainOrgNameAddressView(DomainFormBaseView):
# Org users shouldn't have access to this page # Org users shouldn't have access to this page
is_org_user = self.request.user.is_org_user(self.request) is_org_user = self.request.user.is_org_user(self.request)
if self.request.user.portfolio and is_org_user: portfolio = self.request.session.get("portfolio")
if portfolio and is_org_user:
return False return False
else: else:
return super().has_permission() return super().has_permission()
@ -255,7 +257,8 @@ class DomainSubOrganizationView(DomainFormBaseView):
# non-org users shouldn't have access to this page # non-org users shouldn't have access to this page
is_org_user = self.request.user.is_org_user(self.request) is_org_user = self.request.user.is_org_user(self.request)
if self.request.user.portfolio and is_org_user: portfolio = self.request.session.get("portfolio")
if portfolio and is_org_user:
return super().has_permission() return super().has_permission()
else: else:
return False return False
@ -335,7 +338,8 @@ class DomainSeniorOfficialView(DomainFormBaseView):
# Org users shouldn't have access to this page # Org users shouldn't have access to this page
is_org_user = self.request.user.is_org_user(self.request) is_org_user = self.request.user.is_org_user(self.request)
if self.request.user.portfolio and is_org_user: portfolio = self.request.session.get("portfolio")
if portfolio and is_org_user:
return False return False
else: else:
return super().has_permission() return super().has_permission()

View file

@ -46,7 +46,7 @@ def get_domain_requests_json(request):
domain_requests_data = [ domain_requests_data = [
{ {
"requested_domain": domain_request.requested_domain.name if domain_request.requested_domain else None, "requested_domain": domain_request.requested_domain.name if domain_request.requested_domain else None,
"submission_date": domain_request.submission_date, "last_submitted_date": domain_request.last_submitted_date,
"status": domain_request.get_status_display(), "status": domain_request.get_status_display(),
"created_at": format(domain_request.created_at, "c"), # Serialize to ISO 8601 "created_at": format(domain_request.created_at, "c"), # Serialize to ISO 8601
"id": domain_request.id, "id": domain_request.id,

View file

@ -5,6 +5,7 @@ from django.urls import reverse
from django.contrib import messages from django.contrib import messages
from registrar.forms.portfolio import PortfolioOrgAddressForm, PortfolioSeniorOfficialForm from registrar.forms.portfolio import PortfolioOrgAddressForm, PortfolioSeniorOfficialForm
from registrar.models import Portfolio, User from registrar.models import Portfolio, User
from registrar.models.user_portfolio_permission import UserPortfolioPermission
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from registrar.views.utility.permission_views import ( from registrar.views.utility.permission_views import (
PortfolioDomainRequestsPermissionView, PortfolioDomainRequestsPermissionView,
@ -55,14 +56,17 @@ class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
"""Add additional context data to the template.""" """Add additional context data to the template."""
# We can override the base class. This view only needs this item. # We can override the base class. This view only needs this item.
context = {} context = {}
portfolio = self.request.user.portfolio if self.request and self.request.user else None portfolio = self.request.session.get("portfolio")
if portfolio: if portfolio:
context["portfolio_administrators"] = User.objects.filter( admin_ids = UserPortfolioPermission.objects.filter(
portfolio=portfolio, portfolio=portfolio,
portfolio_roles__overlap=[ roles__overlap=[
UserPortfolioRoleChoices.ORGANIZATION_ADMIN, UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
], ],
) ).values_list("user__id", flat=True)
admin_users = User.objects.filter(id__in=admin_ids)
context["portfolio_administrators"] = admin_users
return context return context
@ -79,12 +83,13 @@ class PortfolioOrganizationView(PortfolioBasePermissionView, FormMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add additional context data to the template.""" """Add additional context data to the template."""
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["has_edit_org_portfolio_permission"] = self.request.user.has_edit_org_portfolio_permission() portfolio = self.request.session.get("portfolio")
context["has_edit_org_portfolio_permission"] = self.request.user.has_edit_org_portfolio_permission(portfolio)
return context return context
def get_object(self, queryset=None): def get_object(self, queryset=None):
"""Get the portfolio object based on the request user.""" """Get the portfolio object based on the session."""
portfolio = self.request.user.portfolio portfolio = self.request.session.get("portfolio")
if portfolio is None: if portfolio is None:
raise Http404("No organization found for this user") raise Http404("No organization found for this user")
return portfolio return portfolio
@ -139,8 +144,8 @@ class PortfolioSeniorOfficialView(PortfolioBasePermissionView, FormMixin):
context_object_name = "portfolio" context_object_name = "portfolio"
def get_object(self, queryset=None): def get_object(self, queryset=None):
"""Get the portfolio object based on the request user.""" """Get the portfolio object based on the session."""
portfolio = self.request.user.portfolio portfolio = self.request.session.get("portfolio")
if portfolio is None: if portfolio is None:
raise Http404("No organization found for this user") raise Http404("No organization found for this user")
return portfolio return portfolio

View file

@ -26,7 +26,7 @@ class AnalyticsView(View):
created_at__gt=thirty_days_ago, status=models.DomainRequest.DomainRequestStatus.APPROVED created_at__gt=thirty_days_ago, status=models.DomainRequest.DomainRequestStatus.APPROVED
) )
avg_approval_time = last_30_days_approved_applications.annotate( avg_approval_time = last_30_days_approved_applications.annotate(
approval_time=F("approved_domain__created_at") - F("submission_date") approval_time=F("approved_domain__created_at") - F("last_submitted_date")
).aggregate(Avg("approval_time"))["approval_time__avg"] ).aggregate(Avg("approval_time"))["approval_time__avg"]
# Format the timedelta to display only days # Format the timedelta to display only days
if avg_approval_time is not None: if avg_approval_time is not None:
@ -104,11 +104,11 @@ class AnalyticsView(View):
filter_submitted_requests_start_date = { filter_submitted_requests_start_date = {
"status": models.DomainRequest.DomainRequestStatus.SUBMITTED, "status": models.DomainRequest.DomainRequestStatus.SUBMITTED,
"submission_date__lte": start_date_formatted, "last_submitted_date__lte": start_date_formatted,
} }
filter_submitted_requests_end_date = { filter_submitted_requests_end_date = {
"status": models.DomainRequest.DomainRequestStatus.SUBMITTED, "status": models.DomainRequest.DomainRequestStatus.SUBMITTED,
"submission_date__lte": end_date_formatted, "last_submitted_date__lte": end_date_formatted,
} }
submitted_requests_sliced_at_start_date = csv_export.DomainRequestExport.get_sliced_requests( submitted_requests_sliced_at_start_date = csv_export.DomainRequestExport.get_sliced_requests(
filter_submitted_requests_start_date filter_submitted_requests_start_date

View file

@ -5,6 +5,9 @@ from registrar.models import FederalAgency, SeniorOfficial
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from registrar.models.portfolio import Portfolio
from registrar.utility.constants import BranchChoices
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -34,3 +37,34 @@ def get_senior_official_from_federal_agency_json(request):
return JsonResponse(so_dict) return JsonResponse(so_dict)
else: else:
return JsonResponse({"error": "Senior Official not found"}, status=404) return JsonResponse({"error": "Senior Official not found"}, status=404)
@login_required
@staff_member_required
def get_federal_and_portfolio_types_from_federal_agency_json(request):
"""Returns specific portfolio information as a JSON. Request must have
both agency_name and organization_type."""
# This API is only accessible to admins and analysts
superuser_perm = request.user.has_perm("registrar.full_access_permission")
analyst_perm = request.user.has_perm("registrar.analyst_access_permission")
if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]):
return JsonResponse({"error": "You do not have access to this resource"}, status=403)
federal_type = None
portfolio_type = None
agency_name = request.GET.get("agency_name")
organization_type = request.GET.get("organization_type")
agency = FederalAgency.objects.filter(agency=agency_name).first()
if agency:
federal_type = Portfolio.get_federal_type(agency)
portfolio_type = Portfolio.get_portfolio_type(organization_type, federal_type)
federal_type = BranchChoices.get_branch_label(federal_type) if federal_type else "-"
response_data = {
"portfolio_type": portfolio_type,
"federal_type": federal_type,
}
return JsonResponse(response_data)

View file

@ -419,7 +419,7 @@ class PortfolioBasePermission(PermissionsLoginMixin):
if not self.request.user.is_authenticated: if not self.request.user.is_authenticated:
return False return False
return self.request.user.has_base_portfolio_permission() return self.request.user.is_org_user(self.request)
class PortfolioDomainsPermission(PortfolioBasePermission): class PortfolioDomainsPermission(PortfolioBasePermission):
@ -432,9 +432,11 @@ class PortfolioDomainsPermission(PortfolioBasePermission):
The user is in self.request.user and the portfolio can be looked The user is in self.request.user and the portfolio can be looked
up from the portfolio's primary key in self.kwargs["pk"]""" up from the portfolio's primary key in self.kwargs["pk"]"""
if not self.request.user.is_authenticated: portfolio = self.request.session.get("portfolio")
if not self.request.user.has_domains_portfolio_permission(portfolio):
return False return False
return self.request.user.has_domains_portfolio_permission()
return super().has_permission()
class PortfolioDomainRequestsPermission(PortfolioBasePermission): class PortfolioDomainRequestsPermission(PortfolioBasePermission):
@ -447,6 +449,8 @@ class PortfolioDomainRequestsPermission(PortfolioBasePermission):
The user is in self.request.user and the portfolio can be looked The user is in self.request.user and the portfolio can be looked
up from the portfolio's primary key in self.kwargs["pk"]""" up from the portfolio's primary key in self.kwargs["pk"]"""
if not self.request.user.is_authenticated: portfolio = self.request.session.get("portfolio")
if not self.request.user.has_domain_requests_portfolio_permission(portfolio):
return False return False
return self.request.user.has_domain_requests_portfolio_permission()
return super().has_permission()