mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-16 17:47:02 +02:00
Update migration and conflict
This commit is contained in:
commit
7d85dced0d
10 changed files with 313 additions and 10 deletions
|
@ -1,7 +1,8 @@
|
|||
from datetime import date
|
||||
import logging
|
||||
import copy
|
||||
|
||||
import json
|
||||
from django.template.loader import get_template
|
||||
from django import forms
|
||||
from django.db.models import Value, CharField, Q
|
||||
from django.db.models.functions import Concat, Coalesce
|
||||
|
@ -1689,6 +1690,10 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
return super().save_model(request, obj, form, change)
|
||||
|
||||
# == Handle non-status changes == #
|
||||
# Change this in #1901. Add a check on "not self.action_needed_reason_email"
|
||||
if obj.action_needed_reason:
|
||||
self._handle_action_needed_reason_email(obj)
|
||||
should_save = True
|
||||
|
||||
# Get the original domain request from the database.
|
||||
original_obj = models.DomainRequest.objects.get(pk=obj.pk)
|
||||
|
@ -1697,14 +1702,17 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
return super().save_model(request, obj, form, change)
|
||||
|
||||
# == Handle status changes == #
|
||||
|
||||
# Run some checks on the current object for invalid status changes
|
||||
obj, should_save = self._handle_status_change(request, obj, original_obj)
|
||||
|
||||
# We should only save if we don't display any errors in the step above.
|
||||
# We should only save if we don't display any errors in the steps above.
|
||||
if should_save:
|
||||
return super().save_model(request, obj, form, change)
|
||||
|
||||
def _handle_action_needed_reason_email(self, obj):
|
||||
text = self._get_action_needed_reason_default_email_text(obj, obj.action_needed_reason)
|
||||
obj.action_needed_reason_email = text.get("email_body_text")
|
||||
|
||||
def _handle_status_change(self, request, obj, original_obj):
|
||||
"""
|
||||
Checks for various conditions when a status change is triggered.
|
||||
|
@ -1898,10 +1906,45 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
# Initialize extra_context and add filtered entries
|
||||
extra_context = extra_context or {}
|
||||
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)
|
||||
|
||||
# Call the superclass method with updated extra_context
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
def get_all_action_needed_reason_emails_as_json(self, domain_request):
|
||||
"""Returns a json dictionary of every action needed reason and its associated email
|
||||
for this particular domain request."""
|
||||
emails = {}
|
||||
for action_needed_reason in domain_request.ActionNeededReasons:
|
||||
enum_value = action_needed_reason.value
|
||||
# Change this in #1901. Just add a check for the current value.
|
||||
emails[enum_value] = self._get_action_needed_reason_default_email_text(domain_request, enum_value)
|
||||
return json.dumps(emails)
|
||||
|
||||
def _get_action_needed_reason_default_email_text(self, domain_request, action_needed_reason: str):
|
||||
"""Returns the default email associated with the given action needed reason"""
|
||||
if action_needed_reason is None or action_needed_reason == domain_request.ActionNeededReasons.OTHER:
|
||||
return {
|
||||
"subject_text": None,
|
||||
"email_body_text": None,
|
||||
}
|
||||
|
||||
# Get the email body
|
||||
template_path = f"emails/action_needed_reasons/{action_needed_reason}.txt"
|
||||
template = get_template(template_path)
|
||||
|
||||
# Get the email subject
|
||||
template_subject_path = f"emails/action_needed_reasons/{action_needed_reason}_subject.txt"
|
||||
subject_template = get_template(template_subject_path)
|
||||
|
||||
# Return the content of the rendered views
|
||||
context = {"domain_request": domain_request}
|
||||
|
||||
return {
|
||||
"subject_text": subject_template.render(context=context),
|
||||
"email_body_text": template.render(context=context),
|
||||
}
|
||||
|
||||
def process_log_entry(self, log_entry):
|
||||
"""Process a log entry and return filtered entry dictionary if applicable."""
|
||||
changes = log_entry.changes
|
||||
|
|
|
@ -8,6 +8,25 @@
|
|||
|
||||
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||
// Helper functions.
|
||||
|
||||
/**
|
||||
* Hide element
|
||||
*
|
||||
*/
|
||||
const hideElement = (element) => {
|
||||
if (element && !element.classList.contains("display-none"))
|
||||
element.classList.add('display-none');
|
||||
};
|
||||
|
||||
/**
|
||||
* Show element
|
||||
*
|
||||
*/
|
||||
const showElement = (element) => {
|
||||
if (element && element.classList.contains("display-none"))
|
||||
element.classList.remove('display-none');
|
||||
};
|
||||
|
||||
/** Either sets attribute target="_blank" to a given element, or removes it */
|
||||
function openInNewTab(el, removeAttribute = false){
|
||||
if(removeAttribute){
|
||||
|
@ -57,6 +76,7 @@ function openInNewTab(el, removeAttribute = false){
|
|||
createPhantomModalFormButtons();
|
||||
})();
|
||||
|
||||
|
||||
/** An IIFE for DomainRequest to hook a modal to a dropdown option.
|
||||
* This intentionally does not interact with createPhantomModalFormButtons()
|
||||
*/
|
||||
|
@ -408,13 +428,21 @@ function initializeWidgetOnList(list, parentId) {
|
|||
function moveStatusChangelog(actionNeededReasonFormGroup, statusSelect) {
|
||||
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
|
||||
|
@ -518,3 +546,60 @@ function initializeWidgetOnList(list, parentId) {
|
|||
handleShowMoreButton(toggleButton, descriptionDiv)
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
/** An IIFE that hooks up to the "show email" button.
|
||||
* which shows the auto generated email on action needed reason.
|
||||
*/
|
||||
(function () {
|
||||
let actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason");
|
||||
let actionNeededEmail = document.querySelector("#action_needed_reason_email_view_more");
|
||||
if(actionNeededReasonDropdown && actionNeededEmail && container) {
|
||||
// Add a change listener to the action needed reason dropdown
|
||||
handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail);
|
||||
}
|
||||
|
||||
function handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail) {
|
||||
actionNeededReasonDropdown.addEventListener("change", function() {
|
||||
let reason = actionNeededReasonDropdown.value;
|
||||
|
||||
// If a reason isn't specified, no email will be sent.
|
||||
// You also cannot save the model in this state.
|
||||
// This flow occurs if you switch back to the empty picker state.
|
||||
if(!reason) {
|
||||
showNoEmailMessage(actionNeededEmail);
|
||||
return;
|
||||
}
|
||||
|
||||
let actionNeededEmails = JSON.parse(document.getElementById('action-needed-emails-data').textContent)
|
||||
let emailData = actionNeededEmails[reason];
|
||||
if (emailData) {
|
||||
let emailBody = emailData.email_body_text
|
||||
if (emailBody) {
|
||||
actionNeededEmail.value = emailBody
|
||||
showActionNeededEmail(actionNeededEmail);
|
||||
}else {
|
||||
showNoEmailMessage(actionNeededEmail);
|
||||
}
|
||||
}else {
|
||||
showNoEmailMessage(actionNeededEmail);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// Show the text field. Hide the "no email" message.
|
||||
function showActionNeededEmail(actionNeededEmail){
|
||||
let noEmailMessage = document.getElementById("no-email-message");
|
||||
showElement(actionNeededEmail);
|
||||
hideElement(noEmailMessage);
|
||||
}
|
||||
|
||||
// Hide the text field. Show the "no email" message.
|
||||
function showNoEmailMessage(actionNeededEmail) {
|
||||
let noEmailMessage = document.getElementById("no-email-message");
|
||||
hideElement(actionNeededEmail);
|
||||
showElement(noEmailMessage);
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
|
@ -786,3 +786,44 @@ div.dja__model-description{
|
|||
.usa-button--dja-link-color {
|
||||
color: var(--link-fg);
|
||||
}
|
||||
|
||||
.dja-readonly-textarea-container {
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-width: 610px;
|
||||
resize: none;
|
||||
cursor: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
// Style the scroll bar handle
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--body-fg);
|
||||
border-radius: 99px;
|
||||
background-clip: content-box;
|
||||
border: 3px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.max-full {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.thin-border {
|
||||
background-color: var(--selected-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
label {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.display-none {
|
||||
// Many elements in django admin try to override this, so we need !important.
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.2.10 on 2024-06-26 14:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0106_create_groups_v14"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="action_needed_reason_email",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,59 @@
|
|||
# Generated by Django 4.2.10 on 2024-06-24 19:00
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0107_domainrequest_action_needed_reason_email"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="domaininformation",
|
||||
name="authorizing_official",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="domainrequest",
|
||||
name="authorizing_official",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domaininformation",
|
||||
name="senior_official",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="information_senior_official",
|
||||
to="registrar.contact",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="senior_official",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="senior_official",
|
||||
to="registrar.contact",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="action_needed_reason",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("eligibility_unclear", "Unclear organization eligibility"),
|
||||
("questionable_senior_official", "Questionable senior official"),
|
||||
("already_has_domains", "Already has domains"),
|
||||
("bad_name", "Doesn’t meet naming requirements"),
|
||||
("other", "Other (no auto-email sent)"),
|
||||
],
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -296,6 +296,11 @@ class DomainRequest(TimeStampedModel):
|
|||
blank=True,
|
||||
)
|
||||
|
||||
action_needed_reason_email = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
federal_agency = models.ForeignKey(
|
||||
"registrar.FederalAgency",
|
||||
on_delete=models.PROTECT,
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
{% block field_sets %}
|
||||
{# Create an invisible <a> tag so that we can use a click event to toggle the modal. #}
|
||||
<a id="invisible-ineligible-modal-toggler" class="display-none" href="#toggle-set-ineligible" aria-controls="toggle-set-ineligible" data-open-modal></a>
|
||||
|
||||
{# Store the current object id so we can access it easier #}
|
||||
<input id="domain_request_id" class="display-none" value="{{original.id}}" />
|
||||
<input id="has_audit_logs" class="display-none" value="{%if filtered_audit_log_entries %}true{% else %}false{% endif %}"/>
|
||||
{% for fieldset in adminform %}
|
||||
{% comment %}
|
||||
TODO: this will eventually need to be changed to something like this
|
||||
|
|
|
@ -68,11 +68,12 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% endblock field_readonly %}
|
||||
|
||||
{% block after_help_text %}
|
||||
{% if field.field.name == "status" and filtered_audit_log_entries %}
|
||||
{% if field.field.name == "status" %}
|
||||
<div class="flex-container" id="dja-status-changelog">
|
||||
<label aria-label="Status changelog"></label>
|
||||
<div>
|
||||
<div class="usa-table-container--scrollable collapse--dgsimple collapsed" tabindex="0">
|
||||
{% if filtered_audit_log_entries %}
|
||||
<table class="usa-table usa-table--borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -105,7 +106,34 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% 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.
|
||||
{% endcomment %}
|
||||
{% if action_needed_reason_emails %}
|
||||
<script id="action-needed-emails-data" type="application/json">
|
||||
{{ action_needed_reason_emails|safe }}
|
||||
</script>
|
||||
{% 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">
|
||||
|
|
|
@ -858,6 +858,7 @@ def completed_domain_request( # noqa
|
|||
is_election_board=False,
|
||||
organization_type=None,
|
||||
federal_agency=None,
|
||||
action_needed_reason=None,
|
||||
):
|
||||
"""A completed domain request."""
|
||||
if not user:
|
||||
|
@ -922,6 +923,9 @@ def completed_domain_request( # noqa
|
|||
if organization_type:
|
||||
domain_request_kwargs["organization_type"] = organization_type
|
||||
|
||||
if action_needed_reason:
|
||||
domain_request_kwargs["action_needed_reason"] = action_needed_reason
|
||||
|
||||
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
|
||||
|
||||
if has_other_contacts:
|
||||
|
|
|
@ -1596,6 +1596,24 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_model_displays_action_needed_email(self):
|
||||
"""Tests if the action needed email is visible for Domain Requests"""
|
||||
|
||||
_domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
action_needed_reason=DomainRequest.ActionNeededReasons.BAD_NAME,
|
||||
)
|
||||
|
||||
p = "userpass"
|
||||
self.client.login(username="staffuser", password=p)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertContains(response, "DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS")
|
||||
|
||||
@override_settings(IS_PRODUCTION=True)
|
||||
def test_save_model_sends_submitted_email_with_bcc_on_prod(self):
|
||||
"""When transitioning to submitted from started or withdrawn on a domain request,
|
||||
|
@ -2161,15 +2179,14 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.assertContains(response, "testy@town.com", count=2)
|
||||
expected_so_fields = [
|
||||
# Field, expected value
|
||||
("title", "Chief Tester"),
|
||||
("phone", "(555) 555 5555"),
|
||||
]
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_so_fields)
|
||||
|
||||
self.assertContains(response, "Testy Tester", count=10)
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_so_fields)
|
||||
self.assertContains(response, "Chief Tester")
|
||||
|
||||
# == Test the other_employees field == #
|
||||
self.assertContains(response, "testy2@town.com", count=2)
|
||||
self.assertContains(response, "testy2@town.com")
|
||||
expected_other_employees_fields = [
|
||||
# Field, expected value
|
||||
("title", "Another Tester"),
|
||||
|
@ -2290,6 +2307,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"status",
|
||||
"rejection_reason",
|
||||
"action_needed_reason",
|
||||
"action_needed_reason_email",
|
||||
"federal_agency",
|
||||
"portfolio",
|
||||
"creator",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue