Skip to content

Commit

Permalink
merge
Browse files Browse the repository at this point in the history
  • Loading branch information
rachidatecs committed Feb 27, 2024
2 parents 22727c5 + 093af61 commit d208241
Show file tree
Hide file tree
Showing 19 changed files with 2,010 additions and 1,022 deletions.
165 changes: 154 additions & 11 deletions src/registrar/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from django import forms
from django.db.models.functions import Concat, Coalesce
from django.db.models import Value, CharField
from django.db.models import Value, CharField, Q
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect
from django_fsm import get_available_FIELD_transitions
Expand All @@ -27,6 +27,7 @@
from django.utils.html import escape
from django.contrib.auth.forms import UserChangeForm, UsernameField

from django.utils.translation import gettext_lazy as _

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -838,6 +839,14 @@ class DomainInformationAdmin(ListHeaderAdmin):
# to activate the edit/delete/view buttons
filter_horizontal = ("other_contacts",)

autocomplete_fields = [
"creator",
"domain_application",
"authorizing_official",
"domain",
"submitter",
]

# Table ordering
ordering = ["domain__name"]

Expand Down Expand Up @@ -881,13 +890,19 @@ def lookups(self, request, model_admin):
)

# Annotate the full name and return a values list that lookups can use
privileged_users_annotated = privileged_users.annotate(
full_name=Coalesce(
Concat("investigator__first_name", Value(" "), "investigator__last_name", output_field=CharField()),
"investigator__email",
output_field=CharField(),
privileged_users_annotated = (
privileged_users.annotate(
full_name=Coalesce(
Concat(
"investigator__first_name", Value(" "), "investigator__last_name", output_field=CharField()
),
"investigator__email",
output_field=CharField(),
)
)
).values_list("investigator__id", "full_name")
.values_list("investigator__id", "full_name")
.distinct()
)

return privileged_users_annotated

Expand All @@ -898,11 +913,35 @@ def queryset(self, request, queryset):
else:
return queryset.filter(investigator__id__exact=self.value())

class ElectionOfficeFilter(admin.SimpleListFilter):
"""Define a custom filter for is_election_board"""

title = _("election office")
parameter_name = "is_election_board"

def lookups(self, request, model_admin):
return (
("1", _("Yes")),
("0", _("No")),
)

def queryset(self, request, queryset):
if self.value() == "1":
return queryset.filter(is_election_board=True)
if self.value() == "0":
return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None))

# Columns
list_display = [
"requested_domain",
"status",
"organization_type",
"federal_type",
"federal_agency",
"organization_name",
"custom_election_board",
"city",
"state_territory",
"created_at",
"submitter",
"investigator",
Expand All @@ -914,8 +953,21 @@ def queryset(self, request, queryset):
("investigator", ["first_name", "last_name"]),
]

def custom_election_board(self, obj):
return "Yes" if obj.is_election_board else "No"

custom_election_board.admin_order_field = "is_election_board" # type: ignore
custom_election_board.short_description = "Election office" # type: ignore

# Filters
list_filter = ("status", "organization_type", InvestigatorFilter)
list_filter = (
"status",
"organization_type",
"federal_type",
ElectionOfficeFilter,
"rejection_reason",
InvestigatorFilter,
)

# Search
search_fields = [
Expand All @@ -927,7 +979,7 @@ def queryset(self, request, queryset):
search_help_text = "Search by domain or submitter."

fieldsets = [
(None, {"fields": ["status", "investigator", "creator", "approved_domain", "notes"]}),
(None, {"fields": ["status", "rejection_reason", "investigator", "creator", "approved_domain", "notes"]}),
(
"Type of organization",
{
Expand Down Expand Up @@ -1028,6 +1080,23 @@ def save_model(self, request, obj, form, change):
"This action is not permitted. The domain is already active.",
)

elif (
obj
and obj.status == models.DomainApplication.ApplicationStatus.REJECTED
and not obj.rejection_reason
):
# This condition should never be triggered.
# The opposite of this condition is acceptable (rejected -> other status and rejection_reason)
# because we clean up the rejection reason in the transition in the model.

# Clear the success message
messages.set_level(request, messages.ERROR)

messages.error(
request,
"A rejection reason is required.",
)

else:
if obj.status != original_obj.status:
status_method_mapping = {
Expand Down Expand Up @@ -1131,6 +1200,14 @@ class DomainInformationInline(admin.StackedInline):
# to activate the edit/delete/view buttons
filter_horizontal = ("other_contacts",)

autocomplete_fields = [
"creator",
"domain_application",
"authorizing_official",
"domain",
"submitter",
]

def formfield_for_manytomany(self, db_field, request, **kwargs):
"""customize the behavior of formfields with manytomany relationships. the customized
behavior includes sorting of objects in lists as well as customizing helper text"""
Expand Down Expand Up @@ -1160,12 +1237,37 @@ def get_readonly_fields(self, request, obj=None):
class DomainAdmin(ListHeaderAdmin):
"""Custom domain admin class to add extra buttons."""

class ElectionOfficeFilter(admin.SimpleListFilter):
"""Define a custom filter for is_election_board"""

title = _("election office")
parameter_name = "is_election_board"

def lookups(self, request, model_admin):
return (
("1", _("Yes")),
("0", _("No")),
)

def queryset(self, request, queryset):
logger.debug(self.value())
if self.value() == "1":
return queryset.filter(domain_info__is_election_board=True)
if self.value() == "0":
return queryset.filter(Q(domain_info__is_election_board=False) | Q(domain_info__is_election_board=None))

inlines = [DomainInformationInline]

# Columns
list_display = [
"name",
"organization_type",
"federal_type",
"federal_agency",
"organization_name",
"custom_election_board",
"city",
"state_territory",
"state",
"expiration_date",
"created_at",
Expand All @@ -1189,8 +1291,42 @@ def organization_type(self, obj):

organization_type.admin_order_field = "domain_info__organization_type" # type: ignore

def federal_agency(self, obj):
return obj.domain_info.federal_agency if obj.domain_info else None

federal_agency.admin_order_field = "domain_info__federal_agency" # type: ignore

def federal_type(self, obj):
return obj.domain_info.federal_type if obj.domain_info else None

federal_type.admin_order_field = "domain_info__federal_type" # type: ignore

def organization_name(self, obj):
return obj.domain_info.organization_name if obj.domain_info else None

organization_name.admin_order_field = "domain_info__organization_name" # type: ignore

def custom_election_board(self, obj):
domain_info = getattr(obj, "domain_info", None)
if domain_info:
return "Yes" if domain_info.is_election_board else "No"
return "No"

custom_election_board.admin_order_field = "domain_info__is_election_board" # type: ignore
custom_election_board.short_description = "Election office" # type: ignore

def city(self, obj):
return obj.domain_info.city if obj.domain_info else None

city.admin_order_field = "domain_info__city" # type: ignore

def state_territory(self, obj):
return obj.domain_info.state_territory if obj.domain_info else None

state_territory.admin_order_field = "domain_info__state_territory" # type: ignore

# Filters
list_filter = ["domain_info__organization_type", "state"]
list_filter = ["domain_info__organization_type", "domain_info__federal_type", ElectionOfficeFilter, "state"]

search_fields = ["name"]
search_help_text = "Search by domain name."
Expand All @@ -1210,7 +1346,14 @@ def changeform_view(self, request, object_id=None, form_url="", extra_context=No
if object_id is not None:
domain = Domain.objects.get(pk=object_id)
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
curr_exp_date = domain.registry_expiration_date

try:
curr_exp_date = domain.registry_expiration_date
except KeyError:
# No expiration date was found. Return none.
extra_context["extended_expiration_date"] = None
return super().changeform_view(request, object_id, form_url, extra_context)

if curr_exp_date < date.today():
extra_context["extended_expiration_date"] = date.today() + relativedelta(years=years_to_extend_by)
else:
Expand Down
43 changes: 43 additions & 0 deletions src/registrar/assets/js/get-gov-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,46 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk,
}

})();

/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request
* status select amd to show/hide the rejection reason
*/
(function (){
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')

if (rejectionReasonFormGroup) {
let statusSelect = document.getElementById('id_status')

// Initial handling of rejectionReasonFormGroup display
if (statusSelect.value != 'rejected')
rejectionReasonFormGroup.style.display = 'none';

// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
statusSelect.addEventListener('change', function() {
if (statusSelect.value == 'rejected') {
rejectionReasonFormGroup.style.display = 'block';
sessionStorage.removeItem('hideRejectionReason');
} else {
rejectionReasonFormGroup.style.display = 'none';
sessionStorage.setItem('hideRejectionReason', 'true');
}
});
}

// Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage

// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
// status select will say (for example) Rejected but the selected option can be something else. To manage the show/hide
// accurately for this edge case, we use cache and test for the back/forward navigation.
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.type === "back_forward") {
if (sessionStorage.getItem('hideRejectionReason'))
document.querySelector('.field-rejection_reason').style.display = 'none';
else
document.querySelector('.field-rejection_reason').style.display = 'block';
}
});
});
observer.observe({ type: "navigation" });
})();
1 change: 1 addition & 0 deletions src/registrar/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@
BOTO_CONFIG = Config(retries={"mode": AWS_RETRY_MODE, "max_attempts": AWS_MAX_ATTEMPTS})

# email address to use for various automated correspondence
# also used as a default to and bcc email
DEFAULT_FROM_EMAIL = "[email protected] <[email protected]>"

# connect to an (external) SMTP server for sending email
Expand Down
1 change: 1 addition & 0 deletions src/registrar/forms/application_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ class OrganizationContactForm(RegistrarForm):
message="Enter a zip code in the form of 12345 or 12345-6789.",
)
],
error_messages={"required": ("Enter a zip code in the form of 12345 or 12345-6789.")},
)
urbanization = forms.CharField(
required=False,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.2.7 on 2024-02-26 22:12

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("registrar", "0069_alter_contact_email_alter_contact_first_name_and_more"),
]

operations = [
migrations.AddField(
model_name="domainapplication",
name="rejection_reason",
field=models.TextField(
blank=True,
choices=[
("purpose_not_met", "Purpose requirements not met"),
("requestor_not_eligible", "Requestor not eligible to make request"),
("org_has_domain", "Org already has a .gov domain"),
("contacts_not_verified", "Org contacts couldn't be verified"),
("org_not_eligible", "Org not eligible for a .gov domain"),
("naming_not_met", "Naming requirements not met"),
("other", "Other/Unspecified"),
],
null=True,
),
),
]
Loading

0 comments on commit d208241

Please sign in to comment.