mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-19 19:09:22 +02:00
Merge branch 'main' into za/1948-remove-draft-domain-and-websites
This commit is contained in:
commit
e1d09a02a3
34 changed files with 1553 additions and 246 deletions
|
@ -19,6 +19,18 @@ role or set of permissions that they have. We use a `UserDomainRole`
|
|||
`User.domains` many-to-many relationship that works through the
|
||||
`UserDomainRole` link table.
|
||||
|
||||
## Migrating changes to Analyst Permissions model
|
||||
Analysts are allowed a certain set of read/write registrar permissions.
|
||||
Setting user permissions requires a migration to change the UserGroup
|
||||
and Permission models, which requires us to manually make a migration
|
||||
file for user permission changes.
|
||||
To update analyst permissions do the following:
|
||||
1. Make desired changes to analyst group permissions in user_group.py.
|
||||
2. Follow the steps in the migration file0037_create_groups_v01.py to
|
||||
create a duplicate migration for the updated user group permissions.
|
||||
3. To migrate locally, run docker-compose up. To migrate on a sandbox,
|
||||
push the new migration onto your sandbox before migrating.
|
||||
|
||||
## Permission decorator
|
||||
|
||||
The Django objects that need to be permission controlled are various views.
|
||||
|
|
|
@ -5,7 +5,7 @@ applications:
|
|||
- python_buildpack
|
||||
path: ../../src
|
||||
instances: 2
|
||||
memory: 512M
|
||||
memory: 1G
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
|
|
|
@ -562,6 +562,8 @@ class MyUserAdmin(BaseUserAdmin):
|
|||
# in autocomplete_fields for user
|
||||
ordering = ["first_name", "last_name", "email"]
|
||||
|
||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||
|
||||
def get_search_results(self, request, queryset, search_term):
|
||||
"""
|
||||
Override for get_search_results. This affects any upstream model using autocomplete_fields,
|
||||
|
@ -666,6 +668,17 @@ class ContactAdmin(ListHeaderAdmin):
|
|||
# in autocomplete_fields for user
|
||||
ordering = ["first_name", "last_name", "email"]
|
||||
|
||||
fieldsets = [
|
||||
(
|
||||
None,
|
||||
{"fields": ["user", "first_name", "middle_name", "last_name", "title", "email", "phone"]},
|
||||
)
|
||||
]
|
||||
|
||||
autocomplete_fields = ["user"]
|
||||
|
||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||
|
||||
# We name the custom prop 'contact' because linter
|
||||
# is not allowing a short_description attr on it
|
||||
# This gets around the linter limitation, for now.
|
||||
|
@ -884,6 +897,8 @@ class DomainInvitationAdmin(ListHeaderAdmin):
|
|||
# error.
|
||||
readonly_fields = ["status"]
|
||||
|
||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||
|
||||
|
||||
class DomainInformationAdmin(ListHeaderAdmin):
|
||||
"""Customize domain information admin class."""
|
||||
|
@ -923,6 +938,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
"fields": [
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
"organization_type",
|
||||
]
|
||||
},
|
||||
),
|
||||
|
@ -965,7 +981,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
]
|
||||
|
||||
# Readonly fields for analysts and superusers
|
||||
readonly_fields = ("other_contacts",)
|
||||
readonly_fields = ("other_contacts", "generic_org_type", "is_election_board")
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
analyst_readonly_fields = [
|
||||
|
@ -1093,6 +1109,7 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
|||
# Columns
|
||||
list_display = [
|
||||
"requested_domain",
|
||||
"submission_date",
|
||||
"status",
|
||||
"generic_org_type",
|
||||
"federal_type",
|
||||
|
@ -1101,7 +1118,6 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
|||
"custom_election_board",
|
||||
"city",
|
||||
"state_territory",
|
||||
"submission_date",
|
||||
"submitter",
|
||||
"investigator",
|
||||
]
|
||||
|
@ -1161,6 +1177,7 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
|||
"fields": [
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
"organization_type",
|
||||
]
|
||||
},
|
||||
),
|
||||
|
@ -1203,7 +1220,13 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
|||
]
|
||||
|
||||
# Readonly fields for analysts and superusers
|
||||
readonly_fields = ("other_contacts", "current_websites", "alternative_domains")
|
||||
readonly_fields = (
|
||||
"other_contacts",
|
||||
"current_websites",
|
||||
"alternative_domains",
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
)
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
analyst_readonly_fields = [
|
||||
|
@ -1229,7 +1252,9 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
|||
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
|
||||
|
||||
# Table ordering
|
||||
ordering = ["requested_domain__name"]
|
||||
# NOTE: This impacts the select2 dropdowns (combobox)
|
||||
# Currentl, there's only one for requests on DomainInfo
|
||||
ordering = ["-submission_date", "requested_domain__name"]
|
||||
|
||||
change_form_template = "django/admin/domain_request_change_form.html"
|
||||
|
||||
|
@ -1441,6 +1466,8 @@ class TransitionDomainAdmin(ListHeaderAdmin):
|
|||
search_fields = ["username", "domain_name"]
|
||||
search_help_text = "Search by user or domain name."
|
||||
|
||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||
|
||||
|
||||
class DomainInformationInline(admin.StackedInline):
|
||||
"""Edit a domain information on the domain page.
|
||||
|
@ -1947,6 +1974,13 @@ class DraftDomainAdmin(ListHeaderAdmin):
|
|||
return super().response_change(request, obj)
|
||||
|
||||
|
||||
class PublicContactAdmin(ListHeaderAdmin):
|
||||
"""Custom PublicContact admin class."""
|
||||
|
||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||
autocomplete_fields = ["domain"]
|
||||
|
||||
|
||||
class VerifiedByStaffAdmin(ListHeaderAdmin):
|
||||
list_display = ("email", "requestor", "truncated_notes", "created_at")
|
||||
search_fields = ["email"]
|
||||
|
@ -1955,6 +1989,8 @@ class VerifiedByStaffAdmin(ListHeaderAdmin):
|
|||
"requestor",
|
||||
]
|
||||
|
||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||
|
||||
def truncated_notes(self, obj):
|
||||
# Truncate the 'notes' field to 50 characters
|
||||
return str(obj.notes)[:50]
|
||||
|
@ -1992,7 +2028,7 @@ admin.site.register(models.FederalAgency, FederalAgencyAdmin)
|
|||
# do not propagate to registry and logic not applied
|
||||
admin.site.register(models.Host, MyHostAdmin)
|
||||
admin.site.register(models.Website, WebsiteAdmin)
|
||||
admin.site.register(models.PublicContact, AuditedAdmin)
|
||||
admin.site.register(models.PublicContact, PublicContactAdmin)
|
||||
admin.site.register(models.DomainRequest, DomainRequestAdmin)
|
||||
admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
|
||||
admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
|
||||
|
|
|
@ -137,6 +137,94 @@ function openInNewTab(el, removeAttribute = false){
|
|||
prepareDjangoAdmin();
|
||||
})();
|
||||
|
||||
/** An IIFE for pages in DjangoAdmin that use a clipboard button
|
||||
*/
|
||||
(function (){
|
||||
|
||||
function copyInnerTextToClipboard(elem) {
|
||||
let text = elem.innerText
|
||||
navigator.clipboard.writeText(text)
|
||||
}
|
||||
|
||||
function copyToClipboardAndChangeIcon(button) {
|
||||
// Assuming the input is the previous sibling of the button
|
||||
let input = button.previousElementSibling;
|
||||
let userId = input.getAttribute("user-id")
|
||||
// Copy input value to clipboard
|
||||
if (input) {
|
||||
navigator.clipboard.writeText(input.value).then(function() {
|
||||
// Change the icon to a checkmark on successful copy
|
||||
let buttonIcon = button.querySelector('.usa-button__clipboard use');
|
||||
if (buttonIcon) {
|
||||
let currentHref = buttonIcon.getAttribute('xlink:href');
|
||||
let baseHref = currentHref.split('#')[0];
|
||||
|
||||
// Append the new icon reference
|
||||
buttonIcon.setAttribute('xlink:href', baseHref + '#check');
|
||||
|
||||
// Change the button text
|
||||
nearestSpan = button.querySelector("span")
|
||||
nearestSpan.innerText = "Copied to clipboard"
|
||||
|
||||
setTimeout(function() {
|
||||
// Change back to the copy icon
|
||||
buttonIcon.setAttribute('xlink:href', currentHref);
|
||||
if (button.classList.contains('usa-button__small-text')) {
|
||||
nearestSpan.innerText = "Copy email";
|
||||
} else {
|
||||
nearestSpan.innerText = "Copy";
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
}
|
||||
|
||||
}).catch(function(error) {
|
||||
console.error('Clipboard copy failed', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleClipboardButtons() {
|
||||
clipboardButtons = document.querySelectorAll(".usa-button__clipboard")
|
||||
clipboardButtons.forEach((button) => {
|
||||
|
||||
// Handle copying the text to your clipboard,
|
||||
// and changing the icon.
|
||||
button.addEventListener("click", ()=>{
|
||||
copyToClipboardAndChangeIcon(button);
|
||||
});
|
||||
|
||||
// Add a class that adds the outline style on click
|
||||
button.addEventListener("mousedown", function() {
|
||||
this.classList.add("no-outline-on-click");
|
||||
});
|
||||
|
||||
// But add it back in after the user clicked,
|
||||
// for accessibility reasons (so we can still tab, etc)
|
||||
button.addEventListener("blur", function() {
|
||||
this.classList.remove("no-outline-on-click");
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function handleClipboardLinks() {
|
||||
let emailButtons = document.querySelectorAll(".usa-button__clipboard-link");
|
||||
if (emailButtons){
|
||||
emailButtons.forEach((button) => {
|
||||
button.addEventListener("click", ()=>{
|
||||
copyInnerTextToClipboard(button);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleClipboardButtons();
|
||||
handleClipboardLinks();
|
||||
|
||||
})();
|
||||
|
||||
|
||||
/**
|
||||
* An IIFE to listen to changes on filter_horizontal and enable or disable the change/delete/view buttons as applicable
|
||||
*
|
||||
|
|
|
@ -392,32 +392,34 @@ address.margin-top-neg-1__detail-list {
|
|||
margin-top: 5px !important;
|
||||
}
|
||||
// Mimic the normal label size
|
||||
dt {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
address {
|
||||
address, dt {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
}
|
||||
|
||||
td button.usa-button__clipboard-link, address.dja-address-contact-list {
|
||||
font-size: unset;
|
||||
}
|
||||
|
||||
address.dja-address-contact-list {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--body-quiet-color);
|
||||
button.usa-button__clipboard-link {
|
||||
font-size: unset;
|
||||
}
|
||||
}
|
||||
|
||||
// Mimic the normal label size
|
||||
@media (max-width: 1024px){
|
||||
.dja-detail-list dt {
|
||||
.dja-detail-list dt, .dja-detail-list address {
|
||||
font-size: 0.875rem;
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
.dja-detail-list address {
|
||||
font-size: 0.875rem;
|
||||
color: var(--body-quiet-color);
|
||||
|
||||
address button.usa-button__clipboard-link, td button.usa-button__clipboard-link {
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.errors span.select2-selection {
|
||||
|
@ -533,3 +535,69 @@ address.dja-address-contact-list {
|
|||
color: var(--link-fg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Make the clipboard button "float" inside of the input box
|
||||
.admin-icon-group {
|
||||
position: relative;
|
||||
display: inline;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
// Allow for padding around the copy button
|
||||
padding-right: 35px !important;
|
||||
// Match the height of other inputs
|
||||
min-height: 2.25rem !important;
|
||||
}
|
||||
|
||||
button {
|
||||
line-height: 14px;
|
||||
width: max-content;
|
||||
font-size: unset;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
button {
|
||||
display: block;
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
padding-left: 0.1rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.admin-icon-group.admin-icon-group__clipboard-link {
|
||||
position: relative;
|
||||
display: inline;
|
||||
align-items: center;
|
||||
|
||||
|
||||
.usa-button__icon {
|
||||
position: absolute;
|
||||
right: auto;
|
||||
left: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
button {
|
||||
font-size: unset !important;
|
||||
display: inline-flex;
|
||||
padding-top: 4px;
|
||||
line-height: 14px;
|
||||
color: var(--link-fg);
|
||||
width: max-content;
|
||||
font-size: unset;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.no-outline-on-click:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.usa-button__small-text {
|
||||
font-size: small;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
// Only apply this custom wrapping to desktop
|
||||
@include at-media(desktop) {
|
||||
.usa-tooltip__body {
|
||||
.usa-tooltip--registrar .usa-tooltip__body {
|
||||
width: 350px;
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
|
@ -10,7 +10,7 @@
|
|||
}
|
||||
|
||||
@include at-media(tablet) {
|
||||
.usa-tooltip__body {
|
||||
.usa-tooltip--registrar .usa-tooltip__body {
|
||||
width: 250px !important;
|
||||
white-space: normal !important;
|
||||
text-align: center !important;
|
||||
|
@ -18,7 +18,7 @@
|
|||
}
|
||||
|
||||
@include at-media(mobile) {
|
||||
.usa-tooltip__body {
|
||||
.usa-tooltip--registrar .usa-tooltip__body {
|
||||
width: 250px !important;
|
||||
white-space: normal !important;
|
||||
text-align: center !important;
|
||||
|
|
|
@ -98,6 +98,8 @@ class DomainRequestFixture:
|
|||
def _set_non_foreign_key_fields(cls, da: DomainRequest, app: dict):
|
||||
"""Helper method used by `load`."""
|
||||
da.status = app["status"] if "status" in app else "started"
|
||||
|
||||
# TODO for a future ticket: Allow for more than just "federal" here
|
||||
da.generic_org_type = app["generic_org_type"] if "generic_org_type" in app else "federal"
|
||||
da.federal_agency = (
|
||||
app["federal_agency"]
|
||||
|
@ -235,9 +237,6 @@ class DomainFixture(DomainRequestFixture):
|
|||
).last()
|
||||
logger.debug(f"Approving {domain_request} for {user}")
|
||||
|
||||
# We don't want fixtures sending out real emails to
|
||||
# fake email addresses, so we just skip that and log it instead
|
||||
|
||||
# All approvals require an investigator, so if there is none,
|
||||
# assign one.
|
||||
if domain_request.investigator is None:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import logging
|
||||
from django import forms
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator, MaxLengthValidator
|
||||
from django.forms import formset_factory
|
||||
from registrar.models import DomainRequest
|
||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||
|
@ -31,7 +31,17 @@ logger = logging.getLogger(__name__)
|
|||
class DomainAddUserForm(forms.Form):
|
||||
"""Form for adding a user to a domain."""
|
||||
|
||||
email = forms.EmailField(label="Email")
|
||||
email = forms.EmailField(
|
||||
label="Email",
|
||||
max_length=None,
|
||||
error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
message="Response must be less than 320 characters.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
"""clean form data by lowercasing email"""
|
||||
|
@ -171,6 +181,8 @@ NameserverFormset = formset_factory(
|
|||
class ContactForm(forms.ModelForm):
|
||||
"""Form for updating contacts."""
|
||||
|
||||
email = forms.EmailField(max_length=None)
|
||||
|
||||
class Meta:
|
||||
model = Contact
|
||||
fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"]
|
||||
|
@ -194,6 +206,10 @@ class ContactForm(forms.ModelForm):
|
|||
# which interferes with out input_with_errors template tag
|
||||
self.fields["phone"].widget.attrs.pop("maxlength", None)
|
||||
|
||||
# Define a custom validator for the email field with a custom error message
|
||||
email_max_length_validator = MaxLengthValidator(320, message="Response must be less than 320 characters.")
|
||||
self.fields["email"].validators.append(email_max_length_validator)
|
||||
|
||||
for field_name in self.required:
|
||||
self.fields[field_name].required = True
|
||||
|
||||
|
@ -291,10 +307,17 @@ class DomainSecurityEmailForm(forms.Form):
|
|||
|
||||
security_email = forms.EmailField(
|
||||
label="Security email (optional)",
|
||||
max_length=None,
|
||||
required=False,
|
||||
error_messages={
|
||||
"invalid": str(SecurityEmailError(code=SecurityEmailErrorCodes.BAD_DATA)),
|
||||
},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
message="Response must be less than 320 characters.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -369,7 +369,14 @@ class AuthorizingOfficialForm(RegistrarForm):
|
|||
)
|
||||
email = forms.EmailField(
|
||||
label="Email",
|
||||
max_length=None,
|
||||
error_messages={"invalid": ("Enter an email address in the required format, like name@example.com.")},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
message="Response must be less than 320 characters.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
@ -566,7 +573,14 @@ class YourContactForm(RegistrarForm):
|
|||
)
|
||||
email = forms.EmailField(
|
||||
label="Email",
|
||||
max_length=None,
|
||||
error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
message="Response must be less than 320 characters.",
|
||||
)
|
||||
],
|
||||
)
|
||||
phone = PhoneNumberField(
|
||||
label="Phone",
|
||||
|
@ -621,10 +635,17 @@ class OtherContactsForm(RegistrarForm):
|
|||
)
|
||||
email = forms.EmailField(
|
||||
label="Email",
|
||||
max_length=None,
|
||||
error_messages={
|
||||
"required": ("Enter an email address in the required format, like name@example.com."),
|
||||
"invalid": ("Enter an email address in the required format, like name@example.com."),
|
||||
},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
message="Response must be less than 320 characters.",
|
||||
)
|
||||
],
|
||||
)
|
||||
phone = PhoneNumberField(
|
||||
label="Phone",
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
# Generated by Django 4.2.10 on 2024-04-01 15:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0081_create_groups_v10"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="domaininformation",
|
||||
name="organization_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("federal", "Federal"),
|
||||
("interstate", "Interstate"),
|
||||
("state_or_territory", "State or territory"),
|
||||
("tribal", "Tribal"),
|
||||
("county", "County"),
|
||||
("city", "City"),
|
||||
("special_district", "Special district"),
|
||||
("school_district", "School district"),
|
||||
("state_or_territory_election", "State or territory - Election"),
|
||||
("tribal_election", "Tribal - Election"),
|
||||
("county_election", "County - Election"),
|
||||
("city_election", "City - Election"),
|
||||
("special_district_election", "Special district - Election"),
|
||||
],
|
||||
help_text="Type of organization - Election office",
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="organization_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("federal", "Federal"),
|
||||
("interstate", "Interstate"),
|
||||
("state_or_territory", "State or territory"),
|
||||
("tribal", "Tribal"),
|
||||
("county", "County"),
|
||||
("city", "City"),
|
||||
("special_district", "Special district"),
|
||||
("school_district", "School district"),
|
||||
("state_or_territory_election", "State or territory - Election"),
|
||||
("tribal_election", "Tribal - Election"),
|
||||
("county_election", "County - Election"),
|
||||
("city_election", "City - Election"),
|
||||
("special_district_election", "Special district - Election"),
|
||||
],
|
||||
help_text="Type of organization - Election office",
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="generic_org_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("federal", "Federal"),
|
||||
("interstate", "Interstate"),
|
||||
("state_or_territory", "State or territory"),
|
||||
("tribal", "Tribal"),
|
||||
("county", "County"),
|
||||
("city", "City"),
|
||||
("special_district", "Special district"),
|
||||
("school_district", "School district"),
|
||||
],
|
||||
help_text="Type of organization",
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.2.10 on 2024-04-09 16:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0082_domaininformation_organization_type_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="contact",
|
||||
name="email",
|
||||
field=models.EmailField(blank=True, db_index=True, max_length=320, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="publiccontact",
|
||||
name="email",
|
||||
field=models.EmailField(help_text="Contact's email address", max_length=320),
|
||||
),
|
||||
]
|
|
@ -40,6 +40,7 @@ class Contact(TimeStampedModel):
|
|||
null=True,
|
||||
blank=True,
|
||||
db_index=True,
|
||||
max_length=320,
|
||||
)
|
||||
phone = PhoneNumberField(
|
||||
null=True,
|
||||
|
|
|
@ -198,7 +198,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
is called in the validate function on the request/domain page
|
||||
|
||||
throws- RegistryError or InvalidDomainError"""
|
||||
|
||||
if not cls.string_could_be_domain(domain):
|
||||
logger.warning("Not a valid domain: %s" % str(domain))
|
||||
# throw invalid domain error so that it can be caught in
|
||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
from django.db import transaction
|
||||
|
||||
from registrar.models.utility.domain_helper import DomainHelper
|
||||
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
|
||||
from .domain_request import DomainRequest
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
|
@ -54,7 +55,23 @@ class DomainInformation(TimeStampedModel):
|
|||
choices=OrganizationChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Type of Organization",
|
||||
help_text="Type of organization",
|
||||
)
|
||||
|
||||
# TODO - Ticket #1911: stub this data from DomainRequest
|
||||
is_election_board = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Is your organization an election office?",
|
||||
)
|
||||
|
||||
# TODO - Ticket #1911: stub this data from DomainRequest
|
||||
organization_type = models.CharField(
|
||||
max_length=255,
|
||||
choices=DomainRequest.OrgChoicesElectionOffice.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Type of organization - Election office",
|
||||
)
|
||||
|
||||
federally_recognized_tribe = models.BooleanField(
|
||||
|
@ -219,6 +236,34 @@ class DomainInformation(TimeStampedModel):
|
|||
except Exception:
|
||||
return ""
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save override for custom properties"""
|
||||
|
||||
# Define mappings between generic org and election org.
|
||||
# These have to be defined here, as you'd get a cyclical import error
|
||||
# otherwise.
|
||||
|
||||
# For any given organization type, return the "_election" variant.
|
||||
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
|
||||
generic_org_map = DomainRequest.OrgChoicesElectionOffice.get_org_generic_to_org_election()
|
||||
|
||||
# For any given "_election" variant, return the base org type.
|
||||
# For example: STATE_OR_TERRITORY_ELECTION => STATE_OR_TERRITORY
|
||||
election_org_map = DomainRequest.OrgChoicesElectionOffice.get_org_election_to_org_generic()
|
||||
|
||||
# Manages the "organization_type" variable and keeps in sync with
|
||||
# "is_election_office" and "generic_organization_type"
|
||||
org_type_helper = CreateOrUpdateOrganizationTypeHelper(
|
||||
sender=self.__class__,
|
||||
instance=self,
|
||||
generic_org_to_org_map=generic_org_map,
|
||||
election_org_to_generic_org_map=election_org_map,
|
||||
)
|
||||
|
||||
# Actually updates the organization_type field
|
||||
org_type_helper.create_or_update_organization_type()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def create_from_da(cls, domain_request: DomainRequest, domain=None):
|
||||
"""Takes in a DomainRequest and converts it into DomainInformation"""
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.db import models
|
|||
from django_fsm import FSMField, transition # type: ignore
|
||||
from django.utils import timezone
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
|
||||
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
||||
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
@ -100,8 +101,8 @@ class DomainRequest(TimeStampedModel):
|
|||
class OrganizationChoices(models.TextChoices):
|
||||
"""
|
||||
Primary organization choices:
|
||||
For use in django admin
|
||||
Keys need to match OrganizationChoicesVerbose
|
||||
For use in the domain request experience
|
||||
Keys need to match OrgChoicesElectionOffice and OrganizationChoicesVerbose
|
||||
"""
|
||||
|
||||
FEDERAL = "federal", "Federal"
|
||||
|
@ -113,9 +114,77 @@ class DomainRequest(TimeStampedModel):
|
|||
SPECIAL_DISTRICT = "special_district", "Special district"
|
||||
SCHOOL_DISTRICT = "school_district", "School district"
|
||||
|
||||
class OrgChoicesElectionOffice(models.TextChoices):
|
||||
"""
|
||||
Primary organization choices for Django admin:
|
||||
Keys need to match OrganizationChoices and OrganizationChoicesVerbose.
|
||||
|
||||
The enums here come in two variants:
|
||||
Regular (matches the choices from OrganizationChoices)
|
||||
Election (Appends " - Election" to the string)
|
||||
|
||||
When adding the election variant, you must append "_election" to the end of the string.
|
||||
"""
|
||||
|
||||
# We can't inherit OrganizationChoices due to models.TextChoices being an enum.
|
||||
# We can redefine these values instead.
|
||||
FEDERAL = "federal", "Federal"
|
||||
INTERSTATE = "interstate", "Interstate"
|
||||
STATE_OR_TERRITORY = "state_or_territory", "State or territory"
|
||||
TRIBAL = "tribal", "Tribal"
|
||||
COUNTY = "county", "County"
|
||||
CITY = "city", "City"
|
||||
SPECIAL_DISTRICT = "special_district", "Special district"
|
||||
SCHOOL_DISTRICT = "school_district", "School district"
|
||||
|
||||
# Election variants
|
||||
STATE_OR_TERRITORY_ELECTION = "state_or_territory_election", "State or territory - Election"
|
||||
TRIBAL_ELECTION = "tribal_election", "Tribal - Election"
|
||||
COUNTY_ELECTION = "county_election", "County - Election"
|
||||
CITY_ELECTION = "city_election", "City - Election"
|
||||
SPECIAL_DISTRICT_ELECTION = "special_district_election", "Special district - Election"
|
||||
|
||||
@classmethod
|
||||
def get_org_election_to_org_generic(cls):
|
||||
"""
|
||||
Creates and returns a dictionary mapping from election-specific organization
|
||||
choice enums to their corresponding general organization choice enums.
|
||||
|
||||
If no such mapping exists, it is simple excluded from the map.
|
||||
"""
|
||||
# This can be mapped automatically but its harder to read.
|
||||
# For clarity reasons, we manually define this.
|
||||
org_election_map = {
|
||||
cls.STATE_OR_TERRITORY_ELECTION: cls.STATE_OR_TERRITORY,
|
||||
cls.TRIBAL_ELECTION: cls.TRIBAL,
|
||||
cls.COUNTY_ELECTION: cls.COUNTY,
|
||||
cls.CITY_ELECTION: cls.CITY,
|
||||
cls.SPECIAL_DISTRICT_ELECTION: cls.SPECIAL_DISTRICT,
|
||||
}
|
||||
return org_election_map
|
||||
|
||||
@classmethod
|
||||
def get_org_generic_to_org_election(cls):
|
||||
"""
|
||||
Creates and returns a dictionary mapping from general organization
|
||||
choice enums to their corresponding election-specific organization enums.
|
||||
|
||||
If no such mapping exists, it is simple excluded from the map.
|
||||
"""
|
||||
# This can be mapped automatically but its harder to read.
|
||||
# For clarity reasons, we manually define this.
|
||||
org_election_map = {
|
||||
cls.STATE_OR_TERRITORY: cls.STATE_OR_TERRITORY_ELECTION,
|
||||
cls.TRIBAL: cls.TRIBAL_ELECTION,
|
||||
cls.COUNTY: cls.COUNTY_ELECTION,
|
||||
cls.CITY: cls.CITY_ELECTION,
|
||||
cls.SPECIAL_DISTRICT: cls.SPECIAL_DISTRICT_ELECTION,
|
||||
}
|
||||
return org_election_map
|
||||
|
||||
class OrganizationChoicesVerbose(models.TextChoices):
|
||||
"""
|
||||
Secondary organization choices
|
||||
Tertiary organization choices
|
||||
For use in the domain request form and on the templates
|
||||
Keys need to match OrganizationChoices
|
||||
"""
|
||||
|
@ -406,6 +475,21 @@ class DomainRequest(TimeStampedModel):
|
|||
help_text="Type of organization",
|
||||
)
|
||||
|
||||
is_election_board = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Is your organization an election office?",
|
||||
)
|
||||
|
||||
# TODO - Ticket #1911: stub this data from DomainRequest
|
||||
organization_type = models.CharField(
|
||||
max_length=255,
|
||||
choices=OrgChoicesElectionOffice.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Type of organization - Election office",
|
||||
)
|
||||
|
||||
federally_recognized_tribe = models.BooleanField(
|
||||
null=True,
|
||||
help_text="Is the tribe federally recognized",
|
||||
|
@ -437,18 +521,13 @@ class DomainRequest(TimeStampedModel):
|
|||
help_text="Federal government branch",
|
||||
)
|
||||
|
||||
is_election_board = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Is your organization an election office?",
|
||||
)
|
||||
|
||||
organization_name = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Organization name",
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
address_line1 = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
|
@ -525,6 +604,7 @@ class DomainRequest(TimeStampedModel):
|
|||
related_name="domain_request",
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
alternative_domains = models.ManyToManyField(
|
||||
"registrar.Website",
|
||||
blank=True,
|
||||
|
@ -586,6 +666,34 @@ class DomainRequest(TimeStampedModel):
|
|||
help_text="Notes about this request",
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save override for custom properties"""
|
||||
|
||||
# Define mappings between generic org and election org.
|
||||
# These have to be defined here, as you'd get a cyclical import error
|
||||
# otherwise.
|
||||
|
||||
# For any given organization type, return the "_election" variant.
|
||||
# For example: STATE_OR_TERRITORY => STATE_OR_TERRITORY_ELECTION
|
||||
generic_org_map = self.OrgChoicesElectionOffice.get_org_generic_to_org_election()
|
||||
|
||||
# For any given "_election" variant, return the base org type.
|
||||
# For example: STATE_OR_TERRITORY_ELECTION => STATE_OR_TERRITORY
|
||||
election_org_map = self.OrgChoicesElectionOffice.get_org_election_to_org_generic()
|
||||
|
||||
# Manages the "organization_type" variable and keeps in sync with
|
||||
# "is_election_office" and "generic_organization_type"
|
||||
org_type_helper = CreateOrUpdateOrganizationTypeHelper(
|
||||
sender=self.__class__,
|
||||
instance=self,
|
||||
generic_org_to_org_map=generic_org_map,
|
||||
election_org_to_generic_org_map=election_org_map,
|
||||
)
|
||||
|
||||
# Actually updates the organization_type field
|
||||
org_type_helper.create_or_update_organization_type()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
if self.requested_domain and self.requested_domain.name:
|
||||
|
|
|
@ -68,7 +68,7 @@ class PublicContact(TimeStampedModel):
|
|||
sp = models.CharField(null=False, help_text="Contact's state or province")
|
||||
pc = models.CharField(null=False, help_text="Contact's postal code")
|
||||
cc = models.CharField(null=False, help_text="Contact's country code")
|
||||
email = models.EmailField(null=False, help_text="Contact's email address")
|
||||
email = models.EmailField(null=False, help_text="Contact's email address", max_length=320)
|
||||
voice = models.CharField(null=False, help_text="Contact's phone number. Must be in ITU.E164.2005 format")
|
||||
fax = models.CharField(
|
||||
null=True,
|
||||
|
|
|
@ -5,6 +5,11 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class UserGroup(Group):
|
||||
"""
|
||||
UserGroup sets read and write permissions for superusers (who have full access)
|
||||
and analysts. For more details, see the dev docs for user-permissions.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = "User group"
|
||||
verbose_name_plural = "User groups"
|
||||
|
|
|
@ -35,3 +35,219 @@ class Timer:
|
|||
self.end = time.time()
|
||||
self.duration = self.end - self.start
|
||||
logger.info(f"Execution time: {self.duration} seconds")
|
||||
|
||||
|
||||
class CreateOrUpdateOrganizationTypeHelper:
|
||||
"""
|
||||
A helper that manages the "organization_type" field in DomainRequest and DomainInformation
|
||||
"""
|
||||
|
||||
def __init__(self, sender, instance, generic_org_to_org_map, election_org_to_generic_org_map):
|
||||
# The "model type"
|
||||
self.sender = sender
|
||||
self.instance = instance
|
||||
self.generic_org_to_org_map = generic_org_to_org_map
|
||||
self.election_org_to_generic_org_map = election_org_to_generic_org_map
|
||||
|
||||
def create_or_update_organization_type(self):
|
||||
"""The organization_type field on DomainRequest and DomainInformation is consituted from the
|
||||
generic_org_type and is_election_board fields. To keep the organization_type
|
||||
field up to date, we need to update it before save based off of those field
|
||||
values.
|
||||
|
||||
If the instance is marked as an election board and the generic_org_type is not
|
||||
one of the excluded types (FEDERAL, INTERSTATE, SCHOOL_DISTRICT), the
|
||||
organization_type is set to a corresponding election variant. Otherwise, it directly
|
||||
mirrors the generic_org_type value.
|
||||
"""
|
||||
|
||||
# A new record is added with organization_type not defined.
|
||||
# This happens from the regular domain request flow.
|
||||
is_new_instance = self.instance.id is None
|
||||
if is_new_instance:
|
||||
self._handle_new_instance()
|
||||
else:
|
||||
self._handle_existing_instance()
|
||||
|
||||
return self.instance
|
||||
|
||||
def _handle_new_instance(self):
|
||||
# == Check for invalid conditions before proceeding == #
|
||||
should_proceed = self._validate_new_instance()
|
||||
if not should_proceed:
|
||||
return None
|
||||
# == Program flow will halt here if there is no reason to update == #
|
||||
|
||||
# == Update the linked values == #
|
||||
organization_type_needs_update = self.instance.organization_type is None
|
||||
generic_org_type_needs_update = self.instance.generic_org_type is None
|
||||
|
||||
# If a field is none, it indicates (per prior checks) that the
|
||||
# related field (generic org type <-> org type) has data and we should update according to that.
|
||||
if organization_type_needs_update:
|
||||
self._update_org_type_from_generic_org_and_election()
|
||||
elif generic_org_type_needs_update:
|
||||
self._update_generic_org_and_election_from_org_type()
|
||||
|
||||
# Update the field
|
||||
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
|
||||
|
||||
def _handle_existing_instance(self):
|
||||
# == Init variables == #
|
||||
# Instance is already in the database, fetch its current state
|
||||
current_instance = self.sender.objects.get(id=self.instance.id)
|
||||
|
||||
# Check the new and old values
|
||||
generic_org_type_changed = self.instance.generic_org_type != current_instance.generic_org_type
|
||||
is_election_board_changed = self.instance.is_election_board != current_instance.is_election_board
|
||||
organization_type_changed = self.instance.organization_type != current_instance.organization_type
|
||||
|
||||
# == Check for invalid conditions before proceeding == #
|
||||
if organization_type_changed and (generic_org_type_changed or is_election_board_changed):
|
||||
# Since organization type is linked with generic_org_type and election board,
|
||||
# we have to update one or the other, not both.
|
||||
# This will not happen in normal flow as it is not possible otherwise.
|
||||
raise ValueError("Cannot update organization_type and generic_org_type simultaneously.")
|
||||
elif not organization_type_changed and (not generic_org_type_changed and not is_election_board_changed):
|
||||
# No values to update - do nothing
|
||||
return None
|
||||
# == Program flow will halt here if there is no reason to update == #
|
||||
|
||||
# == Update the linked values == #
|
||||
# Find out which field needs updating
|
||||
organization_type_needs_update = generic_org_type_changed or is_election_board_changed
|
||||
generic_org_type_needs_update = organization_type_changed
|
||||
|
||||
# Update the field
|
||||
self._update_fields(organization_type_needs_update, generic_org_type_needs_update)
|
||||
|
||||
def _update_fields(self, organization_type_needs_update, generic_org_type_needs_update):
|
||||
"""
|
||||
Validates the conditions for updating organization and generic organization types.
|
||||
|
||||
Raises:
|
||||
ValueError: If both organization_type_needs_update and generic_org_type_needs_update are True,
|
||||
indicating an attempt to update both fields simultaneously, which is not allowed.
|
||||
"""
|
||||
# We shouldn't update both of these at the same time.
|
||||
# It is more useful to have these as seperate variables, but it does impose
|
||||
# this restraint.
|
||||
if organization_type_needs_update and generic_org_type_needs_update:
|
||||
raise ValueError("Cannot update both org type and generic org type at the same time.")
|
||||
|
||||
if organization_type_needs_update:
|
||||
self._update_org_type_from_generic_org_and_election()
|
||||
elif generic_org_type_needs_update:
|
||||
self._update_generic_org_and_election_from_org_type()
|
||||
|
||||
def _update_org_type_from_generic_org_and_election(self):
|
||||
"""Given a field values for generic_org_type and is_election_board, update the
|
||||
organization_type field."""
|
||||
|
||||
# We convert to a string because the enum types are different.
|
||||
generic_org_type = str(self.instance.generic_org_type)
|
||||
if generic_org_type not in self.generic_org_to_org_map:
|
||||
# Election board should always be reset to None if the record
|
||||
# can't have one. For example, federal.
|
||||
if self.instance.is_election_board is not None:
|
||||
# This maintains data consistency.
|
||||
# There is no avenue for this to occur in the UI,
|
||||
# as such - this can only occur if the object is initialized in this way.
|
||||
# Or if there are pre-existing data.
|
||||
logger.warning(
|
||||
"create_or_update_organization_type() -> is_election_board "
|
||||
f"cannot exist for {generic_org_type}. Setting to None."
|
||||
)
|
||||
self.instance.is_election_board = None
|
||||
self.instance.organization_type = generic_org_type
|
||||
else:
|
||||
# This can only happen with manual data tinkering, which causes these to be out of sync.
|
||||
if self.instance.is_election_board is None:
|
||||
logger.warning(
|
||||
"create_or_update_organization_type() -> is_election_board is out of sync. Updating value."
|
||||
)
|
||||
self.instance.is_election_board = False
|
||||
|
||||
if self.instance.is_election_board:
|
||||
self.instance.organization_type = self.generic_org_to_org_map[generic_org_type]
|
||||
else:
|
||||
self.instance.organization_type = generic_org_type
|
||||
|
||||
def _update_generic_org_and_election_from_org_type(self):
|
||||
"""Given the field value for organization_type, update the
|
||||
generic_org_type and is_election_board field."""
|
||||
|
||||
# We convert to a string because the enum types are different
|
||||
# between OrgChoicesElectionOffice and OrganizationChoices.
|
||||
# But their names are the same (for the most part).
|
||||
current_org_type = str(self.instance.organization_type)
|
||||
election_org_map = self.election_org_to_generic_org_map
|
||||
generic_org_map = self.generic_org_to_org_map
|
||||
|
||||
# This essentially means: "_election" in current_org_type.
|
||||
if current_org_type in election_org_map:
|
||||
new_org = election_org_map[current_org_type]
|
||||
self.instance.generic_org_type = new_org
|
||||
self.instance.is_election_board = True
|
||||
elif self.instance.organization_type is not None:
|
||||
self.instance.generic_org_type = current_org_type
|
||||
|
||||
# This basically checks if the given org type
|
||||
# can even have an election board in the first place.
|
||||
# For instance, federal cannot so is_election_board = None
|
||||
if current_org_type in generic_org_map:
|
||||
self.instance.is_election_board = False
|
||||
else:
|
||||
# This maintains data consistency.
|
||||
# There is no avenue for this to occur in the UI,
|
||||
# as such - this can only occur if the object is initialized in this way.
|
||||
# Or if there are pre-existing data.
|
||||
logger.warning(
|
||||
"create_or_update_organization_type() -> is_election_board "
|
||||
f"cannot exist for {current_org_type}. Setting to None."
|
||||
)
|
||||
self.instance.is_election_board = None
|
||||
else:
|
||||
# if self.instance.organization_type is set to None, then this means
|
||||
# we should clear the related fields.
|
||||
# This will not occur if it just is None (i.e. default), only if it is set to be so.
|
||||
self.instance.is_election_board = None
|
||||
self.instance.generic_org_type = None
|
||||
|
||||
def _validate_new_instance(self):
|
||||
"""
|
||||
Validates whether a new instance of DomainRequest or DomainInformation can proceed with the update
|
||||
based on the consistency between organization_type, generic_org_type, and is_election_board.
|
||||
|
||||
Returns a boolean determining if execution should proceed or not.
|
||||
"""
|
||||
|
||||
# We conditionally accept both of these values to exist simultaneously, as long as
|
||||
# those values do not intefere with eachother.
|
||||
# Because this condition can only be triggered through a dev (no user flow),
|
||||
# we throw an error if an invalid state is found here.
|
||||
if self.instance.organization_type and self.instance.generic_org_type:
|
||||
generic_org_type = str(self.instance.generic_org_type)
|
||||
organization_type = str(self.instance.organization_type)
|
||||
|
||||
# Strip "_election" if it exists
|
||||
mapped_org_type = self.election_org_to_generic_org_map.get(organization_type)
|
||||
|
||||
# Do tests on the org update for election board changes.
|
||||
is_election_type = "_election" in organization_type
|
||||
can_have_election_board = organization_type in self.generic_org_to_org_map
|
||||
|
||||
election_board_mismatch = (is_election_type != self.instance.is_election_board) and can_have_election_board
|
||||
org_type_mismatch = mapped_org_type is not None and (generic_org_type != mapped_org_type)
|
||||
if election_board_mismatch or org_type_mismatch:
|
||||
message = (
|
||||
"Cannot add organization_type and generic_org_type simultaneously "
|
||||
"when generic_org_type, is_election_board, and organization_type values do not match."
|
||||
)
|
||||
raise ValueError(message)
|
||||
|
||||
return True
|
||||
elif not self.instance.organization_type and not self.instance.generic_org_type:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
|
39
src/registrar/templates/admin/input_with_clipboard.html
Normal file
39
src/registrar/templates/admin/input_with_clipboard.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
{% load i18n static %}
|
||||
|
||||
{% comment %}
|
||||
Template for an input field with a clipboard
|
||||
{% endcomment %}
|
||||
|
||||
{% if not invisible_input_field %}
|
||||
<div class="admin-icon-group">
|
||||
{{ field }}
|
||||
<button
|
||||
class="usa-button usa-button--unstyled padding-left-1 usa-button__icon usa-button__clipboard"
|
||||
type="button"
|
||||
>
|
||||
<div class="no-outline-on-click">
|
||||
<svg
|
||||
class="usa-icon"
|
||||
>
|
||||
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||
</svg>
|
||||
<span>Copy</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="admin-icon-group admin-icon-group__clipboard-link">
|
||||
<input aria-hidden="true" class="display-none" value="{{ field.email }}" />
|
||||
<button
|
||||
class="usa-button usa-button--unstyled padding-right-1 usa-button__icon usa-button__clipboard text-no-underline"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="usa-icon"
|
||||
>
|
||||
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||
</svg>
|
||||
<span class="padding-left-05">Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
|
@ -0,0 +1,8 @@
|
|||
{% extends 'admin/change_form.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block field_sets %}
|
||||
{% for fieldset in adminform %}
|
||||
{% include "django/admin/includes/email_clipboard_fieldset.html" %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
|
@ -26,10 +26,12 @@
|
|||
{% if user.email or user.contact.email %}
|
||||
{% if user.contact.email %}
|
||||
{{ user.contact.email }}
|
||||
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
||||
{% else %}
|
||||
{{ user.email }}
|
||||
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
||||
{% endif %}
|
||||
<br>
|
||||
<br class="admin-icon-group__br">
|
||||
{% else %}
|
||||
None<br>
|
||||
{% endif %}
|
||||
|
|
|
@ -92,8 +92,25 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
<tr>
|
||||
<th class="padding-left-1" scope="row">{{ contact.get_formatted_name }}</th>
|
||||
<td class="padding-left-1">{{ contact.title }}</td>
|
||||
<td class="padding-left-1">{{ contact.email }}</td>
|
||||
<td class="padding-left-1">
|
||||
{{ contact.email }}
|
||||
|
||||
</td>
|
||||
<td class="padding-left-1">{{ contact.phone }}</td>
|
||||
<td class="padding-left-1 text-size-small">
|
||||
<input aria-hidden="true" class="display-none" value="{{ contact.email }}" />
|
||||
<button
|
||||
class="usa-button usa-button--unstyled padding-right-1 usa-button__icon usa-button__clipboard usa-button__small-text text-no-underline"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="usa-icon"
|
||||
>
|
||||
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||
</svg>
|
||||
<span>Copy email</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
{% extends "django/admin/includes/detail_table_fieldset.html" %}
|
||||
|
||||
{% comment %}
|
||||
This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||
{% endcomment %}
|
||||
|
||||
{% block field_other %}
|
||||
{% if field.field.name == "email" %}
|
||||
{% include "admin/input_with_clipboard.html" with field=field.field %}
|
||||
{% else %}
|
||||
{{ block.super }}
|
||||
{% endif %}
|
||||
{% endblock field_other %}
|
|
@ -41,7 +41,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<p> <b class="review__step__name">Last updated:</b> {{DomainRequest.updated_at|date:"F j, Y"}}<br></p>
|
||||
<p><b class="review__step__name">Last updated:</b> {{DomainRequest.updated_at|date:"F j, Y"}}</p>
|
||||
<p>{% include "includes/domain_request.html" %}</p>
|
||||
<p><a href="{% url 'domain-request-withdraw-confirmation' pk=DomainRequest.id %}" class="usa-button usa-button--outline withdraw_outline">
|
||||
Withdraw request</a>
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
{% else %}
|
||||
<input
|
||||
type="submit"
|
||||
class="usa-button--unstyled disabled-button usa-tooltip"
|
||||
class="usa-button--unstyled disabled-button usa-tooltip usa-tooltip--registrar"
|
||||
value="Remove"
|
||||
data-position="bottom"
|
||||
title="Domains must have at least one domain manager"
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
{{ domain.state|capfirst }}
|
||||
{% endif %}
|
||||
<svg
|
||||
class="usa-icon usa-tooltip text-middle margin-bottom-05 text-accent-cool no-click-outline-and-cursor-help"
|
||||
class="usa-icon usa-tooltip usa-tooltip--registrar text-middle margin-bottom-05 text-accent-cool no-click-outline-and-cursor-help"
|
||||
data-position="top"
|
||||
title="{{domain.get_state_help_text}}"
|
||||
focusable="true"
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<div class="usa-alert usa-alert--emergency margin-y-0 {% if add_class %}{{ add_class }}{% endif %}">
|
||||
<div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %}">
|
||||
<b>Attention:</b> You are on a test site.
|
||||
<div class="usa-site-alert--emergency margin-y-0 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert"
|
||||
>
|
||||
<div class="usa-alert">
|
||||
<div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %}">
|
||||
<b>Attention:</b> You are on a test site.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -158,7 +158,7 @@ class GenericTestHelper(TestCase):
|
|||
Example Usage:
|
||||
```
|
||||
self.assert_sort_helper(
|
||||
self.factory, self.superuser, self.admin, self.url, DomainInformation, "1", ("domain__name",)
|
||||
"1", ("domain__name",)
|
||||
)
|
||||
```
|
||||
|
||||
|
@ -585,7 +585,7 @@ class MockDb(TestCase):
|
|||
generic_org_type="federal",
|
||||
federal_agency="World War I Centennial Commission",
|
||||
federal_type="executive",
|
||||
is_election_board=True,
|
||||
is_election_board=False,
|
||||
)
|
||||
self.domain_information_2, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user, domain=self.domain_2, generic_org_type="interstate", is_election_board=True
|
||||
|
@ -595,14 +595,14 @@ class MockDb(TestCase):
|
|||
domain=self.domain_3,
|
||||
generic_org_type="federal",
|
||||
federal_agency="Armed Forces Retirement Home",
|
||||
is_election_board=True,
|
||||
is_election_board=False,
|
||||
)
|
||||
self.domain_information_4, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user,
|
||||
domain=self.domain_4,
|
||||
generic_org_type="federal",
|
||||
federal_agency="Armed Forces Retirement Home",
|
||||
is_election_board=True,
|
||||
is_election_board=False,
|
||||
)
|
||||
self.domain_information_5, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user,
|
||||
|
@ -652,7 +652,7 @@ class MockDb(TestCase):
|
|||
generic_org_type="federal",
|
||||
federal_agency="World War I Centennial Commission",
|
||||
federal_type="executive",
|
||||
is_election_board=True,
|
||||
is_election_board=False,
|
||||
)
|
||||
self.domain_information_12, _ = DomainInformation.objects.get_or_create(
|
||||
creator=self.user,
|
||||
|
@ -693,6 +693,24 @@ class MockDb(TestCase):
|
|||
user=meoward_user, domain=self.domain_12, role=UserDomainRole.Roles.MANAGER
|
||||
)
|
||||
|
||||
_, created = DomainInvitation.objects.get_or_create(
|
||||
email=meoward_user.email, domain=self.domain_1, status=DomainInvitation.DomainInvitationStatus.RETRIEVED
|
||||
)
|
||||
|
||||
_, created = DomainInvitation.objects.get_or_create(
|
||||
email="woofwardthethird@rocks.com",
|
||||
domain=self.domain_1,
|
||||
status=DomainInvitation.DomainInvitationStatus.INVITED,
|
||||
)
|
||||
|
||||
_, created = DomainInvitation.objects.get_or_create(
|
||||
email="squeaker@rocks.com", domain=self.domain_2, status=DomainInvitation.DomainInvitationStatus.INVITED
|
||||
)
|
||||
|
||||
_, created = DomainInvitation.objects.get_or_create(
|
||||
email="squeaker@rocks.com", domain=self.domain_10, status=DomainInvitation.DomainInvitationStatus.INVITED
|
||||
)
|
||||
|
||||
with less_console_noise():
|
||||
self.domain_request_1 = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED, name="city1.gov"
|
||||
|
@ -722,6 +740,7 @@ class MockDb(TestCase):
|
|||
DomainRequest.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
UserDomainRole.objects.all().delete()
|
||||
DomainInvitation.objects.all().delete()
|
||||
|
||||
|
||||
def mock_user():
|
||||
|
@ -782,6 +801,9 @@ def completed_domain_request(
|
|||
submitter=False,
|
||||
name="city.gov",
|
||||
investigator=None,
|
||||
generic_org_type="federal",
|
||||
is_election_board=False,
|
||||
organization_type=None,
|
||||
):
|
||||
"""A completed domain request."""
|
||||
if not user:
|
||||
|
@ -819,7 +841,8 @@ def completed_domain_request(
|
|||
is_staff=True,
|
||||
)
|
||||
domain_request_kwargs = dict(
|
||||
generic_org_type="federal",
|
||||
generic_org_type=generic_org_type,
|
||||
is_election_board=is_election_board,
|
||||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
is_policy_acknowledged=True,
|
||||
|
@ -840,6 +863,9 @@ def completed_domain_request(
|
|||
if has_anything_else:
|
||||
domain_request_kwargs["anything_else"] = "There is more"
|
||||
|
||||
if organization_type:
|
||||
domain_request_kwargs["organization_type"] = organization_type
|
||||
|
||||
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
|
||||
|
||||
if has_other_contacts:
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from datetime import date
|
||||
from datetime import date, datetime
|
||||
from django.utils import timezone
|
||||
import re
|
||||
from django.test import TestCase, RequestFactory, Client, override_settings
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from contextlib import ExitStack
|
||||
|
@ -845,7 +847,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.test_helper.assert_table_sorted("-1", ("-requested_domain__name",))
|
||||
|
||||
def test_submitter_sortable(self):
|
||||
"""Tests if the DomainRequest sorts by domain correctly"""
|
||||
"""Tests if the DomainRequest sorts by submitter correctly"""
|
||||
with less_console_noise():
|
||||
p = "adminpass"
|
||||
self.client.login(username="superuser", password=p)
|
||||
|
@ -876,7 +878,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
)
|
||||
|
||||
def test_investigator_sortable(self):
|
||||
"""Tests if the DomainRequest sorts by domain correctly"""
|
||||
"""Tests if the DomainRequest sorts by investigator correctly"""
|
||||
with less_console_noise():
|
||||
p = "adminpass"
|
||||
self.client.login(username="superuser", password=p)
|
||||
|
@ -889,7 +891,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
|
||||
# Assert that our sort works correctly
|
||||
self.test_helper.assert_table_sorted(
|
||||
"6",
|
||||
"12",
|
||||
(
|
||||
"investigator__first_name",
|
||||
"investigator__last_name",
|
||||
|
@ -898,13 +900,77 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
|
||||
# Assert that sorting in reverse works correctly
|
||||
self.test_helper.assert_table_sorted(
|
||||
"-6",
|
||||
"-12",
|
||||
(
|
||||
"-investigator__first_name",
|
||||
"-investigator__last_name",
|
||||
),
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_default_sorting_in_domain_requests_list(self):
|
||||
"""
|
||||
Make sure the default sortin in on the domain requests list page is reverse submission_date
|
||||
then alphabetical requested_domain
|
||||
"""
|
||||
|
||||
# Create domain requests with different names
|
||||
domain_requests = [
|
||||
completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, name=name)
|
||||
for name in ["ccc.gov", "bbb.gov", "eee.gov", "aaa.gov", "zzz.gov", "ddd.gov"]
|
||||
]
|
||||
|
||||
domain_requests[0].submission_date = timezone.make_aware(datetime(2024, 10, 16))
|
||||
domain_requests[1].submission_date = timezone.make_aware(datetime(2001, 10, 16))
|
||||
domain_requests[2].submission_date = timezone.make_aware(datetime(1980, 10, 16))
|
||||
domain_requests[3].submission_date = timezone.make_aware(datetime(1998, 10, 16))
|
||||
domain_requests[4].submission_date = timezone.make_aware(datetime(2013, 10, 16))
|
||||
domain_requests[5].submission_date = timezone.make_aware(datetime(1980, 10, 16))
|
||||
|
||||
# Save the modified domain requests to update their attributes in the database
|
||||
for domain_request in domain_requests:
|
||||
domain_request.save()
|
||||
|
||||
# Refresh domain request objects from the database to reflect the changes
|
||||
domain_requests = [DomainRequest.objects.get(pk=domain_request.pk) for domain_request in domain_requests]
|
||||
|
||||
# Login as superuser and retrieve the domain request list page
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get("/admin/registrar/domainrequest/")
|
||||
|
||||
# Check that the response is successful
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Extract the domain names from the response content using regex
|
||||
domain_names_match = re.findall(r"(\w+\.gov)</a>", response.content.decode("utf-8"))
|
||||
|
||||
logger.info(f"domain_names_match {domain_names_match}")
|
||||
|
||||
# Verify that domain names are found
|
||||
self.assertTrue(domain_names_match)
|
||||
|
||||
# Extract the domain names
|
||||
domain_names = [match for match in domain_names_match]
|
||||
|
||||
# Verify that the domain names are displayed in the expected order
|
||||
expected_order = [
|
||||
"ccc.gov",
|
||||
"zzz.gov",
|
||||
"bbb.gov",
|
||||
"aaa.gov",
|
||||
"ddd.gov",
|
||||
"eee.gov",
|
||||
]
|
||||
|
||||
# Remove duplicates
|
||||
# Remove duplicates from domain_names list while preserving order
|
||||
unique_domain_names = []
|
||||
for domain_name in domain_names:
|
||||
if domain_name not in unique_domain_names:
|
||||
unique_domain_names.append(domain_name)
|
||||
|
||||
self.assertEqual(unique_domain_names, expected_order)
|
||||
|
||||
def test_short_org_name_in_domain_requests_list(self):
|
||||
"""
|
||||
Make sure the short name is displaying in admin on the list page
|
||||
|
@ -1569,9 +1635,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, domain_request.requested_domain.name)
|
||||
|
||||
# Check that the modal has the right content
|
||||
# Check for the header
|
||||
|
||||
# == Check for the creator == #
|
||||
|
||||
# Check for the right title, email, and phone number in the response.
|
||||
|
@ -1587,37 +1650,38 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.assertContains(response, "Meoward Jones")
|
||||
|
||||
# == Check for the submitter == #
|
||||
self.assertContains(response, "mayor@igorville.gov", count=2)
|
||||
expected_submitter_fields = [
|
||||
# Field, expected value
|
||||
("title", "Admin Tester"),
|
||||
("email", "mayor@igorville.gov"),
|
||||
("phone", "(555) 555 5556"),
|
||||
]
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields)
|
||||
self.assertContains(response, "Testy2 Tester2")
|
||||
|
||||
# == Check for the authorizing_official == #
|
||||
self.assertContains(response, "testy@town.com", count=2)
|
||||
expected_ao_fields = [
|
||||
# Field, expected value
|
||||
("title", "Chief Tester"),
|
||||
("email", "testy@town.com"),
|
||||
("phone", "(555) 555 5555"),
|
||||
]
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_ao_fields)
|
||||
|
||||
# count=5 because the underlying domain has two users with this name.
|
||||
# The dropdown has 3 of these.
|
||||
self.assertContains(response, "Testy Tester", count=5)
|
||||
self.assertContains(response, "Testy Tester", count=10)
|
||||
|
||||
# == Test the other_employees field == #
|
||||
self.assertContains(response, "testy2@town.com", count=2)
|
||||
expected_other_employees_fields = [
|
||||
# Field, expected value
|
||||
("title", "Another Tester"),
|
||||
("email", "testy2@town.com"),
|
||||
("phone", "(555) 555 5557"),
|
||||
]
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
||||
|
||||
# Test for the copy link
|
||||
self.assertContains(response, "usa-button__clipboard", count=4)
|
||||
|
||||
def test_save_model_sets_restricted_status_on_user(self):
|
||||
with less_console_noise():
|
||||
# make sure there is no user with this email
|
||||
|
@ -1717,6 +1781,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"other_contacts",
|
||||
"current_websites",
|
||||
"alternative_domains",
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
|
@ -1725,12 +1791,13 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"creator",
|
||||
"investigator",
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
"organization_type",
|
||||
"federally_recognized_tribe",
|
||||
"state_recognized_tribe",
|
||||
"tribe_name",
|
||||
"federal_agency",
|
||||
"federal_type",
|
||||
"is_election_board",
|
||||
"organization_name",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
|
@ -1765,6 +1832,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"other_contacts",
|
||||
"current_websites",
|
||||
"alternative_domains",
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
"creator",
|
||||
"about_your_organization",
|
||||
"requested_domain",
|
||||
|
@ -1790,6 +1859,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"other_contacts",
|
||||
"current_websites",
|
||||
"alternative_domains",
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
]
|
||||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
@ -2346,37 +2417,38 @@ class TestDomainInformationAdmin(TestCase):
|
|||
self.assertContains(response, "Meoward Jones")
|
||||
|
||||
# == Check for the submitter == #
|
||||
self.assertContains(response, "mayor@igorville.gov", count=2)
|
||||
expected_submitter_fields = [
|
||||
# Field, expected value
|
||||
("title", "Admin Tester"),
|
||||
("email", "mayor@igorville.gov"),
|
||||
("phone", "(555) 555 5556"),
|
||||
]
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields)
|
||||
self.assertContains(response, "Testy2 Tester2")
|
||||
|
||||
# == Check for the authorizing_official == #
|
||||
self.assertContains(response, "testy@town.com", count=2)
|
||||
expected_ao_fields = [
|
||||
# Field, expected value
|
||||
("title", "Chief Tester"),
|
||||
("email", "testy@town.com"),
|
||||
("phone", "(555) 555 5555"),
|
||||
]
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_ao_fields)
|
||||
|
||||
# count=5 because the underlying domain has two users with this name.
|
||||
# The dropdown has 3 of these.
|
||||
self.assertContains(response, "Testy Tester", count=5)
|
||||
self.assertContains(response, "Testy Tester", count=10)
|
||||
|
||||
# == Test the other_employees field == #
|
||||
self.assertContains(response, "testy2@town.com", count=2)
|
||||
expected_other_employees_fields = [
|
||||
# Field, expected value
|
||||
("title", "Another Tester"),
|
||||
("email", "testy2@town.com"),
|
||||
("phone", "(555) 555 5557"),
|
||||
]
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
||||
|
||||
# Test for the copy link
|
||||
self.assertContains(response, "usa-button__clipboard", count=4)
|
||||
|
||||
def test_readonly_fields_for_analyst(self):
|
||||
"""Ensures that analysts have their permissions setup correctly"""
|
||||
with less_console_noise():
|
||||
|
@ -2387,6 +2459,8 @@ class TestDomainInformationAdmin(TestCase):
|
|||
|
||||
expected_fields = [
|
||||
"other_contacts",
|
||||
"generic_org_type",
|
||||
"is_election_board",
|
||||
"creator",
|
||||
"type_of_work",
|
||||
"more_organization_information",
|
||||
|
|
|
@ -1161,3 +1161,309 @@ class TestContact(TestCase):
|
|||
# test for a contact which is assigned as an authorizing official on a domain request
|
||||
self.assertFalse(self.contact_as_ao.has_more_than_one_join("authorizing_official"))
|
||||
self.assertTrue(self.contact_as_ao.has_more_than_one_join("submitted_domain_requests"))
|
||||
|
||||
|
||||
class TestDomainRequestCustomSave(TestCase):
|
||||
"""Tests custom save behaviour on the DomainRequest object"""
|
||||
|
||||
def tearDown(self):
|
||||
DomainRequest.objects.all().delete()
|
||||
super().tearDown()
|
||||
|
||||
def test_create_or_update_organization_type_new_instance(self):
|
||||
"""Test create_or_update_organization_type when creating a new instance"""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=True,
|
||||
)
|
||||
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
|
||||
|
||||
def test_create_or_update_organization_type_new_instance_federal_does_nothing(self):
|
||||
"""Test if create_or_update_organization_type does nothing when creating a new instance for federal"""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
is_election_board=True,
|
||||
)
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.FEDERAL)
|
||||
self.assertEqual(domain_request.is_election_board, None)
|
||||
|
||||
def test_create_or_update_organization_type_existing_instance_updates_election_board(self):
|
||||
"""Test create_or_update_organization_type for an existing instance."""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=False,
|
||||
)
|
||||
domain_request.is_election_board = True
|
||||
domain_request.save()
|
||||
|
||||
self.assertEqual(domain_request.is_election_board, True)
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
|
||||
|
||||
# Try reverting the election board value
|
||||
domain_request.is_election_board = False
|
||||
domain_request.save()
|
||||
|
||||
self.assertEqual(domain_request.is_election_board, False)
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
|
||||
# Try reverting setting an invalid value for election board (should revert to False)
|
||||
domain_request.is_election_board = None
|
||||
domain_request.save()
|
||||
|
||||
self.assertEqual(domain_request.is_election_board, False)
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
|
||||
def test_create_or_update_organization_type_existing_instance_updates_generic_org_type(self):
|
||||
"""Test create_or_update_organization_type when modifying generic_org_type on an existing instance."""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=True,
|
||||
)
|
||||
|
||||
domain_request.generic_org_type = DomainRequest.OrganizationChoices.INTERSTATE
|
||||
domain_request.save()
|
||||
|
||||
# Election board should be None because interstate cannot have an election board.
|
||||
self.assertEqual(domain_request.is_election_board, None)
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.INTERSTATE)
|
||||
|
||||
# Try changing the org Type to something that CAN have an election board.
|
||||
domain_request_tribal = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="startedTribal.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.TRIBAL,
|
||||
is_election_board=True,
|
||||
)
|
||||
self.assertEqual(
|
||||
domain_request_tribal.organization_type, DomainRequest.OrgChoicesElectionOffice.TRIBAL_ELECTION
|
||||
)
|
||||
|
||||
# Change the org type
|
||||
domain_request_tribal.generic_org_type = DomainRequest.OrganizationChoices.STATE_OR_TERRITORY
|
||||
domain_request_tribal.save()
|
||||
|
||||
self.assertEqual(domain_request_tribal.is_election_board, True)
|
||||
self.assertEqual(
|
||||
domain_request_tribal.organization_type, DomainRequest.OrgChoicesElectionOffice.STATE_OR_TERRITORY_ELECTION
|
||||
)
|
||||
|
||||
def test_create_or_update_organization_type_no_update(self):
|
||||
"""Test create_or_update_organization_type when there are no values to update."""
|
||||
|
||||
# Test for when both generic_org_type and organization_type is declared,
|
||||
# and are both non-election board
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=False,
|
||||
)
|
||||
domain_request.save()
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
self.assertEqual(domain_request.is_election_board, False)
|
||||
self.assertEqual(domain_request.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
# Test for when both generic_org_type and organization_type is declared,
|
||||
# and are both election board
|
||||
domain_request_election = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="startedElection.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=True,
|
||||
organization_type=DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
domain_request_election.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION
|
||||
)
|
||||
self.assertEqual(domain_request_election.is_election_board, True)
|
||||
self.assertEqual(domain_request_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
# Modify an unrelated existing value for both, and ensure that everything is still consistent
|
||||
domain_request.city = "Fudge"
|
||||
domain_request_election.city = "Caramel"
|
||||
domain_request.save()
|
||||
domain_request_election.save()
|
||||
|
||||
self.assertEqual(domain_request.city, "Fudge")
|
||||
self.assertEqual(domain_request_election.city, "Caramel")
|
||||
|
||||
# Test for non-election
|
||||
self.assertEqual(domain_request.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
self.assertEqual(domain_request.is_election_board, False)
|
||||
self.assertEqual(domain_request.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
# Test for election
|
||||
self.assertEqual(
|
||||
domain_request_election.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION
|
||||
)
|
||||
self.assertEqual(domain_request_election.is_election_board, True)
|
||||
self.assertEqual(domain_request_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
|
||||
class TestDomainInformationCustomSave(TestCase):
|
||||
"""Tests custom save behaviour on the DomainInformation object"""
|
||||
|
||||
def tearDown(self):
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
super().tearDown()
|
||||
|
||||
def test_create_or_update_organization_type_new_instance(self):
|
||||
"""Test create_or_update_organization_type when creating a new instance"""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=True,
|
||||
)
|
||||
|
||||
domain_information = DomainInformation.create_from_da(domain_request)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
|
||||
|
||||
def test_create_or_update_organization_type_new_instance_federal_does_nothing(self):
|
||||
"""Test if create_or_update_organization_type does nothing when creating a new instance for federal"""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
is_election_board=True,
|
||||
)
|
||||
|
||||
domain_information = DomainInformation.create_from_da(domain_request)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.FEDERAL)
|
||||
self.assertEqual(domain_information.is_election_board, None)
|
||||
|
||||
def test_create_or_update_organization_type_existing_instance_updates_election_board(self):
|
||||
"""Test create_or_update_organization_type for an existing instance."""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=False,
|
||||
)
|
||||
domain_information = DomainInformation.create_from_da(domain_request)
|
||||
domain_information.is_election_board = True
|
||||
domain_information.save()
|
||||
|
||||
self.assertEqual(domain_information.is_election_board, True)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION)
|
||||
|
||||
# Try reverting the election board value
|
||||
domain_information.is_election_board = False
|
||||
domain_information.save()
|
||||
domain_information.refresh_from_db()
|
||||
|
||||
self.assertEqual(domain_information.is_election_board, False)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
|
||||
# Try reverting setting an invalid value for election board (should revert to False)
|
||||
domain_information.is_election_board = None
|
||||
domain_information.save()
|
||||
|
||||
self.assertEqual(domain_information.is_election_board, False)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
|
||||
def test_create_or_update_organization_type_existing_instance_updates_generic_org_type(self):
|
||||
"""Test create_or_update_organization_type when modifying generic_org_type on an existing instance."""
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=True,
|
||||
)
|
||||
domain_information = DomainInformation.create_from_da(domain_request)
|
||||
|
||||
domain_information.generic_org_type = DomainRequest.OrganizationChoices.INTERSTATE
|
||||
domain_information.save()
|
||||
|
||||
# Election board should be None because interstate cannot have an election board.
|
||||
self.assertEqual(domain_information.is_election_board, None)
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.INTERSTATE)
|
||||
|
||||
# Try changing the org Type to something that CAN have an election board.
|
||||
domain_request_tribal = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="startedTribal.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.TRIBAL,
|
||||
is_election_board=True,
|
||||
)
|
||||
domain_information_tribal = DomainInformation.create_from_da(domain_request_tribal)
|
||||
self.assertEqual(
|
||||
domain_information_tribal.organization_type, DomainRequest.OrgChoicesElectionOffice.TRIBAL_ELECTION
|
||||
)
|
||||
|
||||
# Change the org type
|
||||
domain_information_tribal.generic_org_type = DomainRequest.OrganizationChoices.STATE_OR_TERRITORY
|
||||
domain_information_tribal.save()
|
||||
|
||||
self.assertEqual(domain_information_tribal.is_election_board, True)
|
||||
self.assertEqual(
|
||||
domain_information_tribal.organization_type,
|
||||
DomainRequest.OrgChoicesElectionOffice.STATE_OR_TERRITORY_ELECTION,
|
||||
)
|
||||
|
||||
def test_create_or_update_organization_type_no_update(self):
|
||||
"""Test create_or_update_organization_type when there are no values to update."""
|
||||
|
||||
# Test for when both generic_org_type and organization_type is declared,
|
||||
# and are both non-election board
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="started.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=False,
|
||||
)
|
||||
domain_information = DomainInformation.create_from_da(domain_request)
|
||||
domain_information.save()
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
self.assertEqual(domain_information.is_election_board, False)
|
||||
self.assertEqual(domain_information.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
# Test for when both generic_org_type and organization_type is declared,
|
||||
# and are both election board
|
||||
domain_request_election = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
name="startedElection.gov",
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
is_election_board=True,
|
||||
organization_type=DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION,
|
||||
)
|
||||
domain_information_election = DomainInformation.create_from_da(domain_request_election)
|
||||
|
||||
self.assertEqual(
|
||||
domain_information_election.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION
|
||||
)
|
||||
self.assertEqual(domain_information_election.is_election_board, True)
|
||||
self.assertEqual(domain_information_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
# Modify an unrelated existing value for both, and ensure that everything is still consistent
|
||||
domain_information.city = "Fudge"
|
||||
domain_information_election.city = "Caramel"
|
||||
domain_information.save()
|
||||
domain_information_election.save()
|
||||
|
||||
self.assertEqual(domain_information.city, "Fudge")
|
||||
self.assertEqual(domain_information_election.city, "Caramel")
|
||||
|
||||
# Test for non-election
|
||||
self.assertEqual(domain_information.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY)
|
||||
self.assertEqual(domain_information.is_election_board, False)
|
||||
self.assertEqual(domain_information.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
# Test for election
|
||||
self.assertEqual(
|
||||
domain_information_election.organization_type, DomainRequest.OrgChoicesElectionOffice.CITY_ELECTION
|
||||
)
|
||||
self.assertEqual(domain_information_election.is_election_board, True)
|
||||
self.assertEqual(domain_information_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
|
|
@ -9,10 +9,10 @@ from registrar.utility.csv_export import (
|
|||
export_data_unmanaged_domains_to_csv,
|
||||
get_sliced_domains,
|
||||
get_sliced_requests,
|
||||
write_domains_csv,
|
||||
write_csv_for_domains,
|
||||
get_default_start_date,
|
||||
get_default_end_date,
|
||||
write_requests_csv,
|
||||
write_csv_for_requests,
|
||||
)
|
||||
|
||||
from django.core.management import call_command
|
||||
|
@ -242,8 +242,13 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
}
|
||||
self.maxDiff = None
|
||||
# Call the export functions
|
||||
write_domains_csv(
|
||||
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_condition,
|
||||
should_get_domain_managers=False,
|
||||
should_write_header=True,
|
||||
)
|
||||
|
||||
# Reset the CSV file's position to the beginning
|
||||
|
@ -268,7 +273,7 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
def test_write_domains_csv(self):
|
||||
def test_write_csv_for_domains(self):
|
||||
"""Test that write_body returns the
|
||||
existing domain, test that sort by domain name works,
|
||||
test that filter works"""
|
||||
|
@ -304,8 +309,13 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
],
|
||||
}
|
||||
# Call the export functions
|
||||
write_domains_csv(
|
||||
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_condition,
|
||||
should_get_domain_managers=False,
|
||||
should_write_header=True,
|
||||
)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
|
@ -357,8 +367,13 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
],
|
||||
}
|
||||
# Call the export functions
|
||||
write_domains_csv(
|
||||
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_condition,
|
||||
should_get_domain_managers=False,
|
||||
should_write_header=True,
|
||||
)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
|
@ -433,20 +448,20 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
}
|
||||
|
||||
# Call the export functions
|
||||
write_domains_csv(
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_condition,
|
||||
get_domain_managers=False,
|
||||
should_get_domain_managers=False,
|
||||
should_write_header=True,
|
||||
)
|
||||
write_domains_csv(
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields_for_deleted_domains,
|
||||
filter_conditions_for_deleted_domains,
|
||||
get_domain_managers=False,
|
||||
should_get_domain_managers=False,
|
||||
should_write_header=False,
|
||||
)
|
||||
# Reset the CSV file's position to the beginning
|
||||
|
@ -478,7 +493,12 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
|
||||
def test_export_domains_to_writer_domain_managers(self):
|
||||
"""Test that export_domains_to_writer returns the
|
||||
expected domain managers."""
|
||||
expected domain managers.
|
||||
|
||||
An invited user, woofwardthethird, should also be pulled into this report.
|
||||
|
||||
squeaker@rocks.com is invited to domain2 (DNS_NEEDED) and domain10 (No managers).
|
||||
She should show twice in this report but not in test_export_data_managed_domains_to_csv."""
|
||||
|
||||
with less_console_noise():
|
||||
# Create a CSV file in memory
|
||||
|
@ -508,8 +528,13 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
}
|
||||
self.maxDiff = None
|
||||
# Call the export functions
|
||||
write_domains_csv(
|
||||
writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_condition,
|
||||
should_get_domain_managers=True,
|
||||
should_write_header=True,
|
||||
)
|
||||
|
||||
# Reset the CSV file's position to the beginning
|
||||
|
@ -521,14 +546,16 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
expected_content = (
|
||||
"Domain name,Status,Expiration date,Domain type,Agency,"
|
||||
"Organization name,City,State,AO,AO email,"
|
||||
"Security contact email,Domain manager email 1,Domain manager email 2,Domain manager email 3\n"
|
||||
"adomain10.gov,Ready,,Federal,Armed Forces Retirement Home,,,, , ,\n"
|
||||
"adomain2.gov,Dns needed,,Interstate,,,,, , , ,meoward@rocks.com\n"
|
||||
"cdomain11.govReadyFederal-ExecutiveWorldWarICentennialCommissionmeoward@rocks.com\n"
|
||||
"Security contact email,Domain manager 1,DM1 status,Domain manager 2,DM2 status,"
|
||||
"Domain manager 3,DM3 status,Domain manager 4,DM4 status\n"
|
||||
"adomain10.gov,Ready,,Federal,Armed Forces Retirement Home,,,, , ,squeaker@rocks.com, I\n"
|
||||
"adomain2.gov,Dns needed,,Interstate,,,,, , , ,meoward@rocks.com, R,squeaker@rocks.com, I\n"
|
||||
"cdomain11.govReadyFederal-ExecutiveWorldWarICentennialCommissionmeoward@rocks.comR\n"
|
||||
"cdomain1.gov,Ready,,Federal - Executive,World War I Centennial Commission,,,"
|
||||
", , , ,meoward@rocks.com,info@example.com,big_lebowski@dude.co\n"
|
||||
", , , ,meoward@rocks.com,R,info@example.com,R,big_lebowski@dude.co,R,"
|
||||
"woofwardthethird@rocks.com,I\n"
|
||||
"ddomain3.gov,On hold,,Federal,Armed Forces Retirement Home,,,, , , ,,\n"
|
||||
"zdomain12.govReadyInterstatemeoward@rocks.com\n"
|
||||
"zdomain12.govReadyInterstatemeoward@rocks.comR\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
|
@ -538,7 +565,9 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
|
||||
def test_export_data_managed_domains_to_csv(self):
|
||||
"""Test get counts for domains that have domain managers for two different dates,
|
||||
get list of managed domains at end_date."""
|
||||
get list of managed domains at end_date.
|
||||
|
||||
An invited user, woofwardthethird, should also be pulled into this report."""
|
||||
|
||||
with less_console_noise():
|
||||
# Create a CSV file in memory
|
||||
|
@ -562,12 +591,14 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
"MANAGED DOMAINS COUNTS AT END DATE\n"
|
||||
"Total,Federal,Interstate,State or territory,Tribal,County,City,"
|
||||
"Special district,School district,Election office\n"
|
||||
"3,2,1,0,0,0,0,0,0,2\n"
|
||||
"3,2,1,0,0,0,0,0,0,0\n"
|
||||
"\n"
|
||||
"Domain name,Domain type,Domain manager email 1,Domain manager email 2,Domain manager email 3\n"
|
||||
"cdomain11.govFederal-Executivemeoward@rocks.com\n"
|
||||
"cdomain1.gov,Federal - Executive,meoward@rocks.com,info@example.com,big_lebowski@dude.co\n"
|
||||
"zdomain12.govInterstatemeoward@rocks.com\n"
|
||||
"Domain name,Domain type,Domain manager 1,DM1 status,Domain manager 2,DM2 status,"
|
||||
"Domain manager 3,DM3 status,Domain manager 4,DM4 status\n"
|
||||
"cdomain11.govFederal-Executivemeoward@rocks.com, R\n"
|
||||
"cdomain1.gov,Federal - Executive,meoward@rocks.com,R,info@example.com,R,"
|
||||
"big_lebowski@dude.co,R,woofwardthethird@rocks.com,I\n"
|
||||
"zdomain12.govInterstatemeoward@rocks.com,R\n"
|
||||
)
|
||||
|
||||
# Normalize line endings and remove commas,
|
||||
|
@ -642,7 +673,7 @@ class ExportDataTest(MockDb, MockEppLib):
|
|||
"submission_date__lte": self.end_date,
|
||||
"submission_date__gte": self.start_date,
|
||||
}
|
||||
write_requests_csv(writer, columns, sort_fields, filter_condition, should_write_header=True)
|
||||
write_csv_for_requests(writer, columns, sort_fields, filter_condition, should_write_header=True)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
|
@ -686,13 +717,13 @@ class HelperFunctions(MockDb):
|
|||
"domain__first_ready__lte": self.end_date,
|
||||
}
|
||||
# Test with distinct
|
||||
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition, True)
|
||||
expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 2]
|
||||
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
|
||||
expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
|
||||
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
|
||||
|
||||
# Test without distinct
|
||||
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
|
||||
expected_content = [3, 4, 1, 0, 0, 0, 0, 0, 0, 2]
|
||||
expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
|
||||
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
|
||||
|
||||
def test_get_sliced_requests(self):
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from registrar.models import Contact
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from collections import Counter
|
||||
import csv
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.domain_invitation import DomainInvitation
|
||||
from registrar.models.domain_request import DomainRequest
|
||||
from registrar.models.domain_information import DomainInformation
|
||||
from django.utils import timezone
|
||||
|
@ -11,6 +11,7 @@ from django.db.models import F, Value, CharField
|
|||
from django.db.models.functions import Concat, Coalesce
|
||||
|
||||
from registrar.models.public_contact import PublicContact
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from registrar.utility.enums import DefaultEmail
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -33,7 +34,6 @@ def get_domain_infos(filter_condition, sort_fields):
|
|||
"""
|
||||
domain_infos = (
|
||||
DomainInformation.objects.select_related("domain", "authorizing_official")
|
||||
.prefetch_related("domain__permissions")
|
||||
.filter(**filter_condition)
|
||||
.order_by(*sort_fields)
|
||||
.distinct()
|
||||
|
@ -53,7 +53,14 @@ def get_domain_infos(filter_condition, sort_fields):
|
|||
return domain_infos_cleaned
|
||||
|
||||
|
||||
def parse_domain_row(columns, domain_info: DomainInformation, security_emails_dict=None, get_domain_managers=False):
|
||||
def parse_row_for_domain(
|
||||
columns,
|
||||
domain_info: DomainInformation,
|
||||
dict_security_emails=None,
|
||||
should_get_domain_managers=False,
|
||||
dict_domain_invitations_with_invited_status=None,
|
||||
dict_user_domain_roles=None,
|
||||
):
|
||||
"""Given a set of columns, generate a new row from cleaned column data"""
|
||||
|
||||
# Domain should never be none when parsing this information
|
||||
|
@ -65,8 +72,8 @@ def parse_domain_row(columns, domain_info: DomainInformation, security_emails_di
|
|||
|
||||
# Grab the security email from a preset dictionary.
|
||||
# If nothing exists in the dictionary, grab from .contacts.
|
||||
if security_emails_dict is not None and domain.name in security_emails_dict:
|
||||
_email = security_emails_dict.get(domain.name)
|
||||
if dict_security_emails is not None and domain.name in dict_security_emails:
|
||||
_email = dict_security_emails.get(domain.name)
|
||||
security_email = _email if _email is not None else " "
|
||||
else:
|
||||
# If the dictionary doesn't contain that data, lets filter for it manually.
|
||||
|
@ -103,13 +110,22 @@ def parse_domain_row(columns, domain_info: DomainInformation, security_emails_di
|
|||
"Deleted": domain.deleted,
|
||||
}
|
||||
|
||||
if get_domain_managers:
|
||||
# Get each domain managers email and add to list
|
||||
dm_emails = [dm.user.email for dm in domain.permissions.all()]
|
||||
if should_get_domain_managers:
|
||||
# Get lists of emails for active and invited domain managers
|
||||
|
||||
# Set up the "matching header" + row field data
|
||||
for i, dm_email in enumerate(dm_emails, start=1):
|
||||
FIELDS[f"Domain manager email {i}"] = dm_email
|
||||
dms_active_emails = dict_user_domain_roles.get(domain_info.domain.name, [])
|
||||
dms_invited_emails = dict_domain_invitations_with_invited_status.get(domain_info.domain.name, [])
|
||||
|
||||
# Set up the "matching headers" + row field data for email and status
|
||||
i = 0 # Declare i outside of the loop to avoid a reference before assignment in the second loop
|
||||
for i, dm_email in enumerate(dms_active_emails, start=1):
|
||||
FIELDS[f"Domain manager {i}"] = dm_email
|
||||
FIELDS[f"DM{i} status"] = "R"
|
||||
|
||||
# Continue enumeration from where we left off and add data for invited domain managers
|
||||
for j, dm_email in enumerate(dms_invited_emails, start=i + 1):
|
||||
FIELDS[f"Domain manager {j}"] = dm_email
|
||||
FIELDS[f"DM{j} status"] = "I"
|
||||
|
||||
row = [FIELDS.get(column, "") for column in columns]
|
||||
return row
|
||||
|
@ -119,7 +135,7 @@ def _get_security_emails(sec_contact_ids):
|
|||
"""
|
||||
Retrieve security contact emails for the given security contact IDs.
|
||||
"""
|
||||
security_emails_dict = {}
|
||||
dict_security_emails = {}
|
||||
public_contacts = (
|
||||
PublicContact.objects.only("email", "domain__name")
|
||||
.select_related("domain")
|
||||
|
@ -129,65 +145,151 @@ def _get_security_emails(sec_contact_ids):
|
|||
# Populate a dictionary of domain names and their security contacts
|
||||
for contact in public_contacts:
|
||||
domain: Domain = contact.domain
|
||||
if domain is not None and domain.name not in security_emails_dict:
|
||||
security_emails_dict[domain.name] = contact.email
|
||||
if domain is not None and domain.name not in dict_security_emails:
|
||||
dict_security_emails[domain.name] = contact.email
|
||||
else:
|
||||
logger.warning("csv_export -> Domain was none for PublicContact")
|
||||
|
||||
return security_emails_dict
|
||||
return dict_security_emails
|
||||
|
||||
|
||||
def write_domains_csv(
|
||||
def count_domain_managers(domain_name, dict_domain_invitations_with_invited_status, dict_user_domain_roles):
|
||||
"""Count active and invited domain managers"""
|
||||
dms_active = len(dict_user_domain_roles.get(domain_name, []))
|
||||
dms_invited = len(dict_domain_invitations_with_invited_status.get(domain_name, []))
|
||||
return dms_active, dms_invited
|
||||
|
||||
|
||||
def update_columns(columns, dms_total, should_update_columns):
|
||||
"""Update columns if necessary"""
|
||||
if should_update_columns:
|
||||
for i in range(1, dms_total + 1):
|
||||
email_column_header = f"Domain manager {i}"
|
||||
status_column_header = f"DM{i} status"
|
||||
if email_column_header not in columns:
|
||||
columns.append(email_column_header)
|
||||
columns.append(status_column_header)
|
||||
should_update_columns = False
|
||||
return columns, should_update_columns, dms_total
|
||||
|
||||
|
||||
def update_columns_with_domain_managers(
|
||||
columns,
|
||||
domain_info,
|
||||
should_update_columns,
|
||||
dms_total,
|
||||
dict_domain_invitations_with_invited_status,
|
||||
dict_user_domain_roles,
|
||||
):
|
||||
"""Helper function to update columns with domain manager information"""
|
||||
|
||||
domain_name = domain_info.domain.name
|
||||
|
||||
try:
|
||||
dms_active, dms_invited = count_domain_managers(
|
||||
domain_name, dict_domain_invitations_with_invited_status, dict_user_domain_roles
|
||||
)
|
||||
|
||||
if dms_active + dms_invited > dms_total:
|
||||
dms_total = dms_active + dms_invited
|
||||
should_update_columns = True
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"Exception while parsing domain managers for reports: {err}")
|
||||
|
||||
return update_columns(columns, dms_total, should_update_columns)
|
||||
|
||||
|
||||
def build_dictionaries_for_domain_managers(dict_user_domain_roles, dict_domain_invitations_with_invited_status):
|
||||
"""Helper function that builds dicts for invited users and active domain
|
||||
managers. We do so to avoid filtering within loops."""
|
||||
|
||||
user_domain_roles = UserDomainRole.objects.all()
|
||||
|
||||
# Iterate through each user domain role and populate the dictionary
|
||||
for user_domain_role in user_domain_roles:
|
||||
domain_name = user_domain_role.domain.name
|
||||
email = user_domain_role.user.email
|
||||
if domain_name not in dict_user_domain_roles:
|
||||
dict_user_domain_roles[domain_name] = []
|
||||
dict_user_domain_roles[domain_name].append(email)
|
||||
|
||||
domain_invitations_with_invited_status = None
|
||||
domain_invitations_with_invited_status = DomainInvitation.objects.filter(
|
||||
status=DomainInvitation.DomainInvitationStatus.INVITED
|
||||
).select_related("domain")
|
||||
|
||||
# Iterate through each domain invitation and populate the dictionary
|
||||
for invite in domain_invitations_with_invited_status:
|
||||
domain_name = invite.domain.name
|
||||
email = invite.email
|
||||
if domain_name not in dict_domain_invitations_with_invited_status:
|
||||
dict_domain_invitations_with_invited_status[domain_name] = []
|
||||
dict_domain_invitations_with_invited_status[domain_name].append(email)
|
||||
|
||||
return dict_user_domain_roles, dict_domain_invitations_with_invited_status
|
||||
|
||||
|
||||
def write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_condition,
|
||||
get_domain_managers=False,
|
||||
should_get_domain_managers=False,
|
||||
should_write_header=True,
|
||||
):
|
||||
"""
|
||||
Receives params from the parent methods and outputs a CSV with filtered and sorted domains.
|
||||
Works with write_header as long as the same writer object is passed.
|
||||
get_domain_managers: Conditional bc we only use domain manager info for export_data_full_to_csv
|
||||
should_get_domain_managers: Conditional bc we only use domain manager info for export_data_full_to_csv
|
||||
should_write_header: Conditional bc export_data_domain_growth_to_csv calls write_body twice
|
||||
"""
|
||||
|
||||
# Retrieve domain information and all sec emails
|
||||
all_domain_infos = get_domain_infos(filter_condition, sort_fields)
|
||||
|
||||
# Store all security emails to avoid epp calls or excessive filters
|
||||
sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)
|
||||
|
||||
security_emails_dict = _get_security_emails(sec_contact_ids)
|
||||
|
||||
# Reduce the memory overhead when performing the write operation
|
||||
dict_security_emails = _get_security_emails(sec_contact_ids)
|
||||
paginator = Paginator(all_domain_infos, 1000)
|
||||
|
||||
# The maximum amount of domain managers an account has
|
||||
# We get the max so we can set the column header accurately
|
||||
max_dm_count = 0
|
||||
# Initialize variables
|
||||
dms_total = 0
|
||||
should_update_columns = False
|
||||
total_body_rows = []
|
||||
dict_user_domain_roles = {}
|
||||
dict_domain_invitations_with_invited_status = {}
|
||||
|
||||
# Build dictionaries if necessary
|
||||
if should_get_domain_managers:
|
||||
dict_user_domain_roles, dict_domain_invitations_with_invited_status = build_dictionaries_for_domain_managers(
|
||||
dict_user_domain_roles, dict_domain_invitations_with_invited_status
|
||||
)
|
||||
|
||||
# Process domain information
|
||||
for page_num in paginator.page_range:
|
||||
rows = []
|
||||
page = paginator.page(page_num)
|
||||
for domain_info in page.object_list:
|
||||
|
||||
# Get count of all the domain managers for an account
|
||||
if get_domain_managers:
|
||||
dm_count = domain_info.domain.permissions.count()
|
||||
if dm_count > max_dm_count:
|
||||
max_dm_count = dm_count
|
||||
for i in range(1, max_dm_count + 1):
|
||||
column_name = f"Domain manager email {i}"
|
||||
if column_name not in columns:
|
||||
columns.append(column_name)
|
||||
if should_get_domain_managers:
|
||||
columns, dms_total, should_update_columns = update_columns_with_domain_managers(
|
||||
columns,
|
||||
domain_info,
|
||||
should_update_columns,
|
||||
dms_total,
|
||||
dict_domain_invitations_with_invited_status,
|
||||
dict_user_domain_roles,
|
||||
)
|
||||
|
||||
try:
|
||||
row = parse_domain_row(columns, domain_info, security_emails_dict, get_domain_managers)
|
||||
row = parse_row_for_domain(
|
||||
columns,
|
||||
domain_info,
|
||||
dict_security_emails,
|
||||
should_get_domain_managers,
|
||||
dict_domain_invitations_with_invited_status,
|
||||
dict_user_domain_roles,
|
||||
)
|
||||
rows.append(row)
|
||||
except ValueError:
|
||||
# This should not happen. If it does, just skip this row.
|
||||
# It indicates that DomainInformation.domain is None.
|
||||
logger.error("csv_export -> Error when parsing row, domain was None")
|
||||
continue
|
||||
total_body_rows.extend(rows)
|
||||
|
@ -208,7 +310,7 @@ def get_requests(filter_condition, sort_fields):
|
|||
return requests
|
||||
|
||||
|
||||
def parse_request_row(columns, request: DomainRequest):
|
||||
def parse_row_for_requests(columns, request: DomainRequest):
|
||||
"""Given a set of columns, generate a new row from cleaned column data"""
|
||||
|
||||
requested_domain_name = "No requested domain"
|
||||
|
@ -240,7 +342,7 @@ def parse_request_row(columns, request: DomainRequest):
|
|||
return row
|
||||
|
||||
|
||||
def write_requests_csv(
|
||||
def write_csv_for_requests(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
|
@ -261,7 +363,7 @@ def write_requests_csv(
|
|||
rows = []
|
||||
for request in page.object_list:
|
||||
try:
|
||||
row = parse_request_row(columns, request)
|
||||
row = parse_row_for_requests(columns, request)
|
||||
rows.append(row)
|
||||
except ValueError:
|
||||
# This should not happen. If it does, just skip this row.
|
||||
|
@ -309,8 +411,8 @@ def export_data_type_to_csv(csv_file):
|
|||
Domain.State.ON_HOLD,
|
||||
],
|
||||
}
|
||||
write_domains_csv(
|
||||
writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True
|
||||
write_csv_for_domains(
|
||||
writer, columns, sort_fields, filter_condition, should_get_domain_managers=True, should_write_header=True
|
||||
)
|
||||
|
||||
|
||||
|
@ -342,8 +444,8 @@ def export_data_full_to_csv(csv_file):
|
|||
Domain.State.ON_HOLD,
|
||||
],
|
||||
}
|
||||
write_domains_csv(
|
||||
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||
write_csv_for_domains(
|
||||
writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True
|
||||
)
|
||||
|
||||
|
||||
|
@ -376,8 +478,8 @@ def export_data_federal_to_csv(csv_file):
|
|||
Domain.State.ON_HOLD,
|
||||
],
|
||||
}
|
||||
write_domains_csv(
|
||||
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||
write_csv_for_domains(
|
||||
writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True
|
||||
)
|
||||
|
||||
|
||||
|
@ -446,77 +548,42 @@ def export_data_domain_growth_to_csv(csv_file, start_date, end_date):
|
|||
"domain__deleted__gte": start_date_formatted,
|
||||
}
|
||||
|
||||
write_domains_csv(
|
||||
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||
write_csv_for_domains(
|
||||
writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True
|
||||
)
|
||||
write_domains_csv(
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields_for_deleted_domains,
|
||||
filter_condition_for_deleted_domains,
|
||||
get_domain_managers=False,
|
||||
should_get_domain_managers=False,
|
||||
should_write_header=False,
|
||||
)
|
||||
|
||||
|
||||
def get_sliced_domains(filter_condition, distinct=False):
|
||||
def get_sliced_domains(filter_condition):
|
||||
"""Get filtered domains counts sliced by org type and election office.
|
||||
Pass distinct=True when filtering by permissions so we do not to count multiples
|
||||
when a domain has more that one manager.
|
||||
"""
|
||||
|
||||
# Round trip 1: Get distinct domain names based on filter condition
|
||||
domains_count = DomainInformation.objects.filter(**filter_condition).distinct().count()
|
||||
|
||||
# Round trip 2: Get counts for other slices
|
||||
# This will require either 8 filterd and distinct DB round trips,
|
||||
# or 2 DB round trips plus iteration on domain_permissions for each domain
|
||||
if distinct:
|
||||
generic_org_types_query = DomainInformation.objects.filter(**filter_condition).values_list(
|
||||
"domain_id", "generic_org_type"
|
||||
)
|
||||
# Initialize Counter to store counts for each generic_org_type
|
||||
generic_org_type_counts = Counter()
|
||||
|
||||
# Keep track of domains already counted
|
||||
domains_counted = set()
|
||||
|
||||
# Iterate over distinct domains
|
||||
for domain_id, generic_org_type in generic_org_types_query:
|
||||
# Check if the domain has already been counted
|
||||
if domain_id in domains_counted:
|
||||
continue
|
||||
|
||||
# Get all permissions for the current domain
|
||||
domain_permissions = DomainInformation.objects.filter(domain_id=domain_id, **filter_condition).values_list(
|
||||
"domain__permissions", flat=True
|
||||
)
|
||||
|
||||
# Check if the domain has multiple permissions
|
||||
if len(domain_permissions) > 0:
|
||||
# Mark the domain as counted
|
||||
domains_counted.add(domain_id)
|
||||
|
||||
# Increment the count for the corresponding generic_org_type
|
||||
generic_org_type_counts[generic_org_type] += 1
|
||||
else:
|
||||
generic_org_types_query = DomainInformation.objects.filter(**filter_condition).values_list(
|
||||
"generic_org_type", flat=True
|
||||
)
|
||||
generic_org_type_counts = Counter(generic_org_types_query)
|
||||
|
||||
# Extract counts for each generic_org_type
|
||||
federal = generic_org_type_counts.get(DomainRequest.OrganizationChoices.FEDERAL, 0)
|
||||
interstate = generic_org_type_counts.get(DomainRequest.OrganizationChoices.INTERSTATE, 0)
|
||||
state_or_territory = generic_org_type_counts.get(DomainRequest.OrganizationChoices.STATE_OR_TERRITORY, 0)
|
||||
tribal = generic_org_type_counts.get(DomainRequest.OrganizationChoices.TRIBAL, 0)
|
||||
county = generic_org_type_counts.get(DomainRequest.OrganizationChoices.COUNTY, 0)
|
||||
city = generic_org_type_counts.get(DomainRequest.OrganizationChoices.CITY, 0)
|
||||
special_district = generic_org_type_counts.get(DomainRequest.OrganizationChoices.SPECIAL_DISTRICT, 0)
|
||||
school_district = generic_org_type_counts.get(DomainRequest.OrganizationChoices.SCHOOL_DISTRICT, 0)
|
||||
|
||||
# Round trip 3
|
||||
election_board = DomainInformation.objects.filter(is_election_board=True, **filter_condition).distinct().count()
|
||||
domains = DomainInformation.objects.all().filter(**filter_condition).distinct()
|
||||
domains_count = domains.count()
|
||||
federal = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()
|
||||
interstate = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.INTERSTATE).count()
|
||||
state_or_territory = (
|
||||
domains.filter(generic_org_type=DomainRequest.OrganizationChoices.STATE_OR_TERRITORY).distinct().count()
|
||||
)
|
||||
tribal = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.TRIBAL).distinct().count()
|
||||
county = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.COUNTY).distinct().count()
|
||||
city = domains.filter(generic_org_type=DomainRequest.OrganizationChoices.CITY).distinct().count()
|
||||
special_district = (
|
||||
domains.filter(generic_org_type=DomainRequest.OrganizationChoices.SPECIAL_DISTRICT).distinct().count()
|
||||
)
|
||||
school_district = (
|
||||
domains.filter(generic_org_type=DomainRequest.OrganizationChoices.SCHOOL_DISTRICT).distinct().count()
|
||||
)
|
||||
election_board = domains.filter(is_election_board=True).distinct().count()
|
||||
|
||||
return [
|
||||
domains_count,
|
||||
|
@ -535,26 +602,23 @@ def get_sliced_domains(filter_condition, distinct=False):
|
|||
def get_sliced_requests(filter_condition):
|
||||
"""Get filtered requests counts sliced by org type and election office."""
|
||||
|
||||
# Round trip 1: Get distinct requests based on filter condition
|
||||
requests_count = DomainRequest.objects.filter(**filter_condition).distinct().count()
|
||||
|
||||
# Round trip 2: Get counts for other slices
|
||||
generic_org_types_query = DomainRequest.objects.filter(**filter_condition).values_list(
|
||||
"generic_org_type", flat=True
|
||||
requests = DomainRequest.objects.all().filter(**filter_condition).distinct()
|
||||
requests_count = requests.count()
|
||||
federal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.FEDERAL).distinct().count()
|
||||
interstate = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.INTERSTATE).distinct().count()
|
||||
state_or_territory = (
|
||||
requests.filter(generic_org_type=DomainRequest.OrganizationChoices.STATE_OR_TERRITORY).distinct().count()
|
||||
)
|
||||
generic_org_type_counts = Counter(generic_org_types_query)
|
||||
|
||||
federal = generic_org_type_counts.get(DomainRequest.OrganizationChoices.FEDERAL, 0)
|
||||
interstate = generic_org_type_counts.get(DomainRequest.OrganizationChoices.INTERSTATE, 0)
|
||||
state_or_territory = generic_org_type_counts.get(DomainRequest.OrganizationChoices.STATE_OR_TERRITORY, 0)
|
||||
tribal = generic_org_type_counts.get(DomainRequest.OrganizationChoices.TRIBAL, 0)
|
||||
county = generic_org_type_counts.get(DomainRequest.OrganizationChoices.COUNTY, 0)
|
||||
city = generic_org_type_counts.get(DomainRequest.OrganizationChoices.CITY, 0)
|
||||
special_district = generic_org_type_counts.get(DomainRequest.OrganizationChoices.SPECIAL_DISTRICT, 0)
|
||||
school_district = generic_org_type_counts.get(DomainRequest.OrganizationChoices.SCHOOL_DISTRICT, 0)
|
||||
|
||||
# Round trip 3
|
||||
election_board = DomainRequest.objects.filter(is_election_board=True, **filter_condition).distinct().count()
|
||||
tribal = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.TRIBAL).distinct().count()
|
||||
county = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.COUNTY).distinct().count()
|
||||
city = requests.filter(generic_org_type=DomainRequest.OrganizationChoices.CITY).distinct().count()
|
||||
special_district = (
|
||||
requests.filter(generic_org_type=DomainRequest.OrganizationChoices.SPECIAL_DISTRICT).distinct().count()
|
||||
)
|
||||
school_district = (
|
||||
requests.filter(generic_org_type=DomainRequest.OrganizationChoices.SCHOOL_DISTRICT).distinct().count()
|
||||
)
|
||||
election_board = requests.filter(is_election_board=True).distinct().count()
|
||||
|
||||
return [
|
||||
requests_count,
|
||||
|
@ -588,7 +652,7 @@ def export_data_managed_domains_to_csv(csv_file, start_date, end_date):
|
|||
"domain__permissions__isnull": False,
|
||||
"domain__first_ready__lte": start_date_formatted,
|
||||
}
|
||||
managed_domains_sliced_at_start_date = get_sliced_domains(filter_managed_domains_start_date, True)
|
||||
managed_domains_sliced_at_start_date = get_sliced_domains(filter_managed_domains_start_date)
|
||||
|
||||
writer.writerow(["MANAGED DOMAINS COUNTS AT START DATE"])
|
||||
writer.writerow(
|
||||
|
@ -612,7 +676,7 @@ def export_data_managed_domains_to_csv(csv_file, start_date, end_date):
|
|||
"domain__permissions__isnull": False,
|
||||
"domain__first_ready__lte": end_date_formatted,
|
||||
}
|
||||
managed_domains_sliced_at_end_date = get_sliced_domains(filter_managed_domains_end_date, True)
|
||||
managed_domains_sliced_at_end_date = get_sliced_domains(filter_managed_domains_end_date)
|
||||
|
||||
writer.writerow(["MANAGED DOMAINS COUNTS AT END DATE"])
|
||||
writer.writerow(
|
||||
|
@ -632,12 +696,12 @@ def export_data_managed_domains_to_csv(csv_file, start_date, end_date):
|
|||
writer.writerow(managed_domains_sliced_at_end_date)
|
||||
writer.writerow([])
|
||||
|
||||
write_domains_csv(
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_managed_domains_end_date,
|
||||
get_domain_managers=True,
|
||||
should_get_domain_managers=True,
|
||||
should_write_header=True,
|
||||
)
|
||||
|
||||
|
@ -661,7 +725,7 @@ def export_data_unmanaged_domains_to_csv(csv_file, start_date, end_date):
|
|||
"domain__permissions__isnull": True,
|
||||
"domain__first_ready__lte": start_date_formatted,
|
||||
}
|
||||
unmanaged_domains_sliced_at_start_date = get_sliced_domains(filter_unmanaged_domains_start_date, True)
|
||||
unmanaged_domains_sliced_at_start_date = get_sliced_domains(filter_unmanaged_domains_start_date)
|
||||
|
||||
writer.writerow(["UNMANAGED DOMAINS AT START DATE"])
|
||||
writer.writerow(
|
||||
|
@ -685,7 +749,7 @@ def export_data_unmanaged_domains_to_csv(csv_file, start_date, end_date):
|
|||
"domain__permissions__isnull": True,
|
||||
"domain__first_ready__lte": end_date_formatted,
|
||||
}
|
||||
unmanaged_domains_sliced_at_end_date = get_sliced_domains(filter_unmanaged_domains_end_date, True)
|
||||
unmanaged_domains_sliced_at_end_date = get_sliced_domains(filter_unmanaged_domains_end_date)
|
||||
|
||||
writer.writerow(["UNMANAGED DOMAINS AT END DATE"])
|
||||
writer.writerow(
|
||||
|
@ -705,12 +769,12 @@ def export_data_unmanaged_domains_to_csv(csv_file, start_date, end_date):
|
|||
writer.writerow(unmanaged_domains_sliced_at_end_date)
|
||||
writer.writerow([])
|
||||
|
||||
write_domains_csv(
|
||||
write_csv_for_domains(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_unmanaged_domains_end_date,
|
||||
get_domain_managers=False,
|
||||
should_get_domain_managers=False,
|
||||
should_write_header=True,
|
||||
)
|
||||
|
||||
|
@ -741,4 +805,4 @@ def export_data_requests_growth_to_csv(csv_file, start_date, end_date):
|
|||
"submission_date__gte": start_date_formatted,
|
||||
}
|
||||
|
||||
write_requests_csv(writer, columns, sort_fields, filter_condition, should_write_header=True)
|
||||
write_csv_for_requests(writer, columns, sort_fields, filter_condition, should_write_header=True)
|
||||
|
|
|
@ -49,8 +49,8 @@ class AnalyticsView(View):
|
|||
"domain__permissions__isnull": False,
|
||||
"domain__first_ready__lte": end_date_formatted,
|
||||
}
|
||||
managed_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_managed_domains_start_date, True)
|
||||
managed_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_managed_domains_end_date, True)
|
||||
managed_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_managed_domains_start_date)
|
||||
managed_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_managed_domains_end_date)
|
||||
|
||||
filter_unmanaged_domains_start_date = {
|
||||
"domain__permissions__isnull": True,
|
||||
|
@ -60,10 +60,8 @@ class AnalyticsView(View):
|
|||
"domain__permissions__isnull": True,
|
||||
"domain__first_ready__lte": end_date_formatted,
|
||||
}
|
||||
unmanaged_domains_sliced_at_start_date = csv_export.get_sliced_domains(
|
||||
filter_unmanaged_domains_start_date, True
|
||||
)
|
||||
unmanaged_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_unmanaged_domains_end_date, True)
|
||||
unmanaged_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_unmanaged_domains_start_date)
|
||||
unmanaged_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_unmanaged_domains_end_date)
|
||||
|
||||
filter_ready_domains_start_date = {
|
||||
"domain__state__in": [models.Domain.State.READY],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue