mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-23 03:06:01 +02:00
Merge branch 'main' into za/1620-hide-registrardotgov-email
This commit is contained in:
commit
382fbdb98b
30 changed files with 650 additions and 200 deletions
8
.github/workflows/deploy-development.yaml
vendored
8
.github/workflows/deploy-development.yaml
vendored
|
@ -38,3 +38,11 @@ jobs:
|
|||
cf_org: cisa-dotgov
|
||||
cf_space: development
|
||||
push_arguments: "-f ops/manifests/manifest-development.yaml"
|
||||
- name: Run Django migrations
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
with:
|
||||
cf_username: ${{ secrets.CF_DEVELOPMENT_USERNAME }}
|
||||
cf_password: ${{ secrets.CF_DEVELOPMENT_PASSWORD }}
|
||||
cf_org: cisa-dotgov
|
||||
cf_space: development
|
||||
cf_command: "run-task getgov-development --command 'python manage.py migrate' --name migrate"
|
||||
|
|
|
@ -237,3 +237,7 @@ Bugs on production software need to be documented quickly and triaged to determi
|
|||
3. In the case where the engineering lead is is unresponsive or unavailable to assign the ticket immediately, the product team will make sure an engineer volunteers or is assigned to the ticket/PR review ASAP.
|
||||
4. Once done, the developer must make a PR and should tag the assigned PR reviewers in our Slack dev channel stating that the PR is now waiting on their review. These reviewers should drop other tasks in order to review this promptly.
|
||||
5. See the the section above on [Making bug fixes on stable](#making-bug-fixes-on-stable-during-production) for how to push changes to stable once the PR is approved
|
||||
|
||||
# Investigating and monitoring the health of the Registrar
|
||||
|
||||
Sometimes, we may want individuals to routinely monitor the Registrar's health, such as after big feature launches. The cadence of such monitoring and what we look for is subject to change and is instead documented in [Checklist for production verification document](https://docs.google.com/document/d/15b_qwEZMiL76BHeRHnznV1HxDQcxNRt--vPSEfixBOI). All project team members should feel free to suggest edits to this document and should refer to it if production-level monitoring is underway.
|
|
@ -20,6 +20,8 @@ from . import models
|
|||
from auditlog.models import LogEntry # type: ignore
|
||||
from auditlog.admin import LogEntryAdmin # type: ignore
|
||||
from django_fsm import TransitionNotAllowed # type: ignore
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.html import escape
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -452,6 +454,60 @@ class ContactAdmin(ListHeaderAdmin):
|
|||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields # Read-only fields for analysts
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
"""Extend the change_view for Contact objects in django admin.
|
||||
Customize to display related objects to the Contact. These will be passed
|
||||
through the messages construct to the template for display to the user."""
|
||||
|
||||
# Fetch the Contact instance
|
||||
contact = models.Contact.objects.get(pk=object_id)
|
||||
|
||||
# initialize related_objects array
|
||||
related_objects = []
|
||||
# for all defined fields in the model
|
||||
for related_field in contact._meta.get_fields():
|
||||
# if the field is a relation to another object
|
||||
if related_field.is_relation:
|
||||
# Check if the related field is not None
|
||||
related_manager = getattr(contact, related_field.name)
|
||||
if related_manager is not None:
|
||||
# Check if it's a ManyToManyField/reverse ForeignKey or a OneToOneField
|
||||
# Do this by checking for get_queryset method on the related_manager
|
||||
if hasattr(related_manager, "get_queryset"):
|
||||
# Handles ManyToManyRel and ManyToOneRel
|
||||
queryset = related_manager.get_queryset()
|
||||
else:
|
||||
# Handles OneToOne rels, ie. User
|
||||
queryset = [related_manager]
|
||||
|
||||
for obj in queryset:
|
||||
# for each object, build the edit url in this view and add as tuple
|
||||
# to the related_objects array
|
||||
app_label = obj._meta.app_label
|
||||
model_name = obj._meta.model_name
|
||||
obj_id = obj.id
|
||||
change_url = reverse("admin:%s_%s_change" % (app_label, model_name), args=[obj_id])
|
||||
related_objects.append((change_url, obj))
|
||||
|
||||
if related_objects:
|
||||
message = "<ul class='messagelist_content-list--unstyled'>"
|
||||
for i, (url, obj) in enumerate(related_objects):
|
||||
if i < 5:
|
||||
escaped_obj = escape(obj)
|
||||
message += f"<li>Joined to {obj.__class__.__name__}: <a href='{url}'>{escaped_obj}</a></li>"
|
||||
message += "</ul>"
|
||||
if len(related_objects) > 5:
|
||||
related_objects_over_five = len(related_objects) - 5
|
||||
message += f"<p class='font-sans-3xs'>And {related_objects_over_five} more...</p>"
|
||||
|
||||
message_html = mark_safe(message) # nosec
|
||||
messages.warning(
|
||||
request,
|
||||
message_html,
|
||||
)
|
||||
|
||||
return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
||||
|
||||
|
||||
class WebsiteAdmin(ListHeaderAdmin):
|
||||
"""Custom website admin class."""
|
||||
|
@ -1239,7 +1295,7 @@ class DraftDomainAdmin(ListHeaderAdmin):
|
|||
search_help_text = "Search by draft domain name."
|
||||
|
||||
|
||||
class VeryImportantPersonAdmin(ListHeaderAdmin):
|
||||
class VerifiedByStaffAdmin(ListHeaderAdmin):
|
||||
list_display = ("email", "requestor", "truncated_notes", "created_at")
|
||||
search_fields = ["email"]
|
||||
search_help_text = "Search by email."
|
||||
|
@ -1282,4 +1338,4 @@ admin.site.register(models.Website, WebsiteAdmin)
|
|||
admin.site.register(models.PublicContact, AuditedAdmin)
|
||||
admin.site.register(models.DomainApplication, DomainApplicationAdmin)
|
||||
admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
|
||||
admin.site.register(models.VeryImportantPerson, VeryImportantPersonAdmin)
|
||||
admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
|
||||
|
|
|
@ -143,7 +143,7 @@ h1, h2, h3,
|
|||
|
||||
.module h3 {
|
||||
padding: 0;
|
||||
color: var(--primary);
|
||||
color: var(--link-fg);
|
||||
margin: units(2) 0 units(1) 0;
|
||||
}
|
||||
|
||||
|
@ -258,3 +258,15 @@ h1, h2, h3,
|
|||
#select2-id_user-results {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Content list inside of a DjA alert, unstyled
|
||||
.messagelist_content-list--unstyled {
|
||||
padding-left: 0;
|
||||
li {
|
||||
font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif;
|
||||
font-size: 13.92px!important;
|
||||
background: none!important;
|
||||
padding: 0!important;
|
||||
margin: 0!important;
|
||||
}
|
||||
}
|
||||
|
|
14
src/registrar/assets/sass/_theme/_lists.scss
Normal file
14
src/registrar/assets/sass/_theme/_lists.scss
Normal file
|
@ -0,0 +1,14 @@
|
|||
@use "uswds-core" as *;
|
||||
|
||||
dt {
|
||||
color: color('primary-dark');
|
||||
margin-top: units(2);
|
||||
font-weight: font-weight('semibold');
|
||||
// The units mixin can only get us close, so it's between
|
||||
// hardcoding the value and using in markup
|
||||
font-size: 16.96px;
|
||||
}
|
||||
dd {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
|
@ -6,12 +6,14 @@
|
|||
margin-top: units(-1);
|
||||
}
|
||||
|
||||
//Tighter spacing when H2 is immediatly after H1
|
||||
// Tighter spacing when h2 is immediatly after h1
|
||||
.register-form-step .usa-fieldset:first-of-type h2:first-of-type,
|
||||
.register-form-step h1 + h2 {
|
||||
margin-top: units(1);
|
||||
}
|
||||
|
||||
// register-form-review-header is used on the summary page and
|
||||
// should not be styled like the register form headers
|
||||
.register-form-step h3 {
|
||||
color: color('primary-dark');
|
||||
letter-spacing: $letter-space--xs;
|
||||
|
@ -23,6 +25,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
h3.register-form-review-header {
|
||||
color: color('primary-dark');
|
||||
margin-top: units(2);
|
||||
margin-bottom: 0;
|
||||
font-weight: font-weight('semibold');
|
||||
// The units mixin can only get us close, so it's between
|
||||
// hardcoding the value and using in markup
|
||||
font-size: 16.96px;
|
||||
}
|
||||
|
||||
.register-form-step h4 {
|
||||
margin-bottom: 0;
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
--- Custom Styles ---------------------------------*/
|
||||
@forward "base";
|
||||
@forward "typography";
|
||||
@forward "lists";
|
||||
@forward "buttons";
|
||||
@forward "forms";
|
||||
@forward "fieldsets";
|
||||
|
|
|
@ -182,8 +182,6 @@ class LoadExtraTransitionDomain:
|
|||
# STEP 5: Parse creation and expiration data
|
||||
updated_transition_domain = self.parse_creation_expiration_data(domain_name, transition_domain)
|
||||
|
||||
# Check if the instance has changed before saving
|
||||
updated_transition_domain.save()
|
||||
updated_transition_domains.append(updated_transition_domain)
|
||||
logger.info(f"{TerminalColors.OKCYAN}" f"Successfully updated {domain_name}" f"{TerminalColors.ENDC}")
|
||||
|
||||
|
@ -199,6 +197,28 @@ class LoadExtraTransitionDomain:
|
|||
)
|
||||
failed_transition_domains.append(domain_name)
|
||||
|
||||
updated_fields = [
|
||||
"organization_name",
|
||||
"organization_type",
|
||||
"federal_type",
|
||||
"federal_agency",
|
||||
"first_name",
|
||||
"middle_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"phone",
|
||||
"epp_creation_date",
|
||||
"epp_expiration_date",
|
||||
]
|
||||
|
||||
batch_size = 1000
|
||||
# Create a Paginator object. Bulk_update on the full dataset
|
||||
# is too memory intensive for our current app config, so we can chunk this data instead.
|
||||
paginator = Paginator(updated_transition_domains, batch_size)
|
||||
for page_num in paginator.page_range:
|
||||
page = paginator.page(page_num)
|
||||
TransitionDomain.objects.bulk_update(page.object_list, updated_fields)
|
||||
|
||||
failed_count = len(failed_transition_domains)
|
||||
if failed_count == 0:
|
||||
if self.debug:
|
||||
|
|
37
src/registrar/migrations/0065_create_groups_v06.py
Normal file
37
src/registrar/migrations/0065_create_groups_v06.py
Normal file
|
@ -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", "0064_alter_domainapplication_address_line1_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
create_groups,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
atomic=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 4.2.7 on 2024-01-29 22:21
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0065_create_groups_v06"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name="VeryImportantPerson",
|
||||
new_name="VerifiedByStaff",
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="verifiedbystaff",
|
||||
options={"verbose_name_plural": "Verified by staff"},
|
||||
),
|
||||
]
|
37
src/registrar/migrations/0067_create_groups_v07.py
Normal file
37
src/registrar/migrations/0067_create_groups_v07.py
Normal file
|
@ -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", "0066_rename_veryimportantperson_verifiedbystaff_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
create_groups,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
atomic=True,
|
||||
),
|
||||
]
|
|
@ -13,7 +13,7 @@ from .user import User
|
|||
from .user_group import UserGroup
|
||||
from .website import Website
|
||||
from .transition_domain import TransitionDomain
|
||||
from .very_important_person import VeryImportantPerson
|
||||
from .verified_by_staff import VerifiedByStaff
|
||||
|
||||
__all__ = [
|
||||
"Contact",
|
||||
|
@ -30,7 +30,7 @@ __all__ = [
|
|||
"UserGroup",
|
||||
"Website",
|
||||
"TransitionDomain",
|
||||
"VeryImportantPerson",
|
||||
"VerifiedByStaff",
|
||||
]
|
||||
|
||||
auditlog.register(Contact)
|
||||
|
@ -47,4 +47,4 @@ auditlog.register(User, m2m_fields=["user_permissions", "groups"])
|
|||
auditlog.register(UserGroup, m2m_fields=["permissions"])
|
||||
auditlog.register(Website)
|
||||
auditlog.register(TransitionDomain)
|
||||
auditlog.register(VeryImportantPerson)
|
||||
auditlog.register(VerifiedByStaff)
|
||||
|
|
|
@ -911,10 +911,15 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
raise NotImplementedError()
|
||||
|
||||
def get_security_email(self):
|
||||
logger.info("get_security_email-> getting the contact ")
|
||||
secContact = self.security_contact
|
||||
if secContact is not None:
|
||||
return secContact.email
|
||||
logger.info("get_security_email-> getting the contact")
|
||||
|
||||
security = PublicContact.ContactTypeChoices.SECURITY
|
||||
security_contact = self.generic_contact_getter(security)
|
||||
|
||||
# If we get a valid value for security_contact, pull its email
|
||||
# Otherwise, just return nothing
|
||||
if security_contact is not None and isinstance(security_contact, PublicContact):
|
||||
return security_contact.email
|
||||
else:
|
||||
return None
|
||||
|
||||
|
@ -1122,7 +1127,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
If you wanted to setup getter logic for Security, you would call:
|
||||
cache_contact_helper(PublicContact.ContactTypeChoices.SECURITY),
|
||||
or cache_contact_helper("security").
|
||||
|
||||
"""
|
||||
# registrant_contact(s) are an edge case. They exist on
|
||||
# the "registrant" property as opposed to contacts.
|
||||
|
|
|
@ -7,7 +7,7 @@ from registrar.models.user_domain_role import UserDomainRole
|
|||
|
||||
from .domain_invitation import DomainInvitation
|
||||
from .transition_domain import TransitionDomain
|
||||
from .very_important_person import VeryImportantPerson
|
||||
from .verified_by_staff import VerifiedByStaff
|
||||
from .domain import Domain
|
||||
|
||||
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
||||
|
@ -91,7 +91,7 @@ class User(AbstractUser):
|
|||
return False
|
||||
|
||||
# New users flagged by Staff to bypass ial2
|
||||
if VeryImportantPerson.objects.filter(email=email).exists():
|
||||
if VerifiedByStaff.objects.filter(email=email).exists():
|
||||
return False
|
||||
|
||||
# A new incoming user who is being invited to be a domain manager (that is,
|
||||
|
|
|
@ -66,6 +66,11 @@ class UserGroup(Group):
|
|||
"model": "userdomainrole",
|
||||
"permissions": ["view_userdomainrole", "delete_userdomainrole"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "verifiedbystaff",
|
||||
"permissions": ["add_verifiedbystaff", "change_verifiedbystaff", "delete_verifiedbystaff"],
|
||||
},
|
||||
]
|
||||
|
||||
# Avoid error: You can't execute queries until the end
|
||||
|
|
|
@ -57,6 +57,9 @@ class DomainHelper:
|
|||
# If blank ok is true, just return the domain
|
||||
return domain
|
||||
|
||||
if domain.startswith("www."):
|
||||
domain = domain[4:]
|
||||
|
||||
if domain.endswith(".gov"):
|
||||
domain = domain[:-4]
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.db import models
|
|||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
|
||||
class VeryImportantPerson(TimeStampedModel):
|
||||
class VerifiedByStaff(TimeStampedModel):
|
||||
|
||||
"""emails that get added to this table will bypass ial2 on login."""
|
||||
|
||||
|
@ -28,5 +28,8 @@ class VeryImportantPerson(TimeStampedModel):
|
|||
help_text="Notes",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Verified by staff"
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
|
@ -20,108 +20,158 @@
|
|||
|
||||
{% block form_fields %}
|
||||
{% for step in steps.all|slice:":-1" %}
|
||||
<section class="review__step">
|
||||
<hr />
|
||||
<div class="review__step__title display-flex flex-justify">
|
||||
<div class="review__step__value">
|
||||
<div class="review__step__name">{{ form_titles|get_item:step }}</div>
|
||||
<div>
|
||||
{% if step == Step.ORGANIZATION_TYPE %}
|
||||
{% 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 %}
|
||||
<section class="summary-item margin-top-3">
|
||||
|
||||
{% if step == Step.ORGANIZATION_TYPE %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.organization_type is not None %}
|
||||
{% with title=form_titles|get_item:step value=application.get_organization_type_display|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if step == Step.TRIBAL_GOVERNMENT %}
|
||||
{{ application.tribe_name|default:"Incomplete" }}
|
||||
{% if application.federally_recognized_tribe %}<p>Federally-recognized tribe</p>{% endif %}
|
||||
{% if application.state_recognized_tribe %}<p>State-recognized tribe</p>{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.TRIBAL_GOVERNMENT %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.tribe_name|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% if application.federally_recognized_tribe %}<p>Federally-recognized tribe</p>{% endif %}
|
||||
{% if application.state_recognized_tribe %}<p>State-recognized tribe</p>{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.ORGANIZATION_FEDERAL %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.get_federal_type_display|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ORGANIZATION_ELECTION %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ORGANIZATION_CONTACT %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.organization_name %}
|
||||
{% with title=form_titles|get_item:step value=application %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url address='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value='Incomplete' %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if step == Step.ORGANIZATION_FEDERAL %}
|
||||
{{ application.get_federal_type_display|default:"Incomplete" }}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ABOUT_YOUR_ORGANIZATION %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.about_your_organization|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.AUTHORIZING_OFFICIAL %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.authorizing_official is not None %}
|
||||
{% with title=form_titles|get_item:step value=application.authorizing_official %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url contact='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if step == Step.ORGANIZATION_ELECTION %}
|
||||
{{ application.is_election_board|yesno:"Yes,No,Incomplete" }}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.CURRENT_SITES %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.current_websites.all %}
|
||||
{% with title=form_titles|get_item:step value=application.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url list='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value='None' %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if step == Step.ORGANIZATION_CONTACT %}
|
||||
{% if application.organization_name %}
|
||||
{% include "includes/organization_address.html" with organization=application %}
|
||||
{% else %}
|
||||
Incomplete
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if step == Step.ABOUT_YOUR_ORGANIZATION %}
|
||||
<p>{{ application.about_your_organization|default:"Incomplete" }}</p>
|
||||
{% endif %}
|
||||
{% if step == Step.AUTHORIZING_OFFICIAL %}
|
||||
{% if application.authorizing_official %}
|
||||
<div class="margin-bottom-105">
|
||||
{% include "includes/contact.html" with contact=application.authorizing_official %}
|
||||
</div>
|
||||
{% else %}
|
||||
Incomplete
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if step == Step.CURRENT_SITES %}
|
||||
<ul class="add-list-reset">
|
||||
{% for site in application.current_websites.all %}
|
||||
<li>{{ site.website }}</li>
|
||||
{% empty %}
|
||||
<li>None</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if step == Step.DOTGOV_DOMAIN %}
|
||||
<ul class="add-list-reset margin-bottom-105">
|
||||
<li>{{ application.requested_domain.name|default:"Incomplete" }}</li>
|
||||
</ul>
|
||||
<ul class="add-list-reset">
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.DOTGOV_DOMAIN %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.requested_domain.name|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
|
||||
{% if application.alternative_domains.all %}
|
||||
<h3 class="register-form-review-header">Alternative domains</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% for site in application.alternative_domains.all %}
|
||||
<li>{{ site.website }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if step == Step.PURPOSE %}
|
||||
{{ application.purpose|default:"Incomplete" }}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.PURPOSE %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.purpose|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.YOUR_CONTACT %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.submitter is not None %}
|
||||
{% with title=form_titles|get_item:step value=application.submitter %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url contact='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if step == Step.YOUR_CONTACT %}
|
||||
{% if application.submitter %}
|
||||
<div class="margin-bottom-105">
|
||||
{% include "includes/contact.html" with contact=application.submitter %}
|
||||
</div>
|
||||
{% else %}
|
||||
Incomplete
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.OTHER_CONTACTS %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.other_contacts.all %}
|
||||
{% with title=form_titles|get_item:step value=application.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url contact='true' list='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value=application.no_other_contacts_rationale|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if step == Step.OTHER_CONTACTS %}
|
||||
{% for other in application.other_contacts.all %}
|
||||
<div class="margin-bottom-105">
|
||||
<p class="text-semibold margin-top-1 margin-bottom-0">Contact {{ forloop.counter }}</p>
|
||||
{% include "includes/contact.html" with contact=other %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="margin-bottom-105">
|
||||
<p class="text-semibold margin-top-1 margin-bottom-0">No other employees from your organization?</p>
|
||||
{{ application.no_other_contacts_rationale|default:"Incomplete" }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if step == Step.ANYTHING_ELSE %}
|
||||
{{ application.anything_else|default:"No" }}
|
||||
{% endif %}
|
||||
{% if step == Step.REQUIREMENTS %}
|
||||
{{ application.is_policy_acknowledged|yesno:"I agree.,I do not agree.,I do not agree." }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
aria-describedby="review_step_title__{{step}}"
|
||||
href="{% namespaced_url 'application' step %}"
|
||||
>Edit<span class="sr-only"> {{ form_titles|get_item:step }}</span></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.ANYTHING_ELSE %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.anything_else|default:"No" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.REQUIREMENTS %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.is_policy_acknowledged|yesno:"I agree.,I do not agree.,I do not agree." %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -52,8 +52,8 @@
|
|||
<div class="grid-col desktop:grid-offset-2 maxw-tablet">
|
||||
<h2 class="text-primary-darker"> Summary of your domain request </h2>
|
||||
{% with heading_level='h3' %}
|
||||
{% 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 %}
|
||||
{% with org_type=domainapplication.get_organization_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
|
||||
{% if domainapplication.tribe_name %}
|
||||
|
@ -74,7 +74,9 @@
|
|||
{% endif %}
|
||||
|
||||
{% if domainapplication.is_election_board %}
|
||||
{% include "includes/summary_item.html" with title='Election office' value=domainapplication.is_election_board heading_level=heading_level %}
|
||||
{% with value=domainapplication.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.organization_name %}
|
||||
|
@ -109,7 +111,11 @@
|
|||
{% include "includes/summary_item.html" with title='Your contact information' value=domainapplication.submitter contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=domainapplication.other_contacts.all contact='true' list='true' heading_level=heading_level %}
|
||||
{% if domainapplication.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=domainapplication.other_contacts.all contact='true' list='true' heading_level=heading_level %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=domainapplication.no_other_contacts_rationale heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% include "includes/summary_item.html" with title='Anything else?' value=domainapplication.anything_else|default:"No" heading_level=heading_level %}
|
||||
|
||||
|
|
|
@ -2,9 +2,22 @@ SUMMARY OF YOUR DOMAIN REQUEST
|
|||
|
||||
Type of organization:
|
||||
{{ application.get_organization_type_display }}
|
||||
|
||||
{% if application.show_organization_federal %}
|
||||
Federal government branch:
|
||||
{{ application.get_federal_type_display }}
|
||||
{% elif application.show_tribal_government %}
|
||||
Tribal government:
|
||||
{{ application.tribe_name|default:"Incomplete" }}{% if application.federally_recognized_tribe %}
|
||||
Federally-recognized tribe
|
||||
{% endif %}{% if application.state_recognized_tribe %}
|
||||
State-recognized tribe
|
||||
{% endif %}{% endif %}{% if application.show_organization_election %}
|
||||
Election office:
|
||||
{{ application.is_election_board|yesno:"Yes,No,Incomplete" }}
|
||||
{% endif %}
|
||||
Organization name and mailing address:
|
||||
{% spaceless %}{{ application.organization_name }}
|
||||
{% spaceless %}{{ application.federal_agency }}
|
||||
{{ application.organization_name }}
|
||||
{{ application.address_line1 }}{% if application.address_line2 %}
|
||||
{{ application.address_line2 }}{% endif %}
|
||||
{{ application.city }}, {{ application.state_territory }}
|
||||
|
@ -22,18 +35,21 @@ Current websites: {% for site in application.current_websites.all %}
|
|||
{% endfor %}{% endif %}
|
||||
.gov domain:
|
||||
{{ application.requested_domain.name }}
|
||||
{% if application.alternative_domains.all %}
|
||||
Alternative domains:
|
||||
{% for site in application.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %}
|
||||
{% endfor %}
|
||||
{% endfor %}{% endif %}
|
||||
Purpose of your domain:
|
||||
{{ application.purpose }}
|
||||
|
||||
Your contact information:
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.submitter %}{% endspaceless %}
|
||||
{% if application.other_contacts.all %}
|
||||
Other employees from your organization:
|
||||
{% for other in application.other_contacts.all %}
|
||||
|
||||
Other employees from your organization:{% for other in application.other_contacts.all %}
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=other %}{% endspaceless %}
|
||||
{% endfor %}{% endif %}{% if application.anything_else %}
|
||||
{% empty %}
|
||||
{{ application.no_other_contacts_rationale }}
|
||||
{% endfor %}{% if application.anything_else %}
|
||||
Anything else?
|
||||
{{ application.anything_else }}
|
||||
{% endif %}
|
|
@ -1,4 +1,7 @@
|
|||
<address>
|
||||
{% if organization.federal_agency %}
|
||||
{{ organization.federal_agency }}<br />
|
||||
{% endif %}
|
||||
{% if organization.organization_name %}
|
||||
{{ organization.organization_name }}
|
||||
{% endif %}
|
||||
|
|
|
@ -28,17 +28,22 @@
|
|||
{% if value|length == 1 %}
|
||||
{% include "includes/contact.html" with contact=value|first %}
|
||||
{% else %}
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% for item in value %}
|
||||
<li>
|
||||
<p class="text-semibold margin-top-1 margin-bottom-0">
|
||||
Contact {{forloop.counter}}
|
||||
</p>
|
||||
{% include "includes/contact.html" with contact=item %}</li>
|
||||
{% empty %}
|
||||
<li>None</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if value %}
|
||||
<dl class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% for item in value %}
|
||||
<dt>
|
||||
Contact {{forloop.counter}}
|
||||
</dt>
|
||||
<dd>
|
||||
{% include "includes/contact.html" with contact=item %}
|
||||
</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
{% else %}
|
||||
<p>
|
||||
None
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% include "includes/contact.html" with contact=value %}
|
||||
|
@ -57,10 +62,10 @@
|
|||
{% endspaceless %})
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="margin-top-0">{{ value | first }} </p>
|
||||
<p class="margin-top-0 margin-bottom-0">{{ value | first }} </p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<ul class="usa-list margin-top-0">
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% for item in value %}
|
||||
{% if users %}
|
||||
<li>{{ item.user.email }}</li>
|
||||
|
|
|
@ -526,6 +526,7 @@ def completed_application(
|
|||
has_anything_else=True,
|
||||
status=DomainApplication.ApplicationStatus.STARTED,
|
||||
user=False,
|
||||
submitter=False,
|
||||
name="city.gov",
|
||||
):
|
||||
"""A completed domain application."""
|
||||
|
@ -541,13 +542,14 @@ def completed_application(
|
|||
domain, _ = DraftDomain.objects.get_or_create(name=name)
|
||||
alt, _ = Website.objects.get_or_create(website="city1.gov")
|
||||
current, _ = Website.objects.get_or_create(website="city.com")
|
||||
you, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
title="Admin Tester",
|
||||
email="mayor@igorville.gov",
|
||||
phone="(555) 555 5556",
|
||||
)
|
||||
if not submitter:
|
||||
submitter, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
title="Admin Tester",
|
||||
email="mayor@igorville.gov",
|
||||
phone="(555) 555 5556",
|
||||
)
|
||||
other, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy",
|
||||
last_name="Tester",
|
||||
|
@ -567,7 +569,7 @@ def completed_application(
|
|||
zipcode="10002",
|
||||
authorizing_official=ao,
|
||||
requested_domain=domain,
|
||||
submitter=you,
|
||||
submitter=submitter,
|
||||
creator=user,
|
||||
status=status,
|
||||
)
|
||||
|
|
|
@ -14,11 +14,11 @@ from registrar.admin import (
|
|||
ContactAdmin,
|
||||
DomainInformationAdmin,
|
||||
UserDomainRoleAdmin,
|
||||
VeryImportantPersonAdmin,
|
||||
VerifiedByStaffAdmin,
|
||||
)
|
||||
from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from registrar.models.very_important_person import VeryImportantPerson
|
||||
from registrar.models.verified_by_staff import VerifiedByStaff
|
||||
from .common import (
|
||||
MockSESClient,
|
||||
AuditedAdminMockData,
|
||||
|
@ -1737,11 +1737,90 @@ class ContactAdminTest(TestCase):
|
|||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def test_change_view_for_joined_contact_five_or_less(self):
|
||||
"""Create a contact, join it to 4 domain requests. The 5th join will be a user.
|
||||
Assert that the warning on the contact form lists 5 joins."""
|
||||
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
# Create an instance of the model
|
||||
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
||||
|
||||
# join it to 4 domain requests. The 5th join will be a user.
|
||||
application1 = completed_application(submitter=contact, name="city1.gov")
|
||||
application2 = completed_application(submitter=contact, name="city2.gov")
|
||||
application3 = completed_application(submitter=contact, name="city3.gov")
|
||||
application4 = completed_application(submitter=contact, name="city4.gov")
|
||||
|
||||
with patch("django.contrib.messages.warning") as mock_warning:
|
||||
# Use the test client to simulate the request
|
||||
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
|
||||
|
||||
# Assert that the error message was called with the correct argument
|
||||
# Note: The 5th join will be a user.
|
||||
mock_warning.assert_called_once_with(
|
||||
response.wsgi_request,
|
||||
"<ul class='messagelist_content-list--unstyled'>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application1.pk}/change/'>city1.gov</a></li>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application2.pk}/change/'>city2.gov</a></li>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application3.pk}/change/'>city3.gov</a></li>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application4.pk}/change/'>city4.gov</a></li>"
|
||||
"<li>Joined to User: <a href='/admin/registrar/"
|
||||
f"user/{self.staffuser.pk}/change/'>staff@example.com</a></li>"
|
||||
"</ul>",
|
||||
)
|
||||
|
||||
def test_change_view_for_joined_contact_five_or_more(self):
|
||||
"""Create a contact, join it to 5 domain requests. The 6th join will be a user.
|
||||
Assert that the warning on the contact form lists 5 joins and a '1 more' ellispsis."""
|
||||
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
# Create an instance of the model
|
||||
# join it to 5 domain requests. The 6th join will be a user.
|
||||
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
||||
application1 = completed_application(submitter=contact, name="city1.gov")
|
||||
application2 = completed_application(submitter=contact, name="city2.gov")
|
||||
application3 = completed_application(submitter=contact, name="city3.gov")
|
||||
application4 = completed_application(submitter=contact, name="city4.gov")
|
||||
application5 = completed_application(submitter=contact, name="city5.gov")
|
||||
|
||||
with patch("django.contrib.messages.warning") as mock_warning:
|
||||
# Use the test client to simulate the request
|
||||
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
|
||||
|
||||
logger.info(mock_warning)
|
||||
|
||||
# Assert that the error message was called with the correct argument
|
||||
# Note: The 6th join will be a user.
|
||||
mock_warning.assert_called_once_with(
|
||||
response.wsgi_request,
|
||||
"<ul class='messagelist_content-list--unstyled'>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application1.pk}/change/'>city1.gov</a></li>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application2.pk}/change/'>city2.gov</a></li>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application3.pk}/change/'>city3.gov</a></li>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application4.pk}/change/'>city4.gov</a></li>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application5.pk}/change/'>city5.gov</a></li>"
|
||||
"</ul>"
|
||||
"<p class='font-sans-3xs'>And 1 more...</p>",
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
DomainApplication.objects.all().delete()
|
||||
Contact.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
|
||||
class VeryImportantPersonAdminTestCase(TestCase):
|
||||
class VerifiedByStaffAdminTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.superuser = create_superuser()
|
||||
self.factory = RequestFactory()
|
||||
|
@ -1750,13 +1829,13 @@ class VeryImportantPersonAdminTestCase(TestCase):
|
|||
self.client.force_login(self.superuser)
|
||||
|
||||
# Create an instance of the admin class
|
||||
admin_instance = VeryImportantPersonAdmin(model=VeryImportantPerson, admin_site=None)
|
||||
admin_instance = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=None)
|
||||
|
||||
# Create a VeryImportantPerson instance
|
||||
vip_instance = VeryImportantPerson(email="test@example.com", notes="Test Notes")
|
||||
# Create a VerifiedByStaff instance
|
||||
vip_instance = VerifiedByStaff(email="test@example.com", notes="Test Notes")
|
||||
|
||||
# Create a request object
|
||||
request = self.factory.post("/admin/yourapp/veryimportantperson/add/")
|
||||
request = self.factory.post("/admin/yourapp/VerifiedByStaff/add/")
|
||||
request.user = self.superuser
|
||||
|
||||
# Call the save_model method
|
||||
|
|
|
@ -102,9 +102,9 @@ class TestEmails(TestCase):
|
|||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertNotIn("Other employees from your organization:", body)
|
||||
# spacing should be right between adjacent elements
|
||||
self.assertRegex(body, r"5556\n\nAnything else")
|
||||
self.assertRegex(body, r"5556\n\nOther employees")
|
||||
self.assertRegex(body, r"None\n\nAnything else")
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_alternative_govdomain_spacing(self):
|
||||
|
@ -117,7 +117,7 @@ class TestEmails(TestCase):
|
|||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertIn("city1.gov", body)
|
||||
# spacing should be right between adjacent elements
|
||||
self.assertRegex(body, r"city.gov\ncity1.gov\n\nPurpose of your domain:")
|
||||
self.assertRegex(body, r"city.gov\n\nAlternative domains:\ncity1.gov\n\nPurpose of your domain:")
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_no_alternative_govdomain_spacing(self):
|
||||
|
|
|
@ -64,6 +64,12 @@ class TestFormValidation(MockEppLib):
|
|||
form = DotGovDomainForm(data={"requested_domain": "top-level-agency"})
|
||||
self.assertEqual(len(form.errors), 0)
|
||||
|
||||
def test_requested_domain_starting_www(self):
|
||||
"""Test a valid domain name with .www at the beginning."""
|
||||
form = DotGovDomainForm(data={"requested_domain": "www.top-level-agency"})
|
||||
self.assertEqual(len(form.errors), 0)
|
||||
self.assertEqual(form.cleaned_data["requested_domain"], "top-level-agency")
|
||||
|
||||
def test_requested_domain_ending_dotgov(self):
|
||||
"""Just a valid domain name with .gov at the end."""
|
||||
form = DotGovDomainForm(data={"requested_domain": "top-level-agency.gov"})
|
||||
|
|
|
@ -43,6 +43,9 @@ class TestGroups(TestCase):
|
|||
"change_user",
|
||||
"delete_userdomainrole",
|
||||
"view_userdomainrole",
|
||||
"add_verifiedbystaff",
|
||||
"change_verifiedbystaff",
|
||||
"delete_verifiedbystaff",
|
||||
"change_website",
|
||||
]
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ from registrar.models import (
|
|||
|
||||
import boto3_mocking
|
||||
from registrar.models.transition_domain import TransitionDomain
|
||||
from registrar.models.very_important_person import VeryImportantPerson # type: ignore
|
||||
from registrar.models.verified_by_staff import VerifiedByStaff # type: ignore
|
||||
from .common import MockSESClient, less_console_noise, completed_application
|
||||
from django_fsm import TransitionNotAllowed
|
||||
|
||||
|
@ -656,7 +656,7 @@ class TestUser(TestCase):
|
|||
def test_identity_verification_with_very_important_person(self):
|
||||
"""A Very Important Person should return False
|
||||
when tested with class method needs_identity_verification"""
|
||||
VeryImportantPerson.objects.get_or_create(email=self.user.email)
|
||||
VerifiedByStaff.objects.get_or_create(email=self.user.email)
|
||||
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
|
||||
|
||||
def test_identity_verification_with_invited_user(self):
|
||||
|
|
|
@ -712,7 +712,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
|
||||
# Review page contains all the previously entered data
|
||||
# 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, "Federal")
|
||||
self.assertContains(review_page, "Executive")
|
||||
self.assertContains(review_page, "Testorg")
|
||||
self.assertContains(review_page, "address 1")
|
||||
|
@ -2360,18 +2360,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
|
||||
self.assertContains(type_page, "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.ApplicationStatus.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", index=0)
|
||||
self.assertContains(detail_page, "Federal: an agency of the U.S. government")
|
||||
|
||||
def test_submit_modal_no_domain_text_fallback(self):
|
||||
"""When user clicks on submit your domain request and the requested domain
|
||||
is null (possible through url direct access to the review page), present
|
||||
|
|
|
@ -3,11 +3,12 @@ import logging
|
|||
from datetime import datetime
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.domain_information import DomainInformation
|
||||
from registrar.models.public_contact import PublicContact
|
||||
from django.db.models import Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import F, Value, CharField
|
||||
from django.db.models.functions import Concat, Coalesce
|
||||
|
||||
from registrar.models.public_contact import PublicContact
|
||||
from registrar.utility.enums import DefaultEmail
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -22,50 +23,77 @@ def write_header(writer, columns):
|
|||
|
||||
|
||||
def get_domain_infos(filter_condition, sort_fields):
|
||||
domain_infos = DomainInformation.objects.filter(**filter_condition).order_by(*sort_fields)
|
||||
return domain_infos
|
||||
domain_infos = (
|
||||
DomainInformation.objects.select_related("domain", "authorizing_official")
|
||||
.filter(**filter_condition)
|
||||
.order_by(*sort_fields)
|
||||
)
|
||||
|
||||
# Do a mass concat of the first and last name fields for authorizing_official.
|
||||
# The old operation was computationally heavy for some reason, so if we precompute
|
||||
# this here, it is vastly more efficient.
|
||||
domain_infos_cleaned = domain_infos.annotate(
|
||||
ao=Concat(
|
||||
Coalesce(F("authorizing_official__first_name"), Value("")),
|
||||
Value(" "),
|
||||
Coalesce(F("authorizing_official__last_name"), Value("")),
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
return domain_infos_cleaned
|
||||
|
||||
|
||||
def write_row(writer, columns, domain_info):
|
||||
security_contacts = domain_info.domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY)
|
||||
def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None):
|
||||
"""Given a set of columns, generate a new row from cleaned column data"""
|
||||
|
||||
# For linter
|
||||
ao = " "
|
||||
if domain_info.authorizing_official:
|
||||
first_name = domain_info.authorizing_official.first_name or ""
|
||||
last_name = domain_info.authorizing_official.last_name or ""
|
||||
ao = first_name + " " + last_name
|
||||
# Domain should never be none when parsing this information
|
||||
if domain_info.domain is None:
|
||||
raise ValueError("Domain is none")
|
||||
|
||||
security_email = " "
|
||||
if security_contacts:
|
||||
security_email = security_contacts[0].email
|
||||
domain = domain_info.domain # type: ignore
|
||||
|
||||
# Grab the security email from a preset dictionary.
|
||||
# If nothing exists in the dictionary, grab from .contacts.
|
||||
if security_emails_dict is not None and domain.name in security_emails_dict:
|
||||
_email = security_emails_dict.get(domain.name)
|
||||
security_email = _email if _email is not None else " "
|
||||
else:
|
||||
# If the dictionary doesn't contain that data, lets filter for it manually.
|
||||
# This is a last resort as this is a more expensive operation.
|
||||
security_contacts = domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY)
|
||||
_email = security_contacts[0].email if security_contacts else None
|
||||
security_email = _email if _email is not None else " "
|
||||
|
||||
invalid_emails = {DefaultEmail.LEGACY_DEFAULT, DefaultEmail.PUBLIC_CONTACT_DEFAULT}
|
||||
# These are default emails that should not be displayed in the csv report
|
||||
if security_email is not None and security_email.lower() in invalid_emails:
|
||||
invalid_emails = {DefaultEmail.LEGACY_DEFAULT, DefaultEmail.PUBLIC_CONTACT_DEFAULT}
|
||||
if security_email.lower() in invalid_emails:
|
||||
security_email = "(blank)"
|
||||
|
||||
if domain_info.federal_type:
|
||||
domain_type = f"{domain_info.get_organization_type_display()} - {domain_info.get_federal_type_display()}"
|
||||
else:
|
||||
domain_type = domain_info.get_organization_type_display()
|
||||
|
||||
# create a dictionary of fields which can be included in output
|
||||
FIELDS = {
|
||||
"Domain name": domain_info.domain.name,
|
||||
"Domain type": domain_info.get_organization_type_display() + " - " + domain_info.get_federal_type_display()
|
||||
if domain_info.federal_type
|
||||
else domain_info.get_organization_type_display(),
|
||||
"Domain name": domain.name,
|
||||
"Domain type": domain_type,
|
||||
"Agency": domain_info.federal_agency,
|
||||
"Organization name": domain_info.organization_name,
|
||||
"City": domain_info.city,
|
||||
"State": domain_info.state_territory,
|
||||
"AO": ao,
|
||||
"AO": domain_info.ao, # type: ignore
|
||||
"AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ",
|
||||
"Security contact email": security_email,
|
||||
"Status": domain_info.domain.get_state_display(),
|
||||
"Expiration date": domain_info.domain.expiration_date,
|
||||
"Created at": domain_info.domain.created_at,
|
||||
"First ready": domain_info.domain.first_ready,
|
||||
"Deleted": domain_info.domain.deleted,
|
||||
"Status": domain.get_state_display(),
|
||||
"Expiration date": domain.expiration_date,
|
||||
"Created at": domain.created_at,
|
||||
"First ready": domain.first_ready,
|
||||
"Deleted": domain.deleted,
|
||||
}
|
||||
|
||||
writer.writerow([FIELDS.get(column, "") for column in columns])
|
||||
row = [FIELDS.get(column, "") for column in columns]
|
||||
return row
|
||||
|
||||
|
||||
def write_body(
|
||||
|
@ -80,13 +108,41 @@ def write_body(
|
|||
"""
|
||||
|
||||
# Get the domainInfos
|
||||
domain_infos = get_domain_infos(filter_condition, sort_fields)
|
||||
all_domain_infos = get_domain_infos(filter_condition, sort_fields)
|
||||
|
||||
all_domain_infos = list(domain_infos)
|
||||
# Store all security emails to avoid epp calls or excessive filters
|
||||
sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)
|
||||
security_emails_dict = {}
|
||||
public_contacts = (
|
||||
PublicContact.objects.only("email", "domain__name")
|
||||
.select_related("domain")
|
||||
.filter(registry_id__in=sec_contact_ids)
|
||||
)
|
||||
|
||||
# Write rows to CSV
|
||||
for domain_info in all_domain_infos:
|
||||
write_row(writer, columns, domain_info)
|
||||
# Populate a dictionary of domain names and their security contacts
|
||||
for contact in public_contacts:
|
||||
domain: Domain = contact.domain
|
||||
if domain is not None and domain.name not in security_emails_dict:
|
||||
security_emails_dict[domain.name] = contact.email
|
||||
else:
|
||||
logger.warning("csv_export -> Domain was none for PublicContact")
|
||||
|
||||
# Reduce the memory overhead when performing the write operation
|
||||
paginator = Paginator(all_domain_infos, 1000)
|
||||
for page_num in paginator.page_range:
|
||||
page = paginator.page(page_num)
|
||||
rows = []
|
||||
for domain_info in page.object_list:
|
||||
try:
|
||||
row = parse_row(columns, domain_info, security_emails_dict)
|
||||
rows.append(row)
|
||||
except ValueError:
|
||||
# This should not happen. If it does, just skip this row.
|
||||
# It indicates that DomainInformation.domain is None.
|
||||
logger.error("csv_export -> Error when parsing row, domain was None")
|
||||
continue
|
||||
|
||||
writer.writerows(rows)
|
||||
|
||||
|
||||
def export_data_type_to_csv(csv_file):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue