From 7ee55d2f0b92bb6484b7bbbb2fee20df0336b8d5 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 28 Apr 2026 14:47:29 -0700 Subject: [PATCH 1/9] added ability for fixtures to add standard users for testing --- src/registrar/fixtures/fixtures_requests.py | 2 + .../fixtures_standard_user_domains.py | 238 ++++++++++++++++++ .../fixtures_user_portfolio_permissions.py | 48 ++++ src/registrar/fixtures/fixtures_users.py | 108 +++++++- src/registrar/management/commands/load.py | 9 + src/registrar/tests/test_views_portfolio.py | 2 +- 6 files changed, 396 insertions(+), 11 deletions(-) create mode 100644 src/registrar/fixtures/fixtures_standard_user_domains.py diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index 22bd439585..ce8c0d5556 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -183,6 +183,8 @@ def _set_non_foreign_key_fields(cls, request: DomainRequest, request_dict: dict) @classmethod def _set_foreign_key_fields(cls, request: DomainRequest, request_dict: dict, user: User): """Helper method used by `load`.""" + if not user.is_staff: + user= random.choice(User.objects.filter(is_staff=True)) request.investigator = cls._get_investigator(request, request_dict, user) request.senior_official = cls._get_senior_official(request, request_dict) request.requested_domain = cls._get_requested_domain(request, request_dict) diff --git a/src/registrar/fixtures/fixtures_standard_user_domains.py b/src/registrar/fixtures/fixtures_standard_user_domains.py new file mode 100644 index 0000000000..45764d8155 --- /dev/null +++ b/src/registrar/fixtures/fixtures_standard_user_domains.py @@ -0,0 +1,238 @@ +from datetime import timedelta +import logging +import random + +from django.utils import timezone + +from registrar.fixtures.fixtures_requests import DomainRequestFixture +from registrar.fixtures.fixtures_users import UserFixture +from registrar.models import User, DomainRequest +from registrar.models.domain import Domain + +logger = logging.getLogger(__name__) + + +class StandardUserDomainFixture(DomainRequestFixture): + """ + Creates domain requests and domains for standard test users. + + For each standard user: + - 7 IN_REVIEW requests are approved, creating a domain per request. Each + domain's state is then forced to cover every Domain.State value plus + expired and expiring-soon variants. These domains will show up as "unknown" in EPP still, + - Additional non-approved requests cover the remaining DomainRequestStatus + values (excluding INELIGIBLE). + + Domain states are forced via Domain.objects.filter(...).update(state=...) which + bypasses FSMField(protected=True) on Domain.state — no EPP/OT&E commands are + sent. These domains exist solely for UI filter testing. + + Depends on fixtures_users (STANDARD_USERS must be loaded first). + + Make sure this class' `load` method is called from `handle` + in management/commands/load.py, then use `./manage.py load` + to run this code. + """ + + # Each entry produces one approved domain request and one domain whose state + # is forced to _target_state after approval. + DOMAIN_STATE_CONFIGS = [ + { + "organization_name": "Standard User - Domain Unknown", + "_target_state": Domain.State.UNKNOWN, + }, + { + "organization_name": "Standard User - Domain DNS Needed", + "_target_state": Domain.State.DNS_NEEDED, + }, + { + "organization_name": "Standard User - Domain Ready", + "_target_state": Domain.State.READY, + }, + { + "organization_name": "Standard User - Domain On Hold", + "_target_state": Domain.State.ON_HOLD, + }, + { + "organization_name": "Standard User - Domain Deleted", + "_target_state": Domain.State.DELETED, + }, + { + "organization_name": "Standard User - Domain Expired", + "_target_state": Domain.State.READY, + "_expired": True, + }, + { + "organization_name": "Standard User - Domain Expiring Soon", + "_target_state": Domain.State.READY, + "_expiring_soon": True, + }, + ] + + # Non-approved requests covering all remaining DomainRequestStatus values. + # APPROVED is already represented by the 7 approved requests above. + # INELIGIBLE is intentionally excluded. + STATUS_REQUEST_CONFIGS = [ + { + "status": DomainRequest.DomainRequestStatus.STARTED, + "organization_name": "Standard User - Started", + }, + { + "status": DomainRequest.DomainRequestStatus.SUBMITTED, + "organization_name": "Standard User - Submitted", + }, + { + "status": DomainRequest.DomainRequestStatus.IN_REVIEW, + "organization_name": "Standard User - In Review", + },{ + "status": DomainRequest.DomainRequestStatus.IN_REVIEW, + "organization_name": "Standard User - In Review", + },{ + "status": DomainRequest.DomainRequestStatus.IN_REVIEW, + "organization_name": "Standard User - In Review", + },{ + "status": DomainRequest.DomainRequestStatus.IN_REVIEW, + "organization_name": "Standard User - In Review", + },{ + "status": DomainRequest.DomainRequestStatus.IN_REVIEW, + "organization_name": "Standard User - In Review", + },{ + "status": DomainRequest.DomainRequestStatus.IN_REVIEW, + "organization_name": "Standard User - In Review", + },{ + "status": DomainRequest.DomainRequestStatus.IN_REVIEW, + "organization_name": "Standard User - In Review", + },{ + "status": DomainRequest.DomainRequestStatus.IN_REVIEW, + "organization_name": "Standard User - In Review", + }, + { + "status": DomainRequest.DomainRequestStatus.IN_REVIEW_OMB, + "organization_name": "Standard User - In Review OMB", + }, + { + "status": DomainRequest.DomainRequestStatus.ACTION_NEEDED, + "organization_name": "Standard User - Action Needed", + }, + { + "status": DomainRequest.DomainRequestStatus.WITHDRAWN, + "organization_name": "Standard User - Withdrawn", + }, + { + "status": DomainRequest.DomainRequestStatus.REJECTED, + "organization_name": "Standard User - Rejected", + }, + ] + + @classmethod + def load(cls): + standard_usernames = [u["username"] for u in UserFixture.STANDARD_USERS] + users = list(User.objects.filter(username__in=standard_usernames)) + + if not users: + logger.warning("Standard users not found, skipping StandardUserDomainFixture.") + return + + cls._create_status_requests(users) + cls._create_domains(users) + + @classmethod + def _create_status_requests(cls, users): + """Bulk-creates the non-approved status requests for each standard user.""" + requests_to_create = [] + for user in users: + for config in cls.STATUS_REQUEST_CONFIGS: + try: + request = DomainRequest( + requester=user, + organization_name=config["organization_name"], + ) + cls._set_non_foreign_key_fields(request, config) + cls._set_foreign_key_fields(request, {}, user) + requests_to_create.append(request) + except Exception as e: + logger.warning(f"Error preparing status request for {user}: {e}") + + cls._bulk_create_requests(requests_to_create) + + for request in requests_to_create: + try: + cls._set_many_to_many_relations(request, {}) + except Exception as e: + logger.warning(e) + + + + @classmethod + def _create_domains(cls, users): + """ + Uses the 7 IN_REVIEW requests per standard user and approves them with + approve(), creating a Domain and a UserDomainRole.MANAGER for the requester), then bulk-updates the request + statuses. Domain states are forced in _force_domain_states(). + """ + for user in users: + approved_pairs = [] + # get all the in_review requests for this user, up to the number of domain state configs we have + in_review_requests = list( + DomainRequest.objects.filter( + requester=user, + status=DomainRequest.DomainRequestStatus.IN_REVIEW, + ).order_by("id")[:len (cls.DOMAIN_STATE_CONFIGS)] + ) + + if len(in_review_requests) != len(cls.DOMAIN_STATE_CONFIGS): + logger.warning( + f"Not enough IN_REVIEW requests for user {user.username} to approve. " + f"Expected {len(cls.DOMAIN_STATE_CONFIGS)}, found {len(in_review_requests)}." + ) + continue + + for request, config in zip (in_review_requests, cls.DOMAIN_STATE_CONFIGS): + + try: + request.investigator = random.choice(User.objects.filter(is_staff=True)) # nosec + request.approve(send_email=False) + approved_pairs.append((request, config)) + except Exception as e: + logger.warning(f"Cannot approve domain request for {user}: {e}") + + if approved_pairs: + try: + DomainRequest.objects.bulk_update( + [r for r, _ in approved_pairs], ["status", "investigator"] + ) + except Exception as e: + logger.error(f"Error bulk updating domain requests for {user}: {e}") + + cls._force_domain_states(approved_pairs) + + @classmethod + def _force_domain_states(cls, approved_pairs): + """ + Forces each approved domain into its target state via queryset update(). + Domain.state is FSMField(protected=True), so direct attribute assignment raises AttributeError + — queryset update() issues a raw SQL UPDATE that bypasses the FSM with no EPP calls. + Expect these domains to throw errors or switch back to "unknown" if any code tries to send EPP commands for them + (such as clicking 'manage' on the domain's table) + """ + today = timezone.now().date() + for request, config in approved_pairs: + try: + domain = Domain.objects.get(domain_info__domain_request=request) + except Domain.DoesNotExist: + logger.warning(f"Domain not found for request {request.id}, skipping state update.") + continue + + target_state = config["_target_state"] + + if config.get("_expired"): + expiration_date = today - timedelta(days=random.randint(1, 365)) # nosec + elif config.get("_expiring_soon"): + expiration_date = today + timedelta(days=random.randint(1, 30)) # nosec + else: + expiration_date = today + timedelta(days=random.randint(31, 365)) # nosec + + Domain.objects.filter(id=domain.id).update( + state=target_state, + expiration_date=expiration_date, + ) diff --git a/src/registrar/fixtures/fixtures_user_portfolio_permissions.py b/src/registrar/fixtures/fixtures_user_portfolio_permissions.py index e2c84f8177..1715e35e2e 100644 --- a/src/registrar/fixtures/fixtures_user_portfolio_permissions.py +++ b/src/registrar/fixtures/fixtures_user_portfolio_permissions.py @@ -74,6 +74,54 @@ def load(cls): # Bulk create permissions cls._bulk_create_permissions(user_portfolio_permissions_to_create) + cls.load_standard_user_permissions() + + @classmethod + def load_standard_user_permissions(cls): + """Assigns ORGANIZATION_MEMBER role to standard users on a random portfolio.""" + logger.info("Going to set standard user portfolio permissions") + try: + standard_usernames = [u["username"] for u in UserFixture.STANDARD_USERS] + users = list(User.objects.filter(username__in=standard_usernames)) + organization_names = [p["organization_name"] for p in PortfolioFixture.PORTFOLIOS] + portfolios = list(Portfolio.objects.filter(organization_name__in=organization_names)) + + if not users: + logger.warning("Standard user fixtures missing.") + return + if not portfolios: + logger.warning("Portfolio fixtures missing.") + return + except Exception as e: + logger.warning(f"Error occurred while fetching standard user or portfolio data: {e}", exc_info=True) + return + + permissions_to_create = [] + for user in users: + portfolio = random.choice(portfolios) # nosec + try: + if not UserPortfolioPermission.objects.filter(user=user, portfolio=portfolio).exists(): + permissions_to_create.append( + UserPortfolioPermission( + user=user, + portfolio=portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER], + additional_permissions=[ + UserPortfolioPermissionChoices.EDIT_REQUESTS, + UserPortfolioPermissionChoices.VIEW_MEMBERS, + ], + ) + ) + else: + logger.info( + f"Permission exists for user '{user.username}' " + f"on portfolio '{portfolio.organization_name}'." + ) + except Exception as e: + logger.warning(e) + + # Bulk create permissions for standard users + cls._bulk_create_permissions(permissions_to_create) @classmethod def _bulk_create_permissions(cls, user_portfolio_permissions_to_create): diff --git a/src/registrar/fixtures/fixtures_users.py b/src/registrar/fixtures/fixtures_users.py index cfc3618d96..0a601ed89b 100644 --- a/src/registrar/fixtures/fixtures_users.py +++ b/src/registrar/fixtures/fixtures_users.py @@ -298,6 +298,64 @@ class UserFixture: }, ] + STANDARD_USERS = [ + { + + "username": "21", + "first_name": "FAKEY", + "last_name": "MCFAKERSON_1", + "email": "feedback+1@get.gov", + }, + { + + "username": "2", + "first_name": "FAKEY", + "last_name": "MCFAKERSON_2", + "email": "feedback+2@get.gov", + }, + { + + "username": "3", + "first_name": "FAKEY", + "last_name": "MCFAKERSON_3", + "email": "feedback+3@get.gov", + }, + { + + "username": "4", + "first_name": "FAKEY", + "last_name": "MCFAKERSON_4", + "email": "feedback+4@get.gov", + }, + { + + "username": "5", + "first_name": "FAKEY", + "last_name": "MCFAKERSON_5", + "email": "feedback+5@get.gov", + }, + { + + "username": "6", + "first_name": "FAKEY", + "last_name": "MCFAKERSON_6", + "email": "feedback+6@get.gov", + }, + { + + "username": "7", + "first_name": "FAKEY", + "last_name": "MCFAKERSON_7", + "email": "feedback+7@get.gov", + }, + { + + "username": "8", + "first_name": "FAKEY", + "last_name": "MCFAKERSON_8", + "email": "feedback+8@get.gov", + }, + ] # Additional emails to add to the AllowedEmail whitelist. ADDITIONAL_ALLOWED_EMAILS: list[str] = [] @@ -396,14 +454,16 @@ def _get_existing_users(users): return existing_usernames, existing_user_ids @staticmethod - def _prepare_new_users(users, existing_usernames, existing_user_ids, are_superusers): + def _prepare_new_users(users, existing_usernames, existing_user_ids, are_superusers, is_staff=True): new_users = [] for i, user_data in enumerate(users): - username = user_data.get("username") + id = user_data.get("id") first_name = user_data.get("first_name", "Bob") last_name = user_data.get("last_name", "Builder") - + # If username is not provided, create one (must be unique) + username = user_data.get("username", first_name+last_name+str(id)) + default_email = f"placeholder.{first_name.lower()}.{last_name.lower()}+{i}@igorville.gov" email = user_data.get("email", default_email) if username not in existing_usernames and id not in existing_user_ids: @@ -411,12 +471,12 @@ def _prepare_new_users(users, existing_usernames, existing_user_ids, are_superus id=id, first_name=first_name, last_name=last_name, - username=username, + username= username, email=email, title=user_data.get("title", "team member"), phone=user_data.get("phone", "2022222222"), is_active=user_data.get("is_active", True), - is_staff=True, + is_staff=is_staff, is_superuser=are_superusers, ) new_users.append(user) @@ -434,17 +494,20 @@ def _create_new_users(new_users): logger.info("No new users to create.") @staticmethod - def _get_users_to_update(users): + def _get_users_to_update(users, is_staff=True): users_to_update = [] for user in users: updated = False - if not user.title: + if not user.title and is_staff: user.title = "Team member" updated = True + if not user.title and not is_staff: + user.title = "User testing account" + updated = True if not user.phone: user.phone = "2022222222" updated = True - if not user.is_staff: + if not user.is_staff and is_staff: user.is_staff = True updated = True if updated: @@ -463,13 +526,38 @@ def _assign_users_to_group(group, users): if users_not_in_group.exists(): group.user_set.add(*users_not_in_group) + @classmethod + def load_standard_users(cls): + """Loads standard (non-staff) test users without assigning an admin group.""" + logger.info(f"Going to load {len(cls.STANDARD_USERS)} standard users") + try: + existing_usernames, existing_user_ids = cls._get_existing_users(cls.STANDARD_USERS) + new_users = cls._prepare_new_users( + cls.STANDARD_USERS, + existing_usernames, + existing_user_ids, + are_superusers=False, + is_staff=False, + ) + cls._create_new_users(new_users) + + created_or_existing = User.objects.filter( + username__in=[u["username"] for u in cls.STANDARD_USERS] + ) + users_to_update = cls._get_users_to_update(created_or_existing, is_staff=False) + cls._update_existing_users(users_to_update) + logger.info("Standard users loaded.") + except Exception as e: + logger.warning(e) + @classmethod def load(cls, delete_existing_allowed_emails=False): cls.load_users(cls.ADMINS, "full_access_group", are_superusers=True) cls.load_users(cls.STAFF, "cisa_analysts_group") + cls.load_standard_users() - # Combine ADMINS and STAFF lists - all_users = cls.ADMINS + cls.STAFF + # Combine ADMINS, STAFF, and STANDARD_USERS lists + all_users = cls.ADMINS + cls.STAFF + cls.STANDARD_USERS cls.load_allowed_emails( cls, all_users, diff --git a/src/registrar/management/commands/load.py b/src/registrar/management/commands/load.py index 5d4d0d13a7..dbff1941bc 100644 --- a/src/registrar/management/commands/load.py +++ b/src/registrar/management/commands/load.py @@ -1,9 +1,11 @@ import logging +from django.conf import settings from django.core.management.base import BaseCommand from auditlog.context import disable_auditlog from registrar.fixtures.fixtures_dnsrecord import DnsRecordFixture from registrar.fixtures.fixtures_domains import DomainFixture +from registrar.fixtures.fixtures_standard_user_domains import StandardUserDomainFixture from registrar.fixtures.fixtures_portfolios import PortfolioFixture from registrar.fixtures.fixtures_requests import DomainRequestFixture from registrar.fixtures.fixtures_suborganizations import SuborganizationFixture @@ -23,6 +25,13 @@ def handle(self, *args, **options): SuborganizationFixture.load() DomainRequestFixture.load() DomainFixture.load() + + # set standardUserDomainFixture to not run locally, as these users are for user testing + # user testing should not be done locally AND these fixtures will eventually + # send messages in EPP. EPP code would fail locally. + # if not settings.IS_LOCAL: + if True: + StandardUserDomainFixture.load() UserPortfolioPermissionFixture.load() DnsRecordFixture.load() logger.info("All fixtures loaded.") diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 1ff9d5beec..fa9614a2d7 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -4761,7 +4761,7 @@ def test_edit_member_permissions_admin_to_basic( portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], ) - print(admin_permission) + mock_send_removal_emails.return_value = True mock_send_update_email.return_value = True From c8e8a134c1a9cc23ff821246866906e2bc9b2f47 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 28 Apr 2026 14:58:36 -0700 Subject: [PATCH 2/9] updated documentation --- docs/developer/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/developer/README.md b/docs/developer/README.md index c4c7fd0885..0b358f050d 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -127,6 +127,16 @@ Analysts are a variant of the admin role with limited permissions. The process f Do note that if you wish to have both an analyst and admin account, append `-Analyst` to your first and last name, or use a completely different first/last name to avoid confusion. Example: `Bob-Analyst` +## Adding `Standard` Users & what they are + +Standard users are non-staff, non-admin test accounts used for user research. Each is pre-loaded with domain requests in every `DomainRequestStatus` and domains in every `Domain.State`. + +These users load automatically on sandboxes when `./manage.py load` runs (such as via `reset-db.yml`). They do **not** load locally, as if emails are turned on locally it could interfer with user testing. + +To add or update a standard user, edit the `STANDARD_USERS` list in [fixtures_users](../../src/registrar/fixtures/fixtures_users.py). The `username` field must be the UUID from the user's login.gov identity sandbox account. + +These users are added to the email whitelist by default. + ## Adding an email address to the email whitelist (sandboxes only) On all non-production environments, we use an email whitelist table (called `Allowed emails`). This whitelist is not case sensitive, and it provides an inclusion for +1 emails (like example.person+1@igorville.gov). The content after the `+` can be any _digit_. The whitelist checks for the "base" email (example.person) so even if you only have the +1 email defined, an email will still be sent assuming that it follows those conventions. From 0d5cd22c4c8f66c5415212c6e736eb48419ef506 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 28 Apr 2026 14:58:58 -0700 Subject: [PATCH 3/9] turn on dns hosting for standard user's domains --- src/registrar/fixtures/fixtures_standard_user_domains.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/registrar/fixtures/fixtures_standard_user_domains.py b/src/registrar/fixtures/fixtures_standard_user_domains.py index 45764d8155..377a789920 100644 --- a/src/registrar/fixtures/fixtures_standard_user_domains.py +++ b/src/registrar/fixtures/fixtures_standard_user_domains.py @@ -235,4 +235,5 @@ def _force_domain_states(cls, approved_pairs): Domain.objects.filter(id=domain.id).update( state=target_state, expiration_date=expiration_date, + is_enrolled_in_epp=True, ) From ce91c2ba0d4a98dc8fff9de1449987e84325161f Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Tue, 28 Apr 2026 15:06:16 -0700 Subject: [PATCH 4/9] removed if True to force it to run locally --- src/registrar/management/commands/load.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/registrar/management/commands/load.py b/src/registrar/management/commands/load.py index dbff1941bc..c609dbaeaa 100644 --- a/src/registrar/management/commands/load.py +++ b/src/registrar/management/commands/load.py @@ -25,12 +25,11 @@ def handle(self, *args, **options): SuborganizationFixture.load() DomainRequestFixture.load() DomainFixture.load() - + # set standardUserDomainFixture to not run locally, as these users are for user testing # user testing should not be done locally AND these fixtures will eventually # send messages in EPP. EPP code would fail locally. - # if not settings.IS_LOCAL: - if True: + if not settings.IS_LOCAL: StandardUserDomainFixture.load() UserPortfolioPermissionFixture.load() DnsRecordFixture.load() From 7ec230b8f70af77599e0bca0347a031066e77153 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Wed, 29 Apr 2026 17:14:15 -0700 Subject: [PATCH 5/9] fixed linting issues and updated the uuids --- src/registrar/fixtures/fixtures_requests.py | 2 +- .../fixtures_standard_user_domains.py | 39 +++++++++---------- src/registrar/fixtures/fixtures_users.py | 29 +++++--------- src/registrar/management/commands/load.py | 2 +- src/registrar/tests/test_views_portfolio.py | 2 +- 5 files changed, 31 insertions(+), 43 deletions(-) diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index ce8c0d5556..90090a735d 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -184,7 +184,7 @@ def _set_non_foreign_key_fields(cls, request: DomainRequest, request_dict: dict) def _set_foreign_key_fields(cls, request: DomainRequest, request_dict: dict, user: User): """Helper method used by `load`.""" if not user.is_staff: - user= random.choice(User.objects.filter(is_staff=True)) + user = random.choice(User.objects.filter(is_staff=True)) request.investigator = cls._get_investigator(request, request_dict, user) request.senior_official = cls._get_senior_official(request, request_dict) request.requested_domain = cls._get_requested_domain(request, request_dict) diff --git a/src/registrar/fixtures/fixtures_standard_user_domains.py b/src/registrar/fixtures/fixtures_standard_user_domains.py index 377a789920..07f9d6f3cd 100644 --- a/src/registrar/fixtures/fixtures_standard_user_domains.py +++ b/src/registrar/fixtures/fixtures_standard_user_domains.py @@ -84,25 +84,25 @@ class StandardUserDomainFixture(DomainRequestFixture): { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", - },{ + }, { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", - },{ + }, { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", - },{ + }, { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", - },{ + }, { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", - },{ + }, { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", - },{ + }, { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", - },{ + }, { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", }, @@ -128,11 +128,11 @@ class StandardUserDomainFixture(DomainRequestFixture): def load(cls): standard_usernames = [u["username"] for u in UserFixture.STANDARD_USERS] users = list(User.objects.filter(username__in=standard_usernames)) - + if not users: logger.warning("Standard users not found, skipping StandardUserDomainFixture.") return - + cls._create_status_requests(users) cls._create_domains(users) @@ -160,8 +160,6 @@ def _create_status_requests(cls, users): cls._set_many_to_many_relations(request, {}) except Exception as e: logger.warning(e) - - @classmethod def _create_domains(cls, users): @@ -173,13 +171,13 @@ def _create_domains(cls, users): for user in users: approved_pairs = [] # get all the in_review requests for this user, up to the number of domain state configs we have - in_review_requests = list( + in_review_requests = list( DomainRequest.objects.filter( requester=user, - status=DomainRequest.DomainRequestStatus.IN_REVIEW, - ).order_by("id")[:len (cls.DOMAIN_STATE_CONFIGS)] - ) - + status=DomainRequest.DomainRequestStatus.IN_REVIEW, + ).order_by("id")[:len(cls.DOMAIN_STATE_CONFIGS)] + ) + if len(in_review_requests) != len(cls.DOMAIN_STATE_CONFIGS): logger.warning( f"Not enough IN_REVIEW requests for user {user.username} to approve. " @@ -187,8 +185,7 @@ def _create_domains(cls, users): ) continue - for request, config in zip (in_review_requests, cls.DOMAIN_STATE_CONFIGS): - + for request, config in zip(in_review_requests, cls.DOMAIN_STATE_CONFIGS): try: request.investigator = random.choice(User.objects.filter(is_staff=True)) # nosec request.approve(send_email=False) @@ -203,16 +200,16 @@ def _create_domains(cls, users): ) except Exception as e: logger.error(f"Error bulk updating domain requests for {user}: {e}") - + cls._force_domain_states(approved_pairs) @classmethod def _force_domain_states(cls, approved_pairs): """ Forces each approved domain into its target state via queryset update(). - Domain.state is FSMField(protected=True), so direct attribute assignment raises AttributeError + Domain.state is FSMField(protected=True), so direct attribute assignment raises AttributeError — queryset update() issues a raw SQL UPDATE that bypasses the FSM with no EPP calls. - Expect these domains to throw errors or switch back to "unknown" if any code tries to send EPP commands for them + Expect these domains to throw errors or switch back to "unknown" if any code tries to send EPP commands for them (such as clicking 'manage' on the domain's table) """ today = timezone.now().date() diff --git a/src/registrar/fixtures/fixtures_users.py b/src/registrar/fixtures/fixtures_users.py index 0a601ed89b..8779fabbfc 100644 --- a/src/registrar/fixtures/fixtures_users.py +++ b/src/registrar/fixtures/fixtures_users.py @@ -300,57 +300,49 @@ class UserFixture: STANDARD_USERS = [ { - - "username": "21", + "username": "e2201529-6901-44ee-9698-0d08bfe7fb01", "first_name": "FAKEY", "last_name": "MCFAKERSON_1", "email": "feedback+1@get.gov", }, { - - "username": "2", + "username": "74cca338-21d6-4135-910e-4cf9ce179ea6", "first_name": "FAKEY", "last_name": "MCFAKERSON_2", "email": "feedback+2@get.gov", }, { - - "username": "3", + "username": "22acf459-79d5-43b3-a73e-a81b30cc693c", "first_name": "FAKEY", "last_name": "MCFAKERSON_3", "email": "feedback+3@get.gov", }, { - - "username": "4", + "username": "f88283cc-0a1f-4c23-b1a8-0cd31411d852", "first_name": "FAKEY", "last_name": "MCFAKERSON_4", "email": "feedback+4@get.gov", }, { - - "username": "5", + "username": "c2e02542-8cc4-4d95-b153-fb97706a72f1", "first_name": "FAKEY", "last_name": "MCFAKERSON_5", "email": "feedback+5@get.gov", }, { - - "username": "6", + "username": "fbb06d6c-c58f-49c0-8845-21f0c77b6497", "first_name": "FAKEY", "last_name": "MCFAKERSON_6", "email": "feedback+6@get.gov", }, { - - "username": "7", + "username": "9e820a1c-c570-465a-9fc3-979c65f3ab17", "first_name": "FAKEY", "last_name": "MCFAKERSON_7", "email": "feedback+7@get.gov", }, { - - "username": "8", + "username": "b8194c1a-4dec-4b3e-9cc1-829f3dbf78a7", "first_name": "FAKEY", "last_name": "MCFAKERSON_8", "email": "feedback+8@get.gov", @@ -457,13 +449,12 @@ def _get_existing_users(users): def _prepare_new_users(users, existing_usernames, existing_user_ids, are_superusers, is_staff=True): new_users = [] for i, user_data in enumerate(users): - id = user_data.get("id") first_name = user_data.get("first_name", "Bob") last_name = user_data.get("last_name", "Builder") # If username is not provided, create one (must be unique) username = user_data.get("username", first_name+last_name+str(id)) - + default_email = f"placeholder.{first_name.lower()}.{last_name.lower()}+{i}@igorville.gov" email = user_data.get("email", default_email) if username not in existing_usernames and id not in existing_user_ids: @@ -471,7 +462,7 @@ def _prepare_new_users(users, existing_usernames, existing_user_ids, are_superus id=id, first_name=first_name, last_name=last_name, - username= username, + username=username, email=email, title=user_data.get("title", "team member"), phone=user_data.get("phone", "2022222222"), diff --git a/src/registrar/management/commands/load.py b/src/registrar/management/commands/load.py index c609dbaeaa..a46c32116c 100644 --- a/src/registrar/management/commands/load.py +++ b/src/registrar/management/commands/load.py @@ -28,7 +28,7 @@ def handle(self, *args, **options): # set standardUserDomainFixture to not run locally, as these users are for user testing # user testing should not be done locally AND these fixtures will eventually - # send messages in EPP. EPP code would fail locally. + # send messages in EPP. EPP code would fail locally. if not settings.IS_LOCAL: StandardUserDomainFixture.load() UserPortfolioPermissionFixture.load() diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index fa9614a2d7..ae0ff56f93 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -4761,7 +4761,7 @@ def test_edit_member_permissions_admin_to_basic( portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], ) - + mock_send_removal_emails.return_value = True mock_send_update_email.return_value = True From f621f408d1505a95d2de659bb226829800a11c4f Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Thu, 30 Apr 2026 09:26:45 -0700 Subject: [PATCH 6/9] ran lint --- .../fixtures_standard_user_domains.py | 27 +++++++++++-------- src/registrar/fixtures/fixtures_users.py | 6 ++--- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/registrar/fixtures/fixtures_standard_user_domains.py b/src/registrar/fixtures/fixtures_standard_user_domains.py index 07f9d6f3cd..025f805716 100644 --- a/src/registrar/fixtures/fixtures_standard_user_domains.py +++ b/src/registrar/fixtures/fixtures_standard_user_domains.py @@ -84,25 +84,32 @@ class StandardUserDomainFixture(DomainRequestFixture): { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", - }, { + }, + { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", - }, { + }, + { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", - }, { + }, + { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", - }, { + }, + { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", - }, { + }, + { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", - }, { + }, + { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", - }, { + }, + { "status": DomainRequest.DomainRequestStatus.IN_REVIEW, "organization_name": "Standard User - In Review", }, @@ -175,7 +182,7 @@ def _create_domains(cls, users): DomainRequest.objects.filter( requester=user, status=DomainRequest.DomainRequestStatus.IN_REVIEW, - ).order_by("id")[:len(cls.DOMAIN_STATE_CONFIGS)] + ).order_by("id")[: len(cls.DOMAIN_STATE_CONFIGS)] ) if len(in_review_requests) != len(cls.DOMAIN_STATE_CONFIGS): @@ -195,9 +202,7 @@ def _create_domains(cls, users): if approved_pairs: try: - DomainRequest.objects.bulk_update( - [r for r, _ in approved_pairs], ["status", "investigator"] - ) + DomainRequest.objects.bulk_update([r for r, _ in approved_pairs], ["status", "investigator"]) except Exception as e: logger.error(f"Error bulk updating domain requests for {user}: {e}") diff --git a/src/registrar/fixtures/fixtures_users.py b/src/registrar/fixtures/fixtures_users.py index 8779fabbfc..aca89975aa 100644 --- a/src/registrar/fixtures/fixtures_users.py +++ b/src/registrar/fixtures/fixtures_users.py @@ -453,7 +453,7 @@ def _prepare_new_users(users, existing_usernames, existing_user_ids, are_superus first_name = user_data.get("first_name", "Bob") last_name = user_data.get("last_name", "Builder") # If username is not provided, create one (must be unique) - username = user_data.get("username", first_name+last_name+str(id)) + username = user_data.get("username", first_name + last_name + str(id)) default_email = f"placeholder.{first_name.lower()}.{last_name.lower()}+{i}@igorville.gov" email = user_data.get("email", default_email) @@ -532,9 +532,7 @@ def load_standard_users(cls): ) cls._create_new_users(new_users) - created_or_existing = User.objects.filter( - username__in=[u["username"] for u in cls.STANDARD_USERS] - ) + created_or_existing = User.objects.filter(username__in=[u["username"] for u in cls.STANDARD_USERS]) users_to_update = cls._get_users_to_update(created_or_existing, is_staff=False) cls._update_existing_users(users_to_update) logger.info("Standard users loaded.") From 31ef54b3a6e0e3cf2cc3317197ffb289b55238dd Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Thu, 30 Apr 2026 10:22:44 -0700 Subject: [PATCH 7/9] added no sec lint --- src/registrar/fixtures/fixtures_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index 90090a735d..33c971aacf 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -184,7 +184,7 @@ def _set_non_foreign_key_fields(cls, request: DomainRequest, request_dict: dict) def _set_foreign_key_fields(cls, request: DomainRequest, request_dict: dict, user: User): """Helper method used by `load`.""" if not user.is_staff: - user = random.choice(User.objects.filter(is_staff=True)) + user = random.choice(User.objects.filter(is_staff=True)) #nosec request.investigator = cls._get_investigator(request, request_dict, user) request.senior_official = cls._get_senior_official(request, request_dict) request.requested_domain = cls._get_requested_domain(request, request_dict) From 35146f270d45cfbc9739aa19e8a43ed6d5b5abad Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Thu, 30 Apr 2026 11:36:27 -0700 Subject: [PATCH 8/9] still linting --- src/registrar/fixtures/fixtures_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py index 33c971aacf..474c28441a 100644 --- a/src/registrar/fixtures/fixtures_requests.py +++ b/src/registrar/fixtures/fixtures_requests.py @@ -184,7 +184,7 @@ def _set_non_foreign_key_fields(cls, request: DomainRequest, request_dict: dict) def _set_foreign_key_fields(cls, request: DomainRequest, request_dict: dict, user: User): """Helper method used by `load`.""" if not user.is_staff: - user = random.choice(User.objects.filter(is_staff=True)) #nosec + user = random.choice(User.objects.filter(is_staff=True)) # nosec request.investigator = cls._get_investigator(request, request_dict, user) request.senior_official = cls._get_senior_official(request, request_dict) request.requested_domain = cls._get_requested_domain(request, request_dict) From bf6759caebed44b7955a420a821aa1361c9174a7 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Fri, 8 May 2026 11:43:59 -0700 Subject: [PATCH 9/9] dns hosting typo fixed --- src/registrar/fixtures/fixtures_standard_user_domains.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/fixtures/fixtures_standard_user_domains.py b/src/registrar/fixtures/fixtures_standard_user_domains.py index 025f805716..bd4c0362fd 100644 --- a/src/registrar/fixtures/fixtures_standard_user_domains.py +++ b/src/registrar/fixtures/fixtures_standard_user_domains.py @@ -237,5 +237,5 @@ def _force_domain_states(cls, approved_pairs): Domain.objects.filter(id=domain.id).update( state=target_state, expiration_date=expiration_date, - is_enrolled_in_epp=True, + is_enrolled_in_dns_hosting=True, )