diff --git a/.github/ISSUE_TEMPLATE/issue-default.yml b/.github/ISSUE_TEMPLATE/issue-default.yml index 27ec10415..3a34b2943 100644 --- a/.github/ISSUE_TEMPLATE/issue-default.yml +++ b/.github/ISSUE_TEMPLATE/issue-default.yml @@ -6,13 +6,13 @@ body: id: title-help attributes: value: | - > Titles should be short, descriptive, and compelling. + > Titles should be short, descriptive, and compelling. Use sentence case. - type: textarea id: issue-description attributes: - label: Issue description and context + label: Issue description description: | - Describe the issue so that someone who wasn't present for its discovery can understand the problem and why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). Share desired outcomes or potential next steps. Images or links to other content/context (like documents or Slack discussions) are welcome. + Describe the issue so that someone who wasn't present for its discovery can understand why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). validations: required: true - type: textarea @@ -20,16 +20,22 @@ body: attributes: label: Acceptance criteria description: "If known, share 1-3 statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate." - placeholder: "- [ ] The button does the thing." + placeholder: "- [ ]" + - type: textarea + id: additional-context + attributes: + label: Additional context + description: "Share any other thoughts, like how this might be implemented or fixed. Screenshots and links to documents/discussions are welcome." - type: textarea id: links-to-other-issues attributes: label: Links to other issues description: | - Add the issue #number of other issues this relates to and how (e.g., 🚧 Blocks, ⛔️ Is blocked by, 🔄 Relates to). + "Add issue #numbers this relates to and how (e.g., 🚧 :construction: Blocks, ⛔️ :no_entry: Is blocked by, 🔄 :repeat: Relates to)." placeholder: 🔄 Relates to... - type: markdown id: note attributes: value: | - > We may edit this issue's text to document our understanding and clarify the product work. + > We may edit the text in this issue to document our understanding and clarify the product work. + diff --git a/docs/developer/user-permissions.md b/docs/developer/user-permissions.md index 31b69d3b3..f7c41492d 100644 --- a/docs/developer/user-permissions.md +++ b/docs/developer/user-permissions.md @@ -42,7 +42,7 @@ as health checks used by our platform). ## Adding roles The current MVP design uses only a single role called -`UserDomainRole.Roles.ADMIN` that has all access on a domain. As such, the +`UserDomainRole.Roles.MANAGER` that has all access on a domain. As such, the permission mixin doesn't need to examine the `role` field carefully. In the future, as we add additional roles that our product vision calls for (read-only? editing only some information?), we need to add conditional diff --git a/ops/manifests/manifest-stable.yaml b/ops/manifests/manifest-stable.yaml index bc5e933f6..6295fa63b 100644 --- a/ops/manifests/manifest-stable.yaml +++ b/ops/manifests/manifest-stable.yaml @@ -18,7 +18,7 @@ applications: # Tell Django where to find its configuration DJANGO_SETTINGS_MODULE: registrar.config.settings # Tell Django where it is being hosted - DJANGO_BASE_URL: https://getgov-stable.app.cloud.gov + DJANGO_BASE_URL: https://manage.get.gov # Tell Django how much stuff to log DJANGO_LOG_LEVEL: INFO # default public site location diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py index 0bbe01f03..9eab17bf7 100644 --- a/src/api/tests/test_available.py +++ b/src/api/tests/test_available.py @@ -3,19 +3,27 @@ import json from django.contrib.auth import get_user_model -from django.test import TestCase, RequestFactory +from django.test import RequestFactory -from ..views import available, _domains, in_domains +from ..views import available, in_domains from .common import less_console_noise +from registrar.tests.common import MockEppLib +from unittest.mock import call + +from epplibwrapper import ( + commands, + RegistryError, +) API_BASE_PATH = "/api/v1/available/" -class AvailableViewTest(TestCase): +class AvailableViewTest(MockEppLib): """Test that the view function works as expected.""" def setUp(self): + super().setUp() self.user = get_user_model().objects.create(username="username") self.factory = RequestFactory() @@ -29,26 +37,37 @@ class AvailableViewTest(TestCase): response_object = json.loads(response.content) self.assertIn("available", response_object) - def test_domain_list(self): - """Test the domain list that is returned from Github. + def test_in_domains_makes_calls_(self): + """Domain searches successfully make correct mock EPP calls""" + gsa_available = in_domains("gsa.gov") + igorville_available = in_domains("igorvilleremixed.gov") - This does not mock out the external file, it is actually fetched from - the internet. - """ - domains = _domains() - self.assertIn("gsa.gov", domains) - # entries are all lowercase so GSA.GOV is not in the set - self.assertNotIn("GSA.GOV", domains) - self.assertNotIn("igorvilleremixed.gov", domains) - # all the entries have dots - self.assertNotIn("gsa", domains) + """Domain searches successfully make mock EPP calls""" + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.CheckDomain( + ["gsa.gov"], + ), + cleaned=True, + ), + call( + commands.CheckDomain( + ["igorvilleremixed.gov"], + ), + cleaned=True, + ), + ] + ) + """Domain searches return correct availability results""" + self.assertTrue(gsa_available) + self.assertFalse(igorville_available) - def test_in_domains(self): + def test_in_domains_capitalized(self): + """Domain searches work without case sensitivity""" self.assertTrue(in_domains("gsa.gov")) # input is lowercased so GSA.GOV should be found - self.assertTrue(in_domains("GSA.GOV")) - # This domain should not have been registered - self.assertFalse(in_domains("igorvilleremixed.gov")) + self.assertTrue(in_domains("GSA.gov")) def test_in_domains_dotgov(self): """Domain searches work without trailing .gov""" @@ -86,13 +105,18 @@ class AvailableViewTest(TestCase): request.user = self.user response = available(request, domain=bad_string) self.assertFalse(json.loads(response.content)["available"]) + # domain set to raise error successfully raises error + with self.assertRaises(RegistryError): + error_domain_available = available(request, "errordomain.gov") + self.assertFalse(json.loads(error_domain_available.content)["available"]) -class AvailableAPITest(TestCase): +class AvailableAPITest(MockEppLib): """Test that the API can be called as expected.""" def setUp(self): + super().setUp() self.user = get_user_model().objects.create(username="username") def test_available_get(self): diff --git a/src/api/views.py b/src/api/views.py index e19e060ef..e8b8431de 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -3,8 +3,6 @@ from django.apps import apps from django.views.decorators.http import require_http_methods from django.http import JsonResponse -from django.contrib.auth.decorators import login_required - import requests from cachetools.func import ttl_cache @@ -59,16 +57,15 @@ def in_domains(domain): given domain doesn't end with .gov, ".gov" is added when looking for a match. """ - domain = domain.lower() + Domain = apps.get_model("registrar.Domain") if domain.endswith(".gov"): - return domain.lower() in _domains() + return Domain.available(domain) else: # domain search string doesn't end with .gov, add it on here - return (domain + ".gov") in _domains() + return Domain.available(domain + ".gov") @require_http_methods(["GET"]) -@login_required def available(request, domain=""): """Is a given domain available or not. diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 8d0ed8c2e..904ce66a4 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -219,9 +219,9 @@ class MyUserAdmin(BaseUserAdmin): # (which should in theory be the ONLY group) def group(self, obj): if obj.groups.filter(name="full_access_group").exists(): - return "Full access" + return "full_access_group" elif obj.groups.filter(name="cisa_analysts_group").exists(): - return "Analyst" + return "cisa_analysts_group" return "" def get_list_display(self, request): @@ -294,6 +294,26 @@ class ContactAdmin(ListHeaderAdmin): contact.admin_order_field = "first_name" # type: ignore + # Read only that we'll leverage for CISA Analysts + analyst_readonly_fields = [ + "user", + ] + + def get_readonly_fields(self, request, obj=None): + """Set the read-only state on form elements. + We have 1 conditions that determine which fields are read-only: + admin user permissions. + """ + + readonly_fields = list(self.readonly_fields) + + if request.user.has_perm("registrar.full_access_permission"): + return readonly_fields + # Return restrictive Read-only fields for analysts and + # users who might not belong to groups + readonly_fields.extend([field for field in self.analyst_readonly_fields]) + return readonly_fields # Read-only fields for analysts + class WebsiteAdmin(ListHeaderAdmin): """Custom website admin class.""" @@ -420,9 +440,6 @@ class DomainInformationAdmin(ListHeaderAdmin): "creator", "type_of_work", "more_organization_information", - "address_line1", - "address_line2", - "zipcode", "domain", "submitter", "no_other_contacts_rationale", @@ -557,9 +574,6 @@ class DomainApplicationAdmin(ListHeaderAdmin): analyst_readonly_fields = [ "creator", "about_your_organization", - "address_line1", - "address_line2", - "zipcode", "requested_domain", "alternative_domains", "purpose", @@ -721,7 +735,7 @@ class DomainAdmin(ListHeaderAdmin): ] def organization_type(self, obj): - return obj.domain_info.organization_type + return obj.domain_info.get_organization_type_display() organization_type.admin_order_field = ( # type: ignore "domain_info__organization_type" diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 718bd5792..0857ec603 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -22,15 +22,15 @@ a.breadcrumb__back { } } -a.usa-button { +a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { text-decoration: none; color: color('white'); } -a.usa-button:visited, -a.usa-button:hover, -a.usa-button:focus, -a.usa-button:active { +a.usa-button:not(.usa-button--unstyled, .usa-button--outline):visited, +a.usa-button:not(.usa-button--unstyled, .usa-button--outline):hover, +a.usa-button:not(.usa-button--unstyled, .usa-button--outline):focus, +a.usa-button:not(.usa-button--unstyled, .usa-button--outline):active { color: color('white'); } diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 7b96af5ee..59f00fe61 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -581,7 +581,7 @@ ALLOWED_HOSTS = [ "getgov-bl.app.cloud.gov", "getgov-rjm.app.cloud.gov", "getgov-dk.app.cloud.gov", - "get.gov", + "manage.get.gov", ] # Extend ALLOWED_HOSTS. diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 516683247..2fd78cdd8 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -153,7 +153,8 @@ class RegistrarFormSet(forms.BaseFormSet): class OrganizationTypeForm(RegistrarForm): organization_type = forms.ChoiceField( - choices=DomainApplication.OrganizationChoices.choices, + # use the long names in the application form + choices=DomainApplication.OrganizationChoicesVerbose.choices, widget=forms.RadioSelect, error_messages={"required": "Select the type of organization you represent."}, ) diff --git a/src/registrar/migrations/0040_alter_userdomainrole_role.py b/src/registrar/migrations/0040_alter_userdomainrole_role.py new file mode 100644 index 000000000..39e539f55 --- /dev/null +++ b/src/registrar/migrations/0040_alter_userdomainrole_role.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.1 on 2023-10-20 15:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0039_alter_transitiondomain_status"), + ] + + operations = [ + migrations.AlterField( + model_name="userdomainrole", + name="role", + field=models.TextField(choices=[("manager", "Manager")]), + ), + ] diff --git a/src/registrar/migrations/0041_alter_domainapplication_organization_type_and_more.py b/src/registrar/migrations/0041_alter_domainapplication_organization_type_and_more.py new file mode 100644 index 000000000..07cfe0e77 --- /dev/null +++ b/src/registrar/migrations/0041_alter_domainapplication_organization_type_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.1 on 2023-10-20 21:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0040_alter_userdomainrole_role"), + ] + + operations = [ + migrations.AlterField( + model_name="domainapplication", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of organization", + max_length=255, + null=True, + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ("federal", "Federal"), + ("interstate", "Interstate"), + ("state_or_territory", "State or territory"), + ("tribal", "Tribal"), + ("county", "County"), + ("city", "City"), + ("special_district", "Special district"), + ("school_district", "School district"), + ], + help_text="Type of Organization", + max_length=255, + null=True, + ), + ), + ] diff --git a/src/registrar/migrations/0042_create_groups_v03.py b/src/registrar/migrations/0042_create_groups_v03.py new file mode 100644 index 000000000..01b7985bf --- /dev/null +++ b/src/registrar/migrations/0042_create_groups_v03.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0035 (which populates ContentType and Permissions) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0041_alter_domainapplication_organization_type_and_more"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index d4c634f21..9c0195345 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1416,18 +1416,16 @@ class Domain(TimeStampedModel, DomainHelper): """creates a disclose object that can be added to a contact Create using .disclose= on the command before sending. if item is security email then make sure email is visable""" - isSecurity = contact.contact_type == contact.ContactTypeChoices.SECURITY + is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY DF = epp.DiscloseField - fields = {DF.FAX, DF.VOICE, DF.ADDR} - - if not isSecurity or ( - isSecurity and contact.email == PublicContact.get_default_security().email - ): - fields.add(DF.EMAIL) + fields = {DF.EMAIL} + disclose = ( + is_security and contact.email != PublicContact.get_default_security().email + ) + # Will only disclose DF.EMAIL if its not the default return epp.Disclose( - flag=False, + flag=disclose, fields=fields, - types={DF.ADDR: "loc"}, ) def _make_epp_contact_postal_info(self, contact: PublicContact): # type: ignore diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 68429d381..68fbfab0d 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -105,28 +105,57 @@ class DomainApplication(TimeStampedModel): ARMED_FORCES_AP = "AP", "Armed Forces Pacific (AP)" class OrganizationChoices(models.TextChoices): + + """ + Primary organization choices: + For use in django admin + Keys need to match OrganizationChoicesVerbose + """ + + FEDERAL = "federal", "Federal" + INTERSTATE = "interstate", "Interstate" + STATE_OR_TERRITORY = "state_or_territory", "State or territory" + TRIBAL = "tribal", "Tribal" + COUNTY = "county", "County" + CITY = "city", "City" + SPECIAL_DISTRICT = "special_district", "Special district" + SCHOOL_DISTRICT = "school_district", "School district" + + class OrganizationChoicesVerbose(models.TextChoices): + + """ + Secondary organization choices + For use in the application form and on the templates + Keys need to match OrganizationChoices + """ + FEDERAL = ( "federal", - "Federal: an agency of the U.S. government's executive, legislative, " - "or judicial branches", + "Federal: an agency of the U.S. government's executive, " + "legislative, or judicial branches", ) INTERSTATE = "interstate", "Interstate: an organization of two or more states" - STATE_OR_TERRITORY = "state_or_territory", ( - "State or territory: one of the 50 U.S. states, the District of " - "Columbia, American Samoa, Guam, Northern Mariana Islands, " - "Puerto Rico, or the U.S. Virgin Islands" + STATE_OR_TERRITORY = ( + "state_or_territory", + "State or territory: one of the 50 U.S. states, the District of Columbia, " + "American Samoa, Guam, Northern Mariana Islands, Puerto Rico, or the U.S. " + "Virgin Islands", ) - TRIBAL = "tribal", ( - "Tribal: a tribal government recognized by the federal or " - "a state government" + TRIBAL = ( + "tribal", + "Tribal: a tribal government recognized by the federal or a state " + "government", ) COUNTY = "county", "County: a county, parish, or borough" CITY = "city", "City: a city, town, township, village, etc." - SPECIAL_DISTRICT = "special_district", ( - "Special district: an independent organization within a single state" + SPECIAL_DISTRICT = ( + "special_district", + "Special district: an independent organization within a single state", ) - SCHOOL_DISTRICT = "school_district", ( - "School district: a school district that is not part of a local government" + SCHOOL_DISTRICT = ( + "school_district", + "School district: a school district that is not part of a local " + "government", ) class BranchChoices(models.TextChoices): @@ -297,6 +326,7 @@ class DomainApplication(TimeStampedModel): # ##### data fields from the initial form ##### organization_type = models.CharField( max_length=255, + # use the short names in Django admin choices=OrganizationChoices.choices, null=True, blank=True, @@ -582,7 +612,7 @@ class DomainApplication(TimeStampedModel): # create the permission for the user UserDomainRole = apps.get_model("registrar.UserDomainRole") UserDomainRole.objects.get_or_create( - user=self.creator, domain=created_domain, role=UserDomainRole.Roles.ADMIN + user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER ) self._send_status_update_email( diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 3b93aff48..d2bc5c53d 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -21,6 +21,7 @@ class DomainInformation(TimeStampedModel): StateTerritoryChoices = DomainApplication.StateTerritoryChoices + # use the short names in Django admin OrganizationChoices = DomainApplication.OrganizationChoices BranchChoices = DomainApplication.BranchChoices diff --git a/src/registrar/models/domain_invitation.py b/src/registrar/models/domain_invitation.py index 7cc2a5432..dff03fb87 100644 --- a/src/registrar/models/domain_invitation.py +++ b/src/registrar/models/domain_invitation.py @@ -63,7 +63,7 @@ class DomainInvitation(TimeStampedModel): # and create a role for that user on this domain _, created = UserDomainRole.objects.get_or_create( - user=user, domain=self.domain, role=UserDomainRole.Roles.ADMIN + user=user, domain=self.domain, role=UserDomainRole.Roles.MANAGER ) if not created: # something strange happened and this role already existed when diff --git a/src/registrar/models/user_domain_role.py b/src/registrar/models/user_domain_role.py index e5cb01cc1..7b1f550d3 100644 --- a/src/registrar/models/user_domain_role.py +++ b/src/registrar/models/user_domain_role.py @@ -15,7 +15,7 @@ class UserDomainRole(TimeStampedModel): elsewhere. """ - ADMIN = "manager" + MANAGER = "manager" user = models.ForeignKey( "registrar.User", diff --git a/src/registrar/models/user_group.py b/src/registrar/models/user_group.py index 5cdb1f2ec..568741786 100644 --- a/src/registrar/models/user_group.py +++ b/src/registrar/models/user_group.py @@ -24,7 +24,7 @@ class UserGroup(Group): { "app_label": "registrar", "model": "contact", - "permissions": ["view_contact"], + "permissions": ["change_contact"], }, { "app_label": "registrar", @@ -56,6 +56,11 @@ class UserGroup(Group): "model": "domaininvitation", "permissions": ["add_domaininvitation", "view_domaininvitation"], }, + { + "app_label": "registrar", + "model": "website", + "permissions": ["change_website"], + }, ] # Avoid error: You can't execute queries until the end diff --git a/src/registrar/templates/application_review.html b/src/registrar/templates/application_review.html index be81303b8..6a4dcbffd 100644 --- a/src/registrar/templates/application_review.html +++ b/src/registrar/templates/application_review.html @@ -1,5 +1,6 @@ {% extends 'application_form.html' %} {% load static url_helpers %} +{% load custom_filters %} {% block form_required_fields_help_text %} {# there are no required fields on this page so don't show this #} @@ -26,7 +27,13 @@
{{ form_titles|get_item:step }}
{% if step == Step.ORGANIZATION_TYPE %} - {{ application.get_organization_type_display|default:"Incomplete" }} + {% if application.organization_type is not None %} + {% with long_org_type=application.organization_type|get_organization_long_name %} + {{ long_org_type }} + {% endwith %} + {% else %} + Incomplete + {% endif %} {% endif %} {% if step == Step.TRIBAL_GOVERNMENT %} {{ application.tribe_name|default:"Incomplete" }} diff --git a/src/registrar/templates/application_status.html b/src/registrar/templates/application_status.html index a68c07c8a..79d0f7ff9 100644 --- a/src/registrar/templates/application_status.html +++ b/src/registrar/templates/application_status.html @@ -1,5 +1,7 @@ {% extends 'base.html' %} +{% load custom_filters %} + {% block title %}Domain request status | {{ domainapplication.requested_domain.name }} | {% endblock %} {% load static url_helpers %} @@ -50,7 +52,9 @@

Summary of your domain request

{% with heading_level='h3' %} - {% include "includes/summary_item.html" with title='Type of organization' value=domainapplication.get_organization_type_display heading_level=heading_level %} + {% with long_org_type=domainapplication.organization_type|get_organization_long_name %} + {% include "includes/summary_item.html" with title='Type of organization' value=long_org_type heading_level=heading_level %} + {% endwith %} {% if domainapplication.tribe_name %} {% include "includes/summary_item.html" with title='Tribal government' value=domainapplication.tribe_name heading_level=heading_level %} diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index e0d672093..4ddbd673a 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -52,7 +52,7 @@ {% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url %} {% endif %} {% url 'domain-users' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='User management' users='true' list=True value=domain.permissions.all edit_link=url %} + {% include "includes/summary_item.html" with title='Domain managers' users='true' list=True value=domain.permissions.all edit_link=url %}
{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 1acd87eeb..ac45ad04c 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -100,7 +100,7 @@ - User management + Domain managers diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 22b9d18d1..f66eef5a6 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -1,10 +1,23 @@ {% extends "domain_base.html" %} -{% load static %} +{% load static url_helpers %} -{% block title %}User management | {{ domain.name }} | {% endblock %} +{% block title %}Domain managers | {{ domain.name }} | {% endblock %} {% block domain_content %} -

User management

+

Domain managers

+ +

+ Domain managers can update all information related to a domain within the + .gov registrar, including contact details, authorizing official, security + email, and DNS name servers. +

+ + {% if domain.permissions %}
diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index 3614db18e..14e2c9e3e 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -1,7 +1,10 @@ +import logging from django import template import re +from registrar.models.domain_application import DomainApplication register = template.Library() +logger = logging.getLogger(__name__) @register.filter(name="extract_value") @@ -48,3 +51,16 @@ def contains_checkbox(html_list): if re.search(r']*type="checkbox"', html_string): return True return False + + +@register.filter +def get_organization_long_name(organization_type): + organization_choices_dict = dict( + DomainApplication.OrganizationChoicesVerbose.choices + ) + long_form_type = organization_choices_dict[organization_type] + if long_form_type is None: + logger.error("Organization type error, triggered by a template's custom filter") + return "Error" + + return long_form_type diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index a9f38db03..8cd5fd6ba 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -31,6 +31,7 @@ from epplibwrapper import ( info, RegistryError, ErrorCode, + responses, ) from registrar.models.utility.contact_error import ContactError, ContactErrorCodes @@ -669,6 +670,44 @@ class MockEppLib(TestCase): registrant="regContact", ) + InfoDomainWithDefaultSecurityContact = fakedEppObject( + "fakepw", + cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + contacts=[ + common.DomainContact( + contact="defaultSec", + type=PublicContact.ContactTypeChoices.SECURITY, + ) + ], + hosts=["fake.host.com"], + statuses=[ + common.Status(state="serverTransferProhibited", description="", lang="en"), + common.Status(state="inactive", description="", lang="en"), + ], + ) + + InfoDomainWithDefaultTechnicalContact = fakedEppObject( + "fakepw", + cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + contacts=[ + common.DomainContact( + contact="defaultTech", + type=PublicContact.ContactTypeChoices.TECHNICAL, + ) + ], + hosts=["fake.host.com"], + statuses=[ + common.Status(state="serverTransferProhibited", description="", lang="en"), + common.Status(state="inactive", description="", lang="en"), + ], + ) + + mockDefaultTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData( + "defaultTech", "dotgov@cisa.dhs.gov" + ) + mockDefaultSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData( + "defaultSec", "dotgov@cisa.dhs.gov" + ) mockSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData( "securityContact", "security@mail.gov" ) @@ -784,45 +823,63 @@ class MockEppLib(TestCase): ], ) + def _mockDomainName(self, _name, _avail=False): + return MagicMock( + res_data=[ + responses.check.CheckDomainResultData( + name=_name, avail=_avail, reason=None + ), + ] + ) + + def mockCheckDomainCommand(self, _request, cleaned): + if "gsa.gov" in getattr(_request, "names", None): + return self._mockDomainName("gsa.gov", True) + elif "GSA.gov" in getattr(_request, "names", None): + return self._mockDomainName("GSA.gov", True) + elif "igorvilleremixed.gov" in getattr(_request, "names", None): + return self._mockDomainName("igorvilleremixed.gov", False) + elif "errordomain.gov" in getattr(_request, "names", None): + raise RegistryError("Registry cannot find domain availability.") + else: + return self._mockDomainName("domainnotfound.gov", False) + def mockSend(self, _request, cleaned): """Mocks the registry.send function used inside of domain.py registry is imported from epplibwrapper returns objects that simulate what would be in a epp response but only relevant pieces for tests""" - if isinstance(_request, commands.InfoDomain): - return self.mockInfoDomainCommands(_request, cleaned) - elif isinstance(_request, commands.InfoContact): - return self.mockInfoContactCommands(_request, cleaned) - elif isinstance(_request, commands.UpdateDomain): - return self.mockUpdateDomainCommands(_request, cleaned) - elif isinstance(_request, commands.CreateContact): - return self.mockCreateContactCommands(_request, cleaned) - elif isinstance(_request, commands.CreateHost): - return MagicMock( - res_data=[self.mockDataHostChange], - code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, - ) - elif isinstance(_request, commands.UpdateHost): - return MagicMock( - res_data=[self.mockDataHostChange], - code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, - ) - elif isinstance(_request, commands.DeleteHost): - return MagicMock( - res_data=[self.mockDataHostChange], - code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, - ) - elif ( - isinstance(_request, commands.DeleteDomain) - and getattr(_request, "name", None) == "failDelete.gov" - ): - name = getattr(_request, "name", None) - fake_nameserver = "ns1.failDelete.gov" - if name in fake_nameserver: - raise RegistryError( - code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION + + match type(_request): + case commands.InfoDomain: + return self.mockInfoDomainCommands(_request, cleaned) + case commands.InfoContact: + return self.mockInfoContactCommands(_request, cleaned) + case commands.CreateContact: + return self.mockCreateContactCommands(_request, cleaned) + case commands.UpdateDomain: + return self.mockUpdateDomainCommands(_request, cleaned) + case commands.CreateHost: + return MagicMock( + res_data=[self.mockDataHostChange], + code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, ) - return MagicMock(res_data=[self.mockDataInfoHosts]) + case commands.UpdateHost: + return MagicMock( + res_data=[self.mockDataHostChange], + code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, + ) + case commands.DeleteHost: + return MagicMock( + res_data=[self.mockDataHostChange], + code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, + ) + case commands.CheckDomain: + return self.mockCheckDomainCommand(_request, cleaned) + case commands.DeleteDomain: + return self.mockDeleteDomainCommands(_request, cleaned) + case _: + return MagicMock(res_data=[self.mockDataInfoHosts]) def mockUpdateDomainCommands(self, _request, cleaned): if getattr(_request, "name", None) == "dnssec-invalid.gov": @@ -833,6 +890,16 @@ class MockEppLib(TestCase): code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY, ) + def mockDeleteDomainCommands(self, _request, cleaned): + if getattr(_request, "name", None) == "failDelete.gov": + name = getattr(_request, "name", None) + fake_nameserver = "ns1.failDelete.gov" + if name in fake_nameserver: + raise RegistryError( + code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION + ) + return None + def mockInfoDomainCommands(self, _request, cleaned): request_name = getattr(_request, "name", None) @@ -862,6 +929,8 @@ class MockEppLib(TestCase): "namerserversubdomain.gov": (self.infoDomainCheckHostIPCombo, None), "freeman.gov": (self.InfoDomainWithContacts, None), "threenameserversDomain.gov": (self.infoDomainThreeHosts, None), + "defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None), + "defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None), } # Retrieve the corresponding values from the dictionary @@ -887,6 +956,10 @@ class MockEppLib(TestCase): mocked_result = self.mockAdministrativeContact case "regContact": mocked_result = self.mockRegistrantContact + case "defaultSec": + mocked_result = self.mockDefaultSecurityContact + case "defaultTech": + mocked_result = self.mockDefaultTechnicalContact case _: # Default contact return mocked_result = self.mockDataInfoContact @@ -921,15 +994,11 @@ class MockEppLib(TestCase): self, contact: PublicContact, disclose_email=False, createContact=True ): DF = common.DiscloseField - fields = {DF.FAX, DF.VOICE, DF.ADDR} - - if not disclose_email: - fields.add(DF.EMAIL) + fields = {DF.EMAIL} di = common.Disclose( - flag=False, + flag=disclose_email, fields=fields, - types={DF.ADDR: "loc"}, ) # check docs here looks like we may have more than one address field but diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 51ace34f7..f2db9d5ee 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -11,6 +11,7 @@ from registrar.admin import ( ListHeaderAdmin, MyUserAdmin, AuditedAdmin, + ContactAdmin, ) from registrar.models import ( Domain, @@ -52,6 +53,26 @@ class TestDomainAdmin(MockEppLib): self.factory = RequestFactory() super().setUp() + def test_short_org_name_in_domains_list(self): + """ + Make sure the short name is displaying in admin on the list page + """ + self.client.force_login(self.superuser) + application = completed_application(status=DomainApplication.IN_REVIEW) + application.approve() + + response = self.client.get("/admin/registrar/domain/") + + # There are 3 template references to Federal (3) plus one reference in the table + # for our actual application + self.assertContains(response, "Federal", count=4) + # This may be a bit more robust + self.assertContains( + response, 'Federal', count=1 + ) + # Now let's make sure the long description does not exist + self.assertNotContains(response, "Federal: an agency of the U.S. government") + @skip("Why did this test stop working, and is is a good test") def test_place_and_remove_hold(self): domain = create_ready_domain() @@ -243,8 +264,11 @@ class TestDomainAdmin(MockEppLib): raise def tearDown(self): - User.objects.all().delete() super().tearDown() + Domain.objects.all().delete() + DomainInformation.objects.all().delete() + DomainApplication.objects.all().delete() + User.objects.all().delete() class TestDomainApplicationAdminForm(TestCase): @@ -300,6 +324,23 @@ class TestDomainApplicationAdmin(MockEppLib): self.superuser = create_superuser() self.staffuser = create_user() + def test_short_org_name_in_applications_list(self): + """ + Make sure the short name is displaying in admin on the list page + """ + self.client.force_login(self.superuser) + completed_application() + response = self.client.get("/admin/registrar/domainapplication/") + # There are 3 template references to Federal (3) plus one reference in the table + # for our actual application + self.assertContains(response, "Federal", count=4) + # This may be a bit more robust + self.assertContains( + response, 'Federal', count=1 + ) + # Now let's make sure the long description does not exist + self.assertNotContains(response, "Federal: an agency of the U.S. government") + @boto3_mocking.patching def test_save_model_sends_submitted_email(self): # make sure there is no user with this email @@ -620,9 +661,6 @@ class TestDomainApplicationAdmin(MockEppLib): expected_fields = [ "creator", "about_your_organization", - "address_line1", - "address_line2", - "zipcode", "requested_domain", "alternative_domains", "purpose", @@ -1313,3 +1351,38 @@ class DomainSessionVariableTest(TestCase): {"_edit_domain": "true"}, follow=True, ) + + +class ContactAdminTest(TestCase): + def setUp(self): + self.site = AdminSite() + self.factory = RequestFactory() + self.client = Client(HTTP_HOST="localhost:8080") + self.admin = ContactAdmin(model=get_user_model(), admin_site=None) + self.superuser = create_superuser() + self.staffuser = create_user() + + def test_readonly_when_restricted_staffuser(self): + request = self.factory.get("/") + request.user = self.staffuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [ + "user", + ] + + self.assertEqual(readonly_fields, expected_fields) + + def test_readonly_when_restricted_superuser(self): + request = self.factory.get("/") + request.user = self.superuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [] + + self.assertEqual(readonly_fields, expected_fields) + + def tearDown(self): + User.objects.all().delete() diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py index 95be195ba..4b1aeb12c 100644 --- a/src/registrar/tests/test_forms.py +++ b/src/registrar/tests/test_forms.py @@ -1,6 +1,6 @@ """Test form validation requirements.""" -from django.test import TestCase +from django.test import TestCase, RequestFactory from registrar.forms.application_wizard import ( CurrentSitesForm, @@ -16,9 +16,16 @@ from registrar.forms.application_wizard import ( AboutYourOrganizationForm, ) from registrar.forms.domain import ContactForm +from registrar.tests.common import MockEppLib +from django.contrib.auth import get_user_model -class TestFormValidation(TestCase): +class TestFormValidation(MockEppLib): + def setUp(self): + super().setUp() + self.user = get_user_model().objects.create(username="username") + self.factory = RequestFactory() + def test_org_contact_zip_invalid(self): form = OrganizationContactForm(data={"zipcode": "nah"}) self.assertEqual( diff --git a/src/registrar/tests/test_migrations.py b/src/registrar/tests/test_migrations.py index 95e5853ff..165ef6f71 100644 --- a/src/registrar/tests/test_migrations.py +++ b/src/registrar/tests/test_migrations.py @@ -36,7 +36,7 @@ class TestGroups(TestCase): # Define the expected permission codenames expected_permissions = [ "view_logentry", - "view_contact", + "change_contact", "view_domain", "change_domainapplication", "change_domaininformation", @@ -45,6 +45,7 @@ class TestGroups(TestCase): "change_draftdomain", "analyst_access_permission", "change_user", + "change_website", ] # Get the codenames of actual permissions associated with the group diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 2c6f78ef5..e76dea035 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -601,7 +601,7 @@ class TestInvitations(TestCase): def test_retrieve_existing_role_no_error(self): # make the overlapping role UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER ) # this is not an error but does produce a console warning with less_console_noise(): diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index ef3084f9c..5759df1be 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -19,7 +19,7 @@ from registrar.utility.errors import ActionNotAllowed, NameserverError from registrar.models.utility.contact_error import ContactError, ContactErrorCodes -from .common import MockEppLib + from django_fsm import TransitionNotAllowed # type: ignore from epplibwrapper import ( commands, @@ -29,6 +29,7 @@ from epplibwrapper import ( RegistryError, ErrorCode, ) +from .common import MockEppLib import logging logger = logging.getLogger(__name__) @@ -760,6 +761,198 @@ class TestRegistrantContacts(MockEppLib): self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1) + def test_not_disclosed_on_other_contacts(self): + """ + Scenario: Registrant creates a new domain with multiple contacts + When `domain` has registrant, admin, technical, + and security contacts + Then Domain sends `commands.CreateContact` to the registry + And the field `disclose` is set to false for DF.EMAIL + on all fields except security + """ + # Generates a domain with four existing contacts + domain, _ = Domain.objects.get_or_create(name="freeman.gov") + + # Contact setup + expected_admin = domain.get_default_administrative_contact() + expected_admin.email = self.mockAdministrativeContact.email + + expected_registrant = domain.get_default_registrant_contact() + expected_registrant.email = self.mockRegistrantContact.email + + expected_security = domain.get_default_security_contact() + expected_security.email = self.mockSecurityContact.email + + expected_tech = domain.get_default_technical_contact() + expected_tech.email = self.mockTechnicalContact.email + + domain.administrative_contact = expected_admin + domain.registrant_contact = expected_registrant + domain.security_contact = expected_security + domain.technical_contact = expected_tech + + contacts = [ + (expected_admin, domain.administrative_contact), + (expected_registrant, domain.registrant_contact), + (expected_security, domain.security_contact), + (expected_tech, domain.technical_contact), + ] + + # Test for each contact + for contact in contacts: + expected_contact = contact[0] + actual_contact = contact[1] + is_security = expected_contact.contact_type == "security" + + expectedCreateCommand = self._convertPublicContactToEpp( + expected_contact, disclose_email=is_security + ) + + # Should only be disclosed if the type is security, as the email is valid + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + + # The emails should match on both items + self.assertEqual(expected_contact.email, actual_contact.email) + + def test_convert_public_contact_to_epp(self): + self.maxDiff = None + domain, _ = Domain.objects.get_or_create(name="freeman.gov") + dummy_contact = domain.get_default_security_contact() + test_disclose = self._convertPublicContactToEpp( + dummy_contact, disclose_email=True + ).__dict__ + test_not_disclose = self._convertPublicContactToEpp( + dummy_contact, disclose_email=False + ).__dict__ + + # Separated for linter + disclose_email_field = {common.DiscloseField.EMAIL} + expected_disclose = { + "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), + "disclose": common.Disclose( + flag=True, fields=disclose_email_field, types=None + ), + "email": "dotgov@cisa.dhs.gov", + "extensions": [], + "fax": None, + "id": "ThIq2NcRIDN7PauO", + "ident": None, + "notify_email": None, + "postal_info": common.PostalInfo( + name="Registry Customer Service", + addr=common.ContactAddr( + street=["4200 Wilson Blvd.", None, None], + city="Arlington", + pc="22201", + cc="US", + sp="VA", + ), + org="Cybersecurity and Infrastructure Security Agency", + type="loc", + ), + "vat": None, + "voice": "+1.8882820870", + } + + # Separated for linter + expected_not_disclose = { + "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), + "disclose": common.Disclose( + flag=False, fields=disclose_email_field, types=None + ), + "email": "dotgov@cisa.dhs.gov", + "extensions": [], + "fax": None, + "id": "ThrECENCHI76PGLh", + "ident": None, + "notify_email": None, + "postal_info": common.PostalInfo( + name="Registry Customer Service", + addr=common.ContactAddr( + street=["4200 Wilson Blvd.", None, None], + city="Arlington", + pc="22201", + cc="US", + sp="VA", + ), + org="Cybersecurity and Infrastructure Security Agency", + type="loc", + ), + "vat": None, + "voice": "+1.8882820870", + } + + # Set the ids equal, since this value changes + test_disclose["id"] = expected_disclose["id"] + test_not_disclose["id"] = expected_not_disclose["id"] + + self.assertEqual(test_disclose, expected_disclose) + self.assertEqual(test_not_disclose, expected_not_disclose) + + def test_not_disclosed_on_default_security_contact(self): + """ + Scenario: Registrant creates a new domain with no security email + When `domain.security_contact.email` is equal to the default + Then Domain sends `commands.CreateContact` to the registry + And the field `disclose` is set to false for DF.EMAIL + """ + domain, _ = Domain.objects.get_or_create(name="defaultsecurity.gov") + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = domain + expectedSecContact.registry_id = "defaultSec" + domain.security_contact = expectedSecContact + + expectedCreateCommand = self._convertPublicContactToEpp( + expectedSecContact, disclose_email=False + ) + + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting a default email + self.assertEqual(domain.security_contact.email, expectedSecContact.email) + + def test_not_disclosed_on_default_technical_contact(self): + """ + Scenario: Registrant creates a new domain with no technical contact + When `domain.technical_contact.email` is equal to the default + Then Domain sends `commands.CreateContact` to the registry + And the field `disclose` is set to false for DF.EMAIL + """ + domain, _ = Domain.objects.get_or_create(name="defaulttechnical.gov") + expectedTechContact = PublicContact.get_default_technical() + expectedTechContact.domain = domain + expectedTechContact.registry_id = "defaultTech" + domain.technical_contact = expectedTechContact + + expectedCreateCommand = self._convertPublicContactToEpp( + expectedTechContact, disclose_email=False + ) + + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting a default email + self.assertEqual(domain.technical_contact.email, expectedTechContact.email) + + def test_is_disclosed_on_security_contact(self): + """ + Scenario: Registrant creates a new domain with a security email + When `domain.security_contact.email` is set to a valid email + and is not the default + Then Domain sends `commands.CreateContact` to the registry + And the field `disclose` is set to true for DF.EMAIL + """ + domain, _ = Domain.objects.get_or_create(name="igorville.gov") + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = domain + expectedSecContact.email = "123@mail.gov" + domain.security_contact = expectedSecContact + + expectedCreateCommand = self._convertPublicContactToEpp( + expectedSecContact, disclose_email=True + ) + + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting the desired email + self.assertEqual(domain.security_contact.email, expectedSecContact.email) + @skip("not implemented yet") def test_update_is_unsuccessful(self): """ diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 0e8f895af..1c215ec05 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -89,7 +89,7 @@ class LoggedInTests(TestWithUser): domain, _ = Domain.objects.get_or_create(name="igorville.gov") self.assertNotContains(response, "igorville.gov") role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=domain, role=UserDomainRole.Roles.ADMIN + user=self.user, domain=domain, role=UserDomainRole.Roles.MANAGER ) response = self.client.get("/") # count = 2 because it is also in screenreader content @@ -142,9 +142,12 @@ class DomainApplicationTests(TestWithUser, WebTest): @boto3_mocking.patching def test_application_form_submission(self): - """Can fill out the entire form and submit. + """ + Can fill out the entire form and submit. As we add additional form pages, we need to include them here to make this test work. + + This test also looks for the long organization name on the summary page. """ num_pages_tested = 0 # elections, type_of_work, tribal_government, no_other_contacts @@ -428,7 +431,8 @@ class DomainApplicationTests(TestWithUser, WebTest): review_form = review_page.form # Review page contains all the previously entered data - self.assertContains(review_page, "Federal") + # Let's make sure the long org name is displayed + self.assertContains(review_page, "Federal: an agency of the U.S. government") self.assertContains(review_page, "Executive") self.assertContains(review_page, "Testorg") self.assertContains(review_page, "address 1") @@ -1066,6 +1070,26 @@ class DomainApplicationTests(TestWithUser, WebTest): # page = self.app.get(url) # self.assertNotContains(page, "VALUE") + def test_long_org_name_in_application(self): + """ + Make sure the long name is displaying in the application form, + org step + """ + request = self.app.get(reverse("application:")).follow() + self.assertContains(request, "Federal: an agency of the U.S. government") + + def test_long_org_name_in_application_manage(self): + """ + Make sure the long name is displaying in the application summary + page (manage your application) + """ + completed_application(status=DomainApplication.SUBMITTED, user=self.user) + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Edit" link + detail_page = home_page.click("Manage") + self.assertContains(detail_page, "Federal: an agency of the U.S. government") + class TestWithDomainPermissions(TestWithUser): def setUp(self): @@ -1097,23 +1121,25 @@ class TestWithDomainPermissions(TestWithUser): creator=self.user, domain=self.domain_dnssec_none ) self.role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER ) UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.ADMIN + user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.MANAGER ) UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain_multdsdata, - role=UserDomainRole.Roles.ADMIN, + role=UserDomainRole.Roles.MANAGER, ) UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_keydata, role=UserDomainRole.Roles.ADMIN + user=self.user, + domain=self.domain_keydata, + role=UserDomainRole.Roles.MANAGER, ) UserDomainRole.objects.get_or_create( user=self.user, domain=self.domain_dnssec_none, - role=UserDomainRole.Roles.ADMIN, + role=UserDomainRole.Roles.MANAGER, ) def tearDown(self): @@ -1199,14 +1225,14 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest): self.assertEqual(response.status_code, 403) -class TestDomainUserManagement(TestDomainOverview): - def test_domain_user_management(self): +class TestDomainManagers(TestDomainOverview): + def test_domain_managers(self): response = self.client.get( reverse("domain-users", kwargs={"pk": self.domain.id}) ) - self.assertContains(response, "User management") + self.assertContains(response, "Domain managers") - def test_domain_user_management_add_link(self): + def test_domain_managers_add_link(self): """Button to get to user add page works.""" management_page = self.app.get( reverse("domain-users", kwargs={"pk": self.domain.id}) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 36ac74f94..639b83dfe 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -710,7 +710,7 @@ class DomainSecurityEmailView(DomainFormBaseView): class DomainUsersView(DomainBaseView): - """User management page in the domain details.""" + """Domain managers page in the domain details.""" template_name = "domain_users.html" @@ -790,7 +790,9 @@ class DomainAddUserView(DomainFormBaseView): try: UserDomainRole.objects.create( - user=requested_user, domain=self.object, role=UserDomainRole.Roles.ADMIN + user=requested_user, + domain=self.object, + role=UserDomainRole.Roles.MANAGER, ) except IntegrityError: # User already has the desired role! Do nothing??