Skip to content

Commit

Permalink
Merge pull request #2517 from cisagov/za/2348-csv-export-org-member-d…
Browse files Browse the repository at this point in the history
…omain-export

(on getgov-za) Ticket #2348: Handle portfolio permissions for csv export
  • Loading branch information
zandercymatics authored Aug 9, 2024
2 parents 0cbc543 + b17252d commit 2062653
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 11 deletions.
12 changes: 12 additions & 0 deletions src/registrar/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.db import models
from django.db.models import Q

from registrar.models.domain_information import DomainInformation
from registrar.models.user_domain_role import UserDomainRole
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices

Expand Down Expand Up @@ -265,6 +266,10 @@ def has_domain_requests_portfolio_permission(self):
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
) or self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)

def has_view_all_domains_permission(self):
"""Determines if the current user can view all available domains in a given portfolio"""
return self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)

@classmethod
def needs_identity_verification(cls, email, uuid):
"""A method used by our oidc classes to test whether a user needs email/uuid verification
Expand Down Expand Up @@ -406,3 +411,10 @@ def on_each_login(self):
def is_org_user(self, request):
has_organization_feature_flag = flag_is_active(request, "organization_feature")
return has_organization_feature_flag and self.has_base_portfolio_permission()

def get_user_domain_ids(self, request):
"""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():
return DomainInformation.objects.filter(portfolio=self.portfolio).values_list("domain_id", flat=True)
else:
return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True)
2 changes: 1 addition & 1 deletion src/registrar/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ <h1>Manage your domains</h1>
</a>
</p>

{% include "includes/domains_table.html" %}
{% include "includes/domains_table.html" with user_domain_count=user_domain_count %}
{% include "includes/domain_requests_table.html" %}

</div>
Expand Down
2 changes: 2 additions & 0 deletions src/registrar/templates/includes/domains_table.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ <h2 id="domains-header" class="display-inline-block">Domains</h2>
</form>
</section>
</div>
{% if user_domain_count and user_domain_count > 0 %}
<div class="section--outlined__utility-button mobile-lg:padding-right-105 {% if has_domains_portfolio_permission %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}">
<section aria-label="Domains report component" class="mobile-lg:margin-top-205">
<a href="{% url 'export_data_type_user' %}" class="usa-button usa-button--unstyled" role="button">
Expand All @@ -46,6 +47,7 @@ <h2 id="domains-header" class="display-inline-block">Domains</h2>
</a>
</section>
</div>
{% endif %}
</div>
{% if has_domains_portfolio_permission %}
<div class="display-flex flex-align-center">
Expand Down
2 changes: 1 addition & 1 deletion src/registrar/templates/portfolio_domains.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@

{% block portfolio_content %}
<h1 id="domains-header">Domains</h1>
{% include "includes/domains_table.html" with portfolio=portfolio %}
{% include "includes/domains_table.html" with portfolio=portfolio user_domain_count=user_domain_count %}
{% endblock %}
77 changes: 77 additions & 0 deletions src/registrar/tests/test_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
Domain,
UserDomainRole,
)
from registrar.models import Portfolio
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from registrar.utility.csv_export import (
DomainDataFull,
DomainDataType,
Expand All @@ -32,6 +34,7 @@
from django.utils import timezone
from api.tests.common import less_console_noise_decorator
from .common import MockDbForSharedTests, MockDbForIndividualTests, MockEppLib, less_console_noise, get_time_aware_date
from waffle.testutils import override_flag


class CsvReportsTest(MockDbForSharedTests):
Expand Down Expand Up @@ -311,6 +314,80 @@ def test_domain_data_type_user(self):
self.maxDiff = None
self.assertEqual(csv_content, expected_content)

@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_domain_data_type_user_with_portfolio(self):
"""Tests DomainDataTypeUser export with portfolio permissions"""

# Create a portfolio and assign it to the user
portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
self.user.portfolio = portfolio
self.user.save()

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_3).delete()

# Add portfolios to the first and third domains
self.domain_1.domain_info.portfolio = portfolio
self.domain_3.domain_info.portfolio = portfolio

self.domain_1.domain_info.save()
self.domain_3.domain_info.save()

# Set up user permissions
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
self.user.save()
self.user.refresh_from_db()

# Create a request object
factory = RequestFactory()
request = factory.get("/")
request.user = self.user

# Get the csv content
csv_content = self._run_domain_data_type_user_export(request)

# We expect only domains associated with the user's portfolio
self.assertIn(self.domain_1.name, csv_content)
self.assertIn(self.domain_3.name, csv_content)
self.assertNotIn(self.domain_2.name, csv_content)

# Test the output for readonly admin
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY]
self.user.save()

self.assertIn(self.domain_1.name, csv_content)
self.assertIn(self.domain_3.name, csv_content)
self.assertNotIn(self.domain_2.name, 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)

self.assertNotIn(self.domain_1.name, csv_content)
self.assertNotIn(self.domain_3.name, csv_content)
self.assertIn(self.domain_2.name, csv_content)
self.domain_1.delete()
self.domain_2.delete()
self.domain_3.delete()
portfolio.delete()

def _run_domain_data_type_user_export(self, request):
"""Helper function to run the export_data_to_csv function on DomainDataTypeUser"""
# Create a CSV file in memory
csv_file = StringIO()
# Call the export functions
DomainDataTypeUser.export_data_to_csv(csv_file, request=request)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
# Read the content into a variable
csv_content = csv_file.read()

return csv_content

@less_console_noise_decorator
def test_domain_data_full(self):
"""Shows security contacts, filtered by state"""
Expand Down
7 changes: 3 additions & 4 deletions src/registrar/utility/csv_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,10 +578,9 @@ def get_filter_conditions(cls, request=None):
if request is None or not hasattr(request, "user") or not request.user:
# Return nothing
return Q(id__in=[])

user_domain_roles = UserDomainRole.objects.filter(user=request.user)
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
return Q(domain__id__in=domain_ids)
else:
# Get all domains the user is associated with
return Q(domain__id__in=request.user.get_user_domain_ids(request))


class DomainDataFull(DomainExport):
Expand Down
4 changes: 1 addition & 3 deletions src/registrar/views/domains_json.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import logging
from django.http import JsonResponse
from django.core.paginator import Paginator
from registrar.models import UserDomainRole, Domain
from registrar.models import UserDomainRole, Domain, DomainInformation
from django.contrib.auth.decorators import login_required
from django.urls import reverse
from django.db.models import Q

from registrar.models.domain_information import DomainInformation

logger = logging.getLogger(__name__)


Expand Down
3 changes: 2 additions & 1 deletion src/registrar/views/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ def index(request):
"""This page is available to anyone without logging in."""
context = {}

if request.user.is_authenticated:
if request and request.user and request.user.is_authenticated:
# This controls the creation of a new domain request in the wizard
request.session["new_request"] = True
context["user_domain_count"] = request.user.get_user_domain_ids(request).count()

return render(request, "home.html", context)
5 changes: 4 additions & 1 deletion src/registrar/views/portfolios.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ class PortfolioDomainsView(PortfolioDomainsPermissionView, View):
template_name = "portfolio_domains.html"

def get(self, request):
return render(request, "portfolio_domains.html")
context = {}
if self.request and self.request.user and self.request.user.is_authenticated:
context["user_domain_count"] = self.request.user.get_user_domain_ids(request).count()
return render(request, "portfolio_domains.html", context)


class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):
Expand Down

0 comments on commit 2062653

Please sign in to comment.