mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-06 09:45:23 +02:00
Merge pull request #1690 from cisagov/za/1271-admin-add-notes
(On getgov-ab) Ticket #1271: Add notes field for analysts on Domain and DomainApplication
This commit is contained in:
commit
91cf57052f
7 changed files with 155 additions and 34 deletions
|
@ -626,7 +626,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
search_help_text = "Search by domain."
|
||||
|
||||
fieldsets = [
|
||||
(None, {"fields": ["creator", "domain_application"]}),
|
||||
(None, {"fields": ["creator", "domain_application", "notes"]}),
|
||||
(
|
||||
"Type of organization",
|
||||
{
|
||||
|
@ -793,7 +793,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
# Detail view
|
||||
form = DomainApplicationAdminForm
|
||||
fieldsets = [
|
||||
(None, {"fields": ["status", "investigator", "creator", "approved_domain"]}),
|
||||
(None, {"fields": ["status", "investigator", "creator", "approved_domain", "notes"]}),
|
||||
(
|
||||
"Type of organization",
|
||||
{
|
||||
|
@ -1047,6 +1047,13 @@ class DomainAdmin(ListHeaderAdmin):
|
|||
"deleted",
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{"fields": ["name", "state", "expiration_date", "first_ready", "deleted"]},
|
||||
),
|
||||
)
|
||||
|
||||
# this ordering effects the ordering of results
|
||||
# in autocomplete_fields for domain
|
||||
ordering = ["name"]
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 4.2.7 on 2024-01-26 20:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0067_create_groups_v07"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="domainapplication",
|
||||
name="notes",
|
||||
field=models.TextField(blank=True, help_text="Notes about this request", null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domaininformation",
|
||||
name="notes",
|
||||
field=models.TextField(blank=True, help_text="Notes about the request", null=True),
|
||||
),
|
||||
]
|
|
@ -558,6 +558,12 @@ class DomainApplication(TimeStampedModel):
|
|||
help_text="Date submitted",
|
||||
)
|
||||
|
||||
notes = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Notes about this request",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
if self.requested_domain and self.requested_domain.name:
|
||||
|
@ -707,7 +713,7 @@ class DomainApplication(TimeStampedModel):
|
|||
|
||||
# copy the information from domainapplication into domaininformation
|
||||
DomainInformation = apps.get_model("registrar.DomainInformation")
|
||||
DomainInformation.create_from_da(self, domain=created_domain)
|
||||
DomainInformation.create_from_da(domain_application=self, domain=created_domain)
|
||||
|
||||
# create the permission for the user
|
||||
UserDomainRole = apps.get_model("registrar.UserDomainRole")
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
from __future__ import annotations
|
||||
from django.db import transaction
|
||||
|
||||
from registrar.models.utility.domain_helper import DomainHelper
|
||||
from .domain_application import DomainApplication
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
|
@ -202,6 +205,12 @@ class DomainInformation(TimeStampedModel):
|
|||
help_text="Acknowledged .gov acceptable use policy",
|
||||
)
|
||||
|
||||
notes = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Notes about the request",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
if self.domain and self.domain.name:
|
||||
|
@ -212,37 +221,63 @@ class DomainInformation(TimeStampedModel):
|
|||
return ""
|
||||
|
||||
@classmethod
|
||||
def create_from_da(cls, domain_application, domain=None):
|
||||
"""Takes in a DomainApplication dict and converts it into DomainInformation"""
|
||||
da_dict = domain_application.to_dict()
|
||||
# remove the id so one can be assinged on creation
|
||||
da_id = da_dict.pop("id", None)
|
||||
def create_from_da(cls, domain_application: DomainApplication, domain=None):
|
||||
"""Takes in a DomainApplication and converts it into DomainInformation"""
|
||||
|
||||
# Throw an error if we get None - we can't create something from nothing
|
||||
if domain_application is None:
|
||||
raise ValueError("The provided DomainApplication is None")
|
||||
|
||||
# Throw an error if the da doesn't have an id
|
||||
if not hasattr(domain_application, "id"):
|
||||
raise ValueError("The provided DomainApplication has no id")
|
||||
|
||||
# check if we have a record that corresponds with the domain
|
||||
# application, if so short circuit the create
|
||||
domain_info = cls.objects.filter(domain_application__id=da_id).first()
|
||||
if domain_info:
|
||||
return domain_info
|
||||
# the following information below is not needed in the domain information:
|
||||
da_dict.pop("status", None)
|
||||
da_dict.pop("current_websites", None)
|
||||
da_dict.pop("investigator", None)
|
||||
da_dict.pop("alternative_domains", None)
|
||||
da_dict.pop("requested_domain", None)
|
||||
da_dict.pop("approved_domain", None)
|
||||
da_dict.pop("submission_date", None)
|
||||
other_contacts = da_dict.pop("other_contacts", [])
|
||||
domain_info = cls(**da_dict)
|
||||
domain_info.domain_application = domain_application
|
||||
# Save so the object now have PK
|
||||
# (needed to process the manytomany below before, first)
|
||||
domain_info.save()
|
||||
existing_domain_info = cls.objects.filter(domain_application__id=domain_application.id).first()
|
||||
if existing_domain_info:
|
||||
return existing_domain_info
|
||||
|
||||
# Process the remaining "many to many" stuff
|
||||
domain_info.other_contacts.add(*other_contacts)
|
||||
# Get the fields that exist on both DomainApplication and DomainInformation
|
||||
common_fields = DomainHelper.get_common_fields(DomainApplication, DomainInformation)
|
||||
|
||||
# Get a list of all many_to_many relations on DomainInformation (needs to be saved differently)
|
||||
info_many_to_many_fields = DomainInformation._get_many_to_many_fields()
|
||||
|
||||
# Create a dictionary with only the common fields, and create a DomainInformation from it
|
||||
da_dict = {}
|
||||
da_many_to_many_dict = {}
|
||||
for field in common_fields:
|
||||
# If the field isn't many_to_many, populate the da_dict.
|
||||
# If it is, populate da_many_to_many_dict as we need to save this later.
|
||||
if hasattr(domain_application, field):
|
||||
if field not in info_many_to_many_fields:
|
||||
da_dict[field] = getattr(domain_application, field)
|
||||
else:
|
||||
da_many_to_many_dict[field] = getattr(domain_application, field).all()
|
||||
|
||||
# Create a placeholder DomainInformation object
|
||||
domain_info = DomainInformation(**da_dict)
|
||||
|
||||
# Add the domain_application and domain fields
|
||||
domain_info.domain_application = domain_application
|
||||
if domain:
|
||||
domain_info.domain = domain
|
||||
domain_info.save()
|
||||
|
||||
# Save the instance and set the many-to-many fields.
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
with transaction.atomic():
|
||||
domain_info.save()
|
||||
for field, value in da_many_to_many_dict.items():
|
||||
getattr(domain_info, field).set(value)
|
||||
|
||||
return domain_info
|
||||
|
||||
@staticmethod
|
||||
def _get_many_to_many_fields():
|
||||
"""Returns a set of each field.name that has the many to many relation"""
|
||||
return {field.name for field in DomainInformation._meta.many_to_many} # type: ignore
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Domain information"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import re
|
||||
|
||||
from typing import Type
|
||||
from django.db import models
|
||||
from django import forms
|
||||
from django.http import JsonResponse
|
||||
|
||||
|
@ -29,7 +30,6 @@ class DomainHelper:
|
|||
@classmethod
|
||||
def validate(cls, domain: str, blank_ok=False) -> str:
|
||||
"""Attempt to determine if a domain name could be requested."""
|
||||
|
||||
# Split into pieces for the linter
|
||||
domain = cls._validate_domain_string(domain, blank_ok)
|
||||
|
||||
|
@ -161,3 +161,29 @@ class DomainHelper:
|
|||
"""Get the top level domain. Example: `gsa.gov` -> `gov`."""
|
||||
parts = domain.rsplit(".")
|
||||
return parts[-1] if len(parts) > 1 else ""
|
||||
|
||||
@staticmethod
|
||||
def get_common_fields(model_1: Type[models.Model], model_2: Type[models.Model]):
|
||||
"""
|
||||
Returns a set of field names that two Django models have in common, excluding the 'id' field.
|
||||
|
||||
Args:
|
||||
model_1 (Type[models.Model]): The first Django model class.
|
||||
model_2 (Type[models.Model]): The second Django model class.
|
||||
|
||||
Returns:
|
||||
Set[str]: A set of field names that both models share.
|
||||
|
||||
Example:
|
||||
If model_1 has fields {"id", "name", "color"} and model_2 has fields {"id", "color"},
|
||||
the function will return {"color"}.
|
||||
"""
|
||||
|
||||
# Get a list of the existing fields on model_1 and model_2
|
||||
model_1_fields = set(field.name for field in model_1._meta.get_fields() if field != "id")
|
||||
model_2_fields = set(field.name for field in model_2._meta.get_fields() if field != "id")
|
||||
|
||||
# Get the fields that exist on both DomainApplication and DomainInformation
|
||||
common_fields = model_1_fields & model_2_fields
|
||||
|
||||
return common_fields
|
||||
|
|
|
@ -624,6 +624,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
"submission_date",
|
||||
"notes",
|
||||
"current_websites",
|
||||
"other_contacts",
|
||||
"alternative_domains",
|
||||
|
|
|
@ -548,9 +548,9 @@ class TestPermissions(TestCase):
|
|||
self.assertTrue(UserDomainRole.objects.get(user=user, domain=domain))
|
||||
|
||||
|
||||
class TestDomainInfo(TestCase):
|
||||
class TestDomainInformation(TestCase):
|
||||
|
||||
"""Test creation of Domain Information when approved."""
|
||||
"""Test the DomainInformation model, when approved or otherwise"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
@ -559,12 +559,18 @@ class TestDomainInfo(TestCase):
|
|||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.mock_client.EMAILS_SENT.clear()
|
||||
Domain.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainApplication.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
DraftDomain.objects.all().delete()
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_approval_creates_info(self):
|
||||
self.maxDiff = None
|
||||
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
|
||||
user, _ = User.objects.get_or_create()
|
||||
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
|
||||
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain, notes="test notes")
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
|
@ -574,7 +580,25 @@ class TestDomainInfo(TestCase):
|
|||
|
||||
# should be an information present for this domain
|
||||
domain = Domain.objects.get(name="igorville.gov")
|
||||
self.assertTrue(DomainInformation.objects.get(domain=domain))
|
||||
domain_information = DomainInformation.objects.filter(domain=domain)
|
||||
self.assertTrue(domain_information.exists())
|
||||
|
||||
# Test that both objects are what we expect
|
||||
current_domain_information = domain_information.get().__dict__
|
||||
expected_domain_information = DomainInformation(
|
||||
creator=user,
|
||||
domain=domain,
|
||||
notes="test notes",
|
||||
domain_application=application,
|
||||
).__dict__
|
||||
|
||||
# Test the two records for consistency
|
||||
self.assertEqual(self.clean_dict(current_domain_information), self.clean_dict(expected_domain_information))
|
||||
|
||||
def clean_dict(self, dict_obj):
|
||||
"""Cleans dynamic fields in a dictionary"""
|
||||
bad_fields = ["_state", "created_at", "id", "updated_at"]
|
||||
return {k: v for k, v in dict_obj.items() if k not in bad_fields}
|
||||
|
||||
|
||||
class TestInvitations(TestCase):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue