mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-25 12:08:40 +02:00
Merge branch 'main' into bob/2378-portfolio-senior-official
This commit is contained in:
commit
3e992f9ec8
14 changed files with 188 additions and 43 deletions
|
@ -30,7 +30,19 @@ You should end up with `40_some_migration_from_main`, `41_local_migration`
|
|||
|
||||
Alternatively, assuming that the conflicting migrations are not dependent on each other, you can manually edit the migration file such that your new migration is incremented by one (file name, and definition inside the file) but this approach is not recommended.
|
||||
|
||||
### Scenario 2: Conflicting migrations on sandbox
|
||||
### Scenario 2: Conflicting migrations on sandbox (can be fixed with GH workflow)
|
||||
A 500 error on a sanbox after a fresh push usually indicates a migration issue.
|
||||
Most of the time, these migration issues can easily be fixed by simply running the
|
||||
"reset-db" workflow in Github.
|
||||
|
||||
For the workflow, select the following inputs before running it;
|
||||
"Use workflow from": Branch-main
|
||||
"Which environment should we flush and re-load data for?" <YOUR_TARGET_SANDBOX>
|
||||
|
||||
This is not a cure-all since it simply flushes and re-runs migrations against your sandbox.
|
||||
If running this workflow does not solve your issue, proceed examining the scenarios below.
|
||||
|
||||
### Scenario 3: Conflicting migrations on sandbox (cannot be fixed with GH workflow)
|
||||
|
||||
This occurs when the logs return the following:
|
||||
>Conflicting migrations detected; multiple leaf nodes in the migration graph: (0040_example, 0041_example in base).
|
||||
|
|
|
@ -4,6 +4,7 @@ from django.contrib.auth.models import AbstractUser
|
|||
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
|
||||
|
||||
|
@ -265,6 +266,10 @@ class User(AbstractUser):
|
|||
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
|
||||
|
@ -406,3 +411,10 @@ class User(AbstractUser):
|
|||
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)
|
||||
|
|
|
@ -4,7 +4,6 @@ import time
|
|||
import logging
|
||||
from urllib.parse import urlparse, urlunparse, urlencode
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -173,10 +172,6 @@ class CreateOrUpdateOrganizationTypeHelper:
|
|||
self.instance.is_election_board = None
|
||||
self.instance.organization_type = generic_org_type
|
||||
else:
|
||||
# This can only happen with manual data tinkering, which causes these to be out of sync.
|
||||
if self.instance.is_election_board is None:
|
||||
self.instance.is_election_board = False
|
||||
|
||||
if self.instance.is_election_board:
|
||||
self.instance.organization_type = self.generic_org_to_org_map[generic_org_type]
|
||||
else:
|
||||
|
@ -219,12 +214,15 @@ class CreateOrUpdateOrganizationTypeHelper:
|
|||
self.instance.is_election_board = None
|
||||
self.instance.generic_org_type = None
|
||||
|
||||
def _validate_new_instance(self):
|
||||
def _validate_new_instance(self) -> bool:
|
||||
"""
|
||||
Validates whether a new instance of DomainRequest or DomainInformation can proceed with the update
|
||||
based on the consistency between organization_type, generic_org_type, and is_election_board.
|
||||
|
||||
Returns a boolean determining if execution should proceed or not.
|
||||
|
||||
Raises:
|
||||
ValueError if there is a mismatch between organization_type, generic_org_type, and is_election_board
|
||||
"""
|
||||
|
||||
# We conditionally accept both of these values to exist simultaneously, as long as
|
||||
|
@ -242,13 +240,20 @@ class CreateOrUpdateOrganizationTypeHelper:
|
|||
is_election_type = "_election" in organization_type
|
||||
can_have_election_board = organization_type in self.generic_org_to_org_map
|
||||
|
||||
election_board_mismatch = (is_election_type != self.instance.is_election_board) and can_have_election_board
|
||||
election_board_mismatch = (
|
||||
is_election_type and not self.instance.is_election_board and can_have_election_board
|
||||
)
|
||||
org_type_mismatch = mapped_org_type is not None and (generic_org_type != mapped_org_type)
|
||||
if election_board_mismatch or org_type_mismatch:
|
||||
message = (
|
||||
"Cannot add organization_type and generic_org_type simultaneously "
|
||||
"when generic_org_type, is_election_board, and organization_type values do not match."
|
||||
"Cannot add organization_type and generic_org_type simultaneously when"
|
||||
"generic_org_type ({}), is_election_board ({}), and organization_type ({}) don't match.".format(
|
||||
generic_org_type, self.instance.is_election_board, organization_type
|
||||
)
|
||||
)
|
||||
message = "Mismatch on election board, {}".format(message) if election_board_mismatch else message
|
||||
message = "Mistmatch on org type, {}".format(message) if org_type_mismatch else message
|
||||
logger.error("_validate_new_instance: %s", message)
|
||||
raise ValueError(message)
|
||||
|
||||
return True
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
</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>
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
</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">
|
||||
|
@ -46,6 +47,7 @@
|
|||
</a>
|
||||
</section>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if has_domains_portfolio_permission %}
|
||||
<div class="display-flex flex-align-center">
|
||||
|
|
|
@ -56,12 +56,13 @@
|
|||
{% with toggleable_input=True toggleable_label=True group_classes="usa-form-editable padding-top-2" %}
|
||||
{% input_with_errors form.title %}
|
||||
{% endwith %}
|
||||
|
||||
{% public_site_url "help/account-management/#get-help-with-login.gov" as login_help_url %}
|
||||
|
||||
{% public_site_url "help/account-management/#email-address" as login_help_url %}
|
||||
{% with toggleable_input=True add_class="display-none" group_classes="usa-form-editable usa-form-editable padding-top-2 bold-usa-label" %}
|
||||
|
||||
{% with link_href=login_help_url %}
|
||||
{% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, you’ll need to update your Login.gov account and log back in. Get help with your Login.gov account." %}
|
||||
{% with link_text="Get help with your Login.gov account" target_blank=True do_not_show_max_chars=True %}
|
||||
{% with sublabel_text="We recommend using a Login.gov account that's only connected to your work email address. If you need to change your email, you'll need to make a change to your Login.gov account. Get help with updating your email address." %}
|
||||
{% with link_text="Get help with updating your email address" target_blank=True do_not_show_max_chars=True %}
|
||||
{% input_with_errors form.email %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
|
|
@ -30,11 +30,11 @@
|
|||
|
||||
{% input_with_errors form.title %}
|
||||
|
||||
{% public_site_url "help/account-management/#get-help-with-login.gov" as login_help_url %}
|
||||
{% public_site_url "help/account-management/#email-address" as login_help_url %}
|
||||
|
||||
{% with link_href=login_help_url %}
|
||||
{% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, you’ll need to update your Login.gov account and log back in. Get help with your Login.gov account." %}
|
||||
{% with link_text="Get help with your Login.gov account" %}
|
||||
{% with sublabel_text="We recommend using a Login.gov account that's only connected to your work email address. If you need to change your email, you'll need to make a change to your Login.gov account. Get help with updating your email address." %}
|
||||
{% with link_text="Get help with updating your email address" %}
|
||||
{% with target_blank=True %}
|
||||
{% with do_not_show_max_chars=True %}
|
||||
{% input_with_errors form.email %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -1495,11 +1495,28 @@ class TestDomainRequestCustomSave(TestCase):
|
|||
self.assertEqual(domain_request.is_election_board, False)
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
|
||||
# Try reverting setting an invalid value for election board (should revert to False)
|
||||
@less_console_noise_decorator
|
||||
def test_existing_instance_updates_election_board_to_none(self):
|
||||
"""Test create_or_update_organization_type for an existing instance, first to True and then to None.
|
||||
Start our with is_election_board as none to simulate a situation where the request was started, but
|
||||
only completed to the point of filling out the generic_org_type."""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=None,
|
||||
)
|
||||
domain_request.is_election_board = True
|
||||
domain_request.save()
|
||||
|
||||
self.assertEqual(domain_request.is_election_board, True)
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
|
||||
|
||||
# Try reverting the election board value.
|
||||
domain_request.is_election_board = None
|
||||
domain_request.save()
|
||||
|
||||
self.assertEqual(domain_request.is_election_board, False)
|
||||
self.assertEqual(domain_request.is_election_board, None)
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -1654,11 +1671,30 @@ class TestDomainInformationCustomSave(TestCase):
|
|||
self.assertEqual(domain_information.is_election_board, False)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
|
||||
# Try reverting setting an invalid value for election board (should revert to False)
|
||||
domain_information.is_election_board = None
|
||||
@less_console_noise_decorator
|
||||
def test_existing_instance_update_election_board_to_none(self):
|
||||
"""Test create_or_update_organization_type for an existing instance, first to True and then to None.
|
||||
Start our with is_election_board as none to simulate a situation where the request was started, but
|
||||
only completed to the point of filling out the generic_org_type."""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=None,
|
||||
)
|
||||
domain_information = DomainInformation.create_from_da(domain_request)
|
||||
domain_information.is_election_board = True
|
||||
domain_information.save()
|
||||
|
||||
self.assertEqual(domain_information.is_election_board, False)
|
||||
self.assertEqual(domain_information.is_election_board, True)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
|
||||
|
||||
# Try reverting the election board value
|
||||
domain_information.is_election_board = None
|
||||
domain_information.save()
|
||||
domain_information.refresh_from_db()
|
||||
|
||||
self.assertEqual(domain_information.is_election_board, None)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -1858,8 +1894,7 @@ class TestDomainRequestIncomplete(TestCase):
|
|||
self.assertTrue(self.domain_request._is_state_or_territory_complete())
|
||||
self.domain_request.is_election_board = None
|
||||
self.domain_request.save()
|
||||
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||
self.assertTrue(self.domain_request._is_state_or_territory_complete())
|
||||
self.assertFalse(self.domain_request._is_state_or_territory_complete())
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_is_tribal_complete(self):
|
||||
|
@ -1868,10 +1903,11 @@ class TestDomainRequestIncomplete(TestCase):
|
|||
self.domain_request.is_election_board = False
|
||||
self.domain_request.save()
|
||||
self.assertTrue(self.domain_request._is_tribal_complete())
|
||||
self.domain_request.tribe_name = None
|
||||
self.domain_request.is_election_board = None
|
||||
self.domain_request.save()
|
||||
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||
self.assertFalse(self.domain_request._is_tribal_complete())
|
||||
self.domain_request.tribe_name = None
|
||||
self.domain_request.save()
|
||||
self.assertFalse(self.domain_request._is_tribal_complete())
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -1882,8 +1918,7 @@ class TestDomainRequestIncomplete(TestCase):
|
|||
self.assertTrue(self.domain_request._is_county_complete())
|
||||
self.domain_request.is_election_board = None
|
||||
self.domain_request.save()
|
||||
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||
self.assertTrue(self.domain_request._is_county_complete())
|
||||
self.assertFalse(self.domain_request._is_county_complete())
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_is_city_complete(self):
|
||||
|
@ -1893,8 +1928,7 @@ class TestDomainRequestIncomplete(TestCase):
|
|||
self.assertTrue(self.domain_request._is_city_complete())
|
||||
self.domain_request.is_election_board = None
|
||||
self.domain_request.save()
|
||||
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||
self.assertTrue(self.domain_request._is_city_complete())
|
||||
self.assertFalse(self.domain_request._is_city_complete())
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_is_special_district_complete(self):
|
||||
|
@ -1903,10 +1937,11 @@ class TestDomainRequestIncomplete(TestCase):
|
|||
self.domain_request.is_election_board = False
|
||||
self.domain_request.save()
|
||||
self.assertTrue(self.domain_request._is_special_district_complete())
|
||||
self.domain_request.about_your_organization = None
|
||||
self.domain_request.is_election_board = None
|
||||
self.domain_request.save()
|
||||
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||
self.assertFalse(self.domain_request._is_special_district_complete())
|
||||
self.domain_request.about_your_organization = None
|
||||
self.domain_request.save()
|
||||
self.assertFalse(self.domain_request._is_special_district_complete())
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
|
|
@ -6,6 +6,8 @@ from registrar.models import (
|
|||
Domain,
|
||||
UserDomainRole,
|
||||
)
|
||||
from registrar.models import Portfolio
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||
from registrar.utility.csv_export import (
|
||||
DomainDataFull,
|
||||
DomainDataType,
|
||||
|
@ -32,6 +34,7 @@ from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # typ
|
|||
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):
|
||||
|
@ -311,6 +314,80 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
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"""
|
||||
|
|
|
@ -578,10 +578,9 @@ class DomainDataTypeUser(DomainDataType):
|
|||
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):
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue