Merge branch 'main' into nl/3075-action-td-adjustments

This commit is contained in:
CuriousX 2025-02-04 11:50:59 -07:00 committed by GitHub
commit feb588187f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 571 additions and 40 deletions

View file

@ -1329,6 +1329,14 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin):
get_roles.short_description = "Roles" # type: ignore
def delete_queryset(self, request, queryset):
"""We override the delete method in the model.
When deleting in DJA, if you select multiple items in a table using checkboxes and apply a delete action
the model delete does not get called. This method gets called instead.
This override makes sure our code in the model gets executed in these situations."""
for obj in queryset:
obj.delete() # Calls the overridden delete method on each instance
class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"""Custom user domain role admin class."""
@ -1661,6 +1669,14 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
# Call the parent save method to save the object
super().save_model(request, obj, form, change)
def delete_queryset(self, request, queryset):
"""We override the delete method in the model.
When deleting in DJA, if you select multiple items in a table using checkboxes and apply a delete action,
the model delete does not get called. This method gets called instead.
This override makes sure our code in the model gets executed in these situations."""
for obj in queryset:
obj.delete() # Calls the overridden delete method on each instance
class DomainInformationResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the

View file

@ -20,7 +20,6 @@ from registrar.views.report_views import (
AnalyticsView,
ExportDomainRequestDataFull,
ExportDataTypeUser,
ExportDataTypeRequests,
ExportMembersPortfolio,
)
@ -260,11 +259,6 @@ urlpatterns = [
ExportDataTypeUser.as_view(),
name="export_data_type_user",
),
path(
"reports/export_data_type_requests/",
ExportDataTypeRequests.as_view(),
name="export_data_type_requests",
),
path(
"domain-request/<int:id>/edit/",
views.DomainRequestWizard.as_view(),

View file

@ -8,6 +8,7 @@ from registrar.models import DomainInvitation, UserPortfolioPermission
from .utility.portfolio_helper import (
UserPortfolioPermissionChoices,
UserPortfolioRoleChoices,
cleanup_after_portfolio_member_deletion,
validate_portfolio_invitation,
) # type: ignore
from .utility.time_stamped_model import TimeStampedModel
@ -115,3 +116,27 @@ class PortfolioInvitation(TimeStampedModel):
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
super().clean()
validate_portfolio_invitation(self)
def delete(self, *args, **kwargs):
User = get_user_model()
email = self.email # Capture the email before the instance is deleted
portfolio = self.portfolio # Capture the portfolio before the instance is deleted
# Call the superclass delete method to actually delete the instance
super().delete(*args, **kwargs)
if self.status == self.PortfolioInvitationStatus.INVITED:
# Query the user by email
users = User.objects.filter(email=email)
if users.count() > 1:
# This should never happen, log an error if more than one object is returned
logger.error(f"Multiple users found with the same email: {email}")
# Retrieve the first user, or None if no users are found
user = users.first()
cleanup_after_portfolio_member_deletion(portfolio=portfolio, email=email, user=user)

View file

@ -5,6 +5,7 @@ from registrar.models.utility.portfolio_helper import (
UserPortfolioRoleChoices,
DomainRequestPermissionDisplay,
MemberPermissionDisplay,
cleanup_after_portfolio_member_deletion,
validate_user_portfolio_permission,
)
from .utility.time_stamped_model import TimeStampedModel
@ -188,3 +189,13 @@ class UserPortfolioPermission(TimeStampedModel):
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
super().clean()
validate_user_portfolio_permission(self)
def delete(self, *args, **kwargs):
user = self.user # Capture the user before the instance is deleted
portfolio = self.portfolio # Capture the portfolio before the instance is deleted
# Call the superclass delete method to actually delete the instance
super().delete(*args, **kwargs)
cleanup_after_portfolio_member_deletion(portfolio=portfolio, email=user.email, user=user)

View file

@ -210,3 +210,32 @@ def validate_portfolio_invitation(portfolio_invitation):
"This user is already assigned to a portfolio invitation. "
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
)
def cleanup_after_portfolio_member_deletion(portfolio, email, user=None):
"""
Cleans up after removing a portfolio member or a portfolio invitation.
Args:
portfolio: portfolio
user: passed when removing a portfolio member.
email: passed when removing a portfolio invitation, or passed as user.email
when removing a portfolio member.
"""
DomainInvitation = apps.get_model("registrar.DomainInvitation")
UserDomainRole = apps.get_model("registrar.UserDomainRole")
# Fetch domain invitations matching the criteria
invitations = DomainInvitation.objects.filter(
email=email, domain__domain_info__portfolio=portfolio, status=DomainInvitation.DomainInvitationStatus.INVITED
)
# Call `cancel_invitation` on each invitation
for invitation in invitations:
invitation.cancel_invitation()
invitation.save()
if user:
# Remove user's domain roles for the current portfolio
UserDomainRole.objects.filter(user=user, domain__domain_info__portfolio=portfolio).delete()

View file

@ -51,20 +51,7 @@
</form>
</section>
</div>
{% if portfolio %}
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}" id="export-csv">
<section aria-label="Domain Requests report component" class="margin-top-205">
<!----------------------------------------------------------------------
This link is commented out because we intend to add it back in later.
------------------------------------------------------------------------->
<!-- <a href="{% url 'export_data_type_requests' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right">
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{% static 'img/sprite.svg' %}#file_download"></use>
</svg>Export as CSV
</a> -->
</section>
</div>
{% endif %}
</div>
{% if portfolio %}

View file

@ -92,11 +92,13 @@
{% endif %}
{% if has_organization_members_flag %}
{% if has_view_members_portfolio_permission %}
<li class="usa-nav__primary-item">
<a href="{% url 'members' %}" class="usa-nav-link {% if path|is_members_subpage %} usa-current{% endif %}">
Members
</a>
</li>
{% endif %}
{% endif %}
<li class="usa-nav__primary-item">

View file

@ -164,6 +164,7 @@ class TestPortfolioInvitations(TestCase):
DomainInformation.objects.all().delete()
Domain.objects.all().delete()
UserPortfolioPermission.objects.all().delete()
UserDomainRole.objects.all().delete()
Portfolio.objects.all().delete()
PortfolioInvitation.objects.all().delete()
User.objects.all().delete()
@ -442,6 +443,294 @@ class TestPortfolioInvitations(TestCase):
pass
@less_console_noise_decorator
def test_delete_portfolio_invitation_deletes_portfolio_domain_invitations(self):
"""Deleting a portfolio invitation causes domain invitations for the same email on the same
portfolio to be canceled."""
email_with_no_user = "email-with-no-user@email.gov"
domain_in_portfolio_1, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_1.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_1, portfolio=self.portfolio
)
invite_1, _ = DomainInvitation.objects.get_or_create(email=email_with_no_user, domain=domain_in_portfolio_1)
domain_in_portfolio_2, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_and_invited_2.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_2, portfolio=self.portfolio
)
invite_2, _ = DomainInvitation.objects.get_or_create(email=email_with_no_user, domain=domain_in_portfolio_2)
domain_not_in_portfolio, _ = Domain.objects.get_or_create(
name="domain_not_in_portfolio.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(creator=self.user, domain=domain_not_in_portfolio)
invite_3, _ = DomainInvitation.objects.get_or_create(email=email_with_no_user, domain=domain_not_in_portfolio)
invitation_of_email_with_no_user, _ = PortfolioInvitation.objects.get_or_create(
email=email_with_no_user,
portfolio=self.portfolio,
roles=[self.portfolio_role_base, self.portfolio_role_admin],
additional_permissions=[self.portfolio_permission_1, self.portfolio_permission_2],
)
# The domain invitations start off as INVITED
self.assertEqual(invite_1.status, DomainInvitation.DomainInvitationStatus.INVITED)
self.assertEqual(invite_2.status, DomainInvitation.DomainInvitationStatus.INVITED)
self.assertEqual(invite_3.status, DomainInvitation.DomainInvitationStatus.INVITED)
# Delete member (invite)
invitation_of_email_with_no_user.delete()
# Reload the objects from the database
invite_1 = DomainInvitation.objects.get(pk=invite_1.pk)
invite_2 = DomainInvitation.objects.get(pk=invite_2.pk)
invite_3 = DomainInvitation.objects.get(pk=invite_3.pk)
# The domain invitations to the portfolio domains have been canceled
self.assertEqual(invite_1.status, DomainInvitation.DomainInvitationStatus.CANCELED)
self.assertEqual(invite_2.status, DomainInvitation.DomainInvitationStatus.CANCELED)
# Invite 3 is unaffected
self.assertEqual(invite_3.status, DomainInvitation.DomainInvitationStatus.INVITED)
@less_console_noise_decorator
def test_deleting_a_retrieved_invitation_has_no_side_effects(self):
"""Deleting a retrieved portfolio invitation causes no side effects."""
domain_in_portfolio_1, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_1.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_1, portfolio=self.portfolio
)
invite_1, _ = DomainInvitation.objects.get_or_create(email=self.email, domain=domain_in_portfolio_1)
domain_in_portfolio_2, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_and_invited_2.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_2, portfolio=self.portfolio
)
invite_2, _ = DomainInvitation.objects.get_or_create(email=self.email, domain=domain_in_portfolio_2)
domain_in_portfolio_3, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_3.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_3, portfolio=self.portfolio
)
UserDomainRole.objects.get_or_create(
user=self.user, domain=domain_in_portfolio_3, role=UserDomainRole.Roles.MANAGER
)
domain_in_portfolio_4, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_and_invited_4.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_4, portfolio=self.portfolio
)
UserDomainRole.objects.get_or_create(
user=self.user, domain=domain_in_portfolio_4, role=UserDomainRole.Roles.MANAGER
)
domain_not_in_portfolio_1, _ = Domain.objects.get_or_create(
name="domain_not_in_portfolio.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(creator=self.user, domain=domain_not_in_portfolio_1)
invite_3, _ = DomainInvitation.objects.get_or_create(email=self.email, domain=domain_not_in_portfolio_1)
domain_not_in_portfolio_2, _ = Domain.objects.get_or_create(
name="domain_not_in_portfolio_2.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(creator=self.user, domain=domain_not_in_portfolio_2)
UserDomainRole.objects.get_or_create(
user=self.user, domain=domain_not_in_portfolio_2, role=UserDomainRole.Roles.MANAGER
)
# The domain invitations start off as INVITED
self.assertEqual(invite_1.status, DomainInvitation.DomainInvitationStatus.INVITED)
self.assertEqual(invite_2.status, DomainInvitation.DomainInvitationStatus.INVITED)
self.assertEqual(invite_3.status, DomainInvitation.DomainInvitationStatus.INVITED)
# The user domain roles exist
self.assertTrue(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_in_portfolio_3,
).exists()
)
self.assertTrue(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_in_portfolio_4,
).exists()
)
self.assertTrue(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_not_in_portfolio_2,
).exists()
)
# retrieve the invitation
self.invitation.retrieve()
self.invitation.save()
# Delete member (invite)
self.invitation.delete()
# Reload the objects from the database
invite_1 = DomainInvitation.objects.get(pk=invite_1.pk)
invite_2 = DomainInvitation.objects.get(pk=invite_2.pk)
invite_3 = DomainInvitation.objects.get(pk=invite_3.pk)
# Test that no side effects have been triggered
self.assertEqual(invite_1.status, DomainInvitation.DomainInvitationStatus.INVITED)
self.assertEqual(invite_2.status, DomainInvitation.DomainInvitationStatus.INVITED)
self.assertEqual(invite_3.status, DomainInvitation.DomainInvitationStatus.INVITED)
self.assertTrue(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_in_portfolio_3,
).exists()
)
self.assertTrue(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_in_portfolio_4,
).exists()
)
self.assertTrue(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_not_in_portfolio_2,
).exists()
)
@less_console_noise_decorator
def test_delete_portfolio_invitation_deletes_user_domain_roles(self):
"""Deleting a portfolio invitation causes domain invitations for the same email on the same
portfolio to be canceled, also deletes any exiting user domain roles on the portfolio for the
user if the user exists."""
domain_in_portfolio_1, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_1.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_1, portfolio=self.portfolio
)
invite_1, _ = DomainInvitation.objects.get_or_create(email=self.email, domain=domain_in_portfolio_1)
domain_in_portfolio_2, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_and_invited_2.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_2, portfolio=self.portfolio
)
invite_2, _ = DomainInvitation.objects.get_or_create(email=self.email, domain=domain_in_portfolio_2)
domain_in_portfolio_3, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_3.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_3, portfolio=self.portfolio
)
UserDomainRole.objects.get_or_create(
user=self.user, domain=domain_in_portfolio_3, role=UserDomainRole.Roles.MANAGER
)
domain_in_portfolio_4, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_and_invited_4.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_4, portfolio=self.portfolio
)
UserDomainRole.objects.get_or_create(
user=self.user, domain=domain_in_portfolio_4, role=UserDomainRole.Roles.MANAGER
)
domain_not_in_portfolio_1, _ = Domain.objects.get_or_create(
name="domain_not_in_portfolio.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(creator=self.user, domain=domain_not_in_portfolio_1)
invite_3, _ = DomainInvitation.objects.get_or_create(email=self.email, domain=domain_not_in_portfolio_1)
domain_not_in_portfolio_2, _ = Domain.objects.get_or_create(
name="domain_not_in_portfolio_2.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(creator=self.user, domain=domain_not_in_portfolio_2)
UserDomainRole.objects.get_or_create(
user=self.user, domain=domain_not_in_portfolio_2, role=UserDomainRole.Roles.MANAGER
)
# The domain invitations start off as INVITED
self.assertEqual(invite_1.status, DomainInvitation.DomainInvitationStatus.INVITED)
self.assertEqual(invite_2.status, DomainInvitation.DomainInvitationStatus.INVITED)
self.assertEqual(invite_3.status, DomainInvitation.DomainInvitationStatus.INVITED)
# The user domain roles exist
self.assertTrue(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_in_portfolio_3,
).exists()
)
self.assertTrue(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_in_portfolio_4,
).exists()
)
self.assertTrue(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_not_in_portfolio_2,
).exists()
)
# Delete member (invite)
self.invitation.delete()
# Reload the objects from the database
invite_1 = DomainInvitation.objects.get(pk=invite_1.pk)
invite_2 = DomainInvitation.objects.get(pk=invite_2.pk)
invite_3 = DomainInvitation.objects.get(pk=invite_3.pk)
# The domain invitations to the portfolio domains have been canceled
self.assertEqual(invite_1.status, DomainInvitation.DomainInvitationStatus.CANCELED)
self.assertEqual(invite_2.status, DomainInvitation.DomainInvitationStatus.CANCELED)
# Invite 3 is unaffected
self.assertEqual(invite_3.status, DomainInvitation.DomainInvitationStatus.INVITED)
# The user domain roles have been deleted for the domains in portfolio
self.assertFalse(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_in_portfolio_3,
).exists()
)
self.assertFalse(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_in_portfolio_4,
).exists()
)
# The user domain role on the domain not in portfolio still exists
self.assertTrue(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_not_in_portfolio_2,
).exists()
)
class TestUserPortfolioPermission(TestCase):
@less_console_noise_decorator
@ -457,6 +746,7 @@ class TestUserPortfolioPermission(TestCase):
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
DomainInvitation.objects.all().delete()
UserPortfolioPermission.objects.all().delete()
Portfolio.objects.all().delete()
User.objects.all().delete()
@ -750,6 +1040,129 @@ class TestUserPortfolioPermission(TestCase):
# Should return the forbidden permissions for member role
self.assertEqual(member_only_permissions, set(member_forbidden))
@less_console_noise_decorator
def test_delete_portfolio_permission_deletes_user_domain_roles(self):
"""Deleting a user portfolio permission causes domain invitations for the same email on the same
portfolio to be canceled, also deletes any exiting user domain roles on the portfolio for the
user if the user exists."""
domain_in_portfolio_1, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_1.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_1, portfolio=self.portfolio
)
invite_1, _ = DomainInvitation.objects.get_or_create(email=self.user.email, domain=domain_in_portfolio_1)
domain_in_portfolio_2, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_and_invited_2.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_2, portfolio=self.portfolio
)
invite_2, _ = DomainInvitation.objects.get_or_create(email=self.user.email, domain=domain_in_portfolio_2)
domain_in_portfolio_3, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_3.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_3, portfolio=self.portfolio
)
UserDomainRole.objects.get_or_create(
user=self.user, domain=domain_in_portfolio_3, role=UserDomainRole.Roles.MANAGER
)
domain_in_portfolio_4, _ = Domain.objects.get_or_create(
name="domain_in_portfolio_and_invited_4.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=domain_in_portfolio_4, portfolio=self.portfolio
)
UserDomainRole.objects.get_or_create(
user=self.user, domain=domain_in_portfolio_4, role=UserDomainRole.Roles.MANAGER
)
domain_not_in_portfolio_1, _ = Domain.objects.get_or_create(
name="domain_not_in_portfolio.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(creator=self.user, domain=domain_not_in_portfolio_1)
invite_3, _ = DomainInvitation.objects.get_or_create(email=self.user.email, domain=domain_not_in_portfolio_1)
domain_not_in_portfolio_2, _ = Domain.objects.get_or_create(
name="domain_not_in_portfolio_2.gov", state=Domain.State.READY
)
DomainInformation.objects.get_or_create(creator=self.user, domain=domain_not_in_portfolio_2)
UserDomainRole.objects.get_or_create(
user=self.user, domain=domain_not_in_portfolio_2, role=UserDomainRole.Roles.MANAGER
)
# Create portfolio permission
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
portfolio=self.portfolio, user=self.user, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
# The domain invitations start off as INVITED
self.assertEqual(invite_1.status, DomainInvitation.DomainInvitationStatus.INVITED)
self.assertEqual(invite_2.status, DomainInvitation.DomainInvitationStatus.INVITED)
self.assertEqual(invite_3.status, DomainInvitation.DomainInvitationStatus.INVITED)
# The user domain roles exist
self.assertTrue(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_in_portfolio_3,
).exists()
)
self.assertTrue(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_in_portfolio_4,
).exists()
)
self.assertTrue(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_not_in_portfolio_2,
).exists()
)
# Delete member (user portfolio permission)
portfolio_permission.delete()
# Reload the objects from the database
invite_1 = DomainInvitation.objects.get(pk=invite_1.pk)
invite_2 = DomainInvitation.objects.get(pk=invite_2.pk)
invite_3 = DomainInvitation.objects.get(pk=invite_3.pk)
# The domain invitations to the portfolio domains have been canceled
self.assertEqual(invite_1.status, DomainInvitation.DomainInvitationStatus.CANCELED)
self.assertEqual(invite_2.status, DomainInvitation.DomainInvitationStatus.CANCELED)
# Invite 3 is unaffected
self.assertEqual(invite_3.status, DomainInvitation.DomainInvitationStatus.INVITED)
# The user domain roles have been deleted for the domains in portfolio
self.assertFalse(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_in_portfolio_3,
).exists()
)
self.assertFalse(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_in_portfolio_4,
).exists()
)
# The user domain role on the domain not in portfolio still exists
self.assertTrue(
UserDomainRole.objects.filter(
user=self.user,
domain=domain_not_in_portfolio_2,
).exists()
)
class TestUser(TestCase):
"""Test actions that occur on user login,

View file

@ -1097,8 +1097,10 @@ class TestPortfolio(WebTest):
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@override_flag("organization_members", active=True)
def test_main_nav_when_user_has_no_permissions(self):
"""Test the nav contains a link to the no requests page"""
"""Test the nav contains a link to the no requests page
Also test that members link not present"""
UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
)
@ -1118,20 +1120,23 @@ class TestPortfolio(WebTest):
self.assertNotContains(portfolio_landing_page, "basic-nav-section-two")
# link to requests
self.assertNotContains(portfolio_landing_page, 'href="/requests/')
# link to create
# link to create request
self.assertNotContains(portfolio_landing_page, 'href="/request/')
# link to members
self.assertNotContains(portfolio_landing_page, 'href="/members/')
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@override_flag("organization_members", active=True)
def test_main_nav_when_user_has_all_permissions(self):
"""Test the nav contains a dropdown with a link to create and another link to view requests
Also test for the existence of the Create a new request btn on the requests page"""
Also test for the existence of the Create a new request btn on the requests page
Also test for the existence of the members link"""
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
)
self.client.force_login(self.user)
# create and submit a domain request
@ -1151,6 +1156,8 @@ class TestPortfolio(WebTest):
self.assertContains(portfolio_landing_page, 'href="/requests/')
# link to create
self.assertContains(portfolio_landing_page, 'href="/request/')
# link to members
self.assertContains(portfolio_landing_page, 'href="/members/')
requests_page = self.client.get(reverse("domain-requests"))
@ -1160,15 +1167,18 @@ class TestPortfolio(WebTest):
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@override_flag("organization_members", active=True)
def test_main_nav_when_user_has_view_but_not_edit_permissions(self):
"""Test the nav contains a simple link to view requests
Also test for the existence of the Create a new request btn on the requests page"""
Also test for the existence of the Create a new request btn on the requests page
Also test for the existence of members link"""
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
self.client.force_login(self.user)
@ -1189,6 +1199,8 @@ class TestPortfolio(WebTest):
self.assertContains(portfolio_landing_page, 'href="/requests/')
# link to create
self.assertNotContains(portfolio_landing_page, 'href="/request/')
# link to members
self.assertContains(portfolio_landing_page, 'href="/members/')
requests_page = self.client.get(reverse("domain-requests"))

View file

@ -5,17 +5,20 @@ from django.views import View
from django.shortcuts import render
from django.contrib import admin
from django.db.models import Avg, F
from registrar.views.utility.mixins import DomainAndRequestsReportsPermission, PortfolioReportsPermission
from .. import models
import datetime
from django.utils import timezone
from django.contrib.admin.views.decorators import staff_member_required
from django.utils.decorators import method_decorator
from registrar.utility import csv_export
import logging
logger = logging.getLogger(__name__)
@method_decorator(staff_member_required, name="dispatch")
class AnalyticsView(View):
def get(self, request):
thirty_days_ago = datetime.datetime.today() - datetime.timedelta(days=30)
@ -149,6 +152,7 @@ class AnalyticsView(View):
return render(request, "admin/analytics.html", context)
@method_decorator(staff_member_required, name="dispatch")
class ExportDataType(View):
def get(self, request, *args, **kwargs):
# match the CSV example with all the fields
@ -158,7 +162,7 @@ class ExportDataType(View):
return response
class ExportDataTypeUser(View):
class ExportDataTypeUser(DomainAndRequestsReportsPermission, View):
"""Returns a domain report for a given user on the request"""
def get(self, request, *args, **kwargs):
@ -169,7 +173,7 @@ class ExportDataTypeUser(View):
return response
class ExportMembersPortfolio(View):
class ExportMembersPortfolio(PortfolioReportsPermission, View):
"""Returns a members report for a given portfolio"""
def get(self, request, *args, **kwargs):
@ -197,17 +201,7 @@ class ExportMembersPortfolio(View):
return response
class ExportDataTypeRequests(View):
"""Returns a domain requests report for a given user on the request"""
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = 'attachment; filename="domain-requests.csv"'
csv_export.DomainRequestDataType.export_data_to_csv(response, request=request)
return response
@method_decorator(staff_member_required, name="dispatch")
class ExportDataFull(View):
def get(self, request, *args, **kwargs):
# Smaller export based on 1
@ -217,6 +211,7 @@ class ExportDataFull(View):
return response
@method_decorator(staff_member_required, name="dispatch")
class ExportDataFederal(View):
def get(self, request, *args, **kwargs):
# Federal only
@ -226,6 +221,7 @@ class ExportDataFederal(View):
return response
@method_decorator(staff_member_required, name="dispatch")
class ExportDomainRequestDataFull(View):
"""Generates a downloaded report containing all Domain Requests (except started)"""
@ -237,6 +233,7 @@ class ExportDomainRequestDataFull(View):
return response
@method_decorator(staff_member_required, name="dispatch")
class ExportDataDomainsGrowth(View):
def get(self, request, *args, **kwargs):
start_date = request.GET.get("start_date", "")
@ -249,6 +246,7 @@ class ExportDataDomainsGrowth(View):
return response
@method_decorator(staff_member_required, name="dispatch")
class ExportDataRequestsGrowth(View):
def get(self, request, *args, **kwargs):
start_date = request.GET.get("start_date", "")
@ -261,6 +259,7 @@ class ExportDataRequestsGrowth(View):
return response
@method_decorator(staff_member_required, name="dispatch")
class ExportDataManagedDomains(View):
def get(self, request, *args, **kwargs):
start_date = request.GET.get("start_date", "")
@ -272,6 +271,7 @@ class ExportDataManagedDomains(View):
return response
@method_decorator(staff_member_required, name="dispatch")
class ExportDataUnmanagedDomains(View):
def get(self, request, *args, **kwargs):
start_date = request.GET.get("start_date", "")

View file

@ -153,6 +153,48 @@ class PermissionsLoginMixin(PermissionRequiredMixin):
return super().handle_no_permission()
class DomainAndRequestsReportsPermission(PermissionsLoginMixin):
"""Permission mixin for domain and requests csv downloads"""
def has_permission(self):
"""Check if this user has access to this domain.
The user is in self.request.user and the domain needs to be looked
up from the domain's primary key in self.kwargs["pk"]
"""
if not self.request.user.is_authenticated:
return False
if self.request.user.is_restricted():
return False
return True
class PortfolioReportsPermission(PermissionsLoginMixin):
"""Permission mixin for portfolio csv downloads"""
def has_permission(self):
"""Check if this user has access to this domain.
The user is in self.request.user and the domain needs to be looked
up from the domain's primary key in self.kwargs["pk"]
"""
if not self.request.user.is_authenticated:
return False
if self.request.user.is_restricted():
return False
portfolio = self.request.session.get("portfolio")
if not self.request.user.has_view_members_portfolio_permission(portfolio):
return False
return self.request.user.is_org_user(self.request)
class DomainPermission(PermissionsLoginMixin):
"""Permission mixin that redirects to domain if user has access,
otherwise 403"""