Merge branch 'main' into za/profile-page-update-label

This commit is contained in:
zandercymatics 2024-07-09 09:34:25 -06:00
commit 3ca08ee01e
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
10 changed files with 165 additions and 4 deletions

View file

@ -31,8 +31,8 @@ body:
attributes:
label: Links to other issues
description: |
"Add issue #numbers this relates to and how (e.g., 🚧 [construction] Blocks, ⛔️ [no_entry] Is blocked by, 🔄 [arrows_counterclockwise] Relates to)."
placeholder: 🔄 Relates to...
"With a `-` to start the line, add issue #numbers this relates to and how (e.g., 🚧 [construction] Blocks, ⛔️ [no_entry] Is blocked by, 🔄 [arrows_counterclockwise] Relates to)."
placeholder: "- 🔄 Relates to..."
- type: markdown
id: note
attributes:

View file

@ -1043,6 +1043,19 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
return super().changelist_view(request, extra_context=extra_context)
class SeniorOfficialAdmin(ListHeaderAdmin):
"""Custom Senior Official Admin class."""
# NOTE: these are just placeholders. Not part of ACs (haven't been defined yet). Update in future tickets.
search_fields = ["first_name", "last_name", "email"]
search_help_text = "Search by first name, last name or email."
list_display = ["first_name", "last_name", "email"]
# this ordering effects the ordering of results
# in autocomplete_fields for Senior Official
ordering = ["first_name", "last_name"]
class WebsiteResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
import/export file"""
@ -2799,6 +2812,7 @@ admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
admin.site.register(models.Portfolio, PortfolioAdmin)
admin.site.register(models.DomainGroup, DomainGroupAdmin)
admin.site.register(models.Suborganization, SuborganizationAdmin)
admin.site.register(models.SeniorOfficial, SeniorOfficialAdmin)
# Register our custom waffle implementations
admin.site.register(models.WaffleFlag, WaffleFlagAdmin)

View file

@ -0,0 +1,45 @@
# Generated by Django 4.2.10 on 2024-07-02 21:03
from django.db import migrations, models
import django.db.models.deletion
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
("registrar", "0109_domaininformation_sub_organization_and_more"),
]
operations = [
migrations.CreateModel(
name="SeniorOfficial",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("first_name", models.CharField(verbose_name="first name")),
("last_name", models.CharField(verbose_name="last name")),
("title", models.CharField(verbose_name="title / role")),
(
"phone",
phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None),
),
("email", models.EmailField(blank=True, max_length=320, null=True)),
],
options={
"abstract": False,
},
),
migrations.AddField(
model_name="portfolio",
name="senior_official",
field=models.ForeignKey(
blank=True,
help_text="Associated senior official",
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="registrar.seniorofficial",
),
),
]

View file

@ -0,0 +1,37 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
# It is dependent on 0079 (which populates federal agencies)
# 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", "0110_seniorofficial_portfolio_senior_official"),
]
operations = [
migrations.RunPython(
create_groups,
reverse_code=migrations.RunPython.noop,
atomic=True,
),
]

View file

@ -19,6 +19,7 @@ from .waffle_flag import WaffleFlag
from .portfolio import Portfolio
from .domain_group import DomainGroup
from .suborganization import Suborganization
from .senior_official import SeniorOfficial
__all__ = [
@ -42,6 +43,7 @@ __all__ = [
"Portfolio",
"DomainGroup",
"Suborganization",
"SeniorOfficial",
]
auditlog.register(Contact)
@ -64,3 +66,4 @@ auditlog.register(WaffleFlag)
auditlog.register(Portfolio)
auditlog.register(DomainGroup)
auditlog.register(Suborganization)
auditlog.register(SeniorOfficial)

View file

@ -38,6 +38,15 @@ class Portfolio(TimeStampedModel):
default=FederalAgency.get_non_federal_agency,
)
senior_official = models.ForeignKey(
"registrar.SeniorOfficial",
on_delete=models.PROTECT,
help_text="Associated senior official",
unique=False,
null=True,
blank=True,
)
organization_type = models.CharField(
max_length=255,
choices=OrganizationChoices.choices,

View file

@ -0,0 +1,50 @@
from django.db import models
from .utility.time_stamped_model import TimeStampedModel
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
class SeniorOfficial(TimeStampedModel):
"""
Senior Official is a distinct Contact-like entity (NOT to be inherited
from Contacts) developed for the unique role these individuals have in
managing Portfolios.
"""
first_name = models.CharField(
null=False,
blank=False,
verbose_name="first name",
)
last_name = models.CharField(
null=False,
blank=False,
verbose_name="last name",
)
title = models.CharField(
null=False,
blank=False,
verbose_name="title / role",
)
phone = PhoneNumberField(
null=True,
blank=True,
)
email = models.EmailField(
null=True,
blank=True,
max_length=320,
)
def get_formatted_name(self):
"""Returns the contact's name in Western order."""
names = [n for n in [self.first_name, self.last_name] if n]
return " ".join(names) if names else "Unknown"
def __str__(self):
if self.first_name or self.last_name:
return self.get_formatted_name()
elif self.pk:
return str(self.pk)
else:
return ""

View file

@ -18,7 +18,7 @@
completing your domain request might take around 15 minutes.</p>
{% if has_profile_feature_flag %}
<h2>How well reach you</h2>
<p>While reviewing your domain request, we may need to reach out with questions. Well also email you when we complete our review If the contact information below is not correct, visit <a href="{% url 'user-profile' %}?redirect=domain-request:" class="usa-link">your profile</a> to make updates.</p>
<p>While reviewing your domain request, we may need to reach out with questions. Well also email you when we complete our review. If the contact information below is not correct, visit <a href="{% url 'user-profile' %}?redirect=domain-request:" class="usa-link">your profile</a> to make updates.</p>
{% include "includes/profile_information.html" with user=user%}
{% endif %}

View file

@ -686,6 +686,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
# Add a phone number
finish_setup_form = finish_setup_page.form
finish_setup_form["first_name"] = "firstname"
finish_setup_form["phone"] = "(201) 555-0123"
finish_setup_form["title"] = "CEO"
finish_setup_form["last_name"] = "example"
@ -814,6 +815,8 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
self.assertContains(save_page, "Your profile has been updated.")
# We need to assert that logo is not clickable and links to manage your domain are not present
# NOTE: "anage" is not a typo. It is to accomodate the fact that the "m" is uppercase in one
# instance and lowercase in the other.
self.assertContains(save_page, "anage your domains", count=2)
self.assertNotContains(
save_page, "Before you can manage your domains, we need you to add contact information"

View file

@ -1,5 +1,4 @@
"""Views for a User Profile.
"""
import logging
@ -109,6 +108,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
"""If the form is invalid, conditionally display an additional error."""
if hasattr(self.user, "finished_setup") and not self.user.finished_setup:
messages.error(self.request, "Before you can manage your domain, we need you to add contact information.")
form.initial["redirect"] = form.data.get("redirect")
return super().form_invalid(form)
def form_valid(self, form):