mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-18 02:19:23 +02:00
Merge branch 'main' into meoward/2247-user-contact
This commit is contained in:
commit
d1a390cec9
15 changed files with 392 additions and 127 deletions
4
.github/ISSUE_TEMPLATE/issue-default.yml
vendored
4
.github/ISSUE_TEMPLATE/issue-default.yml
vendored
|
@ -31,8 +31,8 @@ body:
|
||||||
attributes:
|
attributes:
|
||||||
label: Links to other issues
|
label: Links to other issues
|
||||||
description: |
|
description: |
|
||||||
"Add issue #numbers this relates to and how (e.g., 🚧 [construction] Blocks, ⛔️ [no_entry] Is blocked by, 🔄 [arrows_counterclockwise] 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...
|
placeholder: "- 🔄 Relates to..."
|
||||||
- type: markdown
|
- type: markdown
|
||||||
id: note
|
id: note
|
||||||
attributes:
|
attributes:
|
||||||
|
|
|
@ -9,6 +9,7 @@ from django.db.models.functions import Concat, Coalesce
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django_fsm import get_available_FIELD_transitions, FSMField
|
from django_fsm import get_available_FIELD_transitions, FSMField
|
||||||
|
from waffle.decorators import flag_is_active
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
|
@ -18,7 +19,7 @@ from epplibwrapper.errors import ErrorCode, RegistryError
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from waffle.admin import FlagAdmin
|
from waffle.admin import FlagAdmin
|
||||||
from waffle.models import Sample, Switch
|
from waffle.models import Sample, Switch
|
||||||
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website
|
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
|
||||||
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
||||||
from registrar.views.utility.mixins import OrderableFieldsMixin
|
from registrar.views.utility.mixins import OrderableFieldsMixin
|
||||||
from django.contrib.admin.views.main import ORDER_VAR
|
from django.contrib.admin.views.main import ORDER_VAR
|
||||||
|
@ -166,6 +167,9 @@ class DomainRequestAdminForm(forms.ModelForm):
|
||||||
"alternative_domains": NoAutocompleteFilteredSelectMultiple("alternative_domains", False),
|
"alternative_domains": NoAutocompleteFilteredSelectMultiple("alternative_domains", False),
|
||||||
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
|
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
|
||||||
}
|
}
|
||||||
|
labels = {
|
||||||
|
"action_needed_reason_email": "Auto-generated email",
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -444,8 +448,9 @@ class AdminSortFields:
|
||||||
sort_mapping = {
|
sort_mapping = {
|
||||||
# == Contact == #
|
# == Contact == #
|
||||||
"other_contacts": (Contact, _name_sort),
|
"other_contacts": (Contact, _name_sort),
|
||||||
"senior_official": (Contact, _name_sort),
|
|
||||||
"submitter": (Contact, _name_sort),
|
"submitter": (Contact, _name_sort),
|
||||||
|
# == Senior Official == #
|
||||||
|
"senior_official": (SeniorOfficial, _name_sort),
|
||||||
# == User == #
|
# == User == #
|
||||||
"creator": (User, _name_sort),
|
"creator": (User, _name_sort),
|
||||||
"user": (User, _name_sort),
|
"user": (User, _name_sort),
|
||||||
|
@ -997,6 +1002,19 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
return super().changelist_view(request, extra_context=extra_context)
|
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):
|
class WebsiteResource(resources.ModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
import/export file"""
|
import/export file"""
|
||||||
|
@ -1467,6 +1485,13 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
custom_election_board.admin_order_field = "is_election_board" # type: ignore
|
custom_election_board.admin_order_field = "is_election_board" # type: ignore
|
||||||
custom_election_board.short_description = "Election office" # type: ignore
|
custom_election_board.short_description = "Election office" # type: ignore
|
||||||
|
|
||||||
|
# This is just a placeholder. This field will be populated in the detail_table_fieldset view.
|
||||||
|
# This is not a field that exists on the model.
|
||||||
|
def status_history(self, obj):
|
||||||
|
return "No changelog to display."
|
||||||
|
|
||||||
|
status_history.short_description = "Status History" # type: ignore
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
list_filter = (
|
list_filter = (
|
||||||
StatusListFilter,
|
StatusListFilter,
|
||||||
|
@ -1493,9 +1518,11 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"fields": [
|
"fields": [
|
||||||
"portfolio",
|
"portfolio",
|
||||||
"sub_organization",
|
"sub_organization",
|
||||||
|
"status_history",
|
||||||
"status",
|
"status",
|
||||||
"rejection_reason",
|
"rejection_reason",
|
||||||
"action_needed_reason",
|
"action_needed_reason",
|
||||||
|
"action_needed_reason_email",
|
||||||
"investigator",
|
"investigator",
|
||||||
"creator",
|
"creator",
|
||||||
"submitter",
|
"submitter",
|
||||||
|
@ -1575,6 +1602,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"alternative_domains",
|
"alternative_domains",
|
||||||
"is_election_board",
|
"is_election_board",
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
|
"status_history",
|
||||||
|
"action_needed_reason_email",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Read only that we'll leverage for CISA Analysts
|
# Read only that we'll leverage for CISA Analysts
|
||||||
|
@ -1893,6 +1922,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
extra_context = extra_context or {}
|
extra_context = extra_context or {}
|
||||||
extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries
|
extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries
|
||||||
extra_context["action_needed_reason_emails"] = self.get_all_action_needed_reason_emails_as_json(obj)
|
extra_context["action_needed_reason_emails"] = self.get_all_action_needed_reason_emails_as_json(obj)
|
||||||
|
extra_context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
|
||||||
|
|
||||||
# Call the superclass method with updated extra_context
|
# Call the superclass method with updated extra_context
|
||||||
return super().change_view(request, object_id, form_url, extra_context)
|
return super().change_view(request, object_id, form_url, extra_context)
|
||||||
|
@ -1923,9 +1953,13 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
template_subject_path = f"emails/action_needed_reasons/{action_needed_reason}_subject.txt"
|
template_subject_path = f"emails/action_needed_reasons/{action_needed_reason}_subject.txt"
|
||||||
subject_template = get_template(template_subject_path)
|
subject_template = get_template(template_subject_path)
|
||||||
|
|
||||||
# Return the content of the rendered views
|
if flag_is_active(None, "profile_feature"): # type: ignore
|
||||||
context = {"domain_request": domain_request}
|
recipient = domain_request.creator
|
||||||
|
else:
|
||||||
|
recipient = domain_request.submitter
|
||||||
|
|
||||||
|
# Return the content of the rendered views
|
||||||
|
context = {"domain_request": domain_request, "recipient": recipient}
|
||||||
return {
|
return {
|
||||||
"subject_text": subject_template.render(context=context),
|
"subject_text": subject_template.render(context=context),
|
||||||
"email_body_text": template.render(context=context),
|
"email_body_text": template.render(context=context),
|
||||||
|
@ -2737,6 +2771,7 @@ admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
|
||||||
admin.site.register(models.Portfolio, PortfolioAdmin)
|
admin.site.register(models.Portfolio, PortfolioAdmin)
|
||||||
admin.site.register(models.DomainGroup, DomainGroupAdmin)
|
admin.site.register(models.DomainGroup, DomainGroupAdmin)
|
||||||
admin.site.register(models.Suborganization, SuborganizationAdmin)
|
admin.site.register(models.Suborganization, SuborganizationAdmin)
|
||||||
|
admin.site.register(models.SeniorOfficial, SeniorOfficialAdmin)
|
||||||
|
|
||||||
# Register our custom waffle implementations
|
# Register our custom waffle implementations
|
||||||
admin.site.register(models.WaffleFlag, WaffleFlagAdmin)
|
admin.site.register(models.WaffleFlag, WaffleFlagAdmin)
|
||||||
|
|
|
@ -361,9 +361,12 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
*/
|
*/
|
||||||
(function (){
|
(function (){
|
||||||
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
|
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
|
||||||
|
// This is the "action needed reason" field
|
||||||
let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason');
|
let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason');
|
||||||
|
// This is the "auto-generated email" field
|
||||||
|
let actionNeededReasonEmailFormGroup = document.querySelector('.field-action_needed_reason_email')
|
||||||
|
|
||||||
if (rejectionReasonFormGroup && actionNeededReasonFormGroup) {
|
if (rejectionReasonFormGroup && actionNeededReasonFormGroup && actionNeededReasonEmailFormGroup) {
|
||||||
let statusSelect = document.getElementById('id_status')
|
let statusSelect = document.getElementById('id_status')
|
||||||
let isRejected = statusSelect.value == "rejected"
|
let isRejected = statusSelect.value == "rejected"
|
||||||
let isActionNeeded = statusSelect.value == "action needed"
|
let isActionNeeded = statusSelect.value == "action needed"
|
||||||
|
@ -371,6 +374,7 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
// Initial handling of rejectionReasonFormGroup display
|
// Initial handling of rejectionReasonFormGroup display
|
||||||
showOrHideObject(rejectionReasonFormGroup, show=isRejected)
|
showOrHideObject(rejectionReasonFormGroup, show=isRejected)
|
||||||
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
|
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
|
||||||
|
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
|
||||||
|
|
||||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||||
statusSelect.addEventListener('change', function() {
|
statusSelect.addEventListener('change', function() {
|
||||||
|
@ -382,6 +386,7 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
|
|
||||||
isActionNeeded = statusSelect.value == "action needed"
|
isActionNeeded = statusSelect.value == "action needed"
|
||||||
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
|
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
|
||||||
|
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
|
||||||
addOrRemoveSessionBoolean("showActionNeededReason", add=isActionNeeded)
|
addOrRemoveSessionBoolean("showActionNeededReason", add=isActionNeeded)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -398,6 +403,7 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
|
|
||||||
let showActionNeededReason = sessionStorage.getItem("showActionNeededReason") !== null
|
let showActionNeededReason = sessionStorage.getItem("showActionNeededReason") !== null
|
||||||
showOrHideObject(actionNeededReasonFormGroup, show=showActionNeededReason)
|
showOrHideObject(actionNeededReasonFormGroup, show=showActionNeededReason)
|
||||||
|
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -421,42 +427,6 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
sessionStorage.removeItem(name);
|
sessionStorage.removeItem(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
let statusSelect = document.getElementById('id_status');
|
|
||||||
|
|
||||||
function moveStatusChangelog(actionNeededReasonFormGroup, statusSelect) {
|
|
||||||
if (!actionNeededReasonFormGroup || !statusSelect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let flexContainer = actionNeededReasonFormGroup.querySelector('.flex-container');
|
|
||||||
let statusChangelog = document.getElementById('dja-status-changelog');
|
|
||||||
|
|
||||||
// On action needed, show the email that will be sent out
|
|
||||||
let showReasonEmailContainer = document.querySelector("#action_needed_reason_email_readonly")
|
|
||||||
|
|
||||||
// Prepopulate values on page load.
|
|
||||||
if (statusSelect.value === "action needed") {
|
|
||||||
flexContainer.parentNode.insertBefore(statusChangelog, flexContainer.nextSibling);
|
|
||||||
showElement(showReasonEmailContainer);
|
|
||||||
} else {
|
|
||||||
// Move the changelog back to its original location
|
|
||||||
let statusFlexContainer = statusSelect.closest('.flex-container');
|
|
||||||
statusFlexContainer.parentNode.insertBefore(statusChangelog, statusFlexContainer.nextSibling);
|
|
||||||
hideElement(showReasonEmailContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the function on page load
|
|
||||||
moveStatusChangelog(actionNeededReasonFormGroup, statusSelect);
|
|
||||||
|
|
||||||
// Add event listener to handle changes to the selector itself
|
|
||||||
statusSelect.addEventListener('change', function() {
|
|
||||||
moveStatusChangelog(actionNeededReasonFormGroup, statusSelect);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/** An IIFE for toggling the submit bar on domain request forms
|
/** An IIFE for toggling the submit bar on domain request forms
|
||||||
|
@ -552,13 +522,13 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
/** An IIFE that hooks up to the "show email" button.
|
/** An IIFE that hooks to the show/hide button underneath action needed reason.
|
||||||
* which shows the auto generated email on action needed reason.
|
* This shows the auto generated email on action needed reason.
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
let actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason");
|
let actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason");
|
||||||
let actionNeededEmail = document.querySelector("#action_needed_reason_email_view_more");
|
let actionNeededEmail = document.querySelector("#action_needed_reason_email_view_more");
|
||||||
if(actionNeededReasonDropdown && actionNeededEmail && container) {
|
if(actionNeededReasonDropdown && actionNeededEmail) {
|
||||||
// Add a change listener to the action needed reason dropdown
|
// Add a change listener to the action needed reason dropdown
|
||||||
handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail);
|
handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail);
|
||||||
}
|
}
|
||||||
|
|
|
@ -787,10 +787,16 @@ div.dja__model-description{
|
||||||
color: var(--link-fg);
|
color: var(--link-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.textarea-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 610px;
|
||||||
|
}
|
||||||
|
|
||||||
.dja-readonly-textarea-container {
|
.dja-readonly-textarea-container {
|
||||||
|
width: 100%;
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 610px;
|
max-width: 610px;
|
||||||
resize: none;
|
resize: none;
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
|
|
||||||
|
@ -827,3 +833,20 @@ div.dja__model-description{
|
||||||
// Many elements in django admin try to override this, so we need !important.
|
// Many elements in django admin try to override this, so we need !important.
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.margin-top-0 {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-top-0 {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.flex-container {
|
||||||
|
@media screen and (min-width: 700px) and (max-width: 1150px) {
|
||||||
|
&.flex-container--mobile-inline {
|
||||||
|
display: inline !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
37
src/registrar/migrations/0111_create_groups_v15.py
Normal file
37
src/registrar/migrations/0111_create_groups_v15.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 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,
|
||||||
|
),
|
||||||
|
]
|
|
@ -19,6 +19,7 @@ from .waffle_flag import WaffleFlag
|
||||||
from .portfolio import Portfolio
|
from .portfolio import Portfolio
|
||||||
from .domain_group import DomainGroup
|
from .domain_group import DomainGroup
|
||||||
from .suborganization import Suborganization
|
from .suborganization import Suborganization
|
||||||
|
from .senior_official import SeniorOfficial
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -42,6 +43,7 @@ __all__ = [
|
||||||
"Portfolio",
|
"Portfolio",
|
||||||
"DomainGroup",
|
"DomainGroup",
|
||||||
"Suborganization",
|
"Suborganization",
|
||||||
|
"SeniorOfficial",
|
||||||
]
|
]
|
||||||
|
|
||||||
auditlog.register(Contact)
|
auditlog.register(Contact)
|
||||||
|
@ -64,3 +66,4 @@ auditlog.register(WaffleFlag)
|
||||||
auditlog.register(Portfolio)
|
auditlog.register(Portfolio)
|
||||||
auditlog.register(DomainGroup)
|
auditlog.register(DomainGroup)
|
||||||
auditlog.register(Suborganization)
|
auditlog.register(Suborganization)
|
||||||
|
auditlog.register(SeniorOfficial)
|
||||||
|
|
|
@ -38,6 +38,15 @@ class Portfolio(TimeStampedModel):
|
||||||
default=FederalAgency.get_non_federal_agency,
|
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(
|
organization_type = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
choices=OrganizationChoices.choices,
|
choices=OrganizationChoices.choices,
|
||||||
|
|
50
src/registrar/models/senior_official.py
Normal file
50
src/registrar/models/senior_official.py
Normal 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 ""
|
|
@ -32,7 +32,9 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
|
||||||
{% for field in line %}
|
{% for field in line %}
|
||||||
<div>
|
<div>
|
||||||
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
|
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
|
||||||
|
{% block flex_container_start %}
|
||||||
<div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
|
<div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
|
||||||
|
{% endblock flex_container_start %}
|
||||||
{% if field.is_checkbox %}
|
{% if field.is_checkbox %}
|
||||||
{# .gov override #}
|
{# .gov override #}
|
||||||
{% block field_checkbox %}
|
{% block field_checkbox %}
|
||||||
|
@ -52,7 +54,9 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
|
||||||
{% endblock field_other%}
|
{% endblock field_other%}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% block flex_container_end %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock flex_container_end %}
|
||||||
|
|
||||||
{% if field.field.help_text %}
|
{% if field.field.help_text %}
|
||||||
{# .gov override #}
|
{# .gov override #}
|
||||||
|
|
|
@ -6,9 +6,85 @@
|
||||||
This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% block flex_container_start %}
|
||||||
|
{% if field.field.name == "status_history" %}
|
||||||
|
<div class="flex-container flex-container--mobile-inline {% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
|
||||||
|
{% else %}
|
||||||
|
{% comment %} Default flex container element {% endcomment %}
|
||||||
|
<div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
|
||||||
|
{% endif %}
|
||||||
|
{% endblock flex_container_start %}
|
||||||
|
|
||||||
{% block field_readonly %}
|
{% block field_readonly %}
|
||||||
{% with all_contacts=original_object.other_contacts.all %}
|
{% with all_contacts=original_object.other_contacts.all %}
|
||||||
{% if field.field.name == "other_contacts" %}
|
{% if field.field.name == "status_history" %}
|
||||||
|
{% if filtered_audit_log_entries %}
|
||||||
|
<div class="readonly">
|
||||||
|
<div class="usa-table-container--scrollable collapse--dgsimple collapsed margin-top-0" tabindex="0">
|
||||||
|
<table class="usa-table usa-table--borderless">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Changed at</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in filtered_audit_log_entries %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% if entry.status %}
|
||||||
|
{{ entry.status|default:"Error" }}
|
||||||
|
{% else %}
|
||||||
|
Error
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if entry.rejection_reason %}
|
||||||
|
- {{ entry.rejection_reason|default:"Error" }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if entry.action_needed_reason %}
|
||||||
|
- {{ entry.action_needed_reason|default:"Error" }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ entry.actor|default:"Error" }}</td>
|
||||||
|
<td>{{ entry.timestamp|date:"Y-m-d H:i:s" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="collapse-toggle--dgsimple usa-button usa-button--unstyled margin-top-0">
|
||||||
|
<span>Show details</span>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="readonly">
|
||||||
|
No changelog to display.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% elif field.field.name == "action_needed_reason_email" %}
|
||||||
|
<div class="readonly textarea-wrapper">
|
||||||
|
<div id="action_needed_reason_email_readonly" class="dja-readonly-textarea-container padding-1 margin-top-0 padding-top-0 margin-bottom-1 thin-border collapse--dgsimple collapsed">
|
||||||
|
<label class="max-full" for="action_needed_reason_email_view_more">
|
||||||
|
<strong>Sent to {% if has_profile_feature_flag %}creator{%else%}submitter{%endif%}</strong>
|
||||||
|
</label>
|
||||||
|
<textarea id="action_needed_reason_email_view_more" cols="40" rows="20" class="{% if not original_object.action_needed_reason %}display-none{% endif %}" readonly>
|
||||||
|
{{ original_object.action_needed_reason_email }}
|
||||||
|
</textarea>
|
||||||
|
<p id="no-email-message" class="{% if original_object.action_needed_reason %}display-none{% endif %}">No email will be sent.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="collapse-toggle--dgsimple usa-button usa-button--unstyled margin-top-0 margin-bottom-1 margin-left-1">
|
||||||
|
<span>Show details</span>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% elif field.field.name == "other_contacts" %}
|
||||||
{% if all_contacts.count > 2 %}
|
{% if all_contacts.count > 2 %}
|
||||||
<div class="readonly">
|
<div class="readonly">
|
||||||
{% for contact in all_contacts %}
|
{% for contact in all_contacts %}
|
||||||
|
@ -68,80 +144,19 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
{% endblock field_readonly %}
|
{% endblock field_readonly %}
|
||||||
|
|
||||||
{% block after_help_text %}
|
{% block after_help_text %}
|
||||||
{% if field.field.name == "status" %}
|
{% if field.field.name == "action_needed_reason_email" %}
|
||||||
<div class="flex-container" id="dja-status-changelog">
|
{% comment %}
|
||||||
<label aria-label="Status changelog"></label>
|
Store the action needed reason emails in a json-based dictionary.
|
||||||
<div>
|
This allows us to change the action_needed_reason_email field dynamically, depending on value.
|
||||||
<div class="usa-table-container--scrollable collapse--dgsimple collapsed" tabindex="0">
|
The alternative to this is an API endpoint.
|
||||||
{% if filtered_audit_log_entries %}
|
|
||||||
<table class="usa-table usa-table--borderless">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>User</th>
|
|
||||||
<th>Changed at</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for entry in filtered_audit_log_entries %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{% if entry.status %}
|
|
||||||
{{ entry.status|default:"Error" }}
|
|
||||||
{% else %}
|
|
||||||
Error
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if entry.rejection_reason %}
|
|
||||||
- {{ entry.rejection_reason|default:"Error" }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if entry.action_needed_reason %}
|
|
||||||
- {{ entry.action_needed_reason|default:"Error" }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ entry.actor|default:"Error" }}</td>
|
|
||||||
<td>{{ entry.timestamp|date:"Y-m-d H:i:s" }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% else %}
|
|
||||||
<p>No changelog to display.</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% comment %}
|
|
||||||
Store the action needed reason emails in a json-based dictionary.
|
|
||||||
This allows us to change the action_needed_reason_email field dynamically, depending on value.
|
|
||||||
The alternative to this is an API endpoint.
|
|
||||||
|
|
||||||
Given that we have a limited number of emails, doing it this way makes sense.
|
Given that we have a limited number of emails, doing it this way makes sense.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% if action_needed_reason_emails %}
|
{% if action_needed_reason_emails %}
|
||||||
<script id="action-needed-emails-data" type="application/json">
|
<script id="action-needed-emails-data" type="application/json">
|
||||||
{{ action_needed_reason_emails|safe }}
|
{{ action_needed_reason_emails|safe }}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div id="action_needed_reason_email_readonly" class="dja-readonly-textarea-container padding-1 margin-top-2 thin-border display-none">
|
|
||||||
<label class="max-full" for="action_needed_reason_email_view_more">
|
|
||||||
<strong>Auto-generated email (sent to submitter)</strong>
|
|
||||||
</label>
|
|
||||||
<textarea id="action_needed_reason_email_view_more" cols="40" rows="20" class="{% if not original_object.action_needed_reason %}display-none{% endif %}" readonly>
|
|
||||||
{{ original_object.action_needed_reason_email }}
|
|
||||||
</textarea>
|
|
||||||
<p id="no-email-message" class="{% if original_object.action_needed_reason %}display-none{% endif %}">No email will be sent.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<button type="button" class="collapse-toggle--dgsimple usa-button usa-button--unstyled margin-top-2 margin-bottom-1 margin-left-1">
|
|
||||||
<span>Show details</span>
|
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
|
||||||
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% elif field.field.name == "creator" %}
|
{% elif field.field.name == "creator" %}
|
||||||
<div class="flex-container tablet:margin-top-2">
|
<div class="flex-container tablet:margin-top-2">
|
||||||
<label aria-label="Creator contact details"></label>
|
<label aria-label="Creator contact details"></label>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
completing your domain request might take around 15 minutes.</p>
|
completing your domain request might take around 15 minutes.</p>
|
||||||
{% if has_profile_feature_flag %}
|
{% if has_profile_feature_flag %}
|
||||||
<h2>How we’ll reach you</h2>
|
<h2>How we’ll reach you</h2>
|
||||||
<p>While reviewing your domain request, we may need to reach out with questions. We’ll 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. We’ll 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%}
|
{% include "includes/profile_information.html" with user=user%}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ from registrar.models import (
|
||||||
UserGroup,
|
UserGroup,
|
||||||
TransitionDomain,
|
TransitionDomain,
|
||||||
)
|
)
|
||||||
|
from registrar.models.senior_official import SeniorOfficial
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from registrar.models.verified_by_staff import VerifiedByStaff
|
from registrar.models.verified_by_staff import VerifiedByStaff
|
||||||
from .common import (
|
from .common import (
|
||||||
|
@ -931,6 +932,32 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
)
|
)
|
||||||
self.mock_client = MockSESClient()
|
self.mock_client = MockSESClient()
|
||||||
|
|
||||||
|
def test_domain_request_senior_official_is_alphabetically_sorted(self):
|
||||||
|
"""Tests if the senior offical dropdown is alphanetically sorted in the django admin display"""
|
||||||
|
|
||||||
|
SeniorOfficial.objects.get_or_create(first_name="mary", last_name="joe", title="some other guy")
|
||||||
|
SeniorOfficial.objects.get_or_create(first_name="alex", last_name="smoe", title="some guy")
|
||||||
|
SeniorOfficial.objects.get_or_create(first_name="Zoup", last_name="Soup", title="title")
|
||||||
|
|
||||||
|
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
||||||
|
domain_request = completed_domain_request(submitter=contact, name="city1.gov")
|
||||||
|
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
|
||||||
|
model_admin = AuditedAdmin(DomainRequest, self.site)
|
||||||
|
|
||||||
|
# Get the queryset that would be returned for the list
|
||||||
|
senior_offical_queryset = model_admin.formfield_for_foreignkey(
|
||||||
|
DomainInformation.senior_official.field, request
|
||||||
|
).queryset
|
||||||
|
|
||||||
|
# Make the list we're comparing on a bit prettier display-wise. Optional step.
|
||||||
|
current_sort_order = []
|
||||||
|
for official in senior_offical_queryset:
|
||||||
|
current_sort_order.append(f"{official.first_name} {official.last_name}")
|
||||||
|
|
||||||
|
expected_sort_order = ["alex smoe", "mary joe", "Zoup Soup"]
|
||||||
|
|
||||||
|
self.assertEqual(current_sort_order, expected_sort_order)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
|
@ -2233,6 +2260,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"alternative_domains",
|
"alternative_domains",
|
||||||
"is_election_board",
|
"is_election_board",
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
|
"status_history",
|
||||||
|
"action_needed_reason_email",
|
||||||
"id",
|
"id",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
@ -2293,6 +2322,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"alternative_domains",
|
"alternative_domains",
|
||||||
"is_election_board",
|
"is_election_board",
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
|
"status_history",
|
||||||
|
"action_needed_reason_email",
|
||||||
"creator",
|
"creator",
|
||||||
"about_your_organization",
|
"about_your_organization",
|
||||||
"requested_domain",
|
"requested_domain",
|
||||||
|
@ -2323,6 +2354,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"alternative_domains",
|
"alternative_domains",
|
||||||
"is_election_board",
|
"is_election_board",
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
|
"status_history",
|
||||||
|
"action_needed_reason_email",
|
||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
@ -2719,6 +2752,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
Contact.objects.all().delete()
|
Contact.objects.all().delete()
|
||||||
Website.objects.all().delete()
|
Website.objects.all().delete()
|
||||||
|
SeniorOfficial.objects.all().delete()
|
||||||
self.mock_client.EMAILS_SENT.clear()
|
self.mock_client.EMAILS_SENT.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@ -2900,6 +2934,38 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
Contact.objects.all().delete()
|
Contact.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
SeniorOfficial.objects.all().delete()
|
||||||
|
|
||||||
|
def test_domain_information_senior_official_is_alphabetically_sorted(self):
|
||||||
|
"""Tests if the senior offical dropdown is alphanetically sorted in the django admin display"""
|
||||||
|
|
||||||
|
SeniorOfficial.objects.get_or_create(first_name="mary", last_name="joe", title="some other guy")
|
||||||
|
SeniorOfficial.objects.get_or_create(first_name="alex", last_name="smoe", title="some guy")
|
||||||
|
SeniorOfficial.objects.get_or_create(first_name="Zoup", last_name="Soup", title="title")
|
||||||
|
|
||||||
|
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
||||||
|
domain_request = completed_domain_request(
|
||||||
|
submitter=contact, name="city1244.gov", status=DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||||
|
)
|
||||||
|
domain_request.approve()
|
||||||
|
|
||||||
|
domain_info = DomainInformation.objects.get(domain_request=domain_request)
|
||||||
|
request = self.factory.post("/admin/registrar/domaininformation/{}/change/".format(domain_info.pk))
|
||||||
|
model_admin = AuditedAdmin(DomainInformation, self.site)
|
||||||
|
|
||||||
|
# Get the queryset that would be returned for the list
|
||||||
|
senior_offical_queryset = model_admin.formfield_for_foreignkey(
|
||||||
|
DomainInformation.senior_official.field, request
|
||||||
|
).queryset
|
||||||
|
|
||||||
|
# Make the list we're comparing on a bit prettier display-wise. Optional step.
|
||||||
|
current_sort_order = []
|
||||||
|
for official in senior_offical_queryset:
|
||||||
|
current_sort_order.append(f"{official.first_name} {official.last_name}")
|
||||||
|
|
||||||
|
expected_sort_order = ["alex smoe", "mary joe", "Zoup Soup"]
|
||||||
|
|
||||||
|
self.assertEqual(current_sort_order, expected_sort_order)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_admin_can_see_cisa_region_federal(self):
|
def test_admin_can_see_cisa_region_federal(self):
|
||||||
|
@ -3650,6 +3716,7 @@ class AuditedAdminTest(TestCase):
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
self.staffuser = create_user()
|
||||||
|
|
||||||
def order_by_desired_field_helper(self, obj_to_sort: AuditedAdmin, request, field_name, *obj_names):
|
def order_by_desired_field_helper(self, obj_to_sort: AuditedAdmin, request, field_name, *obj_names):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
|
@ -3701,7 +3768,9 @@ class AuditedAdminTest(TestCase):
|
||||||
def test_alphabetically_sorted_fk_fields_domain_request(self):
|
def test_alphabetically_sorted_fk_fields_domain_request(self):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
tested_fields = [
|
tested_fields = [
|
||||||
DomainRequest.senior_official.field,
|
# Senior offical is commented out for now - this is alphabetized
|
||||||
|
# and this test does not accurately reflect that.
|
||||||
|
# DomainRequest.senior_official.field,
|
||||||
DomainRequest.submitter.field,
|
DomainRequest.submitter.field,
|
||||||
# DomainRequest.investigator.field,
|
# DomainRequest.investigator.field,
|
||||||
DomainRequest.creator.field,
|
DomainRequest.creator.field,
|
||||||
|
@ -3759,7 +3828,9 @@ class AuditedAdminTest(TestCase):
|
||||||
def test_alphabetically_sorted_fk_fields_domain_information(self):
|
def test_alphabetically_sorted_fk_fields_domain_information(self):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
tested_fields = [
|
tested_fields = [
|
||||||
DomainInformation.senior_official.field,
|
# Senior offical is commented out for now - this is alphabetized
|
||||||
|
# and this test does not accurately reflect that.
|
||||||
|
# DomainInformation.senior_official.field,
|
||||||
DomainInformation.submitter.field,
|
DomainInformation.submitter.field,
|
||||||
# DomainInformation.creator.field,
|
# DomainInformation.creator.field,
|
||||||
(DomainInformation.domain.field, ["name"]),
|
(DomainInformation.domain.field, ["name"]),
|
||||||
|
@ -3792,7 +3863,6 @@ class AuditedAdminTest(TestCase):
|
||||||
|
|
||||||
# Conforms to the same object structure as desired_order
|
# Conforms to the same object structure as desired_order
|
||||||
current_sort_order_coerced_type = []
|
current_sort_order_coerced_type = []
|
||||||
|
|
||||||
# This is necessary as .queryset and get_queryset
|
# This is necessary as .queryset and get_queryset
|
||||||
# return lists of different types/structures.
|
# return lists of different types/structures.
|
||||||
# We need to parse this data and coerce them into the same type.
|
# We need to parse this data and coerce them into the same type.
|
||||||
|
@ -3869,7 +3939,8 @@ class AuditedAdminTest(TestCase):
|
||||||
if last_name is None:
|
if last_name is None:
|
||||||
return (first_name,)
|
return (first_name,)
|
||||||
|
|
||||||
if first_name.split(queryset_shorthand)[1] == field_name:
|
split_name = first_name.split(queryset_shorthand)
|
||||||
|
if len(split_name) == 2 and split_name[1] == field_name:
|
||||||
return returned_tuple
|
return returned_tuple
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -601,6 +601,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
# Add a phone number
|
# Add a phone number
|
||||||
finish_setup_form = finish_setup_page.form
|
finish_setup_form = finish_setup_page.form
|
||||||
|
finish_setup_form["first_name"] = "firstname"
|
||||||
finish_setup_form["phone"] = "(201) 555-0123"
|
finish_setup_form["phone"] = "(201) 555-0123"
|
||||||
finish_setup_form["title"] = "CEO"
|
finish_setup_form["title"] = "CEO"
|
||||||
finish_setup_form["last_name"] = "example"
|
finish_setup_form["last_name"] = "example"
|
||||||
|
@ -729,6 +730,8 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
|
||||||
self.assertContains(save_page, "Your profile has been updated.")
|
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
|
# 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.assertContains(save_page, "anage your domains", count=2)
|
||||||
self.assertNotContains(
|
self.assertNotContains(
|
||||||
save_page, "Before you can manage your domains, we need you to add contact information"
|
save_page, "Before you can manage your domains, we need you to add contact information"
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""Views for a User Profile.
|
"""Views for a User Profile.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
@ -106,6 +105,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
||||||
"""If the form is invalid, conditionally display an additional error."""
|
"""If the form is invalid, conditionally display an additional error."""
|
||||||
if hasattr(self.user, "finished_setup") and not self.user.finished_setup:
|
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.")
|
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)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue