mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-22 17:21:06 +02:00
Merge branch 'main' into rh/1727-metadata-emaill
This commit is contained in:
commit
177b79ccc3
30 changed files with 3158 additions and 1126 deletions
74
docs/architecture/decisions/0025-caching.md
Normal file
74
docs/architecture/decisions/0025-caching.md
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
# 24. Production Release Cadence
|
||||||
|
|
||||||
|
Date: 2024-14-02
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
In Review
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
We experienced problems with our Cloudfront caching infrastructure early in our November launch. In response, we turned off caching across the application. We would like to utilize caching again without incurring the same issues.
|
||||||
|
|
||||||
|
Details:
|
||||||
|
Originally, Cloudfront was utilized to provide caching capabilities in our application. All incoming HTTP requests first go through a Cloudfront endpoint, which has a caching infrastructure enabled by default. Cloudfront then decides whether to pass each request to our running Django app inside cloud.gov or if it will respond to with cached data. The big problem with this feature is Cloudfront's caching has a default timeout of 24-hours, which we cannot control. This led to issues on our November launch; Incidents reported include the following...
|
||||||
|
- Users couldn't utilize login.gov properly and had to wait a day before they would be able to login. This was traced back to the 24-hour cache timeout.
|
||||||
|
- Changes made by admins would not be reflected in the app (due to the cached data not updating)
|
||||||
|
|
||||||
|
To resolve these issues, we added "no cache" headers throughout our application. Currently, every single HTTP response that comes from Django says "Cache control: no cache" in the headers, which instructs Cloudfront not to cache the associated data. This effectively removes Cloudfront caching for us.
|
||||||
|
|
||||||
|
Although we could leave our architecture as-is, we decided to investigate options for improving our use of caching (instead of just disabling it completely).
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
**Option 1:** Cache static resources using Whitenoise
|
||||||
|
|
||||||
|
Caching static resources should pose little risk to our application's functionality. Currently, every static resource from /public/... is hitting our Django application inside of Cloud.gov. We already use a Django plugin called whitenoise that can do hash-based linking to static assets so that they can be cached forever by Cloudfront. (If the content changes, then the hash changes, then it results in a different filename.)
|
||||||
|
|
||||||
|
See ticket [#1371](https://github.com/cisagov/manage.get.gov/issues/1371) for more information.
|
||||||
|
|
||||||
|
**Option 2:** Leave things as-is (we had some discussion on whether or not caching static pages will make enough of a difference to be worth the effort)
|
||||||
|
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
We decided on Option 2 - leave things as-is (for now).
|
||||||
|
|
||||||
|
Preliminary analysis suggest that implementing caching on static pages will result in negligible improvements to our application load time. A quick look at Kibana logs suggests most of these resources take less than 10ms to load...
|
||||||
|

|
||||||
|
|
||||||
|
If we look at average load times in Kibana (here is [the Kibana page with preloaded query](https://logs.fr.cloud.gov/app/visualize#/create?_a=(filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logs-app*',key:'@cf.app',negate:!f,params:(query:getgov-stable),type:phrase),query:(match_phrase:('@cf.app':getgov-stable)))),linked:!f,query:(language:lucene,query:''),uiState:(),vis:(aggs:!((enabled:!t,id:'1',params:(customLabel:'Average%20Response%20Time%20in%20ms',field:rtr.response_time_ms),schema:metric,type:avg),(enabled:!t,id:'2',params:(drop_partials:!f,extended_bounds:(),field:'@timestamp',interval:d,min_doc_count:1,scaleMetricValues:!f,timeRange:(from:now-20d,to:now),useNormalizedEsInterval:!t),schema:segment,type:date_histogram)),params:(addLegend:!t,addTimeMarker:!f,addTooltip:!t,categoryAxes:!((id:CategoryAxis-1,labels:(filter:!t,show:!t,truncate:100),position:bottom,scale:(type:linear),show:!t,style:(),title:(),type:category)),grid:(categoryLines:!f),labels:(show:!f),legendPosition:right,seriesParams:!((data:(id:'1',label:'Average%20Response%20Time%20in%20ms'),drawLinesBetweenPoints:!t,lineWidth:2,mode:stacked,show:!t,showCircles:!t,type:histogram,valueAxis:ValueAxis-1)),thresholdLine:(color:%23E7664C,show:!f,style:full,value:10,width:1),times:!(),type:histogram,valueAxes:!((id:ValueAxis-1,labels:(filter:!f,rotate:0,show:!t,truncate:100),name:LeftAxis-1,position:left,scale:(mode:normal,type:linear),show:!t,style:(),title:(text:'Average%20Response%20Time%20in%20ms'),type:value))),title:'',type:histogram))&_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-2w,to:now))&indexPattern=logs-app*&type=histogram)), it looks like we are doing great for load times in stable (using the rtr.response_time_ms metric), staying under 200ms (in the last 4 weeks) and usually hovering around 40-80ms. Some google searching suggests that "an ideal page load time is between 0-2 seconds, but 3 seconds is also considered to be an acceptable score. Anything above 3 seconds increases the likelihood of visitors leaving your site." (Quote shamelessly copied from Sematex)
|
||||||
|

|
||||||
|
|
||||||
|
NOTE: While we considered implementing caching in a sandbox (See footnote) in order to examine risks and benefits of OPTION 1 in more detail, this incurred more overhead than expected (mainly due to poor documentation). Therefore, we decided it was not worth the investment.
|
||||||
|
|
||||||
|
Therefore, implementing caching using Whitenoise is not currently worth it for the following reasons;
|
||||||
|
- Minimal gains: We would only be caching static files which would not result in a large performance boost
|
||||||
|
- Risks: Incurs risk of unforeseen loading issues (we can’t entirely rule out that we won’t run into issues like we did in our November launch incident). Although we don’t think static files should pose a problem, due diligence would call us to monitor for any unforeseen issues that might arise, which adds cost to this project that doesn’t seem proportional to the gains.
|
||||||
|
- Maintenance: We would have to provide custom settings in cloudfront (coordinated through Cameron) for any sandboxes and other environments where caching is enabled. If we move down the route of utilizing CDN, it would be good for every environment to have this service enabled so our dev environments reflect stable settings. This could possibly introduce some overhead and maintenance issues. (Although further investigation might reveal these to be negligible.)
|
||||||
|
|
||||||
|
Overall, it is recommended that we SHELVE this caching endeavor for a future scenario where we have exhausted other (likely more lucrative) options for performance improvements. If we then still need to make improvements to our load times, perhaps we can revisit this and examine caching not only static files, but other resources as well (with caution).
|
||||||
|
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
We will forgo (negligible) load-time improvements by leaving caching off.
|
||||||
|
|
||||||
|
## (Footnote - How to implement caching)
|
||||||
|
Here are notes for implementing caching using whitenoise should we decide to pick this up again in the future;
|
||||||
|
|
||||||
|
1 - Add caching capability to a sandbox using the following steps (or [following documentation for command line](https://cloud.gov/docs/services/external-domain-service/))
|
||||||
|
- Log-in to the cloud.gov website
|
||||||
|
- [Navigate to "Services"](https://dashboard.fr.cloud.gov/services). Click "Add Service"...
|
||||||
|
- Choose "Marketplace Service"
|
||||||
|
- For the fields, select Cloud Foundry, Organization = "cisa-dotgov", Space = "[your sandbox. eg. "nl"]". Click "Next"
|
||||||
|
- For the Service, select "External Domain". Click "Next"
|
||||||
|
- For the Plan, select "domain-with-cdn" (here is [documentation on CDN](https://cloud.gov/docs/management/custom-domains/))
|
||||||
|
- If you choose to bind the app, a JSON string will be required (we believe this should do it: {"domains": "example.gov"}, where "example.gov" is replaced with the domain name you want to use for this application)
|
||||||
|
Before you can continue, work with Cameron to setup the DNS in AWS (use the following documentation linked below):
|
||||||
|
https://cloud.gov/docs/services/external-domain-service/
|
||||||
|
- Once the DNS is setup, you *should* be able to continue. We did not test this.
|
||||||
|
|
||||||
|
2- Enable caching in the code with Whitenoise (see [documentation on Whitenoise Caching](https://whitenoise.readthedocs.io/en/latest/djangohtml#add-compression-and-caching-support))
|
||||||
|
|
||||||
|
3- Take performance measurements before/after caching is enabled to determine cost-benefits of implementing caching. (NOTE: [lighthouse](https://developer.chrome.com/blog/lighthouse-load-performance) might be useful for this step)
|
BIN
docs/architecture/doc-images/caching-average-load-times.png
Normal file
BIN
docs/architecture/doc-images/caching-average-load-times.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 434 KiB |
BIN
docs/architecture/doc-images/caching-rtr-logs.png
Normal file
BIN
docs/architecture/doc-images/caching-rtr-logs.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 200 KiB |
30
docs/operations/runbooks/downtime_incident_management.md
Normal file
30
docs/operations/runbooks/downtime_incident_management.md
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Downtime Incident Management Runbook
|
||||||
|
|
||||||
|
Our team has agreed upon steps for handling incidents that cause our site to go offline or become unusable for users. For this document, an incident refers to one in which manage.get.gov is offline or displaying error 400/500 HTTP errors on all pages. However, for this document to apply the cause of the problem must be a critical bug in our code or one of our providers having an outage, not to be confused with a cyber security incident. This document should not be used in response to any type of cyber security incident.
|
||||||
|
|
||||||
|
## Response management rules
|
||||||
|
|
||||||
|
The following set of rules should be followed while an incident is in progress.
|
||||||
|
|
||||||
|
- The person who first notices that the site is down is responsible for using @here and notifying in #dotgov-announce that production is down.
|
||||||
|
- This applies to any team member, including new team members and non-developers.
|
||||||
|
- If no engineer has acknowledged the announcement within 10 minutes, whoever discovered the site was down should call each developer via the Slack DM huddle feature. If there is no response, this should escalate to a phone call.
|
||||||
|
- When calling, go down the [phone call list](https://docs.google.com/document/d/1k4r-1MNCfW8EXSXa-tqJQzOvJxQv0ARvHnOjjAH0LII/edit) from top to bottom until someone answers who is available to help.
|
||||||
|
- If this incident occurs outside of regular working hours, choosing to help is on a volunteer basis, and answering a call doesn't mean an individual is truly available to assist.
|
||||||
|
- Once an engineer is online, they should immediately start a huddle in the #dotgov-redalert channel to begin troubleshooting.
|
||||||
|
- All available engineers should join the huddle once they see it.
|
||||||
|
- If downtime occurs outside of working hours, team members who are off for the day may still be pinged and called but are not required to join if unavailable to do so.
|
||||||
|
- Uncomment the [banner on get.gov](https://github.com/cisagov/get.gov/blob/0365d3d34b041cc9353497b2b5f81b6ab7fe75a9/_includes/header.html#L9), so it is transparent to users that we know about the issue on manage.get.gov.
|
||||||
|
- Designers or Developers should be able to make this change; if designers are online and can help with this task, that will allow developers to focus on fixing the bug.
|
||||||
|
|
||||||
|
## Post Incident
|
||||||
|
|
||||||
|
The following checklist should be followed after the site is back up and running.
|
||||||
|
|
||||||
|
- [ ] Message in #dotgov-announce with an @here saying the issue is resolved
|
||||||
|
- [ ] Remove the [banner on get.gov](https://github.com/cisagov/get.gov/blob/0365d3d34b041cc9353497b2b5f81b6ab7fe75a9/_includes/header.html#L9) by commenting it out.
|
||||||
|
- [ ] Write up what happened and when; if the cause is already known, write that as well. This is a draft for internal communications and not for any public facing site and can be as simple as using bullet points.
|
||||||
|
- [ ] If the cause is not known yet, developers should investigate the issue as the highest priority task.
|
||||||
|
- [ ] As close to the event as possible, such as the next day, perform a team incident retro that is an hour long. The goal of this meeting should be to inform all team members what happened and what is being done now and to collect feedback on what could have been done better. This is where the draft write up of what happened will be useful.
|
||||||
|
- [ ] After the retro and once the bug is fully identified, an engineer should assist in writing an incident report and may be as detailed as possible for future team members to refer to. That document should be places in the [Incidents folder](https://drive.google.com/drive/folders/1LPVICVpI4Xb5KGdrNkSwhX2OAJ6hYTyu).
|
||||||
|
- [ ] After creating the document above, the lead engineer make a draft of content that will go in the get.gov Incidents section. This Word document should be shared and reviewed by the product team before a developer adds it to get.gov.
|
|
@ -3,7 +3,7 @@ import logging
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models.functions import Concat, Coalesce
|
from django.db.models.functions import Concat, Coalesce
|
||||||
from django.db.models import Value, CharField
|
from django.db.models import Value, CharField, Q
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django_fsm import get_available_FIELD_transitions
|
from django_fsm import get_available_FIELD_transitions
|
||||||
|
@ -18,17 +18,94 @@ from registrar.models import Contact, Domain, DomainApplication, DraftDomain, Us
|
||||||
from registrar.utility import csv_export
|
from registrar.utility import csv_export
|
||||||
from registrar.views.utility.mixins import OrderableFieldsMixin
|
from registrar.views.utility.mixins import OrderableFieldsMixin
|
||||||
from django.contrib.admin.views.main import ORDER_VAR
|
from django.contrib.admin.views.main import ORDER_VAR
|
||||||
|
from registrar.widgets import NoAutocompleteFilteredSelectMultiple
|
||||||
from . import models
|
from . import models
|
||||||
from auditlog.models import LogEntry # type: ignore
|
from auditlog.models import LogEntry # type: ignore
|
||||||
from auditlog.admin import LogEntryAdmin # type: ignore
|
from auditlog.admin import LogEntryAdmin # type: ignore
|
||||||
from django_fsm import TransitionNotAllowed # type: ignore
|
from django_fsm import TransitionNotAllowed # type: ignore
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
|
from django.contrib.auth.forms import UserChangeForm, UsernameField
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MyUserAdminForm(UserChangeForm):
|
||||||
|
"""This form utilizes the custom widget for its class's ManyToMany UIs.
|
||||||
|
|
||||||
|
It inherits from UserChangeForm which has special handling for the password and username fields."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.User
|
||||||
|
fields = "__all__"
|
||||||
|
field_classes = {"username": UsernameField}
|
||||||
|
widgets = {
|
||||||
|
"groups": NoAutocompleteFilteredSelectMultiple("groups", False),
|
||||||
|
"user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DomainInformationAdminForm(forms.ModelForm):
|
||||||
|
"""This form utilizes the custom widget for its class's ManyToMany UIs."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.DomainInformation
|
||||||
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DomainInformationInlineForm(forms.ModelForm):
|
||||||
|
"""This form utilizes the custom widget for its class's ManyToMany UIs."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.DomainInformation
|
||||||
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DomainApplicationAdminForm(forms.ModelForm):
|
||||||
|
"""Custom form to limit transitions to available transitions.
|
||||||
|
This form utilizes the custom widget for its class's ManyToMany UIs."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.DomainApplication
|
||||||
|
fields = "__all__"
|
||||||
|
widgets = {
|
||||||
|
"current_websites": NoAutocompleteFilteredSelectMultiple("current_websites", False),
|
||||||
|
"alternative_domains": NoAutocompleteFilteredSelectMultiple("alternative_domains", False),
|
||||||
|
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
application = kwargs.get("instance")
|
||||||
|
if application and application.pk:
|
||||||
|
current_state = application.status
|
||||||
|
|
||||||
|
# first option in status transitions is current state
|
||||||
|
available_transitions = [(current_state, application.get_status_display())]
|
||||||
|
|
||||||
|
transitions = get_available_FIELD_transitions(
|
||||||
|
application, models.DomainApplication._meta.get_field("status")
|
||||||
|
)
|
||||||
|
|
||||||
|
for transition in transitions:
|
||||||
|
available_transitions.append((transition.target, transition.target.label))
|
||||||
|
|
||||||
|
# only set the available transitions if the user is not restricted
|
||||||
|
# from editing the domain application; otherwise, the form will be
|
||||||
|
# readonly and the status field will not have a widget
|
||||||
|
if not application.creator.is_restricted():
|
||||||
|
self.fields["status"].widget.choices = available_transitions
|
||||||
|
|
||||||
|
|
||||||
# Based off of this excellent example: https://djangosnippets.org/snippets/10471/
|
# Based off of this excellent example: https://djangosnippets.org/snippets/10471/
|
||||||
class MultiFieldSortableChangeList(admin.views.main.ChangeList):
|
class MultiFieldSortableChangeList(admin.views.main.ChangeList):
|
||||||
"""
|
"""
|
||||||
|
@ -288,6 +365,8 @@ class UserContactInline(admin.StackedInline):
|
||||||
class MyUserAdmin(BaseUserAdmin):
|
class MyUserAdmin(BaseUserAdmin):
|
||||||
"""Custom user admin class to use our inlines."""
|
"""Custom user admin class to use our inlines."""
|
||||||
|
|
||||||
|
form = MyUserAdminForm
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Contains meta information about this class"""
|
"""Contains meta information about this class"""
|
||||||
|
|
||||||
|
@ -673,6 +752,8 @@ class DomainInvitationAdmin(ListHeaderAdmin):
|
||||||
class DomainInformationAdmin(ListHeaderAdmin):
|
class DomainInformationAdmin(ListHeaderAdmin):
|
||||||
"""Customize domain information admin class."""
|
"""Customize domain information admin class."""
|
||||||
|
|
||||||
|
form = DomainInformationAdminForm
|
||||||
|
|
||||||
# Columns
|
# Columns
|
||||||
list_display = [
|
list_display = [
|
||||||
"domain",
|
"domain",
|
||||||
|
@ -758,6 +839,14 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
||||||
# to activate the edit/delete/view buttons
|
# to activate the edit/delete/view buttons
|
||||||
filter_horizontal = ("other_contacts",)
|
filter_horizontal = ("other_contacts",)
|
||||||
|
|
||||||
|
autocomplete_fields = [
|
||||||
|
"creator",
|
||||||
|
"domain_application",
|
||||||
|
"authorizing_official",
|
||||||
|
"domain",
|
||||||
|
"submitter",
|
||||||
|
]
|
||||||
|
|
||||||
# Table ordering
|
# Table ordering
|
||||||
ordering = ["domain__name"]
|
ordering = ["domain__name"]
|
||||||
|
|
||||||
|
@ -777,40 +866,11 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
||||||
return readonly_fields # Read-only fields for analysts
|
return readonly_fields # Read-only fields for analysts
|
||||||
|
|
||||||
|
|
||||||
class DomainApplicationAdminForm(forms.ModelForm):
|
|
||||||
"""Custom form to limit transitions to available transitions"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.DomainApplication
|
|
||||||
fields = "__all__"
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
application = kwargs.get("instance")
|
|
||||||
if application and application.pk:
|
|
||||||
current_state = application.status
|
|
||||||
|
|
||||||
# first option in status transitions is current state
|
|
||||||
available_transitions = [(current_state, application.get_status_display())]
|
|
||||||
|
|
||||||
transitions = get_available_FIELD_transitions(
|
|
||||||
application, models.DomainApplication._meta.get_field("status")
|
|
||||||
)
|
|
||||||
|
|
||||||
for transition in transitions:
|
|
||||||
available_transitions.append((transition.target, transition.target.label))
|
|
||||||
|
|
||||||
# only set the available transitions if the user is not restricted
|
|
||||||
# from editing the domain application; otherwise, the form will be
|
|
||||||
# readonly and the status field will not have a widget
|
|
||||||
if not application.creator.is_restricted():
|
|
||||||
self.fields["status"].widget.choices = available_transitions
|
|
||||||
|
|
||||||
|
|
||||||
class DomainApplicationAdmin(ListHeaderAdmin):
|
class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
"""Custom domain applications admin class."""
|
"""Custom domain applications admin class."""
|
||||||
|
|
||||||
|
form = DomainApplicationAdminForm
|
||||||
|
|
||||||
class InvestigatorFilter(admin.SimpleListFilter):
|
class InvestigatorFilter(admin.SimpleListFilter):
|
||||||
"""Custom investigator filter that only displays users with the manager role"""
|
"""Custom investigator filter that only displays users with the manager role"""
|
||||||
|
|
||||||
|
@ -830,13 +890,19 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Annotate the full name and return a values list that lookups can use
|
# Annotate the full name and return a values list that lookups can use
|
||||||
privileged_users_annotated = privileged_users.annotate(
|
privileged_users_annotated = (
|
||||||
|
privileged_users.annotate(
|
||||||
full_name=Coalesce(
|
full_name=Coalesce(
|
||||||
Concat("investigator__first_name", Value(" "), "investigator__last_name", output_field=CharField()),
|
Concat(
|
||||||
|
"investigator__first_name", Value(" "), "investigator__last_name", output_field=CharField()
|
||||||
|
),
|
||||||
"investigator__email",
|
"investigator__email",
|
||||||
output_field=CharField(),
|
output_field=CharField(),
|
||||||
)
|
)
|
||||||
).values_list("investigator__id", "full_name")
|
)
|
||||||
|
.values_list("investigator__id", "full_name")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
return privileged_users_annotated
|
return privileged_users_annotated
|
||||||
|
|
||||||
|
@ -847,11 +913,35 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
else:
|
else:
|
||||||
return queryset.filter(investigator__id__exact=self.value())
|
return queryset.filter(investigator__id__exact=self.value())
|
||||||
|
|
||||||
|
class ElectionOfficeFilter(admin.SimpleListFilter):
|
||||||
|
"""Define a custom filter for is_election_board"""
|
||||||
|
|
||||||
|
title = _("election office")
|
||||||
|
parameter_name = "is_election_board"
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
return (
|
||||||
|
("1", _("Yes")),
|
||||||
|
("0", _("No")),
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value() == "1":
|
||||||
|
return queryset.filter(is_election_board=True)
|
||||||
|
if self.value() == "0":
|
||||||
|
return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None))
|
||||||
|
|
||||||
# Columns
|
# Columns
|
||||||
list_display = [
|
list_display = [
|
||||||
"requested_domain",
|
"requested_domain",
|
||||||
"status",
|
"status",
|
||||||
"organization_type",
|
"organization_type",
|
||||||
|
"federal_type",
|
||||||
|
"federal_agency",
|
||||||
|
"organization_name",
|
||||||
|
"custom_election_board",
|
||||||
|
"city",
|
||||||
|
"state_territory",
|
||||||
"created_at",
|
"created_at",
|
||||||
"submitter",
|
"submitter",
|
||||||
"investigator",
|
"investigator",
|
||||||
|
@ -863,8 +953,21 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
("investigator", ["first_name", "last_name"]),
|
("investigator", ["first_name", "last_name"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def custom_election_board(self, obj):
|
||||||
|
return "Yes" if obj.is_election_board else "No"
|
||||||
|
|
||||||
|
custom_election_board.admin_order_field = "is_election_board" # type: ignore
|
||||||
|
custom_election_board.short_description = "Election office" # type: ignore
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
list_filter = ("status", "organization_type", InvestigatorFilter)
|
list_filter = (
|
||||||
|
"status",
|
||||||
|
"organization_type",
|
||||||
|
"federal_type",
|
||||||
|
ElectionOfficeFilter,
|
||||||
|
"rejection_reason",
|
||||||
|
InvestigatorFilter,
|
||||||
|
)
|
||||||
|
|
||||||
# Search
|
# Search
|
||||||
search_fields = [
|
search_fields = [
|
||||||
|
@ -875,10 +978,8 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
]
|
]
|
||||||
search_help_text = "Search by domain or submitter."
|
search_help_text = "Search by domain or submitter."
|
||||||
|
|
||||||
# Detail view
|
|
||||||
form = DomainApplicationAdminForm
|
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(None, {"fields": ["status", "investigator", "creator", "approved_domain", "notes"]}),
|
(None, {"fields": ["status", "rejection_reason", "investigator", "creator", "approved_domain", "notes"]}),
|
||||||
(
|
(
|
||||||
"Type of organization",
|
"Type of organization",
|
||||||
{
|
{
|
||||||
|
@ -979,6 +1080,23 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
"This action is not permitted. The domain is already active.",
|
"This action is not permitted. The domain is already active.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif (
|
||||||
|
obj
|
||||||
|
and obj.status == models.DomainApplication.ApplicationStatus.REJECTED
|
||||||
|
and not obj.rejection_reason
|
||||||
|
):
|
||||||
|
# This condition should never be triggered.
|
||||||
|
# The opposite of this condition is acceptable (rejected -> other status and rejection_reason)
|
||||||
|
# because we clean up the rejection reason in the transition in the model.
|
||||||
|
|
||||||
|
# Clear the success message
|
||||||
|
messages.set_level(request, messages.ERROR)
|
||||||
|
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"A rejection reason is required.",
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if obj.status != original_obj.status:
|
if obj.status != original_obj.status:
|
||||||
status_method_mapping = {
|
status_method_mapping = {
|
||||||
|
@ -1072,6 +1190,8 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
classes conflict, so we'll just pull what we need
|
classes conflict, so we'll just pull what we need
|
||||||
from DomainInformationAdmin"""
|
from DomainInformationAdmin"""
|
||||||
|
|
||||||
|
form = DomainInformationInlineForm
|
||||||
|
|
||||||
model = models.DomainInformation
|
model = models.DomainInformation
|
||||||
|
|
||||||
fieldsets = DomainInformationAdmin.fieldsets
|
fieldsets = DomainInformationAdmin.fieldsets
|
||||||
|
@ -1080,6 +1200,14 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
# to activate the edit/delete/view buttons
|
# to activate the edit/delete/view buttons
|
||||||
filter_horizontal = ("other_contacts",)
|
filter_horizontal = ("other_contacts",)
|
||||||
|
|
||||||
|
autocomplete_fields = [
|
||||||
|
"creator",
|
||||||
|
"domain_application",
|
||||||
|
"authorizing_official",
|
||||||
|
"domain",
|
||||||
|
"submitter",
|
||||||
|
]
|
||||||
|
|
||||||
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||||
"""customize the behavior of formfields with manytomany relationships. the customized
|
"""customize the behavior of formfields with manytomany relationships. the customized
|
||||||
behavior includes sorting of objects in lists as well as customizing helper text"""
|
behavior includes sorting of objects in lists as well as customizing helper text"""
|
||||||
|
@ -1109,12 +1237,37 @@ class DomainInformationInline(admin.StackedInline):
|
||||||
class DomainAdmin(ListHeaderAdmin):
|
class DomainAdmin(ListHeaderAdmin):
|
||||||
"""Custom domain admin class to add extra buttons."""
|
"""Custom domain admin class to add extra buttons."""
|
||||||
|
|
||||||
|
class ElectionOfficeFilter(admin.SimpleListFilter):
|
||||||
|
"""Define a custom filter for is_election_board"""
|
||||||
|
|
||||||
|
title = _("election office")
|
||||||
|
parameter_name = "is_election_board"
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
return (
|
||||||
|
("1", _("Yes")),
|
||||||
|
("0", _("No")),
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
logger.debug(self.value())
|
||||||
|
if self.value() == "1":
|
||||||
|
return queryset.filter(domain_info__is_election_board=True)
|
||||||
|
if self.value() == "0":
|
||||||
|
return queryset.filter(Q(domain_info__is_election_board=False) | Q(domain_info__is_election_board=None))
|
||||||
|
|
||||||
inlines = [DomainInformationInline]
|
inlines = [DomainInformationInline]
|
||||||
|
|
||||||
# Columns
|
# Columns
|
||||||
list_display = [
|
list_display = [
|
||||||
"name",
|
"name",
|
||||||
"organization_type",
|
"organization_type",
|
||||||
|
"federal_type",
|
||||||
|
"federal_agency",
|
||||||
|
"organization_name",
|
||||||
|
"custom_election_board",
|
||||||
|
"city",
|
||||||
|
"state_territory",
|
||||||
"state",
|
"state",
|
||||||
"expiration_date",
|
"expiration_date",
|
||||||
"created_at",
|
"created_at",
|
||||||
|
@ -1138,8 +1291,42 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
organization_type.admin_order_field = "domain_info__organization_type" # type: ignore
|
organization_type.admin_order_field = "domain_info__organization_type" # type: ignore
|
||||||
|
|
||||||
|
def federal_agency(self, obj):
|
||||||
|
return obj.domain_info.federal_agency if obj.domain_info else None
|
||||||
|
|
||||||
|
federal_agency.admin_order_field = "domain_info__federal_agency" # type: ignore
|
||||||
|
|
||||||
|
def federal_type(self, obj):
|
||||||
|
return obj.domain_info.federal_type if obj.domain_info else None
|
||||||
|
|
||||||
|
federal_type.admin_order_field = "domain_info__federal_type" # type: ignore
|
||||||
|
|
||||||
|
def organization_name(self, obj):
|
||||||
|
return obj.domain_info.organization_name if obj.domain_info else None
|
||||||
|
|
||||||
|
organization_name.admin_order_field = "domain_info__organization_name" # type: ignore
|
||||||
|
|
||||||
|
def custom_election_board(self, obj):
|
||||||
|
domain_info = getattr(obj, "domain_info", None)
|
||||||
|
if domain_info:
|
||||||
|
return "Yes" if domain_info.is_election_board else "No"
|
||||||
|
return "No"
|
||||||
|
|
||||||
|
custom_election_board.admin_order_field = "domain_info__is_election_board" # type: ignore
|
||||||
|
custom_election_board.short_description = "Election office" # type: ignore
|
||||||
|
|
||||||
|
def city(self, obj):
|
||||||
|
return obj.domain_info.city if obj.domain_info else None
|
||||||
|
|
||||||
|
city.admin_order_field = "domain_info__city" # type: ignore
|
||||||
|
|
||||||
|
def state_territory(self, obj):
|
||||||
|
return obj.domain_info.state_territory if obj.domain_info else None
|
||||||
|
|
||||||
|
state_territory.admin_order_field = "domain_info__state_territory" # type: ignore
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
list_filter = ["domain_info__organization_type", "state"]
|
list_filter = ["domain_info__organization_type", "domain_info__federal_type", ElectionOfficeFilter, "state"]
|
||||||
|
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
search_help_text = "Search by domain name."
|
search_help_text = "Search by domain name."
|
||||||
|
@ -1159,7 +1346,14 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
if object_id is not None:
|
if object_id is not None:
|
||||||
domain = Domain.objects.get(pk=object_id)
|
domain = Domain.objects.get(pk=object_id)
|
||||||
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
|
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
|
||||||
|
|
||||||
|
try:
|
||||||
curr_exp_date = domain.registry_expiration_date
|
curr_exp_date = domain.registry_expiration_date
|
||||||
|
except KeyError:
|
||||||
|
# No expiration date was found. Return none.
|
||||||
|
extra_context["extended_expiration_date"] = None
|
||||||
|
return super().changeform_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
if curr_exp_date < date.today():
|
if curr_exp_date < date.today():
|
||||||
extra_context["extended_expiration_date"] = date.today() + relativedelta(years=years_to_extend_by)
|
extra_context["extended_expiration_date"] = date.today() + relativedelta(years=years_to_extend_by)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -162,7 +162,11 @@ function initializeWidgetOnToList(toList, toListId) {
|
||||||
'websites': '/admin/registrar/website/__fk__/change/?_to_field=id',
|
'websites': '/admin/registrar/website/__fk__/change/?_to_field=id',
|
||||||
'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id',
|
'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id',
|
||||||
},
|
},
|
||||||
false,
|
// NOTE: If we open view in the same window then use the back button
|
||||||
|
// to go back, the 'chosen' list will fail to initialize correctly in
|
||||||
|
// sandbozes (but will work fine on local). This is related to how the
|
||||||
|
// Django JS runs (SelectBox.js) and is probably due to a race condition.
|
||||||
|
true,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -339,3 +343,46 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk,
|
||||||
}
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request
|
||||||
|
* status select amd to show/hide the rejection reason
|
||||||
|
*/
|
||||||
|
(function (){
|
||||||
|
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
|
||||||
|
|
||||||
|
if (rejectionReasonFormGroup) {
|
||||||
|
let statusSelect = document.getElementById('id_status')
|
||||||
|
|
||||||
|
// Initial handling of rejectionReasonFormGroup display
|
||||||
|
if (statusSelect.value != 'rejected')
|
||||||
|
rejectionReasonFormGroup.style.display = 'none';
|
||||||
|
|
||||||
|
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||||
|
statusSelect.addEventListener('change', function() {
|
||||||
|
if (statusSelect.value == 'rejected') {
|
||||||
|
rejectionReasonFormGroup.style.display = 'block';
|
||||||
|
sessionStorage.removeItem('hideRejectionReason');
|
||||||
|
} else {
|
||||||
|
rejectionReasonFormGroup.style.display = 'none';
|
||||||
|
sessionStorage.setItem('hideRejectionReason', 'true');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage
|
||||||
|
|
||||||
|
// When you navigate using forward/back after changing status but not saving, when you land back on the DA page the
|
||||||
|
// status select will say (for example) Rejected but the selected option can be something else. To manage the show/hide
|
||||||
|
// accurately for this edge case, we use cache and test for the back/forward navigation.
|
||||||
|
const observer = new PerformanceObserver((list) => {
|
||||||
|
list.getEntries().forEach((entry) => {
|
||||||
|
if (entry.type === "back_forward") {
|
||||||
|
if (sessionStorage.getItem('hideRejectionReason'))
|
||||||
|
document.querySelector('.field-rejection_reason').style.display = 'none';
|
||||||
|
else
|
||||||
|
document.querySelector('.field-rejection_reason').style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe({ type: "navigation" });
|
||||||
|
})();
|
||||||
|
|
|
@ -289,6 +289,7 @@ AWS_MAX_ATTEMPTS = 3
|
||||||
BOTO_CONFIG = Config(retries={"mode": AWS_RETRY_MODE, "max_attempts": AWS_MAX_ATTEMPTS})
|
BOTO_CONFIG = Config(retries={"mode": AWS_RETRY_MODE, "max_attempts": AWS_MAX_ATTEMPTS})
|
||||||
|
|
||||||
# email address to use for various automated correspondence
|
# email address to use for various automated correspondence
|
||||||
|
# also used as a default to and bcc email
|
||||||
DEFAULT_FROM_EMAIL = "help@get.gov <help@get.gov>"
|
DEFAULT_FROM_EMAIL = "help@get.gov <help@get.gov>"
|
||||||
|
|
||||||
# connect to an (external) SMTP server for sending email
|
# connect to an (external) SMTP server for sending email
|
||||||
|
|
|
@ -284,6 +284,7 @@ class OrganizationContactForm(RegistrarForm):
|
||||||
message="Enter a zip code in the form of 12345 or 12345-6789.",
|
message="Enter a zip code in the form of 12345 or 12345-6789.",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
error_messages={"required": ("Enter a zip code in the form of 12345 or 12345-6789.")},
|
||||||
)
|
)
|
||||||
urbanization = forms.CharField(
|
urbanization = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 4.2.7 on 2024-02-26 22:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0069_alter_contact_email_alter_contact_first_name_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domainapplication",
|
||||||
|
name="rejection_reason",
|
||||||
|
field=models.TextField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("purpose_not_met", "Purpose requirements not met"),
|
||||||
|
("requestor_not_eligible", "Requestor not eligible to make request"),
|
||||||
|
("org_has_domain", "Org already has a .gov domain"),
|
||||||
|
("contacts_not_verified", "Org contacts couldn't be verified"),
|
||||||
|
("org_not_eligible", "Org not eligible for a .gov domain"),
|
||||||
|
("naming_not_met", "Naming requirements not met"),
|
||||||
|
("other", "Other/Unspecified"),
|
||||||
|
],
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,864 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-02-28 04:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import phonenumber_field.modelfields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0070_domainapplication_rejection_reason"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="contact",
|
||||||
|
name="first_name",
|
||||||
|
field=models.CharField(blank=True, db_index=True, null=True, verbose_name="first name / given name"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="contact",
|
||||||
|
name="last_name",
|
||||||
|
field=models.CharField(blank=True, db_index=True, null=True, verbose_name="last name / family name"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="contact",
|
||||||
|
name="middle_name",
|
||||||
|
field=models.CharField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="contact",
|
||||||
|
name="title",
|
||||||
|
field=models.CharField(blank=True, null=True, verbose_name="title or role in your organization"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainapplication",
|
||||||
|
name="address_line1",
|
||||||
|
field=models.CharField(blank=True, help_text="Street address", null=True, verbose_name="Address line 1"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainapplication",
|
||||||
|
name="address_line2",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, help_text="Street address line 2 (optional)", null=True, verbose_name="Address line 2"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainapplication",
|
||||||
|
name="city",
|
||||||
|
field=models.CharField(blank=True, help_text="City", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainapplication",
|
||||||
|
name="federal_agency",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
(
|
||||||
|
"Administrative Conference of the United States",
|
||||||
|
"Administrative Conference of the United States",
|
||||||
|
),
|
||||||
|
("Advisory Council on Historic Preservation", "Advisory Council on Historic Preservation"),
|
||||||
|
("American Battle Monuments Commission", "American Battle Monuments Commission"),
|
||||||
|
("AMTRAK", "AMTRAK"),
|
||||||
|
("Appalachian Regional Commission", "Appalachian Regional Commission"),
|
||||||
|
(
|
||||||
|
"Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
|
||||||
|
"Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
|
||||||
|
),
|
||||||
|
("Appraisal Subcommittee", "Appraisal Subcommittee"),
|
||||||
|
("Architect of the Capitol", "Architect of the Capitol"),
|
||||||
|
("Armed Forces Retirement Home", "Armed Forces Retirement Home"),
|
||||||
|
(
|
||||||
|
"Barry Goldwater Scholarship and Excellence in Education Foundation",
|
||||||
|
"Barry Goldwater Scholarship and Excellence in Education Foundation",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Barry Goldwater Scholarship and Excellence in Education Program",
|
||||||
|
"Barry Goldwater Scholarship and Excellence in Education Program",
|
||||||
|
),
|
||||||
|
("Central Intelligence Agency", "Central Intelligence Agency"),
|
||||||
|
("Chemical Safety Board", "Chemical Safety Board"),
|
||||||
|
("Christopher Columbus Fellowship Foundation", "Christopher Columbus Fellowship Foundation"),
|
||||||
|
("Civil Rights Cold Case Records Review Board", "Civil Rights Cold Case Records Review Board"),
|
||||||
|
(
|
||||||
|
"Commission for the Preservation of America's Heritage Abroad",
|
||||||
|
"Commission for the Preservation of America's Heritage Abroad",
|
||||||
|
),
|
||||||
|
("Commission of Fine Arts", "Commission of Fine Arts"),
|
||||||
|
(
|
||||||
|
"Committee for Purchase From People Who Are Blind or Severely Disabled",
|
||||||
|
"Committee for Purchase From People Who Are Blind or Severely Disabled",
|
||||||
|
),
|
||||||
|
("Commodity Futures Trading Commission", "Commodity Futures Trading Commission"),
|
||||||
|
("Congressional Budget Office", "Congressional Budget Office"),
|
||||||
|
("Consumer Financial Protection Bureau", "Consumer Financial Protection Bureau"),
|
||||||
|
("Consumer Product Safety Commission", "Consumer Product Safety Commission"),
|
||||||
|
("Corporation for National & Community Service", "Corporation for National & Community Service"),
|
||||||
|
(
|
||||||
|
"Corporation for National and Community Service",
|
||||||
|
"Corporation for National and Community Service",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Council of Inspectors General on Integrity and Efficiency",
|
||||||
|
"Council of Inspectors General on Integrity and Efficiency",
|
||||||
|
),
|
||||||
|
("Court Services and Offender Supervision", "Court Services and Offender Supervision"),
|
||||||
|
("Cyberspace Solarium Commission", "Cyberspace Solarium Commission"),
|
||||||
|
(
|
||||||
|
"DC Court Services and Offender Supervision Agency",
|
||||||
|
"DC Court Services and Offender Supervision Agency",
|
||||||
|
),
|
||||||
|
("DC Pre-trial Services", "DC Pre-trial Services"),
|
||||||
|
("Defense Nuclear Facilities Safety Board", "Defense Nuclear Facilities Safety Board"),
|
||||||
|
("Delta Regional Authority", "Delta Regional Authority"),
|
||||||
|
("Denali Commission", "Denali Commission"),
|
||||||
|
("Department of Agriculture", "Department of Agriculture"),
|
||||||
|
("Department of Commerce", "Department of Commerce"),
|
||||||
|
("Department of Defense", "Department of Defense"),
|
||||||
|
("Department of Education", "Department of Education"),
|
||||||
|
("Department of Energy", "Department of Energy"),
|
||||||
|
("Department of Health and Human Services", "Department of Health and Human Services"),
|
||||||
|
("Department of Homeland Security", "Department of Homeland Security"),
|
||||||
|
("Department of Housing and Urban Development", "Department of Housing and Urban Development"),
|
||||||
|
("Department of Justice", "Department of Justice"),
|
||||||
|
("Department of Labor", "Department of Labor"),
|
||||||
|
("Department of State", "Department of State"),
|
||||||
|
("Department of the Interior", "Department of the Interior"),
|
||||||
|
("Department of the Treasury", "Department of the Treasury"),
|
||||||
|
("Department of Transportation", "Department of Transportation"),
|
||||||
|
("Department of Veterans Affairs", "Department of Veterans Affairs"),
|
||||||
|
("Director of National Intelligence", "Director of National Intelligence"),
|
||||||
|
("Dwight D. Eisenhower Memorial Commission", "Dwight D. Eisenhower Memorial Commission"),
|
||||||
|
("Election Assistance Commission", "Election Assistance Commission"),
|
||||||
|
("Environmental Protection Agency", "Environmental Protection Agency"),
|
||||||
|
("Equal Employment Opportunity Commission", "Equal Employment Opportunity Commission"),
|
||||||
|
("Executive Office of the President", "Executive Office of the President"),
|
||||||
|
("Export-Import Bank of the United States", "Export-Import Bank of the United States"),
|
||||||
|
("Export/Import Bank of the U.S.", "Export/Import Bank of the U.S."),
|
||||||
|
("Farm Credit Administration", "Farm Credit Administration"),
|
||||||
|
("Farm Credit System Insurance Corporation", "Farm Credit System Insurance Corporation"),
|
||||||
|
("Federal Communications Commission", "Federal Communications Commission"),
|
||||||
|
("Federal Deposit Insurance Corporation", "Federal Deposit Insurance Corporation"),
|
||||||
|
("Federal Election Commission", "Federal Election Commission"),
|
||||||
|
("Federal Energy Regulatory Commission", "Federal Energy Regulatory Commission"),
|
||||||
|
(
|
||||||
|
"Federal Financial Institutions Examination Council",
|
||||||
|
"Federal Financial Institutions Examination Council",
|
||||||
|
),
|
||||||
|
("Federal Housing Finance Agency", "Federal Housing Finance Agency"),
|
||||||
|
("Federal Judiciary", "Federal Judiciary"),
|
||||||
|
("Federal Labor Relations Authority", "Federal Labor Relations Authority"),
|
||||||
|
("Federal Maritime Commission", "Federal Maritime Commission"),
|
||||||
|
("Federal Mediation and Conciliation Service", "Federal Mediation and Conciliation Service"),
|
||||||
|
(
|
||||||
|
"Federal Mine Safety and Health Review Commission",
|
||||||
|
"Federal Mine Safety and Health Review Commission",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Federal Permitting Improvement Steering Council",
|
||||||
|
"Federal Permitting Improvement Steering Council",
|
||||||
|
),
|
||||||
|
("Federal Reserve Board of Governors", "Federal Reserve Board of Governors"),
|
||||||
|
("Federal Reserve System", "Federal Reserve System"),
|
||||||
|
("Federal Trade Commission", "Federal Trade Commission"),
|
||||||
|
("General Services Administration", "General Services Administration"),
|
||||||
|
("gov Administration", "gov Administration"),
|
||||||
|
("Government Accountability Office", "Government Accountability Office"),
|
||||||
|
("Government Publishing Office", "Government Publishing Office"),
|
||||||
|
("Gulf Coast Ecosystem Restoration Council", "Gulf Coast Ecosystem Restoration Council"),
|
||||||
|
("Harry S Truman Scholarship Foundation", "Harry S Truman Scholarship Foundation"),
|
||||||
|
("Harry S. Truman Scholarship Foundation", "Harry S. Truman Scholarship Foundation"),
|
||||||
|
("Institute of Museum and Library Services", "Institute of Museum and Library Services"),
|
||||||
|
("Institute of Peace", "Institute of Peace"),
|
||||||
|
("Inter-American Foundation", "Inter-American Foundation"),
|
||||||
|
(
|
||||||
|
"International Boundary and Water Commission: United States and Mexico",
|
||||||
|
"International Boundary and Water Commission: United States and Mexico",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"International Boundary Commission: United States and Canada",
|
||||||
|
"International Boundary Commission: United States and Canada",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"International Joint Commission: United States and Canada",
|
||||||
|
"International Joint Commission: United States and Canada",
|
||||||
|
),
|
||||||
|
("James Madison Memorial Fellowship Foundation", "James Madison Memorial Fellowship Foundation"),
|
||||||
|
("Japan-United States Friendship Commission", "Japan-United States Friendship Commission"),
|
||||||
|
("Japan-US Friendship Commission", "Japan-US Friendship Commission"),
|
||||||
|
("John F. Kennedy Center for Performing Arts", "John F. Kennedy Center for Performing Arts"),
|
||||||
|
(
|
||||||
|
"John F. Kennedy Center for the Performing Arts",
|
||||||
|
"John F. Kennedy Center for the Performing Arts",
|
||||||
|
),
|
||||||
|
("Legal Services Corporation", "Legal Services Corporation"),
|
||||||
|
("Legislative Branch", "Legislative Branch"),
|
||||||
|
("Library of Congress", "Library of Congress"),
|
||||||
|
("Marine Mammal Commission", "Marine Mammal Commission"),
|
||||||
|
(
|
||||||
|
"Medicaid and CHIP Payment and Access Commission",
|
||||||
|
"Medicaid and CHIP Payment and Access Commission",
|
||||||
|
),
|
||||||
|
("Medical Payment Advisory Commission", "Medical Payment Advisory Commission"),
|
||||||
|
("Medicare Payment Advisory Commission", "Medicare Payment Advisory Commission"),
|
||||||
|
("Merit Systems Protection Board", "Merit Systems Protection Board"),
|
||||||
|
("Millennium Challenge Corporation", "Millennium Challenge Corporation"),
|
||||||
|
(
|
||||||
|
"Morris K. Udall and Stewart L. Udall Foundation",
|
||||||
|
"Morris K. Udall and Stewart L. Udall Foundation",
|
||||||
|
),
|
||||||
|
("National Aeronautics and Space Administration", "National Aeronautics and Space Administration"),
|
||||||
|
("National Archives and Records Administration", "National Archives and Records Administration"),
|
||||||
|
("National Capital Planning Commission", "National Capital Planning Commission"),
|
||||||
|
("National Council on Disability", "National Council on Disability"),
|
||||||
|
("National Credit Union Administration", "National Credit Union Administration"),
|
||||||
|
("National Endowment for the Arts", "National Endowment for the Arts"),
|
||||||
|
("National Endowment for the Humanities", "National Endowment for the Humanities"),
|
||||||
|
(
|
||||||
|
"National Foundation on the Arts and the Humanities",
|
||||||
|
"National Foundation on the Arts and the Humanities",
|
||||||
|
),
|
||||||
|
("National Gallery of Art", "National Gallery of Art"),
|
||||||
|
("National Indian Gaming Commission", "National Indian Gaming Commission"),
|
||||||
|
("National Labor Relations Board", "National Labor Relations Board"),
|
||||||
|
("National Mediation Board", "National Mediation Board"),
|
||||||
|
("National Science Foundation", "National Science Foundation"),
|
||||||
|
(
|
||||||
|
"National Security Commission on Artificial Intelligence",
|
||||||
|
"National Security Commission on Artificial Intelligence",
|
||||||
|
),
|
||||||
|
("National Transportation Safety Board", "National Transportation Safety Board"),
|
||||||
|
(
|
||||||
|
"Networking Information Technology Research and Development",
|
||||||
|
"Networking Information Technology Research and Development",
|
||||||
|
),
|
||||||
|
("Non-Federal Agency", "Non-Federal Agency"),
|
||||||
|
("Northern Border Regional Commission", "Northern Border Regional Commission"),
|
||||||
|
("Nuclear Regulatory Commission", "Nuclear Regulatory Commission"),
|
||||||
|
("Nuclear Safety Oversight Committee", "Nuclear Safety Oversight Committee"),
|
||||||
|
("Nuclear Waste Technical Review Board", "Nuclear Waste Technical Review Board"),
|
||||||
|
(
|
||||||
|
"Occupational Safety & Health Review Commission",
|
||||||
|
"Occupational Safety & Health Review Commission",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Occupational Safety and Health Review Commission",
|
||||||
|
"Occupational Safety and Health Review Commission",
|
||||||
|
),
|
||||||
|
("Office of Compliance", "Office of Compliance"),
|
||||||
|
("Office of Congressional Workplace Rights", "Office of Congressional Workplace Rights"),
|
||||||
|
("Office of Government Ethics", "Office of Government Ethics"),
|
||||||
|
("Office of Navajo and Hopi Indian Relocation", "Office of Navajo and Hopi Indian Relocation"),
|
||||||
|
("Office of Personnel Management", "Office of Personnel Management"),
|
||||||
|
("Open World Leadership Center", "Open World Leadership Center"),
|
||||||
|
("Overseas Private Investment Corporation", "Overseas Private Investment Corporation"),
|
||||||
|
("Peace Corps", "Peace Corps"),
|
||||||
|
("Pension Benefit Guaranty Corporation", "Pension Benefit Guaranty Corporation"),
|
||||||
|
("Postal Regulatory Commission", "Postal Regulatory Commission"),
|
||||||
|
("Presidio Trust", "Presidio Trust"),
|
||||||
|
("Privacy and Civil Liberties Oversight Board", "Privacy and Civil Liberties Oversight Board"),
|
||||||
|
("Public Buildings Reform Board", "Public Buildings Reform Board"),
|
||||||
|
(
|
||||||
|
"Public Defender Service for the District of Columbia",
|
||||||
|
"Public Defender Service for the District of Columbia",
|
||||||
|
),
|
||||||
|
("Railroad Retirement Board", "Railroad Retirement Board"),
|
||||||
|
("Securities and Exchange Commission", "Securities and Exchange Commission"),
|
||||||
|
("Selective Service System", "Selective Service System"),
|
||||||
|
("Small Business Administration", "Small Business Administration"),
|
||||||
|
("Smithsonian Institution", "Smithsonian Institution"),
|
||||||
|
("Social Security Administration", "Social Security Administration"),
|
||||||
|
("Social Security Advisory Board", "Social Security Advisory Board"),
|
||||||
|
("Southeast Crescent Regional Commission", "Southeast Crescent Regional Commission"),
|
||||||
|
("Southwest Border Regional Commission", "Southwest Border Regional Commission"),
|
||||||
|
("State Justice Institute", "State Justice Institute"),
|
||||||
|
("State, Local, and Tribal Government", "State, Local, and Tribal Government"),
|
||||||
|
("Stennis Center for Public Service", "Stennis Center for Public Service"),
|
||||||
|
("Surface Transportation Board", "Surface Transportation Board"),
|
||||||
|
("Tennessee Valley Authority", "Tennessee Valley Authority"),
|
||||||
|
("The Executive Office of the President", "The Executive Office of the President"),
|
||||||
|
("The Intelligence Community", "The Intelligence Community"),
|
||||||
|
("The Legislative Branch", "The Legislative Branch"),
|
||||||
|
("The Supreme Court", "The Supreme Court"),
|
||||||
|
(
|
||||||
|
"The United States World War One Centennial Commission",
|
||||||
|
"The United States World War One Centennial Commission",
|
||||||
|
),
|
||||||
|
("U.S. Access Board", "U.S. Access Board"),
|
||||||
|
("U.S. Agency for Global Media", "U.S. Agency for Global Media"),
|
||||||
|
("U.S. Agency for International Development", "U.S. Agency for International Development"),
|
||||||
|
("U.S. Capitol Police", "U.S. Capitol Police"),
|
||||||
|
("U.S. Chemical Safety Board", "U.S. Chemical Safety Board"),
|
||||||
|
(
|
||||||
|
"U.S. China Economic and Security Review Commission",
|
||||||
|
"U.S. China Economic and Security Review Commission",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"U.S. Commission for the Preservation of Americas Heritage Abroad",
|
||||||
|
"U.S. Commission for the Preservation of Americas Heritage Abroad",
|
||||||
|
),
|
||||||
|
("U.S. Commission of Fine Arts", "U.S. Commission of Fine Arts"),
|
||||||
|
("U.S. Commission on Civil Rights", "U.S. Commission on Civil Rights"),
|
||||||
|
(
|
||||||
|
"U.S. Commission on International Religious Freedom",
|
||||||
|
"U.S. Commission on International Religious Freedom",
|
||||||
|
),
|
||||||
|
("U.S. Courts", "U.S. Courts"),
|
||||||
|
("U.S. Department of Agriculture", "U.S. Department of Agriculture"),
|
||||||
|
("U.S. Interagency Council on Homelessness", "U.S. Interagency Council on Homelessness"),
|
||||||
|
("U.S. International Trade Commission", "U.S. International Trade Commission"),
|
||||||
|
("U.S. Nuclear Waste Technical Review Board", "U.S. Nuclear Waste Technical Review Board"),
|
||||||
|
("U.S. Office of Special Counsel", "U.S. Office of Special Counsel"),
|
||||||
|
("U.S. Peace Corps", "U.S. Peace Corps"),
|
||||||
|
("U.S. Postal Service", "U.S. Postal Service"),
|
||||||
|
("U.S. Semiquincentennial Commission", "U.S. Semiquincentennial Commission"),
|
||||||
|
("U.S. Trade and Development Agency", "U.S. Trade and Development Agency"),
|
||||||
|
(
|
||||||
|
"U.S.-China Economic and Security Review Commission",
|
||||||
|
"U.S.-China Economic and Security Review Commission",
|
||||||
|
),
|
||||||
|
("Udall Foundation", "Udall Foundation"),
|
||||||
|
("United States AbilityOne", "United States AbilityOne"),
|
||||||
|
("United States Access Board", "United States Access Board"),
|
||||||
|
("United States African Development Foundation", "United States African Development Foundation"),
|
||||||
|
("United States Agency for Global Media", "United States Agency for Global Media"),
|
||||||
|
("United States Arctic Research Commission", "United States Arctic Research Commission"),
|
||||||
|
("United States Global Change Research Program", "United States Global Change Research Program"),
|
||||||
|
("United States Holocaust Memorial Museum", "United States Holocaust Memorial Museum"),
|
||||||
|
("United States Institute of Peace", "United States Institute of Peace"),
|
||||||
|
(
|
||||||
|
"United States Interagency Council on Homelessness",
|
||||||
|
"United States Interagency Council on Homelessness",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"United States International Development Finance Corporation",
|
||||||
|
"United States International Development Finance Corporation",
|
||||||
|
),
|
||||||
|
("United States International Trade Commission", "United States International Trade Commission"),
|
||||||
|
("United States Postal Service", "United States Postal Service"),
|
||||||
|
("United States Senate", "United States Senate"),
|
||||||
|
("United States Trade and Development Agency", "United States Trade and Development Agency"),
|
||||||
|
(
|
||||||
|
"Utah Reclamation Mitigation and Conservation Commission",
|
||||||
|
"Utah Reclamation Mitigation and Conservation Commission",
|
||||||
|
),
|
||||||
|
("Vietnam Education Foundation", "Vietnam Education Foundation"),
|
||||||
|
("Western Hemisphere Drug Policy Commission", "Western Hemisphere Drug Policy Commission"),
|
||||||
|
(
|
||||||
|
"Woodrow Wilson International Center for Scholars",
|
||||||
|
"Woodrow Wilson International Center for Scholars",
|
||||||
|
),
|
||||||
|
("World War I Centennial Commission", "World War I Centennial Commission"),
|
||||||
|
],
|
||||||
|
help_text="Federal agency",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainapplication",
|
||||||
|
name="organization_name",
|
||||||
|
field=models.CharField(blank=True, db_index=True, help_text="Organization name", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainapplication",
|
||||||
|
name="tribe_name",
|
||||||
|
field=models.CharField(blank=True, help_text="Name of tribe", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainapplication",
|
||||||
|
name="urbanization",
|
||||||
|
field=models.CharField(blank=True, help_text="Urbanization (required for Puerto Rico only)", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="address_line1",
|
||||||
|
field=models.CharField(blank=True, help_text="Street address", null=True, verbose_name="Street address"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="address_line2",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Street address line 2 (optional)",
|
||||||
|
null=True,
|
||||||
|
verbose_name="Street address line 2 (optional)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="city",
|
||||||
|
field=models.CharField(blank=True, help_text="City", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="federal_agency",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
(
|
||||||
|
"Administrative Conference of the United States",
|
||||||
|
"Administrative Conference of the United States",
|
||||||
|
),
|
||||||
|
("Advisory Council on Historic Preservation", "Advisory Council on Historic Preservation"),
|
||||||
|
("American Battle Monuments Commission", "American Battle Monuments Commission"),
|
||||||
|
("AMTRAK", "AMTRAK"),
|
||||||
|
("Appalachian Regional Commission", "Appalachian Regional Commission"),
|
||||||
|
(
|
||||||
|
"Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
|
||||||
|
"Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
|
||||||
|
),
|
||||||
|
("Appraisal Subcommittee", "Appraisal Subcommittee"),
|
||||||
|
("Architect of the Capitol", "Architect of the Capitol"),
|
||||||
|
("Armed Forces Retirement Home", "Armed Forces Retirement Home"),
|
||||||
|
(
|
||||||
|
"Barry Goldwater Scholarship and Excellence in Education Foundation",
|
||||||
|
"Barry Goldwater Scholarship and Excellence in Education Foundation",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Barry Goldwater Scholarship and Excellence in Education Program",
|
||||||
|
"Barry Goldwater Scholarship and Excellence in Education Program",
|
||||||
|
),
|
||||||
|
("Central Intelligence Agency", "Central Intelligence Agency"),
|
||||||
|
("Chemical Safety Board", "Chemical Safety Board"),
|
||||||
|
("Christopher Columbus Fellowship Foundation", "Christopher Columbus Fellowship Foundation"),
|
||||||
|
("Civil Rights Cold Case Records Review Board", "Civil Rights Cold Case Records Review Board"),
|
||||||
|
(
|
||||||
|
"Commission for the Preservation of America's Heritage Abroad",
|
||||||
|
"Commission for the Preservation of America's Heritage Abroad",
|
||||||
|
),
|
||||||
|
("Commission of Fine Arts", "Commission of Fine Arts"),
|
||||||
|
(
|
||||||
|
"Committee for Purchase From People Who Are Blind or Severely Disabled",
|
||||||
|
"Committee for Purchase From People Who Are Blind or Severely Disabled",
|
||||||
|
),
|
||||||
|
("Commodity Futures Trading Commission", "Commodity Futures Trading Commission"),
|
||||||
|
("Congressional Budget Office", "Congressional Budget Office"),
|
||||||
|
("Consumer Financial Protection Bureau", "Consumer Financial Protection Bureau"),
|
||||||
|
("Consumer Product Safety Commission", "Consumer Product Safety Commission"),
|
||||||
|
("Corporation for National & Community Service", "Corporation for National & Community Service"),
|
||||||
|
(
|
||||||
|
"Corporation for National and Community Service",
|
||||||
|
"Corporation for National and Community Service",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Council of Inspectors General on Integrity and Efficiency",
|
||||||
|
"Council of Inspectors General on Integrity and Efficiency",
|
||||||
|
),
|
||||||
|
("Court Services and Offender Supervision", "Court Services and Offender Supervision"),
|
||||||
|
("Cyberspace Solarium Commission", "Cyberspace Solarium Commission"),
|
||||||
|
(
|
||||||
|
"DC Court Services and Offender Supervision Agency",
|
||||||
|
"DC Court Services and Offender Supervision Agency",
|
||||||
|
),
|
||||||
|
("DC Pre-trial Services", "DC Pre-trial Services"),
|
||||||
|
("Defense Nuclear Facilities Safety Board", "Defense Nuclear Facilities Safety Board"),
|
||||||
|
("Delta Regional Authority", "Delta Regional Authority"),
|
||||||
|
("Denali Commission", "Denali Commission"),
|
||||||
|
("Department of Agriculture", "Department of Agriculture"),
|
||||||
|
("Department of Commerce", "Department of Commerce"),
|
||||||
|
("Department of Defense", "Department of Defense"),
|
||||||
|
("Department of Education", "Department of Education"),
|
||||||
|
("Department of Energy", "Department of Energy"),
|
||||||
|
("Department of Health and Human Services", "Department of Health and Human Services"),
|
||||||
|
("Department of Homeland Security", "Department of Homeland Security"),
|
||||||
|
("Department of Housing and Urban Development", "Department of Housing and Urban Development"),
|
||||||
|
("Department of Justice", "Department of Justice"),
|
||||||
|
("Department of Labor", "Department of Labor"),
|
||||||
|
("Department of State", "Department of State"),
|
||||||
|
("Department of the Interior", "Department of the Interior"),
|
||||||
|
("Department of the Treasury", "Department of the Treasury"),
|
||||||
|
("Department of Transportation", "Department of Transportation"),
|
||||||
|
("Department of Veterans Affairs", "Department of Veterans Affairs"),
|
||||||
|
("Director of National Intelligence", "Director of National Intelligence"),
|
||||||
|
("Dwight D. Eisenhower Memorial Commission", "Dwight D. Eisenhower Memorial Commission"),
|
||||||
|
("Election Assistance Commission", "Election Assistance Commission"),
|
||||||
|
("Environmental Protection Agency", "Environmental Protection Agency"),
|
||||||
|
("Equal Employment Opportunity Commission", "Equal Employment Opportunity Commission"),
|
||||||
|
("Executive Office of the President", "Executive Office of the President"),
|
||||||
|
("Export-Import Bank of the United States", "Export-Import Bank of the United States"),
|
||||||
|
("Export/Import Bank of the U.S.", "Export/Import Bank of the U.S."),
|
||||||
|
("Farm Credit Administration", "Farm Credit Administration"),
|
||||||
|
("Farm Credit System Insurance Corporation", "Farm Credit System Insurance Corporation"),
|
||||||
|
("Federal Communications Commission", "Federal Communications Commission"),
|
||||||
|
("Federal Deposit Insurance Corporation", "Federal Deposit Insurance Corporation"),
|
||||||
|
("Federal Election Commission", "Federal Election Commission"),
|
||||||
|
("Federal Energy Regulatory Commission", "Federal Energy Regulatory Commission"),
|
||||||
|
(
|
||||||
|
"Federal Financial Institutions Examination Council",
|
||||||
|
"Federal Financial Institutions Examination Council",
|
||||||
|
),
|
||||||
|
("Federal Housing Finance Agency", "Federal Housing Finance Agency"),
|
||||||
|
("Federal Judiciary", "Federal Judiciary"),
|
||||||
|
("Federal Labor Relations Authority", "Federal Labor Relations Authority"),
|
||||||
|
("Federal Maritime Commission", "Federal Maritime Commission"),
|
||||||
|
("Federal Mediation and Conciliation Service", "Federal Mediation and Conciliation Service"),
|
||||||
|
(
|
||||||
|
"Federal Mine Safety and Health Review Commission",
|
||||||
|
"Federal Mine Safety and Health Review Commission",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Federal Permitting Improvement Steering Council",
|
||||||
|
"Federal Permitting Improvement Steering Council",
|
||||||
|
),
|
||||||
|
("Federal Reserve Board of Governors", "Federal Reserve Board of Governors"),
|
||||||
|
("Federal Reserve System", "Federal Reserve System"),
|
||||||
|
("Federal Trade Commission", "Federal Trade Commission"),
|
||||||
|
("General Services Administration", "General Services Administration"),
|
||||||
|
("gov Administration", "gov Administration"),
|
||||||
|
("Government Accountability Office", "Government Accountability Office"),
|
||||||
|
("Government Publishing Office", "Government Publishing Office"),
|
||||||
|
("Gulf Coast Ecosystem Restoration Council", "Gulf Coast Ecosystem Restoration Council"),
|
||||||
|
("Harry S Truman Scholarship Foundation", "Harry S Truman Scholarship Foundation"),
|
||||||
|
("Harry S. Truman Scholarship Foundation", "Harry S. Truman Scholarship Foundation"),
|
||||||
|
("Institute of Museum and Library Services", "Institute of Museum and Library Services"),
|
||||||
|
("Institute of Peace", "Institute of Peace"),
|
||||||
|
("Inter-American Foundation", "Inter-American Foundation"),
|
||||||
|
(
|
||||||
|
"International Boundary and Water Commission: United States and Mexico",
|
||||||
|
"International Boundary and Water Commission: United States and Mexico",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"International Boundary Commission: United States and Canada",
|
||||||
|
"International Boundary Commission: United States and Canada",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"International Joint Commission: United States and Canada",
|
||||||
|
"International Joint Commission: United States and Canada",
|
||||||
|
),
|
||||||
|
("James Madison Memorial Fellowship Foundation", "James Madison Memorial Fellowship Foundation"),
|
||||||
|
("Japan-United States Friendship Commission", "Japan-United States Friendship Commission"),
|
||||||
|
("Japan-US Friendship Commission", "Japan-US Friendship Commission"),
|
||||||
|
("John F. Kennedy Center for Performing Arts", "John F. Kennedy Center for Performing Arts"),
|
||||||
|
(
|
||||||
|
"John F. Kennedy Center for the Performing Arts",
|
||||||
|
"John F. Kennedy Center for the Performing Arts",
|
||||||
|
),
|
||||||
|
("Legal Services Corporation", "Legal Services Corporation"),
|
||||||
|
("Legislative Branch", "Legislative Branch"),
|
||||||
|
("Library of Congress", "Library of Congress"),
|
||||||
|
("Marine Mammal Commission", "Marine Mammal Commission"),
|
||||||
|
(
|
||||||
|
"Medicaid and CHIP Payment and Access Commission",
|
||||||
|
"Medicaid and CHIP Payment and Access Commission",
|
||||||
|
),
|
||||||
|
("Medical Payment Advisory Commission", "Medical Payment Advisory Commission"),
|
||||||
|
("Medicare Payment Advisory Commission", "Medicare Payment Advisory Commission"),
|
||||||
|
("Merit Systems Protection Board", "Merit Systems Protection Board"),
|
||||||
|
("Millennium Challenge Corporation", "Millennium Challenge Corporation"),
|
||||||
|
(
|
||||||
|
"Morris K. Udall and Stewart L. Udall Foundation",
|
||||||
|
"Morris K. Udall and Stewart L. Udall Foundation",
|
||||||
|
),
|
||||||
|
("National Aeronautics and Space Administration", "National Aeronautics and Space Administration"),
|
||||||
|
("National Archives and Records Administration", "National Archives and Records Administration"),
|
||||||
|
("National Capital Planning Commission", "National Capital Planning Commission"),
|
||||||
|
("National Council on Disability", "National Council on Disability"),
|
||||||
|
("National Credit Union Administration", "National Credit Union Administration"),
|
||||||
|
("National Endowment for the Arts", "National Endowment for the Arts"),
|
||||||
|
("National Endowment for the Humanities", "National Endowment for the Humanities"),
|
||||||
|
(
|
||||||
|
"National Foundation on the Arts and the Humanities",
|
||||||
|
"National Foundation on the Arts and the Humanities",
|
||||||
|
),
|
||||||
|
("National Gallery of Art", "National Gallery of Art"),
|
||||||
|
("National Indian Gaming Commission", "National Indian Gaming Commission"),
|
||||||
|
("National Labor Relations Board", "National Labor Relations Board"),
|
||||||
|
("National Mediation Board", "National Mediation Board"),
|
||||||
|
("National Science Foundation", "National Science Foundation"),
|
||||||
|
(
|
||||||
|
"National Security Commission on Artificial Intelligence",
|
||||||
|
"National Security Commission on Artificial Intelligence",
|
||||||
|
),
|
||||||
|
("National Transportation Safety Board", "National Transportation Safety Board"),
|
||||||
|
(
|
||||||
|
"Networking Information Technology Research and Development",
|
||||||
|
"Networking Information Technology Research and Development",
|
||||||
|
),
|
||||||
|
("Non-Federal Agency", "Non-Federal Agency"),
|
||||||
|
("Northern Border Regional Commission", "Northern Border Regional Commission"),
|
||||||
|
("Nuclear Regulatory Commission", "Nuclear Regulatory Commission"),
|
||||||
|
("Nuclear Safety Oversight Committee", "Nuclear Safety Oversight Committee"),
|
||||||
|
("Nuclear Waste Technical Review Board", "Nuclear Waste Technical Review Board"),
|
||||||
|
(
|
||||||
|
"Occupational Safety & Health Review Commission",
|
||||||
|
"Occupational Safety & Health Review Commission",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Occupational Safety and Health Review Commission",
|
||||||
|
"Occupational Safety and Health Review Commission",
|
||||||
|
),
|
||||||
|
("Office of Compliance", "Office of Compliance"),
|
||||||
|
("Office of Congressional Workplace Rights", "Office of Congressional Workplace Rights"),
|
||||||
|
("Office of Government Ethics", "Office of Government Ethics"),
|
||||||
|
("Office of Navajo and Hopi Indian Relocation", "Office of Navajo and Hopi Indian Relocation"),
|
||||||
|
("Office of Personnel Management", "Office of Personnel Management"),
|
||||||
|
("Open World Leadership Center", "Open World Leadership Center"),
|
||||||
|
("Overseas Private Investment Corporation", "Overseas Private Investment Corporation"),
|
||||||
|
("Peace Corps", "Peace Corps"),
|
||||||
|
("Pension Benefit Guaranty Corporation", "Pension Benefit Guaranty Corporation"),
|
||||||
|
("Postal Regulatory Commission", "Postal Regulatory Commission"),
|
||||||
|
("Presidio Trust", "Presidio Trust"),
|
||||||
|
("Privacy and Civil Liberties Oversight Board", "Privacy and Civil Liberties Oversight Board"),
|
||||||
|
("Public Buildings Reform Board", "Public Buildings Reform Board"),
|
||||||
|
(
|
||||||
|
"Public Defender Service for the District of Columbia",
|
||||||
|
"Public Defender Service for the District of Columbia",
|
||||||
|
),
|
||||||
|
("Railroad Retirement Board", "Railroad Retirement Board"),
|
||||||
|
("Securities and Exchange Commission", "Securities and Exchange Commission"),
|
||||||
|
("Selective Service System", "Selective Service System"),
|
||||||
|
("Small Business Administration", "Small Business Administration"),
|
||||||
|
("Smithsonian Institution", "Smithsonian Institution"),
|
||||||
|
("Social Security Administration", "Social Security Administration"),
|
||||||
|
("Social Security Advisory Board", "Social Security Advisory Board"),
|
||||||
|
("Southeast Crescent Regional Commission", "Southeast Crescent Regional Commission"),
|
||||||
|
("Southwest Border Regional Commission", "Southwest Border Regional Commission"),
|
||||||
|
("State Justice Institute", "State Justice Institute"),
|
||||||
|
("State, Local, and Tribal Government", "State, Local, and Tribal Government"),
|
||||||
|
("Stennis Center for Public Service", "Stennis Center for Public Service"),
|
||||||
|
("Surface Transportation Board", "Surface Transportation Board"),
|
||||||
|
("Tennessee Valley Authority", "Tennessee Valley Authority"),
|
||||||
|
("The Executive Office of the President", "The Executive Office of the President"),
|
||||||
|
("The Intelligence Community", "The Intelligence Community"),
|
||||||
|
("The Legislative Branch", "The Legislative Branch"),
|
||||||
|
("The Supreme Court", "The Supreme Court"),
|
||||||
|
(
|
||||||
|
"The United States World War One Centennial Commission",
|
||||||
|
"The United States World War One Centennial Commission",
|
||||||
|
),
|
||||||
|
("U.S. Access Board", "U.S. Access Board"),
|
||||||
|
("U.S. Agency for Global Media", "U.S. Agency for Global Media"),
|
||||||
|
("U.S. Agency for International Development", "U.S. Agency for International Development"),
|
||||||
|
("U.S. Capitol Police", "U.S. Capitol Police"),
|
||||||
|
("U.S. Chemical Safety Board", "U.S. Chemical Safety Board"),
|
||||||
|
(
|
||||||
|
"U.S. China Economic and Security Review Commission",
|
||||||
|
"U.S. China Economic and Security Review Commission",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"U.S. Commission for the Preservation of Americas Heritage Abroad",
|
||||||
|
"U.S. Commission for the Preservation of Americas Heritage Abroad",
|
||||||
|
),
|
||||||
|
("U.S. Commission of Fine Arts", "U.S. Commission of Fine Arts"),
|
||||||
|
("U.S. Commission on Civil Rights", "U.S. Commission on Civil Rights"),
|
||||||
|
(
|
||||||
|
"U.S. Commission on International Religious Freedom",
|
||||||
|
"U.S. Commission on International Religious Freedom",
|
||||||
|
),
|
||||||
|
("U.S. Courts", "U.S. Courts"),
|
||||||
|
("U.S. Department of Agriculture", "U.S. Department of Agriculture"),
|
||||||
|
("U.S. Interagency Council on Homelessness", "U.S. Interagency Council on Homelessness"),
|
||||||
|
("U.S. International Trade Commission", "U.S. International Trade Commission"),
|
||||||
|
("U.S. Nuclear Waste Technical Review Board", "U.S. Nuclear Waste Technical Review Board"),
|
||||||
|
("U.S. Office of Special Counsel", "U.S. Office of Special Counsel"),
|
||||||
|
("U.S. Peace Corps", "U.S. Peace Corps"),
|
||||||
|
("U.S. Postal Service", "U.S. Postal Service"),
|
||||||
|
("U.S. Semiquincentennial Commission", "U.S. Semiquincentennial Commission"),
|
||||||
|
("U.S. Trade and Development Agency", "U.S. Trade and Development Agency"),
|
||||||
|
(
|
||||||
|
"U.S.-China Economic and Security Review Commission",
|
||||||
|
"U.S.-China Economic and Security Review Commission",
|
||||||
|
),
|
||||||
|
("Udall Foundation", "Udall Foundation"),
|
||||||
|
("United States AbilityOne", "United States AbilityOne"),
|
||||||
|
("United States Access Board", "United States Access Board"),
|
||||||
|
("United States African Development Foundation", "United States African Development Foundation"),
|
||||||
|
("United States Agency for Global Media", "United States Agency for Global Media"),
|
||||||
|
("United States Arctic Research Commission", "United States Arctic Research Commission"),
|
||||||
|
("United States Global Change Research Program", "United States Global Change Research Program"),
|
||||||
|
("United States Holocaust Memorial Museum", "United States Holocaust Memorial Museum"),
|
||||||
|
("United States Institute of Peace", "United States Institute of Peace"),
|
||||||
|
(
|
||||||
|
"United States Interagency Council on Homelessness",
|
||||||
|
"United States Interagency Council on Homelessness",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"United States International Development Finance Corporation",
|
||||||
|
"United States International Development Finance Corporation",
|
||||||
|
),
|
||||||
|
("United States International Trade Commission", "United States International Trade Commission"),
|
||||||
|
("United States Postal Service", "United States Postal Service"),
|
||||||
|
("United States Senate", "United States Senate"),
|
||||||
|
("United States Trade and Development Agency", "United States Trade and Development Agency"),
|
||||||
|
(
|
||||||
|
"Utah Reclamation Mitigation and Conservation Commission",
|
||||||
|
"Utah Reclamation Mitigation and Conservation Commission",
|
||||||
|
),
|
||||||
|
("Vietnam Education Foundation", "Vietnam Education Foundation"),
|
||||||
|
("Western Hemisphere Drug Policy Commission", "Western Hemisphere Drug Policy Commission"),
|
||||||
|
(
|
||||||
|
"Woodrow Wilson International Center for Scholars",
|
||||||
|
"Woodrow Wilson International Center for Scholars",
|
||||||
|
),
|
||||||
|
("World War I Centennial Commission", "World War I Centennial Commission"),
|
||||||
|
],
|
||||||
|
help_text="Federal agency",
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="organization_name",
|
||||||
|
field=models.CharField(blank=True, db_index=True, help_text="Organization name", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="tribe_name",
|
||||||
|
field=models.CharField(blank=True, help_text="Name of tribe", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="urbanization",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Urbanization (required for Puerto Rico only)",
|
||||||
|
null=True,
|
||||||
|
verbose_name="Urbanization (required for Puerto Rico only)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="publiccontact",
|
||||||
|
name="cc",
|
||||||
|
field=models.CharField(help_text="Contact's country code"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="publiccontact",
|
||||||
|
name="city",
|
||||||
|
field=models.CharField(help_text="Contact's city"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="publiccontact",
|
||||||
|
name="email",
|
||||||
|
field=models.EmailField(help_text="Contact's email address", max_length=254),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="publiccontact",
|
||||||
|
name="fax",
|
||||||
|
field=phonenumber_field.modelfields.PhoneNumberField(
|
||||||
|
help_text="Contact's fax number (null ok). Must be in ITU.E164.2005 format.",
|
||||||
|
max_length=128,
|
||||||
|
null=True,
|
||||||
|
region=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="publiccontact",
|
||||||
|
name="name",
|
||||||
|
field=models.CharField(help_text="Contact's full name"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="publiccontact",
|
||||||
|
name="org",
|
||||||
|
field=models.CharField(help_text="Contact's organization (null ok)", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="publiccontact",
|
||||||
|
name="pc",
|
||||||
|
field=models.CharField(help_text="Contact's postal code"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="publiccontact",
|
||||||
|
name="pw",
|
||||||
|
field=models.CharField(help_text="Contact's authorization code. 16 characters minimum."),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="publiccontact",
|
||||||
|
name="sp",
|
||||||
|
field=models.CharField(help_text="Contact's state or province"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="publiccontact",
|
||||||
|
name="street1",
|
||||||
|
field=models.CharField(help_text="Contact's street"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="publiccontact",
|
||||||
|
name="street2",
|
||||||
|
field=models.CharField(help_text="Contact's street (null ok)", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="publiccontact",
|
||||||
|
name="street3",
|
||||||
|
field=models.CharField(help_text="Contact's street (null ok)", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="publiccontact",
|
||||||
|
name="voice",
|
||||||
|
field=phonenumber_field.modelfields.PhoneNumberField(
|
||||||
|
help_text="Contact's phone number. Must be in ITU.E164.2005 format", max_length=128, region=None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="address_line",
|
||||||
|
field=models.CharField(blank=True, help_text="Street address", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="city",
|
||||||
|
field=models.CharField(blank=True, help_text="City", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="domain_name",
|
||||||
|
field=models.CharField(blank=True, null=True, verbose_name="Domain name"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="email",
|
||||||
|
field=models.EmailField(blank=True, help_text="Email", max_length=254, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="federal_agency",
|
||||||
|
field=models.CharField(blank=True, help_text="Federal agency", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="federal_type",
|
||||||
|
field=models.CharField(blank=True, help_text="Federal government branch", max_length=50, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="first_name",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, db_index=True, help_text="First name", null=True, verbose_name="first name / given name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="last_name",
|
||||||
|
field=models.CharField(blank=True, help_text="Last name", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="middle_name",
|
||||||
|
field=models.CharField(blank=True, help_text="Middle name (optional)", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="organization_name",
|
||||||
|
field=models.CharField(blank=True, db_index=True, help_text="Organization name", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="organization_type",
|
||||||
|
field=models.CharField(blank=True, help_text="Type of organization", max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="phone",
|
||||||
|
field=models.CharField(blank=True, help_text="Phone", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="title",
|
||||||
|
field=models.CharField(blank=True, help_text="Title", null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="username",
|
||||||
|
field=models.CharField(help_text="Username - this will be an email address", verbose_name="Username"),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,9 +1,9 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
|
||||||
|
|
||||||
from .utility.time_stamped_model import TimeStampedModel
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
|
||||||
|
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class Contact(TimeStampedModel):
|
class Contact(TimeStampedModel):
|
||||||
"""Contact information follows a similar pattern for each contact."""
|
"""Contact information follows a similar pattern for each contact."""
|
||||||
|
@ -15,23 +15,23 @@ class Contact(TimeStampedModel):
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
first_name = models.TextField(
|
first_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="first name / given name",
|
verbose_name="first name / given name",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
middle_name = models.TextField(
|
middle_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
last_name = models.TextField(
|
last_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="last name / family name",
|
verbose_name="last name / family name",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
title = models.TextField(
|
title = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="title or role in your organization",
|
verbose_name="title or role in your organization",
|
||||||
|
|
|
@ -13,6 +13,7 @@ from typing import Any
|
||||||
from registrar.models.host import Host
|
from registrar.models.host import Host
|
||||||
from registrar.models.host_ip import HostIP
|
from registrar.models.host_ip import HostIP
|
||||||
from registrar.utility.enums import DefaultEmail
|
from registrar.utility.enums import DefaultEmail
|
||||||
|
from registrar.utility import errors
|
||||||
|
|
||||||
from registrar.utility.errors import (
|
from registrar.utility.errors import (
|
||||||
ActionNotAllowed,
|
ActionNotAllowed,
|
||||||
|
@ -192,9 +193,17 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def available(cls, domain: str) -> bool:
|
def available(cls, domain: str) -> bool:
|
||||||
"""Check if a domain is available."""
|
"""Check if a domain is available.
|
||||||
|
This is called by the availablility api and
|
||||||
|
is called in the validate function on the request/domain page
|
||||||
|
|
||||||
|
throws- RegistryError or InvalidDomainError"""
|
||||||
if not cls.string_could_be_domain(domain):
|
if not cls.string_could_be_domain(domain):
|
||||||
raise ValueError("Not a valid domain: %s" % str(domain))
|
logger.warning("Not a valid domain: %s" % str(domain))
|
||||||
|
# throw invalid domain error so that it can be caught in
|
||||||
|
# validate_and_handle_errors in domain_helper
|
||||||
|
raise errors.InvalidDomainError()
|
||||||
|
|
||||||
domain_name = domain.lower()
|
domain_name = domain.lower()
|
||||||
req = commands.CheckDomain([domain_name])
|
req = commands.CheckDomain([domain_name])
|
||||||
return registry.send(req, cleaned=True).res_data[0].avail
|
return registry.send(req, cleaned=True).res_data[0].avail
|
||||||
|
@ -429,7 +438,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
raise NameserverError(code=nsErrorCodes.INVALID_HOST, nameserver=nameserver)
|
raise NameserverError(code=nsErrorCodes.INVALID_HOST, nameserver=nameserver)
|
||||||
elif cls.isSubdomain(name, nameserver) and (ip is None or ip == []):
|
elif cls.isSubdomain(name, nameserver) and (ip is None or ip == []):
|
||||||
raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver)
|
raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver)
|
||||||
|
|
||||||
elif not cls.isSubdomain(name, nameserver) and (ip is not None and ip != []):
|
elif not cls.isSubdomain(name, nameserver) and (ip is not None and ip != []):
|
||||||
raise NameserverError(code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, nameserver=nameserver, ip=ip)
|
raise NameserverError(code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, nameserver=nameserver, ip=ip)
|
||||||
elif ip is not None and ip != []:
|
elif ip is not None and ip != []:
|
||||||
|
@ -1780,6 +1788,10 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
for cleaned_host in cleaned_hosts:
|
for cleaned_host in cleaned_hosts:
|
||||||
# Check if the cleaned_host already exists
|
# Check if the cleaned_host already exists
|
||||||
host_in_db, host_created = Host.objects.get_or_create(domain=self, name=cleaned_host["name"])
|
host_in_db, host_created = Host.objects.get_or_create(domain=self, name=cleaned_host["name"])
|
||||||
|
# Check if the nameserver is a subdomain of the current domain
|
||||||
|
# If it is NOT a subdomain, we remove the IP address
|
||||||
|
if not Domain.isSubdomain(self.name, cleaned_host["name"]):
|
||||||
|
cleaned_host["addrs"] = []
|
||||||
# Get cleaned list of ips for update
|
# Get cleaned list of ips for update
|
||||||
cleaned_ips = cleaned_host["addrs"]
|
cleaned_ips = cleaned_host["addrs"]
|
||||||
if not host_created:
|
if not host_created:
|
||||||
|
|
|
@ -4,6 +4,7 @@ from typing import Union
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_fsm import FSMField, transition # type: ignore
|
from django_fsm import FSMField, transition # type: ignore
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -350,12 +351,34 @@ class DomainApplication(TimeStampedModel):
|
||||||
]
|
]
|
||||||
AGENCY_CHOICES = [(v, v) for v in AGENCIES]
|
AGENCY_CHOICES = [(v, v) for v in AGENCIES]
|
||||||
|
|
||||||
|
class RejectionReasons(models.TextChoices):
|
||||||
|
DOMAIN_PURPOSE = "purpose_not_met", "Purpose requirements not met"
|
||||||
|
REQUESTOR = "requestor_not_eligible", "Requestor not eligible to make request"
|
||||||
|
SECOND_DOMAIN_REASONING = (
|
||||||
|
"org_has_domain",
|
||||||
|
"Org already has a .gov domain",
|
||||||
|
)
|
||||||
|
CONTACTS_OR_ORGANIZATION_LEGITIMACY = (
|
||||||
|
"contacts_not_verified",
|
||||||
|
"Org contacts couldn't be verified",
|
||||||
|
)
|
||||||
|
ORGANIZATION_ELIGIBILITY = "org_not_eligible", "Org not eligible for a .gov domain"
|
||||||
|
NAMING_REQUIREMENTS = "naming_not_met", "Naming requirements not met"
|
||||||
|
OTHER = "other", "Other/Unspecified"
|
||||||
|
|
||||||
# #### Internal fields about the application #####
|
# #### Internal fields about the application #####
|
||||||
status = FSMField(
|
status = FSMField(
|
||||||
choices=ApplicationStatus.choices, # possible states as an array of constants
|
choices=ApplicationStatus.choices, # possible states as an array of constants
|
||||||
default=ApplicationStatus.STARTED, # sensible default
|
default=ApplicationStatus.STARTED, # sensible default
|
||||||
protected=False, # can change state directly, particularly in Django admin
|
protected=False, # can change state directly, particularly in Django admin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
rejection_reason = models.TextField(
|
||||||
|
choices=RejectionReasons.choices,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
# This is the application user who created this application. The contact
|
# This is the application user who created this application. The contact
|
||||||
# information that they gave is in the `submitter` field
|
# information that they gave is in the `submitter` field
|
||||||
creator = models.ForeignKey(
|
creator = models.ForeignKey(
|
||||||
|
@ -363,6 +386,7 @@ class DomainApplication(TimeStampedModel):
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name="applications_created",
|
related_name="applications_created",
|
||||||
)
|
)
|
||||||
|
|
||||||
investigator = models.ForeignKey(
|
investigator = models.ForeignKey(
|
||||||
"registrar.User",
|
"registrar.User",
|
||||||
null=True,
|
null=True,
|
||||||
|
@ -391,13 +415,13 @@ class DomainApplication(TimeStampedModel):
|
||||||
help_text="Is the tribe recognized by a state",
|
help_text="Is the tribe recognized by a state",
|
||||||
)
|
)
|
||||||
|
|
||||||
tribe_name = models.TextField(
|
tribe_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Name of tribe",
|
help_text="Name of tribe",
|
||||||
)
|
)
|
||||||
|
|
||||||
federal_agency = models.TextField(
|
federal_agency = models.CharField(
|
||||||
choices=AGENCY_CHOICES,
|
choices=AGENCY_CHOICES,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -418,25 +442,25 @@ class DomainApplication(TimeStampedModel):
|
||||||
help_text="Is your organization an election office?",
|
help_text="Is your organization an election office?",
|
||||||
)
|
)
|
||||||
|
|
||||||
organization_name = models.TextField(
|
organization_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Organization name",
|
help_text="Organization name",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
address_line1 = models.TextField(
|
address_line1 = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address",
|
help_text="Street address",
|
||||||
verbose_name="Address line 1",
|
verbose_name="Address line 1",
|
||||||
)
|
)
|
||||||
address_line2 = models.TextField(
|
address_line2 = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address line 2 (optional)",
|
help_text="Street address line 2 (optional)",
|
||||||
verbose_name="Address line 2",
|
verbose_name="Address line 2",
|
||||||
)
|
)
|
||||||
city = models.TextField(
|
city = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="City",
|
help_text="City",
|
||||||
|
@ -455,7 +479,7 @@ class DomainApplication(TimeStampedModel):
|
||||||
help_text="Zip code",
|
help_text="Zip code",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
urbanization = models.TextField(
|
urbanization = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Urbanization (required for Puerto Rico only)",
|
help_text="Urbanization (required for Puerto Rico only)",
|
||||||
|
@ -588,7 +612,9 @@ class DomainApplication(TimeStampedModel):
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
logger.error(f"Can't query an approved domain while attempting {called_from}")
|
logger.error(f"Can't query an approved domain while attempting {called_from}")
|
||||||
|
|
||||||
def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True):
|
def _send_status_update_email(
|
||||||
|
self, new_status, email_template, email_template_subject, send_email=True, bcc_address=""
|
||||||
|
):
|
||||||
"""Send a status update email to the submitter.
|
"""Send a status update email to the submitter.
|
||||||
|
|
||||||
The email goes to the email address that the submitter gave as their
|
The email goes to the email address that the submitter gave as their
|
||||||
|
@ -613,6 +639,7 @@ class DomainApplication(TimeStampedModel):
|
||||||
email_template_subject,
|
email_template_subject,
|
||||||
self.submitter.email,
|
self.submitter.email,
|
||||||
context={"application": self},
|
context={"application": self},
|
||||||
|
bcc_address=bcc_address,
|
||||||
)
|
)
|
||||||
logger.info(f"The {new_status} email sent to: {self.submitter.email}")
|
logger.info(f"The {new_status} email sent to: {self.submitter.email}")
|
||||||
except EmailSendingError:
|
except EmailSendingError:
|
||||||
|
@ -654,11 +681,17 @@ class DomainApplication(TimeStampedModel):
|
||||||
# Limit email notifications to transitions from Started and Withdrawn
|
# Limit email notifications to transitions from Started and Withdrawn
|
||||||
limited_statuses = [self.ApplicationStatus.STARTED, self.ApplicationStatus.WITHDRAWN]
|
limited_statuses = [self.ApplicationStatus.STARTED, self.ApplicationStatus.WITHDRAWN]
|
||||||
|
|
||||||
|
bcc_address = ""
|
||||||
|
if settings.IS_PRODUCTION:
|
||||||
|
bcc_address = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
if self.status in limited_statuses:
|
if self.status in limited_statuses:
|
||||||
self._send_status_update_email(
|
self._send_status_update_email(
|
||||||
"submission confirmation",
|
"submission confirmation",
|
||||||
"emails/submission_confirmation.txt",
|
"emails/submission_confirmation.txt",
|
||||||
"emails/submission_confirmation_subject.txt",
|
"emails/submission_confirmation_subject.txt",
|
||||||
|
True,
|
||||||
|
bcc_address,
|
||||||
)
|
)
|
||||||
|
|
||||||
@transition(
|
@transition(
|
||||||
|
@ -678,12 +711,17 @@ class DomainApplication(TimeStampedModel):
|
||||||
|
|
||||||
This action is logged.
|
This action is logged.
|
||||||
|
|
||||||
|
This action cleans up the rejection status if moving away from rejected.
|
||||||
|
|
||||||
As side effects this will delete the domain and domain_information
|
As side effects this will delete the domain and domain_information
|
||||||
(will cascade) when they exist."""
|
(will cascade) when they exist."""
|
||||||
|
|
||||||
if self.status == self.ApplicationStatus.APPROVED:
|
if self.status == self.ApplicationStatus.APPROVED:
|
||||||
self.delete_and_clean_up_domain("in_review")
|
self.delete_and_clean_up_domain("in_review")
|
||||||
|
|
||||||
|
if self.status == self.ApplicationStatus.REJECTED:
|
||||||
|
self.rejection_reason = None
|
||||||
|
|
||||||
literal = DomainApplication.ApplicationStatus.IN_REVIEW
|
literal = DomainApplication.ApplicationStatus.IN_REVIEW
|
||||||
# Check if the tuple exists, then grab its value
|
# Check if the tuple exists, then grab its value
|
||||||
in_review = literal if literal is not None else "In Review"
|
in_review = literal if literal is not None else "In Review"
|
||||||
|
@ -705,12 +743,17 @@ class DomainApplication(TimeStampedModel):
|
||||||
|
|
||||||
This action is logged.
|
This action is logged.
|
||||||
|
|
||||||
|
This action cleans up the rejection status if moving away from rejected.
|
||||||
|
|
||||||
As side effects this will delete the domain and domain_information
|
As side effects this will delete the domain and domain_information
|
||||||
(will cascade) when they exist."""
|
(will cascade) when they exist."""
|
||||||
|
|
||||||
if self.status == self.ApplicationStatus.APPROVED:
|
if self.status == self.ApplicationStatus.APPROVED:
|
||||||
self.delete_and_clean_up_domain("reject_with_prejudice")
|
self.delete_and_clean_up_domain("reject_with_prejudice")
|
||||||
|
|
||||||
|
if self.status == self.ApplicationStatus.REJECTED:
|
||||||
|
self.rejection_reason = None
|
||||||
|
|
||||||
literal = DomainApplication.ApplicationStatus.ACTION_NEEDED
|
literal = DomainApplication.ApplicationStatus.ACTION_NEEDED
|
||||||
# Check if the tuple is setup correctly, then grab its value
|
# Check if the tuple is setup correctly, then grab its value
|
||||||
action_needed = literal if literal is not None else "Action Needed"
|
action_needed = literal if literal is not None else "Action Needed"
|
||||||
|
@ -729,6 +772,8 @@ class DomainApplication(TimeStampedModel):
|
||||||
def approve(self, send_email=True):
|
def approve(self, send_email=True):
|
||||||
"""Approve an application that has been submitted.
|
"""Approve an application that has been submitted.
|
||||||
|
|
||||||
|
This action cleans up the rejection status if moving away from rejected.
|
||||||
|
|
||||||
This has substantial side-effects because it creates another database
|
This has substantial side-effects because it creates another database
|
||||||
object for the approved Domain and makes the user who created the
|
object for the approved Domain and makes the user who created the
|
||||||
application into an admin on that domain. It also triggers an email
|
application into an admin on that domain. It also triggers an email
|
||||||
|
@ -751,6 +796,9 @@ class DomainApplication(TimeStampedModel):
|
||||||
user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER
|
user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.status == self.ApplicationStatus.REJECTED:
|
||||||
|
self.rejection_reason = None
|
||||||
|
|
||||||
self._send_status_update_email(
|
self._send_status_update_email(
|
||||||
"application approved",
|
"application approved",
|
||||||
"emails/status_change_approved.txt",
|
"emails/status_change_approved.txt",
|
||||||
|
|
|
@ -67,13 +67,13 @@ class DomainInformation(TimeStampedModel):
|
||||||
help_text="Is the tribe recognized by a state",
|
help_text="Is the tribe recognized by a state",
|
||||||
)
|
)
|
||||||
|
|
||||||
tribe_name = models.TextField(
|
tribe_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Name of tribe",
|
help_text="Name of tribe",
|
||||||
)
|
)
|
||||||
|
|
||||||
federal_agency = models.TextField(
|
federal_agency = models.CharField(
|
||||||
choices=AGENCY_CHOICES,
|
choices=AGENCY_CHOICES,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -94,25 +94,25 @@ class DomainInformation(TimeStampedModel):
|
||||||
help_text="Is your organization an election office?",
|
help_text="Is your organization an election office?",
|
||||||
)
|
)
|
||||||
|
|
||||||
organization_name = models.TextField(
|
organization_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Organization name",
|
help_text="Organization name",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
address_line1 = models.TextField(
|
address_line1 = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address",
|
help_text="Street address",
|
||||||
verbose_name="Street address",
|
verbose_name="Street address",
|
||||||
)
|
)
|
||||||
address_line2 = models.TextField(
|
address_line2 = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address line 2 (optional)",
|
help_text="Street address line 2 (optional)",
|
||||||
verbose_name="Street address line 2 (optional)",
|
verbose_name="Street address line 2 (optional)",
|
||||||
)
|
)
|
||||||
city = models.TextField(
|
city = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="City",
|
help_text="City",
|
||||||
|
@ -132,7 +132,7 @@ class DomainInformation(TimeStampedModel):
|
||||||
help_text="Zip code",
|
help_text="Zip code",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
urbanization = models.TextField(
|
urbanization = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Urbanization (required for Puerto Rico only)",
|
help_text="Urbanization (required for Puerto Rico only)",
|
||||||
|
|
|
@ -8,6 +8,8 @@ from registrar.utility.enums import DefaultEmail
|
||||||
|
|
||||||
from .utility.time_stamped_model import TimeStampedModel
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
|
||||||
|
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def get_id():
|
def get_id():
|
||||||
"""Generate a 16 character registry ID with a low probability of collision."""
|
"""Generate a 16 character registry ID with a low probability of collision."""
|
||||||
|
@ -59,22 +61,22 @@ class PublicContact(TimeStampedModel):
|
||||||
related_name="contacts",
|
related_name="contacts",
|
||||||
)
|
)
|
||||||
|
|
||||||
name = models.TextField(null=False, help_text="Contact's full name")
|
name = models.CharField(null=False, help_text="Contact's full name")
|
||||||
org = models.TextField(null=True, help_text="Contact's organization (null ok)")
|
org = models.CharField(null=True, help_text="Contact's organization (null ok)")
|
||||||
street1 = models.TextField(null=False, help_text="Contact's street")
|
street1 = models.CharField(null=False, help_text="Contact's street")
|
||||||
street2 = models.TextField(null=True, help_text="Contact's street (null ok)")
|
street2 = models.CharField(null=True, help_text="Contact's street (null ok)")
|
||||||
street3 = models.TextField(null=True, help_text="Contact's street (null ok)")
|
street3 = models.CharField(null=True, help_text="Contact's street (null ok)")
|
||||||
city = models.TextField(null=False, help_text="Contact's city")
|
city = models.CharField(null=False, help_text="Contact's city")
|
||||||
sp = models.TextField(null=False, help_text="Contact's state or province")
|
sp = models.CharField(null=False, help_text="Contact's state or province")
|
||||||
pc = models.TextField(null=False, help_text="Contact's postal code")
|
pc = models.CharField(null=False, help_text="Contact's postal code")
|
||||||
cc = models.TextField(null=False, help_text="Contact's country code")
|
cc = models.CharField(null=False, help_text="Contact's country code")
|
||||||
email = models.TextField(null=False, help_text="Contact's email address")
|
email = models.EmailField(null=False, help_text="Contact's email address")
|
||||||
voice = models.TextField(null=False, help_text="Contact's phone number. Must be in ITU.E164.2005 format")
|
voice = PhoneNumberField(null=False, help_text="Contact's phone number. Must be in ITU.E164.2005 format")
|
||||||
fax = models.TextField(
|
fax = PhoneNumberField(
|
||||||
null=True,
|
null=True,
|
||||||
help_text="Contact's fax number (null ok). Must be in ITU.E164.2005 format.",
|
help_text="Contact's fax number (null ok). Must be in ITU.E164.2005 format.",
|
||||||
)
|
)
|
||||||
pw = models.TextField(null=False, help_text="Contact's authorization code. 16 characters minimum.")
|
pw = models.CharField(null=False, help_text="Contact's authorization code. 16 characters minimum.")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_default_registrant(cls):
|
def get_default_registrant(cls):
|
||||||
|
|
|
@ -17,13 +17,13 @@ class TransitionDomain(TimeStampedModel):
|
||||||
# classes that import TransitionDomain
|
# classes that import TransitionDomain
|
||||||
StatusChoices = StatusChoices
|
StatusChoices = StatusChoices
|
||||||
|
|
||||||
username = models.TextField(
|
username = models.CharField(
|
||||||
null=False,
|
null=False,
|
||||||
blank=False,
|
blank=False,
|
||||||
verbose_name="Username",
|
verbose_name="Username",
|
||||||
help_text="Username - this will be an email address",
|
help_text="Username - this will be an email address",
|
||||||
)
|
)
|
||||||
domain_name = models.TextField(
|
domain_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="Domain name",
|
verbose_name="Domain name",
|
||||||
|
@ -49,25 +49,25 @@ class TransitionDomain(TimeStampedModel):
|
||||||
verbose_name="Processed",
|
verbose_name="Processed",
|
||||||
help_text="Indicates whether this TransitionDomain was already processed",
|
help_text="Indicates whether this TransitionDomain was already processed",
|
||||||
)
|
)
|
||||||
organization_type = models.TextField(
|
organization_type = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Type of organization",
|
help_text="Type of organization",
|
||||||
)
|
)
|
||||||
organization_name = models.TextField(
|
organization_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Organization name",
|
help_text="Organization name",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
federal_type = models.TextField(
|
federal_type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Federal government branch",
|
help_text="Federal government branch",
|
||||||
)
|
)
|
||||||
federal_agency = models.TextField(
|
federal_agency = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Federal agency",
|
help_text="Federal agency",
|
||||||
|
@ -80,44 +80,44 @@ class TransitionDomain(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
help_text=("Duplication of registry's expiration " "date saved for ease of reporting"),
|
help_text=("Duplication of registry's expiration " "date saved for ease of reporting"),
|
||||||
)
|
)
|
||||||
first_name = models.TextField(
|
first_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="First name",
|
help_text="First name",
|
||||||
verbose_name="first name / given name",
|
verbose_name="first name / given name",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
middle_name = models.TextField(
|
middle_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Middle name (optional)",
|
help_text="Middle name (optional)",
|
||||||
)
|
)
|
||||||
last_name = models.TextField(
|
last_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Last name",
|
help_text="Last name",
|
||||||
)
|
)
|
||||||
title = models.TextField(
|
title = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Title",
|
help_text="Title",
|
||||||
)
|
)
|
||||||
email = models.TextField(
|
email = models.EmailField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Email",
|
help_text="Email",
|
||||||
)
|
)
|
||||||
phone = models.TextField(
|
phone = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Phone",
|
help_text="Phone",
|
||||||
)
|
)
|
||||||
address_line = models.TextField(
|
address_line = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address",
|
help_text="Street address",
|
||||||
)
|
)
|
||||||
city = models.TextField(
|
city = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="City",
|
help_text="City",
|
||||||
|
|
|
@ -33,6 +33,7 @@ class DomainHelper:
|
||||||
# Split into pieces for the linter
|
# Split into pieces for the linter
|
||||||
domain = cls._validate_domain_string(domain, blank_ok)
|
domain = cls._validate_domain_string(domain, blank_ok)
|
||||||
|
|
||||||
|
if domain != "":
|
||||||
try:
|
try:
|
||||||
if not check_domain_available(domain):
|
if not check_domain_available(domain):
|
||||||
raise errors.DomainUnavailableError()
|
raise errors.DomainUnavailableError()
|
||||||
|
|
|
@ -15,7 +15,15 @@
|
||||||
{% if filters %}
|
{% if filters %}
|
||||||
filtered by
|
filtered by
|
||||||
{% for filter_param in filters %}
|
{% for filter_param in filters %}
|
||||||
|
{% if filter_param.parameter_name == 'is_election_board' %}
|
||||||
|
{%if filter_param.parameter_value == '0' %}
|
||||||
|
election office = No
|
||||||
|
{% else %}
|
||||||
|
election office = Yes
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
{{ filter_param.parameter_name }} = {{ filter_param.parameter_value }}
|
{{ filter_param.parameter_name }} = {{ filter_param.parameter_value }}
|
||||||
|
{% endif %}
|
||||||
{% if not forloop.last %}, {% endif %}
|
{% if not forloop.last %}, {% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -8,7 +8,56 @@ REQUEST RECEIVED ON: {{ application.submission_date|date }}
|
||||||
STATUS: Rejected
|
STATUS: Rejected
|
||||||
|
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
|
{% if application.rejection_reason != 'other' %}
|
||||||
|
REJECTION REASON{% endif %}{% if application.rejection_reason == 'purpose_not_met' %}
|
||||||
|
Your domain request was rejected because the purpose you provided did not meet our
|
||||||
|
requirements. You didn’t provide enough information about how you intend to use the
|
||||||
|
domain.
|
||||||
|
|
||||||
|
Learn more about:
|
||||||
|
- Eligibility for a .gov domain <https://get.gov/domains/eligibility>
|
||||||
|
- What you can and can’t do with .gov domains <https://get.gov/domains/requirements/>
|
||||||
|
|
||||||
|
If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'requestor_not_eligible' %}
|
||||||
|
Your domain request was rejected because we don’t believe you’re eligible to request a
|
||||||
|
.gov domain on behalf of {{ application.organization_name }}. You must be a government employee, or be
|
||||||
|
working on behalf of a government organization, to request a .gov domain.
|
||||||
|
|
||||||
|
|
||||||
|
DEMONSTRATE ELIGIBILITY
|
||||||
|
If you can provide more information that demonstrates your eligibility, or you want to
|
||||||
|
discuss further, reply to this email.{% elif application.rejection_reason == 'org_has_domain' %}
|
||||||
|
Your domain request was rejected because {{ application.organization_name }} has a .gov domain. Our
|
||||||
|
practice is to approve one domain per online service per government organization. We
|
||||||
|
evaluate additional requests on a case-by-case basis. You did not provide sufficient
|
||||||
|
justification for an additional domain.
|
||||||
|
|
||||||
|
Read more about our practice of approving one domain per online service
|
||||||
|
<https://get.gov/domains/before/#one-domain-per-service>.
|
||||||
|
|
||||||
|
If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'contacts_not_verified' %}
|
||||||
|
Your domain request was rejected because we could not verify the organizational
|
||||||
|
contacts you provided. If you have questions or comments, reply to this email.{% elif application.rejection_reason == 'org_not_eligible' %}
|
||||||
|
Your domain request was rejected because we determined that {{ application.organization_name }} is not
|
||||||
|
eligible for a .gov domain. .Gov domains are only available to official U.S.-based
|
||||||
|
government organizations.
|
||||||
|
|
||||||
|
|
||||||
|
DEMONSTRATE ELIGIBILITY
|
||||||
|
If you can provide documentation that demonstrates your eligibility, reply to this email.
|
||||||
|
This can include links to (or copies of) your authorizing legislation, your founding
|
||||||
|
charter or bylaws, or other similar documentation. Without this, we can’t approve a
|
||||||
|
.gov domain for your organization. Learn more about eligibility for .gov domains
|
||||||
|
<https://get.gov/domains/eligibility/>.{% elif application.rejection_reason == 'naming_not_met' %}
|
||||||
|
Your domain request was rejected because it does not meet our naming requirements.
|
||||||
|
Domains should uniquely identify a government organization and be clear to the
|
||||||
|
general public. Learn more about naming requirements for your type of organization
|
||||||
|
<https://get.gov/domains/choosing/>.
|
||||||
|
|
||||||
|
|
||||||
|
YOU CAN SUBMIT A NEW REQUEST
|
||||||
|
We encourage you to request a domain that meets our requirements. If you have
|
||||||
|
questions or want to discuss potential domain names, reply to this email.{% elif application.rejection_reason == 'other' %}
|
||||||
YOU CAN SUBMIT A NEW REQUEST
|
YOU CAN SUBMIT A NEW REQUEST
|
||||||
If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request.
|
If your organization is eligible for a .gov domain and you meet our other requirements, you can submit a new request.
|
||||||
|
|
||||||
|
@ -19,7 +68,7 @@ Learn more about:
|
||||||
|
|
||||||
NEED ASSISTANCE?
|
NEED ASSISTANCE?
|
||||||
If you have questions about this domain request or need help choosing a new domain name, reply to this email.
|
If you have questions about this domain request or need help choosing a new domain name, reply to this email.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
THANK YOU
|
THANK YOU
|
||||||
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
|
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
|
||||||
|
|
|
@ -692,6 +692,56 @@ class MockEppLib(TestCase):
|
||||||
],
|
],
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=datetime.date(2023, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mockDataInfoDomainSubdomain = fakedEppObject(
|
||||||
|
"fakePw",
|
||||||
|
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
|
hosts=["fake.meoward.gov"],
|
||||||
|
statuses=[
|
||||||
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
|
],
|
||||||
|
ex_date=datetime.date(2023, 5, 25),
|
||||||
|
)
|
||||||
|
|
||||||
|
mockDataInfoDomainSubdomainAndIPAddress = fakedEppObject(
|
||||||
|
"fakePw",
|
||||||
|
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
|
hosts=["fake.meow.gov"],
|
||||||
|
statuses=[
|
||||||
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
|
],
|
||||||
|
ex_date=datetime.date(2023, 5, 25),
|
||||||
|
addrs=[common.Ip(addr="2.0.0.8")],
|
||||||
|
)
|
||||||
|
|
||||||
|
mockDataInfoDomainNotSubdomainNoIP = fakedEppObject(
|
||||||
|
"fakePw",
|
||||||
|
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
|
hosts=["fake.meow.com"],
|
||||||
|
statuses=[
|
||||||
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
|
],
|
||||||
|
ex_date=datetime.date(2023, 5, 25),
|
||||||
|
)
|
||||||
|
|
||||||
|
mockDataInfoDomainSubdomainNoIP = fakedEppObject(
|
||||||
|
"fakePw",
|
||||||
|
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
|
hosts=["fake.subdomainwoip.gov"],
|
||||||
|
statuses=[
|
||||||
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
|
],
|
||||||
|
ex_date=datetime.date(2023, 5, 25),
|
||||||
|
)
|
||||||
|
|
||||||
mockDataExtensionDomain = fakedEppObject(
|
mockDataExtensionDomain = fakedEppObject(
|
||||||
"fakePw",
|
"fakePw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
|
@ -829,6 +879,24 @@ class MockEppLib(TestCase):
|
||||||
addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")],
|
addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mockDataInfoHosts1IP = fakedEppObject(
|
||||||
|
"lastPw",
|
||||||
|
cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)),
|
||||||
|
addrs=[common.Ip(addr="2.0.0.8")],
|
||||||
|
)
|
||||||
|
|
||||||
|
mockDataInfoHostsNotSubdomainNoIP = fakedEppObject(
|
||||||
|
"lastPw",
|
||||||
|
cr_date=make_aware(datetime.datetime(2023, 8, 26, 19, 45, 35)),
|
||||||
|
addrs=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
mockDataInfoHostsSubdomainNoIP = fakedEppObject(
|
||||||
|
"lastPw",
|
||||||
|
cr_date=make_aware(datetime.datetime(2023, 8, 27, 19, 45, 35)),
|
||||||
|
addrs=[],
|
||||||
|
)
|
||||||
|
|
||||||
mockDataHostChange = fakedEppObject("lastPw", cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)))
|
mockDataHostChange = fakedEppObject("lastPw", cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)))
|
||||||
addDsData1 = {
|
addDsData1 = {
|
||||||
"keyTag": 1234,
|
"keyTag": 1234,
|
||||||
|
@ -995,6 +1063,8 @@ class MockEppLib(TestCase):
|
||||||
return self.mockDeleteDomainCommands(_request, cleaned)
|
return self.mockDeleteDomainCommands(_request, cleaned)
|
||||||
case commands.RenewDomain:
|
case commands.RenewDomain:
|
||||||
return self.mockRenewDomainCommand(_request, cleaned)
|
return self.mockRenewDomainCommand(_request, cleaned)
|
||||||
|
case commands.InfoHost:
|
||||||
|
return self.mockInfoHostCommmands(_request, cleaned)
|
||||||
case _:
|
case _:
|
||||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||||
|
|
||||||
|
@ -1009,6 +1079,25 @@ class MockEppLib(TestCase):
|
||||||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def mockInfoHostCommmands(self, _request, cleaned):
|
||||||
|
request_name = getattr(_request, "name", None)
|
||||||
|
|
||||||
|
# Define a dictionary to map request names to data and extension values
|
||||||
|
request_mappings = {
|
||||||
|
"fake.meow.gov": (self.mockDataInfoHosts1IP, None), # is subdomain and has ip
|
||||||
|
"fake.meow.com": (self.mockDataInfoHostsNotSubdomainNoIP, None), # not subdomain w no ip
|
||||||
|
"fake.subdomainwoip.gov": (self.mockDataInfoHostsSubdomainNoIP, None), # subdomain w no ip
|
||||||
|
}
|
||||||
|
|
||||||
|
# Retrieve the corresponding values from the dictionary
|
||||||
|
default_mapping = (self.mockDataInfoHosts, None)
|
||||||
|
res_data, extensions = request_mappings.get(request_name, default_mapping)
|
||||||
|
|
||||||
|
return MagicMock(
|
||||||
|
res_data=[res_data],
|
||||||
|
extensions=[extensions] if extensions is not None else [],
|
||||||
|
)
|
||||||
|
|
||||||
def mockUpdateHostCommands(self, _request, cleaned):
|
def mockUpdateHostCommands(self, _request, cleaned):
|
||||||
test_ws_ip = common.Ip(addr="1.1. 1.1")
|
test_ws_ip = common.Ip(addr="1.1. 1.1")
|
||||||
addrs_submitted = getattr(_request, "addrs", [])
|
addrs_submitted = getattr(_request, "addrs", [])
|
||||||
|
@ -1097,6 +1186,10 @@ class MockEppLib(TestCase):
|
||||||
"adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None),
|
"adomain2.gov": (self.InfoDomainWithVerisignSecurityContact, None),
|
||||||
"defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),
|
"defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),
|
||||||
"justnameserver.com": (self.justNameserver, None),
|
"justnameserver.com": (self.justNameserver, None),
|
||||||
|
"meoward.gov": (self.mockDataInfoDomainSubdomain, None),
|
||||||
|
"meow.gov": (self.mockDataInfoDomainSubdomainAndIPAddress, None),
|
||||||
|
"fakemeow.gov": (self.mockDataInfoDomainNotSubdomainNoIP, None),
|
||||||
|
"subdomainwoip.gov": (self.mockDataInfoDomainSubdomainNoIP, None),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Retrieve the corresponding values from the dictionary
|
# Retrieve the corresponding values from the dictionary
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from django.test import TestCase, RequestFactory, Client
|
from django.test import TestCase, RequestFactory, Client, override_settings
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
|
@ -243,9 +243,9 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
||||||
|
|
||||||
response = self.client.get("/admin/registrar/domain/")
|
response = self.client.get("/admin/registrar/domain/")
|
||||||
|
|
||||||
# There are 3 template references to Federal (3) plus one reference in the table
|
# There are 4 template references to Federal (4) plus four references in the table
|
||||||
# for our actual application
|
# for our actual application
|
||||||
self.assertContains(response, "Federal", count=4)
|
self.assertContains(response, "Federal", count=8)
|
||||||
# This may be a bit more robust
|
# This may be a bit more robust
|
||||||
self.assertContains(response, '<td class="field-organization_type">Federal</td>', count=1)
|
self.assertContains(response, '<td class="field-organization_type">Federal</td>', count=1)
|
||||||
# Now let's make sure the long description does not exist
|
# Now let's make sure the long description does not exist
|
||||||
|
@ -445,6 +445,7 @@ class TestDomainApplicationAdminForm(TestCase):
|
||||||
self.application = completed_application()
|
self.application = completed_application()
|
||||||
|
|
||||||
def test_form_choices(self):
|
def test_form_choices(self):
|
||||||
|
with less_console_noise():
|
||||||
# Create a form instance with the test application
|
# Create a form instance with the test application
|
||||||
form = DomainApplicationAdminForm(instance=self.application)
|
form = DomainApplicationAdminForm(instance=self.application)
|
||||||
|
|
||||||
|
@ -453,6 +454,7 @@ class TestDomainApplicationAdminForm(TestCase):
|
||||||
self.assertEqual(form.fields["status"].widget.choices, expected_choices)
|
self.assertEqual(form.fields["status"].widget.choices, expected_choices)
|
||||||
|
|
||||||
def test_form_choices_when_no_instance(self):
|
def test_form_choices_when_no_instance(self):
|
||||||
|
with less_console_noise():
|
||||||
# Create a form instance without an instance
|
# Create a form instance without an instance
|
||||||
form = DomainApplicationAdminForm()
|
form = DomainApplicationAdminForm()
|
||||||
|
|
||||||
|
@ -467,6 +469,7 @@ class TestDomainApplicationAdminForm(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_form_choices_when_ineligible(self):
|
def test_form_choices_when_ineligible(self):
|
||||||
|
with less_console_noise():
|
||||||
# Create a form instance with a domain application with ineligible status
|
# Create a form instance with a domain application with ineligible status
|
||||||
ineligible_application = DomainApplication(status="ineligible")
|
ineligible_application = DomainApplication(status="ineligible")
|
||||||
|
|
||||||
|
@ -502,6 +505,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
|
|
||||||
def test_domain_sortable(self):
|
def test_domain_sortable(self):
|
||||||
"""Tests if the DomainApplication sorts by domain correctly"""
|
"""Tests if the DomainApplication sorts by domain correctly"""
|
||||||
|
with less_console_noise():
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
@ -515,6 +519,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
|
|
||||||
def test_submitter_sortable(self):
|
def test_submitter_sortable(self):
|
||||||
"""Tests if the DomainApplication sorts by domain correctly"""
|
"""Tests if the DomainApplication sorts by domain correctly"""
|
||||||
|
with less_console_noise():
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
@ -527,7 +532,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
|
|
||||||
# Assert that our sort works correctly
|
# Assert that our sort works correctly
|
||||||
self.test_helper.assert_table_sorted(
|
self.test_helper.assert_table_sorted(
|
||||||
"5",
|
"11",
|
||||||
(
|
(
|
||||||
"submitter__first_name",
|
"submitter__first_name",
|
||||||
"submitter__last_name",
|
"submitter__last_name",
|
||||||
|
@ -536,7 +541,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
|
|
||||||
# Assert that sorting in reverse works correctly
|
# Assert that sorting in reverse works correctly
|
||||||
self.test_helper.assert_table_sorted(
|
self.test_helper.assert_table_sorted(
|
||||||
"-5",
|
"-11",
|
||||||
(
|
(
|
||||||
"-submitter__first_name",
|
"-submitter__first_name",
|
||||||
"-submitter__last_name",
|
"-submitter__last_name",
|
||||||
|
@ -545,6 +550,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
|
|
||||||
def test_investigator_sortable(self):
|
def test_investigator_sortable(self):
|
||||||
"""Tests if the DomainApplication sorts by domain correctly"""
|
"""Tests if the DomainApplication sorts by domain correctly"""
|
||||||
|
with less_console_noise():
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
@ -576,18 +582,19 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
"""
|
"""
|
||||||
Make sure the short name is displaying in admin on the list page
|
Make sure the short name is displaying in admin on the list page
|
||||||
"""
|
"""
|
||||||
|
with less_console_noise():
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
completed_application()
|
completed_application()
|
||||||
response = self.client.get("/admin/registrar/domainapplication/")
|
response = self.client.get("/admin/registrar/domainapplication/")
|
||||||
# There are 3 template references to Federal (3) plus one reference in the table
|
# There are 4 template references to Federal (4) plus two references in the table
|
||||||
# for our actual application
|
# for our actual application
|
||||||
self.assertContains(response, "Federal", count=4)
|
self.assertContains(response, "Federal", count=6)
|
||||||
# This may be a bit more robust
|
# This may be a bit more robust
|
||||||
self.assertContains(response, '<td class="field-organization_type">Federal</td>', count=1)
|
self.assertContains(response, '<td class="field-organization_type">Federal</td>', count=1)
|
||||||
# Now let's make sure the long description does not exist
|
# Now let's make sure the long description does not exist
|
||||||
self.assertNotContains(response, "Federal: an agency of the U.S. government")
|
self.assertNotContains(response, "Federal: an agency of the U.S. government")
|
||||||
|
|
||||||
def transition_state_and_send_email(self, application, status):
|
def transition_state_and_send_email(self, application, status, rejection_reason=None):
|
||||||
"""Helper method for the email test cases."""
|
"""Helper method for the email test cases."""
|
||||||
|
|
||||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
|
@ -595,16 +602,20 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
# Create a mock request
|
# Create a mock request
|
||||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||||
|
|
||||||
# Modify the application's property
|
# Modify the application's properties
|
||||||
application.status = status
|
application.status = status
|
||||||
|
application.rejection_reason = rejection_reason
|
||||||
|
|
||||||
# Use the model admin's save_model method
|
# Use the model admin's save_model method
|
||||||
self.admin.save_model(request, application, form=None, change=True)
|
self.admin.save_model(request, application, form=None, change=True)
|
||||||
|
|
||||||
def assert_email_is_accurate(self, expected_string, email_index, email_address):
|
def assert_email_is_accurate(
|
||||||
|
self, expected_string, email_index, email_address, test_that_no_bcc=False, bcc_email_address=""
|
||||||
|
):
|
||||||
"""Helper method for the email test cases.
|
"""Helper method for the email test cases.
|
||||||
email_index is the index of the email in mock_client."""
|
email_index is the index of the email in mock_client."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
# Access the arguments passed to send_email
|
# Access the arguments passed to send_email
|
||||||
call_args = self.mock_client.EMAILS_SENT
|
call_args = self.mock_client.EMAILS_SENT
|
||||||
kwargs = call_args[email_index]["kwargs"]
|
kwargs = call_args[email_index]["kwargs"]
|
||||||
|
@ -620,13 +631,28 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.assertEqual(to_email, email_address)
|
self.assertEqual(to_email, email_address)
|
||||||
self.assertIn(expected_string, email_body)
|
self.assertIn(expected_string, email_body)
|
||||||
|
|
||||||
|
if test_that_no_bcc:
|
||||||
|
_ = ""
|
||||||
|
with self.assertRaises(KeyError):
|
||||||
|
with less_console_noise():
|
||||||
|
_ = kwargs["Destination"]["BccAddresses"][0]
|
||||||
|
self.assertEqual(_, "")
|
||||||
|
|
||||||
|
if bcc_email_address:
|
||||||
|
bcc_email = kwargs["Destination"]["BccAddresses"][0]
|
||||||
|
self.assertEqual(bcc_email, bcc_email_address)
|
||||||
|
|
||||||
def test_save_model_sends_submitted_email(self):
|
def test_save_model_sends_submitted_email(self):
|
||||||
"""When transitioning to submitted from started or withdrawn on a domain request,
|
"""When transitioning to submitted from started or withdrawn on a domain request,
|
||||||
an email is sent out.
|
an email is sent out.
|
||||||
|
|
||||||
When transitioning to submitted from dns needed or in review on a domain request,
|
When transitioning to submitted from dns needed or in review on a domain request,
|
||||||
no email is sent out."""
|
no email is sent out.
|
||||||
|
|
||||||
|
Also test that the default email set in settings is NOT BCCd on non-prod whenever
|
||||||
|
an email does go out."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
# Ensure there is no user with this email
|
# Ensure there is no user with this email
|
||||||
EMAIL = "mayor@igorville.gov"
|
EMAIL = "mayor@igorville.gov"
|
||||||
User.objects.filter(email=EMAIL).delete()
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
@ -636,13 +662,13 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
|
|
||||||
# Test Submitted Status from started
|
# Test Submitted Status from started
|
||||||
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
|
||||||
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL)
|
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, True)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
# Test Withdrawn Status
|
# Test Withdrawn Status
|
||||||
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN)
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN)
|
||||||
self.assert_email_is_accurate(
|
self.assert_email_is_accurate(
|
||||||
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL
|
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL, True
|
||||||
)
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
|
@ -670,10 +696,69 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||||
|
|
||||||
|
@override_settings(IS_PRODUCTION=True)
|
||||||
|
def test_save_model_sends_submitted_email_with_bcc_on_prod(self):
|
||||||
|
"""When transitioning to submitted from started or withdrawn on a domain request,
|
||||||
|
an email is sent out.
|
||||||
|
|
||||||
|
When transitioning to submitted from dns needed or in review on a domain request,
|
||||||
|
no email is sent out.
|
||||||
|
|
||||||
|
Also test that the default email set in settings IS BCCd on prod whenever
|
||||||
|
an email does go out."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
# Ensure there is no user with this email
|
||||||
|
EMAIL = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
|
||||||
|
BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
|
# Create a sample application
|
||||||
|
application = completed_application()
|
||||||
|
|
||||||
|
# Test Submitted Status from started
|
||||||
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
|
||||||
|
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
|
# Test Withdrawn Status
|
||||||
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN)
|
||||||
|
self.assert_email_is_accurate(
|
||||||
|
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL
|
||||||
|
)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
|
# Test Submitted Status Again (from withdrawn)
|
||||||
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
|
||||||
|
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||||
|
|
||||||
|
# Move it to IN_REVIEW
|
||||||
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||||
|
|
||||||
|
# Test Submitted Status Again from in IN_REVIEW, no new email should be sent
|
||||||
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||||
|
|
||||||
|
# Move it to IN_REVIEW
|
||||||
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||||
|
|
||||||
|
# Move it to ACTION_NEEDED
|
||||||
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||||
|
|
||||||
|
# Test Submitted Status Again from in ACTION_NEEDED, no new email should be sent
|
||||||
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||||
|
|
||||||
def test_save_model_sends_approved_email(self):
|
def test_save_model_sends_approved_email(self):
|
||||||
"""When transitioning to approved on a domain request,
|
"""When transitioning to approved on a domain request,
|
||||||
an email is sent out every time."""
|
an email is sent out every time."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
# Ensure there is no user with this email
|
# Ensure there is no user with this email
|
||||||
EMAIL = "mayor@igorville.gov"
|
EMAIL = "mayor@igorville.gov"
|
||||||
User.objects.filter(email=EMAIL).delete()
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
@ -687,7 +772,11 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
# Test Withdrawn Status
|
# Test Withdrawn Status
|
||||||
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
|
self.transition_state_and_send_email(
|
||||||
|
application,
|
||||||
|
DomainApplication.ApplicationStatus.REJECTED,
|
||||||
|
DomainApplication.RejectionReasons.DOMAIN_PURPOSE,
|
||||||
|
)
|
||||||
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL)
|
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
|
@ -695,10 +784,11 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||||
|
|
||||||
def test_save_model_sends_rejected_email(self):
|
def test_save_model_sends_rejected_email_purpose_not_met(self):
|
||||||
"""When transitioning to rejected on a domain request,
|
"""When transitioning to rejected on a domain request, an email is sent
|
||||||
an email is sent out every time."""
|
explaining why when the reason is domain purpose."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
# Ensure there is no user with this email
|
# Ensure there is no user with this email
|
||||||
EMAIL = "mayor@igorville.gov"
|
EMAIL = "mayor@igorville.gov"
|
||||||
User.objects.filter(email=EMAIL).delete()
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
@ -706,24 +796,256 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
# Create a sample application
|
# Create a sample application
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
|
|
||||||
# Test Submitted Status
|
# Reject for reason DOMAIN_PURPOSE and test email
|
||||||
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
|
self.transition_state_and_send_email(
|
||||||
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 0, EMAIL)
|
application,
|
||||||
|
DomainApplication.ApplicationStatus.REJECTED,
|
||||||
|
DomainApplication.RejectionReasons.DOMAIN_PURPOSE,
|
||||||
|
)
|
||||||
|
self.assert_email_is_accurate(
|
||||||
|
"Your domain request was rejected because the purpose you provided did not meet our \nrequirements.",
|
||||||
|
0,
|
||||||
|
EMAIL,
|
||||||
|
)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
# Test Withdrawn Status
|
# Approve
|
||||||
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
|
||||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
# Test Submitted Status Again (No new email should be sent)
|
def test_save_model_sends_rejected_email_requestor(self):
|
||||||
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
|
"""When transitioning to rejected on a domain request, an email is sent
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
explaining why when the reason is requestor."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
# Ensure there is no user with this email
|
||||||
|
EMAIL = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
|
||||||
|
# Create a sample application
|
||||||
|
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
|
|
||||||
|
# Reject for reason REQUESTOR and test email including dynamic organization name
|
||||||
|
self.transition_state_and_send_email(
|
||||||
|
application, DomainApplication.ApplicationStatus.REJECTED, DomainApplication.RejectionReasons.REQUESTOR
|
||||||
|
)
|
||||||
|
self.assert_email_is_accurate(
|
||||||
|
"Your domain request was rejected because we don’t believe you’re eligible to request a \n.gov "
|
||||||
|
"domain on behalf of Testorg",
|
||||||
|
0,
|
||||||
|
EMAIL,
|
||||||
|
)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
|
# Approve
|
||||||
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
|
||||||
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
|
def test_save_model_sends_rejected_email_org_has_domain(self):
|
||||||
|
"""When transitioning to rejected on a domain request, an email is sent
|
||||||
|
explaining why when the reason is second domain."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
# Ensure there is no user with this email
|
||||||
|
EMAIL = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
|
||||||
|
# Create a sample application
|
||||||
|
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
|
|
||||||
|
# Reject for reason SECOND_DOMAIN_REASONING and test email including dynamic organization name
|
||||||
|
self.transition_state_and_send_email(
|
||||||
|
application,
|
||||||
|
DomainApplication.ApplicationStatus.REJECTED,
|
||||||
|
DomainApplication.RejectionReasons.SECOND_DOMAIN_REASONING,
|
||||||
|
)
|
||||||
|
self.assert_email_is_accurate(
|
||||||
|
"Your domain request was rejected because Testorg has a .gov domain.", 0, EMAIL
|
||||||
|
)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
|
# Approve
|
||||||
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
|
||||||
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
|
def test_save_model_sends_rejected_email_contacts_or_org_legitimacy(self):
|
||||||
|
"""When transitioning to rejected on a domain request, an email is sent
|
||||||
|
explaining why when the reason is contacts or org legitimacy."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
# Ensure there is no user with this email
|
||||||
|
EMAIL = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
|
||||||
|
# Create a sample application
|
||||||
|
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
|
|
||||||
|
# Reject for reason CONTACTS_OR_ORGANIZATION_LEGITIMACY and test email including dynamic organization name
|
||||||
|
self.transition_state_and_send_email(
|
||||||
|
application,
|
||||||
|
DomainApplication.ApplicationStatus.REJECTED,
|
||||||
|
DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY,
|
||||||
|
)
|
||||||
|
self.assert_email_is_accurate(
|
||||||
|
"Your domain request was rejected because we could not verify the organizational \n"
|
||||||
|
"contacts you provided. If you have questions or comments, reply to this email.",
|
||||||
|
0,
|
||||||
|
EMAIL,
|
||||||
|
)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
|
# Approve
|
||||||
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
|
||||||
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
|
def test_save_model_sends_rejected_email_org_eligibility(self):
|
||||||
|
"""When transitioning to rejected on a domain request, an email is sent
|
||||||
|
explaining why when the reason is org eligibility."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
# Ensure there is no user with this email
|
||||||
|
EMAIL = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
|
||||||
|
# Create a sample application
|
||||||
|
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
|
|
||||||
|
# Reject for reason ORGANIZATION_ELIGIBILITY and test email including dynamic organization name
|
||||||
|
self.transition_state_and_send_email(
|
||||||
|
application,
|
||||||
|
DomainApplication.ApplicationStatus.REJECTED,
|
||||||
|
DomainApplication.RejectionReasons.ORGANIZATION_ELIGIBILITY,
|
||||||
|
)
|
||||||
|
self.assert_email_is_accurate(
|
||||||
|
"Your domain request was rejected because we determined that Testorg is not \neligible for "
|
||||||
|
"a .gov domain.",
|
||||||
|
0,
|
||||||
|
EMAIL,
|
||||||
|
)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
|
# Approve
|
||||||
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
|
||||||
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
|
def test_save_model_sends_rejected_email_naming(self):
|
||||||
|
"""When transitioning to rejected on a domain request, an email is sent
|
||||||
|
explaining why when the reason is naming."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
# Ensure there is no user with this email
|
||||||
|
EMAIL = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
|
||||||
|
# Create a sample application
|
||||||
|
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
|
|
||||||
|
# Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name
|
||||||
|
self.transition_state_and_send_email(
|
||||||
|
application,
|
||||||
|
DomainApplication.ApplicationStatus.REJECTED,
|
||||||
|
DomainApplication.RejectionReasons.NAMING_REQUIREMENTS,
|
||||||
|
)
|
||||||
|
self.assert_email_is_accurate(
|
||||||
|
"Your domain request was rejected because it does not meet our naming requirements.", 0, EMAIL
|
||||||
|
)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
|
# Approve
|
||||||
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
|
||||||
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
|
def test_save_model_sends_rejected_email_other(self):
|
||||||
|
"""When transitioning to rejected on a domain request, an email is sent
|
||||||
|
explaining why when the reason is other."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
# Ensure there is no user with this email
|
||||||
|
EMAIL = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
|
||||||
|
# Create a sample application
|
||||||
|
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
|
|
||||||
|
# Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name
|
||||||
|
self.transition_state_and_send_email(
|
||||||
|
application,
|
||||||
|
DomainApplication.ApplicationStatus.REJECTED,
|
||||||
|
DomainApplication.RejectionReasons.OTHER,
|
||||||
|
)
|
||||||
|
self.assert_email_is_accurate("Choosing a .gov domain name", 0, EMAIL)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||||
|
|
||||||
|
# Approve
|
||||||
|
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
|
||||||
|
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
||||||
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||||
|
|
||||||
|
def test_transition_to_rejected_without_rejection_reason_does_trigger_error(self):
|
||||||
|
"""
|
||||||
|
When transitioning to rejected without a rejection reason, admin throws a user friendly message.
|
||||||
|
|
||||||
|
The transition fails.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||||
|
|
||||||
|
# Create a request object with a superuser
|
||||||
|
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||||
|
request.user = self.superuser
|
||||||
|
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.enter_context(patch.object(messages, "error"))
|
||||||
|
application.status = DomainApplication.ApplicationStatus.REJECTED
|
||||||
|
|
||||||
|
self.admin.save_model(request, application, None, True)
|
||||||
|
|
||||||
|
messages.error.assert_called_once_with(
|
||||||
|
request,
|
||||||
|
"A rejection reason is required.",
|
||||||
|
)
|
||||||
|
|
||||||
|
application.refresh_from_db()
|
||||||
|
self.assertEqual(application.status, DomainApplication.ApplicationStatus.APPROVED)
|
||||||
|
|
||||||
|
def test_transition_to_rejected_with_rejection_reason_does_not_trigger_error(self):
|
||||||
|
"""
|
||||||
|
When transitioning to rejected with a rejection reason, admin does not throw an error alert.
|
||||||
|
|
||||||
|
The transition is successful.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||||
|
|
||||||
|
# Create a request object with a superuser
|
||||||
|
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||||
|
request.user = self.superuser
|
||||||
|
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.enter_context(patch.object(messages, "error"))
|
||||||
|
application.status = DomainApplication.ApplicationStatus.REJECTED
|
||||||
|
application.rejection_reason = DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY
|
||||||
|
|
||||||
|
self.admin.save_model(request, application, None, True)
|
||||||
|
|
||||||
|
messages.error.assert_not_called()
|
||||||
|
|
||||||
|
application.refresh_from_db()
|
||||||
|
self.assertEqual(application.status, DomainApplication.ApplicationStatus.REJECTED)
|
||||||
|
|
||||||
def test_save_model_sends_withdrawn_email(self):
|
def test_save_model_sends_withdrawn_email(self):
|
||||||
"""When transitioning to withdrawn on a domain request,
|
"""When transitioning to withdrawn on a domain request,
|
||||||
an email is sent out every time."""
|
an email is sent out every time."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
# Ensure there is no user with this email
|
# Ensure there is no user with this email
|
||||||
EMAIL = "mayor@igorville.gov"
|
EMAIL = "mayor@igorville.gov"
|
||||||
User.objects.filter(email=EMAIL).delete()
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
@ -748,6 +1070,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||||
|
|
||||||
def test_save_model_sets_approved_domain(self):
|
def test_save_model_sets_approved_domain(self):
|
||||||
|
with less_console_noise():
|
||||||
# make sure there is no user with this email
|
# make sure there is no user with this email
|
||||||
EMAIL = "mayor@igorville.gov"
|
EMAIL = "mayor@igorville.gov"
|
||||||
User.objects.filter(email=EMAIL).delete()
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
@ -759,7 +1082,6 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||||
|
|
||||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
with less_console_noise():
|
|
||||||
# Modify the application's property
|
# Modify the application's property
|
||||||
application.status = DomainApplication.ApplicationStatus.APPROVED
|
application.status = DomainApplication.ApplicationStatus.APPROVED
|
||||||
|
|
||||||
|
@ -770,6 +1092,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.assertEqual(application.requested_domain.name, application.approved_domain.name)
|
self.assertEqual(application.requested_domain.name, application.approved_domain.name)
|
||||||
|
|
||||||
def test_save_model_sets_restricted_status_on_user(self):
|
def test_save_model_sets_restricted_status_on_user(self):
|
||||||
|
with less_console_noise():
|
||||||
# make sure there is no user with this email
|
# make sure there is no user with this email
|
||||||
EMAIL = "mayor@igorville.gov"
|
EMAIL = "mayor@igorville.gov"
|
||||||
User.objects.filter(email=EMAIL).delete()
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
@ -781,7 +1104,6 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||||
|
|
||||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
with less_console_noise():
|
|
||||||
# Modify the application's property
|
# Modify the application's property
|
||||||
application.status = DomainApplication.ApplicationStatus.INELIGIBLE
|
application.status = DomainApplication.ApplicationStatus.INELIGIBLE
|
||||||
|
|
||||||
|
@ -792,9 +1114,9 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.assertEqual(application.creator.status, "restricted")
|
self.assertEqual(application.creator.status, "restricted")
|
||||||
|
|
||||||
def test_readonly_when_restricted_creator(self):
|
def test_readonly_when_restricted_creator(self):
|
||||||
|
with less_console_noise():
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
with less_console_noise():
|
|
||||||
application.creator.status = User.RESTRICTED
|
application.creator.status = User.RESTRICTED
|
||||||
application.creator.save()
|
application.creator.save()
|
||||||
|
|
||||||
|
@ -808,6 +1130,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"status",
|
"status",
|
||||||
|
"rejection_reason",
|
||||||
"creator",
|
"creator",
|
||||||
"investigator",
|
"investigator",
|
||||||
"organization_type",
|
"organization_type",
|
||||||
|
@ -843,6 +1166,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
def test_readonly_fields_for_analyst(self):
|
def test_readonly_fields_for_analyst(self):
|
||||||
|
with less_console_noise():
|
||||||
request = self.factory.get("/") # Use the correct method and path
|
request = self.factory.get("/") # Use the correct method and path
|
||||||
request.user = self.staffuser
|
request.user = self.staffuser
|
||||||
|
|
||||||
|
@ -864,6 +1188,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
def test_readonly_fields_for_superuser(self):
|
def test_readonly_fields_for_superuser(self):
|
||||||
|
with less_console_noise():
|
||||||
request = self.factory.get("/") # Use the correct method and path
|
request = self.factory.get("/") # Use the correct method and path
|
||||||
request.user = self.superuser
|
request.user = self.superuser
|
||||||
|
|
||||||
|
@ -874,10 +1199,10 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
def test_saving_when_restricted_creator(self):
|
def test_saving_when_restricted_creator(self):
|
||||||
|
with less_console_noise():
|
||||||
# Create an instance of the model
|
# Create an instance of the model
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
with less_console_noise():
|
|
||||||
application.creator.status = User.RESTRICTED
|
application.creator.status = User.RESTRICTED
|
||||||
application.creator.save()
|
application.creator.save()
|
||||||
|
|
||||||
|
@ -899,10 +1224,10 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW)
|
self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
|
|
||||||
def test_change_view_with_restricted_creator(self):
|
def test_change_view_with_restricted_creator(self):
|
||||||
|
with less_console_noise():
|
||||||
# Create an instance of the model
|
# Create an instance of the model
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
with less_console_noise():
|
|
||||||
application.creator.status = User.RESTRICTED
|
application.creator.status = User.RESTRICTED
|
||||||
application.creator.save()
|
application.creator.save()
|
||||||
|
|
||||||
|
@ -919,13 +1244,14 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
"Cannot edit an application with a restricted creator.",
|
"Cannot edit an application with a restricted creator.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def trigger_saving_approved_to_another_state(self, domain_is_active, another_state):
|
def trigger_saving_approved_to_another_state(self, domain_is_active, another_state, rejection_reason=None):
|
||||||
"""Helper method that triggers domain request state changes from approved to another state,
|
"""Helper method that triggers domain request state changes from approved to another state,
|
||||||
with an associated domain that can be either active (READY) or not.
|
with an associated domain that can be either active (READY) or not.
|
||||||
|
|
||||||
Used to test errors when saving a change with an active domain, also used to test side effects
|
Used to test errors when saving a change with an active domain, also used to test side effects
|
||||||
when saving a change goes through."""
|
when saving a change goes through."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
# Create an instance of the model
|
# Create an instance of the model
|
||||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
domain = Domain.objects.create(name=application.requested_domain.name)
|
||||||
|
@ -948,6 +1274,8 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
stack.enter_context(patch.object(messages, "error"))
|
stack.enter_context(patch.object(messages, "error"))
|
||||||
|
|
||||||
application.status = another_state
|
application.status = another_state
|
||||||
|
application.rejection_reason = rejection_reason
|
||||||
|
|
||||||
self.admin.save_model(request, application, None, True)
|
self.admin.save_model(request, application, None, True)
|
||||||
|
|
||||||
# Assert that the error message was called with the correct argument
|
# Assert that the error message was called with the correct argument
|
||||||
|
@ -989,7 +1317,11 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
||||||
|
|
||||||
def test_side_effects_when_saving_approved_to_rejected(self):
|
def test_side_effects_when_saving_approved_to_rejected(self):
|
||||||
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED)
|
self.trigger_saving_approved_to_another_state(
|
||||||
|
False,
|
||||||
|
DomainApplication.ApplicationStatus.REJECTED,
|
||||||
|
DomainApplication.RejectionReasons.CONTACTS_OR_ORGANIZATION_LEGITIMACY,
|
||||||
|
)
|
||||||
|
|
||||||
def test_side_effects_when_saving_approved_to_ineligible(self):
|
def test_side_effects_when_saving_approved_to_ineligible(self):
|
||||||
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.INELIGIBLE)
|
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.INELIGIBLE)
|
||||||
|
@ -1001,12 +1333,20 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
It retrieves the current list of filters from DomainApplicationAdmin
|
It retrieves the current list of filters from DomainApplicationAdmin
|
||||||
and checks that it matches the expected list of filters.
|
and checks that it matches the expected list of filters.
|
||||||
"""
|
"""
|
||||||
|
with less_console_noise():
|
||||||
request = self.factory.get("/")
|
request = self.factory.get("/")
|
||||||
request.user = self.superuser
|
request.user = self.superuser
|
||||||
|
|
||||||
# Grab the current list of table filters
|
# Grab the current list of table filters
|
||||||
readonly_fields = self.admin.get_list_filter(request)
|
readonly_fields = self.admin.get_list_filter(request)
|
||||||
expected_fields = ("status", "organization_type", DomainApplicationAdmin.InvestigatorFilter)
|
expected_fields = (
|
||||||
|
"status",
|
||||||
|
"organization_type",
|
||||||
|
"federal_type",
|
||||||
|
DomainApplicationAdmin.ElectionOfficeFilter,
|
||||||
|
"rejection_reason",
|
||||||
|
DomainApplicationAdmin.InvestigatorFilter,
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
|
@ -1020,6 +1360,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
that it matches the expected queryset,
|
that it matches the expected queryset,
|
||||||
which is sorted alphabetically by the 'requested_domain__name' field.
|
which is sorted alphabetically by the 'requested_domain__name' field.
|
||||||
"""
|
"""
|
||||||
|
with less_console_noise():
|
||||||
# Creates a list of DomainApplications in scrambled order
|
# Creates a list of DomainApplications in scrambled order
|
||||||
multiple_unalphabetical_domain_objects("application")
|
multiple_unalphabetical_domain_objects("application")
|
||||||
|
|
||||||
|
@ -1052,6 +1393,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
the filter displays correctly, when the filter isn't filtering correctly.
|
the filter displays correctly, when the filter isn't filtering correctly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
# Create a mock DomainApplication object, with a fake investigator
|
# Create a mock DomainApplication object, with a fake investigator
|
||||||
application: DomainApplication = generic_domain_object("application", "SomeGuy")
|
application: DomainApplication = generic_domain_object("application", "SomeGuy")
|
||||||
investigator_user = User.objects.filter(username=application.investigator.username).get()
|
investigator_user = User.objects.filter(username=application.investigator.username).get()
|
||||||
|
@ -1095,6 +1437,8 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
It then retrieves the queryset for the 'investigator' dropdown from DomainApplicationAdmin
|
It then retrieves the queryset for the 'investigator' dropdown from DomainApplicationAdmin
|
||||||
and checks that it matches the expected queryset, which only includes staff users.
|
and checks that it matches the expected queryset, which only includes staff users.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
# Create a mock DomainApplication object, with a fake investigator
|
# Create a mock DomainApplication object, with a fake investigator
|
||||||
application: DomainApplication = generic_domain_object("application", "SomeGuy")
|
application: DomainApplication = generic_domain_object("application", "SomeGuy")
|
||||||
investigator_user = User.objects.filter(username=application.investigator.username).get()
|
investigator_user = User.objects.filter(username=application.investigator.username).get()
|
||||||
|
@ -1138,6 +1482,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
||||||
This test verifies that filter list for the 'investigator'
|
This test verifies that filter list for the 'investigator'
|
||||||
is displayed alphabetically
|
is displayed alphabetically
|
||||||
"""
|
"""
|
||||||
|
with less_console_noise():
|
||||||
# Create a mock DomainApplication object, with a fake investigator
|
# Create a mock DomainApplication object, with a fake investigator
|
||||||
application: DomainApplication = generic_domain_object("application", "SomeGuy")
|
application: DomainApplication = generic_domain_object("application", "SomeGuy")
|
||||||
investigator_user = User.objects.filter(username=application.investigator.username).get()
|
investigator_user = User.objects.filter(username=application.investigator.username).get()
|
||||||
|
@ -1200,6 +1545,7 @@ class DomainInvitationAdminTest(TestCase):
|
||||||
|
|
||||||
def test_get_filters(self):
|
def test_get_filters(self):
|
||||||
"""Ensures that our filters are displaying correctly"""
|
"""Ensures that our filters are displaying correctly"""
|
||||||
|
with less_console_noise():
|
||||||
# Have to get creative to get past linter
|
# Have to get creative to get past linter
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
@ -1278,6 +1624,7 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
|
|
||||||
def test_readonly_fields_for_analyst(self):
|
def test_readonly_fields_for_analyst(self):
|
||||||
"""Ensures that analysts have their permissions setup correctly"""
|
"""Ensures that analysts have their permissions setup correctly"""
|
||||||
|
with less_console_noise():
|
||||||
request = self.factory.get("/")
|
request = self.factory.get("/")
|
||||||
request.user = self.staffuser
|
request.user = self.staffuser
|
||||||
|
|
||||||
|
@ -1299,6 +1646,7 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
|
|
||||||
def test_domain_sortable(self):
|
def test_domain_sortable(self):
|
||||||
"""Tests if DomainInformation sorts by domain correctly"""
|
"""Tests if DomainInformation sorts by domain correctly"""
|
||||||
|
with less_console_noise():
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
@ -1310,6 +1658,7 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
|
|
||||||
def test_submitter_sortable(self):
|
def test_submitter_sortable(self):
|
||||||
"""Tests if DomainInformation sorts by submitter correctly"""
|
"""Tests if DomainInformation sorts by submitter correctly"""
|
||||||
|
with less_console_noise():
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
@ -1347,6 +1696,7 @@ class UserDomainRoleAdminTest(TestCase):
|
||||||
|
|
||||||
def test_domain_sortable(self):
|
def test_domain_sortable(self):
|
||||||
"""Tests if the UserDomainrole sorts by domain correctly"""
|
"""Tests if the UserDomainrole sorts by domain correctly"""
|
||||||
|
with less_console_noise():
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
@ -1368,6 +1718,7 @@ class UserDomainRoleAdminTest(TestCase):
|
||||||
|
|
||||||
def test_user_sortable(self):
|
def test_user_sortable(self):
|
||||||
"""Tests if the UserDomainrole sorts by user correctly"""
|
"""Tests if the UserDomainrole sorts by user correctly"""
|
||||||
|
with less_console_noise():
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
@ -1390,6 +1741,7 @@ class UserDomainRoleAdminTest(TestCase):
|
||||||
def test_email_not_in_search(self):
|
def test_email_not_in_search(self):
|
||||||
"""Tests the search bar in Django Admin for UserDomainRoleAdmin.
|
"""Tests the search bar in Django Admin for UserDomainRoleAdmin.
|
||||||
Should return no results for an invalid email."""
|
Should return no results for an invalid email."""
|
||||||
|
with less_console_noise():
|
||||||
# Have to get creative to get past linter
|
# Have to get creative to get past linter
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
@ -1422,6 +1774,7 @@ class UserDomainRoleAdminTest(TestCase):
|
||||||
def test_email_in_search(self):
|
def test_email_in_search(self):
|
||||||
"""Tests the search bar in Django Admin for UserDomainRoleAdmin.
|
"""Tests the search bar in Django Admin for UserDomainRoleAdmin.
|
||||||
Should return results for an valid email."""
|
Should return results for an valid email."""
|
||||||
|
with less_console_noise():
|
||||||
# Have to get creative to get past linter
|
# Have to get creative to get past linter
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
@ -1531,6 +1884,7 @@ class MyUserAdminTest(TestCase):
|
||||||
self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site)
|
self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site)
|
||||||
|
|
||||||
def test_list_display_without_username(self):
|
def test_list_display_without_username(self):
|
||||||
|
with less_console_noise():
|
||||||
request = self.client.request().wsgi_request
|
request = self.client.request().wsgi_request
|
||||||
request.user = create_user()
|
request.user = create_user()
|
||||||
|
|
||||||
|
@ -1547,6 +1901,7 @@ class MyUserAdminTest(TestCase):
|
||||||
self.assertNotIn("username", list_display)
|
self.assertNotIn("username", list_display)
|
||||||
|
|
||||||
def test_get_fieldsets_superuser(self):
|
def test_get_fieldsets_superuser(self):
|
||||||
|
with less_console_noise():
|
||||||
request = self.client.request().wsgi_request
|
request = self.client.request().wsgi_request
|
||||||
request.user = create_superuser()
|
request.user = create_superuser()
|
||||||
fieldsets = self.admin.get_fieldsets(request)
|
fieldsets = self.admin.get_fieldsets(request)
|
||||||
|
@ -1554,6 +1909,7 @@ class MyUserAdminTest(TestCase):
|
||||||
self.assertEqual(fieldsets, expected_fieldsets)
|
self.assertEqual(fieldsets, expected_fieldsets)
|
||||||
|
|
||||||
def test_get_fieldsets_cisa_analyst(self):
|
def test_get_fieldsets_cisa_analyst(self):
|
||||||
|
with less_console_noise():
|
||||||
request = self.client.request().wsgi_request
|
request = self.client.request().wsgi_request
|
||||||
request.user = create_user()
|
request.user = create_user()
|
||||||
fieldsets = self.admin.get_fieldsets(request)
|
fieldsets = self.admin.get_fieldsets(request)
|
||||||
|
@ -1576,6 +1932,7 @@ class AuditedAdminTest(TestCase):
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
|
||||||
def order_by_desired_field_helper(self, obj_to_sort: AuditedAdmin, request, field_name, *obj_names):
|
def order_by_desired_field_helper(self, obj_to_sort: AuditedAdmin, request, field_name, *obj_names):
|
||||||
|
with less_console_noise():
|
||||||
formatted_sort_fields = []
|
formatted_sort_fields = []
|
||||||
for obj in obj_names:
|
for obj in obj_names:
|
||||||
formatted_sort_fields.append("{}__{}".format(field_name, obj))
|
formatted_sort_fields.append("{}__{}".format(field_name, obj))
|
||||||
|
@ -1622,6 +1979,7 @@ class AuditedAdminTest(TestCase):
|
||||||
|
|
||||||
# This test case should be refactored in general, as it is too overly specific and engineered
|
# This test case should be refactored in general, as it is too overly specific and engineered
|
||||||
def test_alphabetically_sorted_fk_fields_domain_application(self):
|
def test_alphabetically_sorted_fk_fields_domain_application(self):
|
||||||
|
with less_console_noise():
|
||||||
tested_fields = [
|
tested_fields = [
|
||||||
DomainApplication.authorizing_official.field,
|
DomainApplication.authorizing_official.field,
|
||||||
DomainApplication.submitter.field,
|
DomainApplication.submitter.field,
|
||||||
|
@ -1679,6 +2037,7 @@ class AuditedAdminTest(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_alphabetically_sorted_fk_fields_domain_information(self):
|
def test_alphabetically_sorted_fk_fields_domain_information(self):
|
||||||
|
with less_console_noise():
|
||||||
tested_fields = [
|
tested_fields = [
|
||||||
DomainInformation.authorizing_official.field,
|
DomainInformation.authorizing_official.field,
|
||||||
DomainInformation.submitter.field,
|
DomainInformation.submitter.field,
|
||||||
|
@ -1738,6 +2097,7 @@ class AuditedAdminTest(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_alphabetically_sorted_fk_fields_domain_invitation(self):
|
def test_alphabetically_sorted_fk_fields_domain_invitation(self):
|
||||||
|
with less_console_noise():
|
||||||
tested_fields = [DomainInvitation.domain.field]
|
tested_fields = [DomainInvitation.domain.field]
|
||||||
|
|
||||||
# Creates multiple domain applications - review status does not matter
|
# Creates multiple domain applications - review status does not matter
|
||||||
|
@ -1811,6 +2171,7 @@ class DomainSessionVariableTest(TestCase):
|
||||||
def test_session_vars_set_correctly(self):
|
def test_session_vars_set_correctly(self):
|
||||||
"""Checks if session variables are being set correctly"""
|
"""Checks if session variables are being set correctly"""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
@ -1826,6 +2187,7 @@ class DomainSessionVariableTest(TestCase):
|
||||||
def test_session_vars_set_correctly_hardcoded_domain(self):
|
def test_session_vars_set_correctly_hardcoded_domain(self):
|
||||||
"""Checks if session variables are being set correctly"""
|
"""Checks if session variables are being set correctly"""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
@ -1840,6 +2202,7 @@ class DomainSessionVariableTest(TestCase):
|
||||||
def test_session_variables_reset_correctly(self):
|
def test_session_variables_reset_correctly(self):
|
||||||
"""Checks if incorrect session variables get overridden"""
|
"""Checks if incorrect session variables get overridden"""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
@ -1857,6 +2220,7 @@ class DomainSessionVariableTest(TestCase):
|
||||||
def test_session_variables_retain_information(self):
|
def test_session_variables_retain_information(self):
|
||||||
"""Checks to see if session variables retain old information"""
|
"""Checks to see if session variables retain old information"""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
@ -1871,6 +2235,7 @@ class DomainSessionVariableTest(TestCase):
|
||||||
def test_session_variables_concurrent_requests(self):
|
def test_session_variables_concurrent_requests(self):
|
||||||
"""Simulates two requests at once"""
|
"""Simulates two requests at once"""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
p = "adminpass"
|
p = "adminpass"
|
||||||
self.client.login(username="superuser", password=p)
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
@ -1930,6 +2295,7 @@ class ContactAdminTest(TestCase):
|
||||||
self.staffuser = create_user()
|
self.staffuser = create_user()
|
||||||
|
|
||||||
def test_readonly_when_restricted_staffuser(self):
|
def test_readonly_when_restricted_staffuser(self):
|
||||||
|
with less_console_noise():
|
||||||
request = self.factory.get("/")
|
request = self.factory.get("/")
|
||||||
request.user = self.staffuser
|
request.user = self.staffuser
|
||||||
|
|
||||||
|
@ -1942,6 +2308,7 @@ class ContactAdminTest(TestCase):
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
def test_readonly_when_restricted_superuser(self):
|
def test_readonly_when_restricted_superuser(self):
|
||||||
|
with less_console_noise():
|
||||||
request = self.factory.get("/")
|
request = self.factory.get("/")
|
||||||
request.user = self.superuser
|
request.user = self.superuser
|
||||||
|
|
||||||
|
@ -1954,7 +2321,7 @@ class ContactAdminTest(TestCase):
|
||||||
def test_change_view_for_joined_contact_five_or_less(self):
|
def test_change_view_for_joined_contact_five_or_less(self):
|
||||||
"""Create a contact, join it to 4 domain requests. The 5th join will be a user.
|
"""Create a contact, join it to 4 domain requests. The 5th join will be a user.
|
||||||
Assert that the warning on the contact form lists 5 joins."""
|
Assert that the warning on the contact form lists 5 joins."""
|
||||||
|
with less_console_noise():
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
# Create an instance of the model
|
# Create an instance of the model
|
||||||
|
@ -2036,6 +2403,7 @@ class VerifiedByStaffAdminTestCase(TestCase):
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
def test_save_model_sets_user_field(self):
|
def test_save_model_sets_user_field(self):
|
||||||
|
with less_console_noise():
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
# Create an instance of the admin class
|
# Create an instance of the admin class
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
from django.test import Client, TestCase, override_settings
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
|
|
||||||
|
|
||||||
class MyTestCase(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.client = Client()
|
|
||||||
username = "test_user"
|
|
||||||
first_name = "First"
|
|
||||||
last_name = "Last"
|
|
||||||
email = "info@example.com"
|
|
||||||
self.user = get_user_model().objects.create(
|
|
||||||
username=username, first_name=first_name, last_name=last_name, email=email
|
|
||||||
)
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
super().tearDown()
|
|
||||||
self.user.delete()
|
|
||||||
|
|
||||||
@override_settings(IS_PRODUCTION=True)
|
|
||||||
def test_production_environment(self):
|
|
||||||
"""No banner on prod."""
|
|
||||||
home_page = self.client.get("/")
|
|
||||||
self.assertNotContains(home_page, "You are on a test site.")
|
|
||||||
|
|
||||||
@override_settings(IS_PRODUCTION=False)
|
|
||||||
def test_non_production_environment(self):
|
|
||||||
"""Banner on non-prod."""
|
|
||||||
home_page = self.client.get("/")
|
|
||||||
self.assertContains(home_page, "You are on a test site.")
|
|
|
@ -574,6 +574,56 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
self.approved_application.reject_with_prejudice()
|
self.approved_application.reject_with_prejudice()
|
||||||
|
|
||||||
|
def test_approve_from_rejected_clears_rejection_reason(self):
|
||||||
|
"""When transitioning from rejected to approved on a domain request,
|
||||||
|
the rejection_reason is cleared."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
# Create a sample application
|
||||||
|
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
|
||||||
|
application.rejection_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE
|
||||||
|
|
||||||
|
# Approve
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
|
application.approve()
|
||||||
|
|
||||||
|
self.assertEqual(application.status, DomainApplication.ApplicationStatus.APPROVED)
|
||||||
|
self.assertEqual(application.rejection_reason, None)
|
||||||
|
|
||||||
|
def test_in_review_from_rejected_clears_rejection_reason(self):
|
||||||
|
"""When transitioning from rejected to in_review on a domain request,
|
||||||
|
the rejection_reason is cleared."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
# Create a sample application
|
||||||
|
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
|
||||||
|
application.domain_is_not_active = True
|
||||||
|
application.rejection_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE
|
||||||
|
|
||||||
|
# Approve
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
|
application.in_review()
|
||||||
|
|
||||||
|
self.assertEqual(application.status, DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||||
|
self.assertEqual(application.rejection_reason, None)
|
||||||
|
|
||||||
|
def test_action_needed_from_rejected_clears_rejection_reason(self):
|
||||||
|
"""When transitioning from rejected to action_needed on a domain request,
|
||||||
|
the rejection_reason is cleared."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
# Create a sample application
|
||||||
|
application = completed_application(status=DomainApplication.ApplicationStatus.REJECTED)
|
||||||
|
application.domain_is_not_active = True
|
||||||
|
application.rejection_reason = DomainApplication.RejectionReasons.DOMAIN_PURPOSE
|
||||||
|
|
||||||
|
# Approve
|
||||||
|
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||||
|
application.action_needed()
|
||||||
|
|
||||||
|
self.assertEqual(application.status, DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
||||||
|
self.assertEqual(application.rejection_reason, None)
|
||||||
|
|
||||||
def test_has_rationale_returns_true(self):
|
def test_has_rationale_returns_true(self):
|
||||||
"""has_rationale() returns true when an application has no_other_contacts_rationale"""
|
"""has_rationale() returns true when an application has no_other_contacts_rationale"""
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
|
|
|
@ -20,7 +20,7 @@ from registrar.models.user import User
|
||||||
from registrar.utility.errors import ActionNotAllowed, NameserverError
|
from registrar.utility.errors import ActionNotAllowed, NameserverError
|
||||||
|
|
||||||
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
|
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
|
||||||
|
from registrar.utility import errors
|
||||||
|
|
||||||
from django_fsm import TransitionNotAllowed # type: ignore
|
from django_fsm import TransitionNotAllowed # type: ignore
|
||||||
from epplibwrapper import (
|
from epplibwrapper import (
|
||||||
|
@ -96,7 +96,7 @@ class TestDomainCache(MockEppLib):
|
||||||
|
|
||||||
self.mockedSendFunction.assert_has_calls(expectedCalls)
|
self.mockedSendFunction.assert_has_calls(expectedCalls)
|
||||||
|
|
||||||
def test_cache_nested_elements(self):
|
def test_cache_nested_elements_not_subdomain(self):
|
||||||
"""Cache works correctly with the nested objects cache and hosts"""
|
"""Cache works correctly with the nested objects cache and hosts"""
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
|
@ -113,7 +113,7 @@ class TestDomainCache(MockEppLib):
|
||||||
}
|
}
|
||||||
expectedHostsDict = {
|
expectedHostsDict = {
|
||||||
"name": self.mockDataInfoDomain.hosts[0],
|
"name": self.mockDataInfoDomain.hosts[0],
|
||||||
"addrs": [item.addr for item in self.mockDataInfoHosts.addrs],
|
"addrs": [], # should return empty bc fake.host.com is not a subdomain of igorville.gov
|
||||||
"cr_date": self.mockDataInfoHosts.cr_date,
|
"cr_date": self.mockDataInfoHosts.cr_date,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,6 +138,59 @@ class TestDomainCache(MockEppLib):
|
||||||
# invalidate cache
|
# invalidate cache
|
||||||
domain._cache = {}
|
domain._cache = {}
|
||||||
|
|
||||||
|
# get host
|
||||||
|
domain._get_property("hosts")
|
||||||
|
# Should return empty bc fake.host.com is not a subdomain of igorville.gov
|
||||||
|
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||||
|
|
||||||
|
# get contacts
|
||||||
|
domain._get_property("contacts")
|
||||||
|
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||||
|
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
|
||||||
|
|
||||||
|
def test_cache_nested_elements_is_subdomain(self):
|
||||||
|
"""Cache works correctly with the nested objects cache and hosts"""
|
||||||
|
with less_console_noise():
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="meoward.gov")
|
||||||
|
|
||||||
|
# The contact list will initially contain objects of type 'DomainContact'
|
||||||
|
# this is then transformed into PublicContact, and cache should NOT
|
||||||
|
# hold onto the DomainContact object
|
||||||
|
expectedUnfurledContactsList = [
|
||||||
|
common.DomainContact(contact="123", type="security"),
|
||||||
|
]
|
||||||
|
expectedContactsDict = {
|
||||||
|
PublicContact.ContactTypeChoices.ADMINISTRATIVE: None,
|
||||||
|
PublicContact.ContactTypeChoices.SECURITY: "123",
|
||||||
|
PublicContact.ContactTypeChoices.TECHNICAL: None,
|
||||||
|
}
|
||||||
|
expectedHostsDict = {
|
||||||
|
"name": self.mockDataInfoDomainSubdomain.hosts[0],
|
||||||
|
"addrs": [item.addr for item in self.mockDataInfoHosts.addrs],
|
||||||
|
"cr_date": self.mockDataInfoHosts.cr_date,
|
||||||
|
}
|
||||||
|
|
||||||
|
# this can be changed when the getter for contacts is implemented
|
||||||
|
domain._get_property("contacts")
|
||||||
|
|
||||||
|
# check domain info is still correct and not overridden
|
||||||
|
self.assertEqual(domain._cache["auth_info"], self.mockDataInfoDomainSubdomain.auth_info)
|
||||||
|
self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomainSubdomain.cr_date)
|
||||||
|
|
||||||
|
# check contacts
|
||||||
|
self.assertEqual(domain._cache["_contacts"], self.mockDataInfoDomainSubdomain.contacts)
|
||||||
|
# The contact list should not contain what is sent by the registry by default,
|
||||||
|
# as _fetch_cache will transform the type to PublicContact
|
||||||
|
self.assertNotEqual(domain._cache["contacts"], expectedUnfurledContactsList)
|
||||||
|
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
|
||||||
|
|
||||||
|
# get and check hosts is set correctly
|
||||||
|
domain._get_property("hosts")
|
||||||
|
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||||
|
self.assertEqual(domain._cache["contacts"], expectedContactsDict)
|
||||||
|
# invalidate cache
|
||||||
|
domain._cache = {}
|
||||||
|
|
||||||
# get host
|
# get host
|
||||||
domain._get_property("hosts")
|
domain._get_property("hosts")
|
||||||
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
self.assertEqual(domain._cache["hosts"], [expectedHostsDict])
|
||||||
|
@ -268,13 +321,13 @@ class TestDomainCreation(MockEppLib):
|
||||||
Then a Domain exists in the database with the same `name`
|
Then a Domain exists in the database with the same `name`
|
||||||
But a domain object does not exist in the registry
|
But a domain object does not exist in the registry
|
||||||
"""
|
"""
|
||||||
|
with less_console_noise():
|
||||||
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
|
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
|
||||||
user, _ = User.objects.get_or_create()
|
user, _ = User.objects.get_or_create()
|
||||||
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
|
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
|
||||||
|
|
||||||
mock_client = MockSESClient()
|
mock_client = MockSESClient()
|
||||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||||
with less_console_noise():
|
|
||||||
# skip using the submit method
|
# skip using the submit method
|
||||||
application.status = DomainApplication.ApplicationStatus.SUBMITTED
|
application.status = DomainApplication.ApplicationStatus.SUBMITTED
|
||||||
# transition to approve state
|
# transition to approve state
|
||||||
|
@ -293,6 +346,7 @@ class TestDomainCreation(MockEppLib):
|
||||||
And `domain.state` is set to `UNKNOWN`
|
And `domain.state` is set to `UNKNOWN`
|
||||||
And `domain.is_active()` returns False
|
And `domain.is_active()` returns False
|
||||||
"""
|
"""
|
||||||
|
with less_console_noise():
|
||||||
domain = Domain.objects.create(name="beef-tongue.gov")
|
domain = Domain.objects.create(name="beef-tongue.gov")
|
||||||
# trigger getter
|
# trigger getter
|
||||||
_ = domain.statuses
|
_ = domain.statuses
|
||||||
|
@ -329,6 +383,7 @@ class TestDomainCreation(MockEppLib):
|
||||||
|
|
||||||
def test_minimal_creation(self):
|
def test_minimal_creation(self):
|
||||||
"""Can create with just a name."""
|
"""Can create with just a name."""
|
||||||
|
with less_console_noise():
|
||||||
Domain.objects.create(name="igorville.gov")
|
Domain.objects.create(name="igorville.gov")
|
||||||
|
|
||||||
@skip("assertion broken with mock addition")
|
@skip("assertion broken with mock addition")
|
||||||
|
@ -451,6 +506,7 @@ class TestDomainAvailable(MockEppLib):
|
||||||
res_data=[responses.check.CheckDomainResultData(name="available.gov", avail=True, reason=None)],
|
res_data=[responses.check.CheckDomainResultData(name="available.gov", avail=True, reason=None)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
patcher = patch("registrar.models.domain.registry.send")
|
patcher = patch("registrar.models.domain.registry.send")
|
||||||
mocked_send = patcher.start()
|
mocked_send = patcher.start()
|
||||||
mocked_send.side_effect = side_effect
|
mocked_send.side_effect = side_effect
|
||||||
|
@ -484,6 +540,7 @@ class TestDomainAvailable(MockEppLib):
|
||||||
res_data=[responses.check.CheckDomainResultData(name="unavailable.gov", avail=False, reason="In Use")],
|
res_data=[responses.check.CheckDomainResultData(name="unavailable.gov", avail=False, reason="In Use")],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
patcher = patch("registrar.models.domain.registry.send")
|
patcher = patch("registrar.models.domain.registry.send")
|
||||||
mocked_send = patcher.start()
|
mocked_send = patcher.start()
|
||||||
mocked_send.side_effect = side_effect
|
mocked_send.side_effect = side_effect
|
||||||
|
@ -502,16 +559,28 @@ class TestDomainAvailable(MockEppLib):
|
||||||
self.assertFalse(available)
|
self.assertFalse(available)
|
||||||
patcher.stop()
|
patcher.stop()
|
||||||
|
|
||||||
def test_domain_available_with_value_error(self):
|
def test_domain_available_with_invalid_error(self):
|
||||||
"""
|
"""
|
||||||
Scenario: Testing whether an invalid domain is available
|
Scenario: Testing whether an invalid domain is available
|
||||||
Should throw ValueError
|
Should throw InvalidDomainError
|
||||||
|
|
||||||
Validate ValueError is raised
|
Validate InvalidDomainError is raised
|
||||||
"""
|
"""
|
||||||
with self.assertRaises(ValueError):
|
with less_console_noise():
|
||||||
|
with self.assertRaises(errors.InvalidDomainError):
|
||||||
Domain.available("invalid-string")
|
Domain.available("invalid-string")
|
||||||
|
|
||||||
|
def test_domain_available_with_empty_string(self):
|
||||||
|
"""
|
||||||
|
Scenario: Testing whether an empty string domain name is available
|
||||||
|
Should throw InvalidDomainError
|
||||||
|
|
||||||
|
Validate InvalidDomainError is raised
|
||||||
|
"""
|
||||||
|
with less_console_noise():
|
||||||
|
with self.assertRaises(errors.InvalidDomainError):
|
||||||
|
Domain.available("")
|
||||||
|
|
||||||
def test_domain_available_unsuccessful(self):
|
def test_domain_available_unsuccessful(self):
|
||||||
"""
|
"""
|
||||||
Scenario: Testing behavior when registry raises a RegistryError
|
Scenario: Testing behavior when registry raises a RegistryError
|
||||||
|
@ -522,6 +591,7 @@ class TestDomainAvailable(MockEppLib):
|
||||||
def side_effect(_request, cleaned):
|
def side_effect(_request, cleaned):
|
||||||
raise RegistryError(code=ErrorCode.COMMAND_SYNTAX_ERROR)
|
raise RegistryError(code=ErrorCode.COMMAND_SYNTAX_ERROR)
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
patcher = patch("registrar.models.domain.registry.send")
|
patcher = patch("registrar.models.domain.registry.send")
|
||||||
mocked_send = patcher.start()
|
mocked_send = patcher.start()
|
||||||
mocked_send.side_effect = side_effect
|
mocked_send.side_effect = side_effect
|
||||||
|
@ -1572,31 +1642,100 @@ class TestRegistrantNameservers(MockEppLib):
|
||||||
self.assertEqual(nameservers[0][1], ["1.1.1.1"])
|
self.assertEqual(nameservers[0][1], ["1.1.1.1"])
|
||||||
patcher.stop()
|
patcher.stop()
|
||||||
|
|
||||||
def test_nameservers_stored_on_fetch_cache(self):
|
def test_nameservers_stored_on_fetch_cache_a_subdomain_with_ip(self):
|
||||||
|
"""
|
||||||
|
#1: Nameserver is a subdomain, and has an IP address
|
||||||
|
referenced by mockDataInfoDomainSubdomainAndIPAddress
|
||||||
|
"""
|
||||||
|
with less_console_noise():
|
||||||
|
# make the domain
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="meow.gov", state=Domain.State.READY)
|
||||||
|
|
||||||
|
# mock the get_or_create methods for Host and HostIP
|
||||||
|
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
|
||||||
|
HostIP.objects, "get_or_create"
|
||||||
|
) as mock_host_ip_get_or_create:
|
||||||
|
mock_host_get_or_create.return_value = (Host(domain=domain), True)
|
||||||
|
mock_host_ip_get_or_create.return_value = (HostIP(), True)
|
||||||
|
|
||||||
|
# force fetch_cache to be called, which will return above documented mocked hosts
|
||||||
|
domain.nameservers
|
||||||
|
|
||||||
|
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.meow.gov")
|
||||||
|
# Retrieve the mocked_host from the return value of the mock
|
||||||
|
actual_mocked_host, _ = mock_host_get_or_create.return_value
|
||||||
|
mock_host_ip_get_or_create.assert_called_with(address="2.0.0.8", host=actual_mocked_host)
|
||||||
|
self.assertEqual(mock_host_ip_get_or_create.call_count, 1)
|
||||||
|
|
||||||
|
def test_nameservers_stored_on_fetch_cache_a_subdomain_without_ip(self):
|
||||||
|
"""
|
||||||
|
#2: Nameserver is a subdomain, but doesn't have an IP address associated
|
||||||
|
referenced by mockDataInfoDomainSubdomainNoIP
|
||||||
|
"""
|
||||||
|
with less_console_noise():
|
||||||
|
# make the domain
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="subdomainwoip.gov", state=Domain.State.READY)
|
||||||
|
|
||||||
|
# mock the get_or_create methods for Host and HostIP
|
||||||
|
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
|
||||||
|
HostIP.objects, "get_or_create"
|
||||||
|
) as mock_host_ip_get_or_create:
|
||||||
|
mock_host_get_or_create.return_value = (Host(domain=domain), True)
|
||||||
|
mock_host_ip_get_or_create.return_value = (HostIP(), True)
|
||||||
|
|
||||||
|
# force fetch_cache to be called, which will return above documented mocked hosts
|
||||||
|
domain.nameservers
|
||||||
|
|
||||||
|
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.subdomainwoip.gov")
|
||||||
|
mock_host_ip_get_or_create.assert_not_called()
|
||||||
|
self.assertEqual(mock_host_ip_get_or_create.call_count, 0)
|
||||||
|
|
||||||
|
def test_nameservers_stored_on_fetch_cache_not_subdomain_with_ip(self):
|
||||||
"""
|
"""
|
||||||
Scenario: Nameservers are stored in db when they are retrieved from fetch_cache.
|
Scenario: Nameservers are stored in db when they are retrieved from fetch_cache.
|
||||||
Verify the success of this by asserting get_or_create calls to db.
|
Verify the success of this by asserting get_or_create calls to db.
|
||||||
The mocked data for the EPP calls returns a host name
|
The mocked data for the EPP calls returns a host name
|
||||||
of 'fake.host.com' from InfoDomain and an array of 2 IPs: 1.2.3.4 and 2.3.4.5
|
of 'fake.host.com' from InfoDomain and an array of 2 IPs: 1.2.3.4 and 2.3.4.5
|
||||||
from InfoHost
|
from InfoHost
|
||||||
|
|
||||||
|
#3: Nameserver is not a subdomain, but it does have an IP address returned
|
||||||
|
due to how we set up our defaults
|
||||||
"""
|
"""
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||||
# mock the get_or_create methods for Host and HostIP
|
|
||||||
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
|
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
|
||||||
HostIP.objects, "get_or_create"
|
HostIP.objects, "get_or_create"
|
||||||
) as mock_host_ip_get_or_create:
|
) as mock_host_ip_get_or_create:
|
||||||
# Set the return value for the mocks
|
mock_host_get_or_create.return_value = (Host(domain=domain), True)
|
||||||
mock_host_get_or_create.return_value = (Host(), True)
|
|
||||||
mock_host_ip_get_or_create.return_value = (HostIP(), True)
|
mock_host_ip_get_or_create.return_value = (HostIP(), True)
|
||||||
|
|
||||||
# force fetch_cache to be called, which will return above documented mocked hosts
|
# force fetch_cache to be called, which will return above documented mocked hosts
|
||||||
domain.nameservers
|
domain.nameservers
|
||||||
# assert that the mocks are called
|
|
||||||
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.host.com")
|
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.host.com")
|
||||||
# Retrieve the mocked_host from the return value of the mock
|
mock_host_ip_get_or_create.assert_not_called()
|
||||||
actual_mocked_host, _ = mock_host_get_or_create.return_value
|
self.assertEqual(mock_host_ip_get_or_create.call_count, 0)
|
||||||
mock_host_ip_get_or_create.assert_called_with(address="2.3.4.5", host=actual_mocked_host)
|
|
||||||
self.assertEqual(mock_host_ip_get_or_create.call_count, 2)
|
def test_nameservers_stored_on_fetch_cache_not_subdomain_without_ip(self):
|
||||||
|
"""
|
||||||
|
#4: Nameserver is not a subdomain and doesn't have an associated IP address
|
||||||
|
referenced by self.mockDataInfoDomainNotSubdomainNoIP
|
||||||
|
"""
|
||||||
|
with less_console_noise():
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="fakemeow.gov", state=Domain.State.READY)
|
||||||
|
|
||||||
|
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
|
||||||
|
HostIP.objects, "get_or_create"
|
||||||
|
) as mock_host_ip_get_or_create:
|
||||||
|
mock_host_get_or_create.return_value = (Host(domain=domain), True)
|
||||||
|
mock_host_ip_get_or_create.return_value = (HostIP(), True)
|
||||||
|
|
||||||
|
# force fetch_cache to be called, which will return above documented mocked hosts
|
||||||
|
domain.nameservers
|
||||||
|
mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.meow.com")
|
||||||
|
mock_host_ip_get_or_create.assert_not_called()
|
||||||
|
self.assertEqual(mock_host_ip_get_or_create.call_count, 0)
|
||||||
|
|
||||||
@skip("not implemented yet")
|
@skip("not implemented yet")
|
||||||
def test_update_is_unsuccessful(self):
|
def test_update_is_unsuccessful(self):
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase, override_settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from .common import MockEppLib # type: ignore
|
from .common import MockEppLib # type: ignore
|
||||||
|
@ -50,3 +50,32 @@ class TestWithUser(MockEppLib):
|
||||||
DomainApplication.objects.all().delete()
|
DomainApplication.objects.all().delete()
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
self.user.delete()
|
self.user.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnvironmentVariablesEffects(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
username = "test_user"
|
||||||
|
first_name = "First"
|
||||||
|
last_name = "Last"
|
||||||
|
email = "info@example.com"
|
||||||
|
self.user = get_user_model().objects.create(
|
||||||
|
username=username, first_name=first_name, last_name=last_name, email=email
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
self.user.delete()
|
||||||
|
|
||||||
|
@override_settings(IS_PRODUCTION=True)
|
||||||
|
def test_production_environment(self):
|
||||||
|
"""No banner on prod."""
|
||||||
|
home_page = self.client.get("/")
|
||||||
|
self.assertNotContains(home_page, "You are on a test site.")
|
||||||
|
|
||||||
|
@override_settings(IS_PRODUCTION=False)
|
||||||
|
def test_non_production_environment(self):
|
||||||
|
"""Banner on non-prod."""
|
||||||
|
home_page = self.client.get("/")
|
||||||
|
self.assertContains(home_page, "You are on a test site.")
|
||||||
|
|
|
@ -821,14 +821,15 @@ class TestDomainNameservers(TestDomainOverview):
|
||||||
nameserver1 = "ns1.igorville.gov"
|
nameserver1 = "ns1.igorville.gov"
|
||||||
nameserver2 = "ns2.igorville.gov"
|
nameserver2 = "ns2.igorville.gov"
|
||||||
valid_ip = "1.1. 1.1"
|
valid_ip = "1.1. 1.1"
|
||||||
# initial nameservers page has one server with two ips
|
valid_ip_2 = "2.2. 2.2"
|
||||||
# have to throw an error in order to test that the whitespace has been stripped from ip
|
# have to throw an error in order to test that the whitespace has been stripped from ip
|
||||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
# attempt to submit the form without one host and an ip with whitespace
|
# attempt to submit the form without one host and an ip with whitespace
|
||||||
nameservers_page.form["form-0-server"] = nameserver1
|
nameservers_page.form["form-0-server"] = nameserver1
|
||||||
nameservers_page.form["form-1-ip"] = valid_ip
|
nameservers_page.form["form-0-ip"] = valid_ip
|
||||||
|
nameservers_page.form["form-1-ip"] = valid_ip_2
|
||||||
nameservers_page.form["form-1-server"] = nameserver2
|
nameservers_page.form["form-1-server"] = nameserver2
|
||||||
with less_console_noise(): # swallow log warning message
|
with less_console_noise(): # swallow log warning message
|
||||||
result = nameservers_page.form.submit()
|
result = nameservers_page.form.submit()
|
||||||
|
@ -937,15 +938,14 @@ class TestDomainNameservers(TestDomainOverview):
|
||||||
nameserver1 = "ns1.igorville.gov"
|
nameserver1 = "ns1.igorville.gov"
|
||||||
nameserver2 = "ns2.igorville.gov"
|
nameserver2 = "ns2.igorville.gov"
|
||||||
valid_ip = "127.0.0.1"
|
valid_ip = "127.0.0.1"
|
||||||
# initial nameservers page has one server with two ips
|
valid_ip_2 = "128.0.0.2"
|
||||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
# attempt to submit the form without two hosts, both subdomains,
|
|
||||||
# only one has ips
|
|
||||||
nameservers_page.form["form-0-server"] = nameserver1
|
nameservers_page.form["form-0-server"] = nameserver1
|
||||||
|
nameservers_page.form["form-0-ip"] = valid_ip
|
||||||
nameservers_page.form["form-1-server"] = nameserver2
|
nameservers_page.form["form-1-server"] = nameserver2
|
||||||
nameservers_page.form["form-1-ip"] = valid_ip
|
nameservers_page.form["form-1-ip"] = valid_ip_2
|
||||||
with less_console_noise(): # swallow log warning message
|
with less_console_noise(): # swallow log warning message
|
||||||
result = nameservers_page.form.submit()
|
result = nameservers_page.form.submit()
|
||||||
# form submission was a successful post, response should be a 302
|
# form submission was a successful post, response should be a 302
|
||||||
|
|
|
@ -26,6 +26,7 @@ def write_header(writer, columns):
|
||||||
def get_domain_infos(filter_condition, sort_fields):
|
def get_domain_infos(filter_condition, sort_fields):
|
||||||
domain_infos = (
|
domain_infos = (
|
||||||
DomainInformation.objects.select_related("domain", "authorizing_official")
|
DomainInformation.objects.select_related("domain", "authorizing_official")
|
||||||
|
.prefetch_related("domain__permissions")
|
||||||
.filter(**filter_condition)
|
.filter(**filter_condition)
|
||||||
.order_by(*sort_fields)
|
.order_by(*sort_fields)
|
||||||
)
|
)
|
||||||
|
@ -49,6 +50,7 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None
|
||||||
|
|
||||||
# Domain should never be none when parsing this information
|
# Domain should never be none when parsing this information
|
||||||
if domain_info.domain is None:
|
if domain_info.domain is None:
|
||||||
|
logger.error("Attemting to parse row for csv exports but Domain is none in a DomainInfo")
|
||||||
raise ValueError("Domain is none")
|
raise ValueError("Domain is none")
|
||||||
|
|
||||||
domain = domain_info.domain # type: ignore
|
domain = domain_info.domain # type: ignore
|
||||||
|
@ -127,15 +129,6 @@ def _get_security_emails(sec_contact_ids):
|
||||||
return security_emails_dict
|
return security_emails_dict
|
||||||
|
|
||||||
|
|
||||||
def update_columns_with_domain_managers(columns, max_dm_count):
|
|
||||||
"""
|
|
||||||
Update the columns list to include "Domain manager email {#}" headers
|
|
||||||
based on the maximum domain manager count.
|
|
||||||
"""
|
|
||||||
for i in range(1, max_dm_count + 1):
|
|
||||||
columns.append(f"Domain manager email {i}")
|
|
||||||
|
|
||||||
|
|
||||||
def write_csv(
|
def write_csv(
|
||||||
writer,
|
writer,
|
||||||
columns,
|
columns,
|
||||||
|
@ -161,16 +154,26 @@ def write_csv(
|
||||||
# Reduce the memory overhead when performing the write operation
|
# Reduce the memory overhead when performing the write operation
|
||||||
paginator = Paginator(all_domain_infos, 1000)
|
paginator = Paginator(all_domain_infos, 1000)
|
||||||
|
|
||||||
if get_domain_managers and len(all_domain_infos) > 0:
|
# The maximum amount of domain managers an account has
|
||||||
# We want to get the max amont of domain managers an
|
# We get the max so we can set the column header accurately
|
||||||
# account has to set the column header dynamically
|
max_dm_count = 0
|
||||||
max_dm_count = max(len(domain_info.domain.permissions.all()) for domain_info in all_domain_infos)
|
total_body_rows = []
|
||||||
update_columns_with_domain_managers(columns, max_dm_count)
|
|
||||||
|
|
||||||
for page_num in paginator.page_range:
|
for page_num in paginator.page_range:
|
||||||
page = paginator.page(page_num)
|
|
||||||
rows = []
|
rows = []
|
||||||
|
page = paginator.page(page_num)
|
||||||
for domain_info in page.object_list:
|
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)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
row = parse_row(columns, domain_info, security_emails_dict, get_domain_managers)
|
row = parse_row(columns, domain_info, security_emails_dict, get_domain_managers)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
@ -179,11 +182,11 @@ def write_csv(
|
||||||
# It indicates that DomainInformation.domain is None.
|
# It indicates that DomainInformation.domain is None.
|
||||||
logger.error("csv_export -> Error when parsing row, domain was None")
|
logger.error("csv_export -> Error when parsing row, domain was None")
|
||||||
continue
|
continue
|
||||||
|
total_body_rows.extend(rows)
|
||||||
|
|
||||||
if should_write_header:
|
if should_write_header:
|
||||||
write_header(writer, columns)
|
write_header(writer, columns)
|
||||||
|
writer.writerows(total_body_rows)
|
||||||
writer.writerows(rows)
|
|
||||||
|
|
||||||
|
|
||||||
def export_data_type_to_csv(csv_file):
|
def export_data_type_to_csv(csv_file):
|
||||||
|
|
|
@ -20,7 +20,7 @@ class EmailSendingError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def send_templated_email(template_name: str, subject_template_name: str, to_address: str, context={}, file: str = None):
|
def send_templated_email(template_name: str, subject_template_name: str, to_address: str, bcc_address="", context={}, file: str = None):
|
||||||
"""Send an email built from a template to one email address.
|
"""Send an email built from a template to one email address.
|
||||||
|
|
||||||
template_name and subject_template_name are relative to the same template
|
template_name and subject_template_name are relative to the same template
|
||||||
|
@ -45,7 +45,12 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise EmailSendingError("Could not access the SES client.") from exc
|
raise EmailSendingError("Could not access the SES client.") from exc
|
||||||
|
|
||||||
|
destination = {"ToAddresses": [to_address]}
|
||||||
|
if bcc_address:
|
||||||
|
destination["BccAddresses"] = [bcc_address]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
if file is None:
|
if file is None:
|
||||||
ses_client.send_email(
|
ses_client.send_email(
|
||||||
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
||||||
|
@ -55,7 +60,6 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr
|
||||||
"Subject": {"Data": subject},
|
"Subject": {"Data": subject},
|
||||||
"Body": {"Text": {"Data": email_body}},
|
"Body": {"Text": {"Data": email_body}},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ses_client = boto3.client(
|
ses_client = boto3.client(
|
||||||
|
|
|
@ -14,6 +14,7 @@ from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
Domain,
|
Domain,
|
||||||
|
@ -707,7 +708,7 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
adding a success message to the view if the email sending succeeds"""
|
adding a success message to the view if the email sending succeeds"""
|
||||||
|
|
||||||
# Set a default email address to send to for staff
|
# Set a default email address to send to for staff
|
||||||
requestor_email = "help@get.gov"
|
requestor_email = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
# Check if the email requestor has a valid email address
|
# Check if the email requestor has a valid email address
|
||||||
if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":
|
if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":
|
||||||
|
|
16
src/registrar/widgets.py
Normal file
16
src/registrar/widgets.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# widgets.py
|
||||||
|
|
||||||
|
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
|
||||||
|
class NoAutocompleteFilteredSelectMultiple(FilteredSelectMultiple):
|
||||||
|
"""Firefox and Edge are unable to correctly initialize the source select in filter_horizontal
|
||||||
|
widgets. We add the attribute autocomplete=off to fix that."""
|
||||||
|
|
||||||
|
def render(self, name, value, attrs=None, renderer=None):
|
||||||
|
if attrs is None:
|
||||||
|
attrs = {}
|
||||||
|
attrs["autocomplete"] = "off"
|
||||||
|
output = super().render(name, value, attrs=attrs, renderer=renderer)
|
||||||
|
return mark_safe(output) # nosec
|
Loading…
Add table
Add a link
Reference in a new issue