Merge remote-tracking branch 'origin/main' into nl/2136-CISA-rep-additional-details

This commit is contained in:
CocoByte 2024-06-09 14:53:59 -06:00
commit 09f872061f
No known key found for this signature in database
GPG key ID: BBFAA2526384C97F
24 changed files with 369 additions and 44 deletions

View file

@ -25,6 +25,8 @@ jobs:
|| startsWith(github.head_ref, 'meoward/')
|| startsWith(github.head_ref, 'bob/')
|| startsWith(github.head_ref, 'cb/')
|| startsWith(github.head_ref, 'hotgov/')
|| startsWith(github.head_ref, 'litterbox/')
outputs:
environment: ${{ steps.var.outputs.environment}}
runs-on: "ubuntu-latest"

View file

@ -16,6 +16,8 @@ on:
- stable
- staging
- development
- litterbox
- hotgov
- cb
- bob
- meoward

View file

@ -16,6 +16,8 @@ on:
options:
- staging
- development
- litterbox
- hotgov
- cb
- bob
- meoward

View file

@ -320,16 +320,6 @@ it may help to resync your laptop with time.nist.gov:
sudo sntp -sS time.nist.gov
```
### Settings
The config for the connection pool exists inside the `settings.py` file.
| Name | Purpose |
| ------------------------ | ------------------------------------------------------------------------------------------------- |
| EPP_CONNECTION_POOL_SIZE | Determines the number of concurrent sockets that should exist in the pool. |
| POOL_KEEP_ALIVE | Determines the interval in which we ping open connections in seconds. Calculated as POOL_KEEP_ALIVE / EPP_CONNECTION_POOL_SIZE |
| POOL_TIMEOUT | Determines how long we try to keep a pool alive for, before restarting it. |
Consider updating the `POOL_TIMEOUT` or `POOL_KEEP_ALIVE` periods if the pool often restarts. If the pool only restarts after a period of inactivity, update `POOL_KEEP_ALIVE`. If it restarts during the EPP call itself, then `POOL_TIMEOUT` needs to be updated.
## Adding a S3 instance to your sandbox
This can either be done through the CLI, or through the cloud.gov dashboard. Generally, it is better to do it through the dashboard as it handles app binding for you.

View file

@ -668,3 +668,32 @@ Example: `cf ssh getgov-za`
#### Step 1: Running the script
```docker-compose exec app ./manage.py populate_verification_type```
## Copy names from contacts to users
### Running on sandboxes
#### Step 1: Login to CloudFoundry
```cf login -a api.fr.cloud.gov --sso```
#### Step 2: SSH into your environment
```cf ssh getgov-{space}```
Example: `cf ssh getgov-za`
#### Step 3: Create a shell instance
```/tmp/lifecycle/shell```
#### Step 4: Running the script
```./manage.py copy_names_from_contacts_to_users --debug```
### Running locally
#### Step 1: Running the script
```docker-compose exec app ./manage.py copy_names_from_contacts_to_users --debug```
##### Optional parameters
| | Parameter | Description |
|:-:|:-------------------------- |:----------------------------------------------------------------------------|
| 1 | **debug** | Increases logging detail. Defaults to False. |

View file

@ -0,0 +1,32 @@
---
applications:
- name: getgov-hotgov
buildpacks:
- python_buildpack
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http
health-check-http-endpoint: /health
health-check-invocation-timeout: 40
env:
# Send stdout and stderr straight to the terminal without buffering
PYTHONUNBUFFERED: yup
# 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-hotgov.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# default public site location
GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments
IS_PRODUCTION: False
routes:
- route: getgov-hotgov.app.cloud.gov
services:
- getgov-credentials
- getgov-hotgov-database

View file

@ -0,0 +1,32 @@
---
applications:
- name: getgov-litterbox
buildpacks:
- python_buildpack
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http
health-check-http-endpoint: /health
health-check-invocation-timeout: 40
env:
# Send stdout and stderr straight to the terminal without buffering
PYTHONUNBUFFERED: yup
# 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-litterbox.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# default public site location
GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments
IS_PRODUCTION: False
routes:
- route: getgov-litterbox.app.cloud.gov
services:
- getgov-credentials
- getgov-litterbox-database

View file

@ -383,6 +383,39 @@ class CustomLogEntryAdmin(LogEntryAdmin):
change_form_template = "admin/change_form_no_submit.html"
add_form_template = "admin/change_form_no_submit.html"
# Select log entry to change -> Log entries
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Log entries"
return super().changelist_view(request, extra_context=extra_context)
# #786: Skipping on updating audit log tab titles for now
# def change_view(self, request, object_id, form_url="", extra_context=None):
# if extra_context is None:
# extra_context = {}
# log_entry = self.get_object(request, object_id)
# if log_entry:
# # Reset title to empty string
# extra_context["subtitle"] = ""
# extra_context["tabtitle"] = ""
# object_repr = log_entry.object_repr # Hold name of the object
# changes = log_entry.changes
# # Check if this is a log entry for an addition and related to the contact model
# # Created [name] -> Created [name] contact | Change log entry
# if (
# all(new_value != "None" for field, (old_value, new_value) in changes.items())
# and log_entry.content_type.model == "contact"
# ):
# extra_context["subtitle"] = f"Created {object_repr} contact"
# extra_context["tabtitle"] = "Change log entry"
# return super().change_view(request, object_id, form_url, extra_context=extra_context)
class AdminSortFields:
_name_sort = ["first_name", "last_name", "email"]
@ -594,7 +627,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
None,
{"fields": ("username", "password", "status", "verification_type")},
),
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "email", "title")}),
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
(
"Permissions",
{
@ -625,7 +658,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
)
},
),
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "email", "title")}),
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
(
"Permissions",
{
@ -778,6 +811,14 @@ class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin):
search_help_text = "Search by domain or host name."
inlines = [HostIPInline]
# Select host to change -> Host
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Host"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
class HostIpResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
@ -793,6 +834,14 @@ class HostIpAdmin(AuditedAdmin, ImportExportModelAdmin):
resource_classes = [HostIpResource]
model = models.HostIP
# Select host ip to change -> Host ip
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Host IP"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
class ContactResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
@ -926,6 +975,14 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
return super().change_view(request, object_id, form_url, extra_context=extra_context)
# Select contact to change -> Contacts
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Contacts"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
class WebsiteResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
@ -1043,6 +1100,21 @@ class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin):
else:
return response
# User Domain manager [email] is manager on domain [domain name] ->
# Domain manager [email] on [domain name]
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
if extra_context is None:
extra_context = {}
if object_id:
obj = self.get_object(request, object_id)
if obj:
email = obj.user.email
domain_name = obj.domain.name
extra_context["subtitle"] = f"Domain manager {email} on {domain_name}"
return super().changeform_view(request, object_id, form_url, extra_context=extra_context)
class DomainInvitationAdmin(ListHeaderAdmin):
"""Custom domain invitation admin class."""
@ -1079,6 +1151,14 @@ class DomainInvitationAdmin(ListHeaderAdmin):
change_form_template = "django/admin/email_clipboard_change_form.html"
# Select domain invitations to change -> Domain invitations
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Domain invitations"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
class DomainInformationResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
@ -1219,6 +1299,14 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields # Read-only fields for analysts
# Select domain information to change -> Domain information
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Domain information"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
class DomainRequestResource(FsmModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
@ -1678,11 +1766,17 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
if next_char.isdigit():
should_apply_default_filter = True
# Select domain request to change -> Domain requests
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Domain requests"
if should_apply_default_filter:
# modify the GET of the request to set the selected filter
modified_get = copy.deepcopy(request.GET)
modified_get["status__in"] = "submitted,in review,action needed"
request.GET = modified_get
response = super().changelist_view(request, extra_context=extra_context)
return response
@ -2248,6 +2342,14 @@ class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# If no redirection is needed, return the original response
return response
# Select draft domain to change -> Draft domains
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Draft domains"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
class PublicContactResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
@ -2292,6 +2394,20 @@ class PublicContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
change_form_template = "django/admin/email_clipboard_change_form.html"
autocomplete_fields = ["domain"]
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
if extra_context is None:
extra_context = {}
if object_id:
obj = self.get_object(request, object_id)
if obj:
name = obj.name
email = obj.email
registry_id = obj.registry_id
extra_context["subtitle"] = f"{name} <{email}> id: {registry_id}"
return super().changeform_view(request, object_id, form_url, extra_context=extra_context)
class VerifiedByStaffAdmin(ListHeaderAdmin):
list_display = ("email", "requestor", "truncated_notes", "created_at")
@ -2344,6 +2460,14 @@ class UserGroupAdmin(AuditedAdmin):
def user_group(self, obj):
return obj.name
# Select user groups to change -> User groups
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "User groups"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
class WaffleFlagAdmin(FlagAdmin):
"""Custom admin implementation of django-waffle's Flag class"""

View file

@ -659,6 +659,8 @@ ALLOWED_HOSTS = [
"getgov-stable.app.cloud.gov",
"getgov-staging.app.cloud.gov",
"getgov-development.app.cloud.gov",
"getgov-litterbox.app.cloud.gov",
"getgov-hotgov.app.cloud.gov",
"getgov-cb.app.cloud.gov",
"getgov-bob.app.cloud.gov",
"getgov-meoward.app.cloud.gov",

View file

@ -10,6 +10,7 @@ from registrar.management.commands.utility.terminal_helper import (
)
from registrar.models.contact import Contact
from registrar.models.user import User
from registrar.models.utility.domain_helper import DomainHelper
logger = logging.getLogger(__name__)
@ -110,13 +111,19 @@ class Command(BaseCommand):
{TerminalColors.ENDC}""", # noqa
)
# ---- UPDATE THE USER IF IT DOES NOT HAVE A FIRST AND LAST NAMES
# ---- LET'S KEEP A LIGHT TOUCH
if not eligible_user.first_name and not eligible_user.last_name:
# (expression has type "str | None", variable has type "str | int | Combinable")
# so we'll ignore type
eligible_user.first_name = contact.first_name # type: ignore
eligible_user.last_name = contact.last_name # type: ignore
# Get the fields that exist on both User and Contact. Excludes id.
common_fields = DomainHelper.get_common_fields(User, Contact)
if "email" in common_fields:
# Don't change the email field.
common_fields.remove("email")
for field in common_fields:
# Grab the value that contact has stored for this field
new_value = getattr(contact, field)
# Set it on the user field
setattr(eligible_user, field, new_value)
eligible_user.save()
processed_user = eligible_user

View file

@ -0,0 +1,19 @@
# Generated by Django 4.2.10 on 2024-06-06 18:38
from django.db import migrations
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
("registrar", "0096_alter_contact_email_alter_contact_first_name_and_more"),
]
operations = [
migrations.AlterField(
model_name="user",
name="phone",
field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None),
),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 4.2.10 on 2024-06-07 15:27
from django.db import migrations
import django_fsm
class Migration(migrations.Migration):
dependencies = [
("registrar", "0097_alter_user_phone"),
]
operations = [
migrations.AlterField(
model_name="domainrequest",
name="status",
field=django_fsm.FSMField(
choices=[
("in review", "In review"),
("action needed", "Action needed"),
("approved", "Approved"),
("rejected", "Rejected"),
("ineligible", "Ineligible"),
("submitted", "Submitted"),
("withdrawn", "Withdrawn"),
("started", "Started"),
],
default="started",
max_length=50,
),
),
]

View file

@ -123,11 +123,21 @@ class Contact(TimeStampedModel):
self.user.last_name = self.last_name
updated = True
# Update middle_name if necessary
if not self.user.middle_name:
self.user.middle_name = self.middle_name
updated = True
# Update phone if necessary
if not self.user.phone:
self.user.phone = self.phone
updated = True
# Update title if necessary
if not self.user.title:
self.user.title = self.title
updated = True
# Save user if any updates were made
if updated:
self.user.save()

View file

@ -43,14 +43,14 @@ class DomainRequest(TimeStampedModel):
# Constants for choice fields
class DomainRequestStatus(models.TextChoices):
STARTED = "started", "Started"
SUBMITTED = "submitted", "Submitted"
IN_REVIEW = "in review", "In review"
ACTION_NEEDED = "action needed", "Action needed"
APPROVED = "approved", "Approved"
WITHDRAWN = "withdrawn", "Withdrawn"
REJECTED = "rejected", "Rejected"
INELIGIBLE = "ineligible", "Ineligible"
SUBMITTED = "submitted", "Submitted"
WITHDRAWN = "withdrawn", "Withdrawn"
STARTED = "started", "Started"
class StateTerritoryChoices(models.TextChoices):
ALABAMA = "AL", "Alabama (AL)"

View file

@ -87,7 +87,6 @@ class User(AbstractUser):
phone = PhoneNumberField(
null=True,
blank=True,
help_text="Phone",
)
middle_name = models.CharField(

View file

@ -24,9 +24,11 @@ def handle_profile(sender, instance, **kwargs):
"""
first_name = getattr(instance, "first_name", "")
middle_name = getattr(instance, "middle_name", "")
last_name = getattr(instance, "last_name", "")
email = getattr(instance, "email", "")
phone = getattr(instance, "phone", "")
title = getattr(instance, "title", "")
is_new_user = kwargs.get("created", False)
@ -39,9 +41,11 @@ def handle_profile(sender, instance, **kwargs):
Contact.objects.create(
user=instance,
first_name=first_name,
middle_name=middle_name,
last_name=last_name,
email=email,
phone=phone,
title=title,
)
if len(contacts) >= 1 and is_new_user: # a matching contact

View file

@ -26,7 +26,18 @@
<script type="application/javascript" src="{% static 'js/dja-collapse.js' %}" defer></script>
{% endblock %}
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
{% block title %}
{% if subtitle %}
{{ subtitle }} |
{% endif %}
{% if tabtitle %}
{{ tabtitle }} |
{% else %}
{{ title }} |
{% endif %}
{{ site_title|default:_('Django site admin') }}
{% endblock %}
{% block extrastyle %}{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}" />
{% endblock %}

View file

@ -1,7 +1,7 @@
{% extends "domain_base.html" %}
{% load static field_helpers url_helpers %}
{% block title %}Domain authorizing official | {{ domain.name }} | {% endblock %}
{% block title %}Authorizing official | {{ domain.name }} | {% endblock %}
{% block domain_content %}
{# this is right after the messages block in the parent template #}

View file

@ -1,7 +1,7 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Domain: {{ domain.name }} | {% endblock %}
{% block title %}{{ domain.name }} | {% endblock %}
{% block content %}
<div class="grid-container">

View file

@ -1,6 +1,8 @@
{% extends 'base.html' %}
{% load static form_helpers url_helpers %}
{% block title %} Start a request | {% endblock %}
{% block content %}
<main id="main-content" class="grid-container">
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">

View file

@ -3,7 +3,7 @@
<div class="usa-logo display-inline-block" id="extended-logo">
<strong class="usa-logo__text" >
{% if logo_clickable %}
<a href="{% url 'home' %}">.gov Registrar </a>
<a href="{% url 'home' %}">.gov Registrar</a>
{% else %}
<button
class="usa-button--unstyled disabled-button usa-tooltip"

View file

@ -3538,7 +3538,7 @@ class TestMyUserAdmin(TestCase):
)
},
),
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "email", "title")}),
("Personal Info", {"fields": ("first_name", "middle_name", "last_name", "title", "email", "phone")}),
("Permissions", {"fields": ("is_active", "groups")}),
("Important dates", {"fields": ("last_login", "date_joined")}),
)

View file

@ -23,15 +23,32 @@ class TestDataUpdates(TestCase):
self.bs_user = User.objects.create()
self.contact1 = Contact.objects.create(
user=self.user1, email="email1@igorville.gov", first_name="first1", last_name="last1"
user=self.user1,
email="email1@igorville.gov",
first_name="first1",
last_name="last1",
middle_name="middle1",
title="title1",
)
self.contact2 = Contact.objects.create(
user=self.user2, email="email2@igorville.gov", first_name="first2", last_name="last2"
user=self.user2,
email="email2@igorville.gov",
first_name="first2",
last_name="last2",
middle_name="middle2",
title="title2",
)
self.contact3 = Contact.objects.create(
user=self.user3, email="email3@igorville.gov", first_name="first3", last_name="last3"
user=self.user3,
email="email3@igorville.gov",
first_name="first3",
last_name="last3",
middle_name="middle3",
title="title3",
)
self.contact4 = Contact.objects.create(
email="email4@igorville.gov", first_name="first4", last_name="last4", middle_name="middle4", title="title4"
)
self.contact4 = Contact.objects.create(email="email4@igorville.gov", first_name="first4", last_name="last4")
self.command = Command()
@ -42,14 +59,15 @@ class TestDataUpdates(TestCase):
Contact.objects.all().delete()
def test_script_updates_linked_users(self):
"""Test the script that copies contacts' first and last names into associated users that
are eligible (first or last are blank or undefined)"""
"""Test the script that copies contact information to the user object"""
# Set up the users' first and last names here so
# they that they don't get overwritten by Contact's save()
# User with no first or last names
self.user1.first_name = ""
self.user1.last_name = ""
self.user1.title = "dummytitle"
self.user1.middle_name = "dummymiddle"
self.user1.save()
# User with a first name but no last name
@ -87,12 +105,20 @@ class TestDataUpdates(TestCase):
# The user that has no first and last names will get them from the contact
self.assertEqual(self.user1.first_name, "first1")
self.assertEqual(self.user1.last_name, "last1")
# The user that has a first but no last will be left alone
self.assertEqual(self.user2.first_name, "First name but no last name")
self.assertEqual(self.user2.last_name, "")
# The user that has a first and a last will be left alone
self.assertEqual(self.user3.first_name, "An existing first name")
self.assertEqual(self.user3.last_name, "An existing last name")
self.assertEqual(self.user1.middle_name, "middle1")
self.assertEqual(self.user1.title, "title1")
# The user that has a first but no last will be updated
self.assertEqual(self.user2.first_name, "first2")
self.assertEqual(self.user2.last_name, "last2")
self.assertEqual(self.user2.middle_name, "middle2")
self.assertEqual(self.user2.title, "title2")
# The user that has a first and a last will be updated
self.assertEqual(self.user3.first_name, "first3")
self.assertEqual(self.user3.last_name, "last3")
self.assertEqual(self.user3.middle_name, "middle3")
self.assertEqual(self.user3.title, "title3")
# The unlinked user will be left alone
self.assertEqual(self.user4.first_name, "")
self.assertEqual(self.user4.last_name, "")
self.assertEqual(self.user4.middle_name, None)
self.assertEqual(self.user4.title, None)

View file

@ -1087,7 +1087,7 @@ class TestDomainAuthorizingOfficial(TestDomainOverview):
"""Can load domain's authorizing official page."""
page = self.client.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id}))
# once on the sidebar, once in the title
self.assertContains(page, "Authorizing official", count=2)
self.assertContains(page, "Authorizing official", count=3)
def test_domain_authorizing_official_content(self):
"""Authorizing official information appears on the page."""