mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-16 14:34:10 +02:00
Merge pull request #2517 from cisagov/za/2348-csv-export-org-member-domain-export
(on getgov-za) Ticket #2348: Handle portfolio permissions for csv export
This commit is contained in:
commit
20626539c9
9 changed files with 103 additions and 11 deletions
|
@ -4,6 +4,7 @@ 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 registrar.models.domain_information import DomainInformation
|
||||||
from registrar.models.user_domain_role import 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
|
||||||
|
|
||||||
|
@ -265,6 +266,10 @@ class User(AbstractUser):
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
||||||
) or self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_CREATED_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
|
@classmethod
|
||||||
def needs_identity_verification(cls, email, uuid):
|
def needs_identity_verification(cls, email, uuid):
|
||||||
"""A method used by our oidc classes to test whether a user needs email/uuid verification
|
"""A method used by our oidc classes to test whether a user needs email/uuid verification
|
||||||
|
@ -406,3 +411,10 @@ class User(AbstractUser):
|
||||||
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()
|
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)
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% include "includes/domains_table.html" %}
|
{% include "includes/domains_table.html" with user_domain_count=user_domain_count %}
|
||||||
{% include "includes/domain_requests_table.html" %}
|
{% include "includes/domain_requests_table.html" %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</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 %}">
|
<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">
|
<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">
|
<a href="{% url 'export_data_type_user' %}" class="usa-button usa-button--unstyled" role="button">
|
||||||
|
@ -46,6 +47,7 @@
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if has_domains_portfolio_permission %}
|
{% if has_domains_portfolio_permission %}
|
||||||
<div class="display-flex flex-align-center">
|
<div class="display-flex flex-align-center">
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
<h1 id="domains-header">Domains</h1>
|
<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 %}
|
{% endblock %}
|
||||||
|
|
|
@ -6,6 +6,8 @@ from registrar.models import (
|
||||||
Domain,
|
Domain,
|
||||||
UserDomainRole,
|
UserDomainRole,
|
||||||
)
|
)
|
||||||
|
from registrar.models import Portfolio
|
||||||
|
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||||
from registrar.utility.csv_export import (
|
from registrar.utility.csv_export import (
|
||||||
DomainDataFull,
|
DomainDataFull,
|
||||||
DomainDataType,
|
DomainDataType,
|
||||||
|
@ -32,6 +34,7 @@ from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # typ
|
||||||
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, less_console_noise, get_time_aware_date
|
||||||
|
from waffle.testutils import override_flag
|
||||||
|
|
||||||
|
|
||||||
class CsvReportsTest(MockDbForSharedTests):
|
class CsvReportsTest(MockDbForSharedTests):
|
||||||
|
@ -311,6 +314,80 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
self.assertEqual(csv_content, expected_content)
|
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
|
@less_console_noise_decorator
|
||||||
def test_domain_data_full(self):
|
def test_domain_data_full(self):
|
||||||
"""Shows security contacts, filtered by state"""
|
"""Shows security contacts, filtered by state"""
|
||||||
|
|
|
@ -578,10 +578,9 @@ class DomainDataTypeUser(DomainDataType):
|
||||||
if request is None or not hasattr(request, "user") or not request.user:
|
if request is None or not hasattr(request, "user") or not request.user:
|
||||||
# Return nothing
|
# Return nothing
|
||||||
return Q(id__in=[])
|
return Q(id__in=[])
|
||||||
|
else:
|
||||||
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
|
# Get all domains the user is associated with
|
||||||
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
return Q(domain__id__in=request.user.get_user_domain_ids(request))
|
||||||
return Q(domain__id__in=domain_ids)
|
|
||||||
|
|
||||||
|
|
||||||
class DomainDataFull(DomainExport):
|
class DomainDataFull(DomainExport):
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import logging
|
import logging
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.core.paginator import Paginator
|
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.contrib.auth.decorators import login_required
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from registrar.models.domain_information import DomainInformation
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,9 @@ def index(request):
|
||||||
"""This page is available to anyone without logging in."""
|
"""This page is available to anyone without logging in."""
|
||||||
context = {}
|
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
|
# This controls the creation of a new domain request in the wizard
|
||||||
request.session["new_request"] = True
|
request.session["new_request"] = True
|
||||||
|
context["user_domain_count"] = request.user.get_user_domain_ids(request).count()
|
||||||
|
|
||||||
return render(request, "home.html", context)
|
return render(request, "home.html", context)
|
||||||
|
|
|
@ -22,7 +22,10 @@ class PortfolioDomainsView(PortfolioDomainsPermissionView, View):
|
||||||
template_name = "portfolio_domains.html"
|
template_name = "portfolio_domains.html"
|
||||||
|
|
||||||
def get(self, request):
|
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):
|
class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue