mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-16 17:47:02 +02:00
Merge branch 'main' of https://github.com/cisagov/getgov into rh/446-remove-type-of-work-q
This commit is contained in:
commit
02220feb7a
26 changed files with 862 additions and 71 deletions
|
@ -6,10 +6,36 @@ from django.http.response import HttpResponseRedirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from registrar.models.utility.admin_sort_fields import AdminSortFields
|
from registrar.models.utility.admin_sort_fields import AdminSortFields
|
||||||
from . import models
|
from . import models
|
||||||
|
from auditlog.models import LogEntry # type: ignore
|
||||||
|
from auditlog.admin import LogEntryAdmin # type: ignore
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLogEntryAdmin(LogEntryAdmin):
|
||||||
|
"""Overwrite the generated LogEntry admin class"""
|
||||||
|
|
||||||
|
list_display = [
|
||||||
|
"created",
|
||||||
|
"resource",
|
||||||
|
"action",
|
||||||
|
"msg_short",
|
||||||
|
"user_url",
|
||||||
|
]
|
||||||
|
|
||||||
|
# We name the custom prop 'resource' because linter
|
||||||
|
# is not allowing a short_description attr on it
|
||||||
|
# This gets around the linter limitation, for now.
|
||||||
|
def resource(self, obj):
|
||||||
|
# Return the field value without a link
|
||||||
|
return f"{obj.content_type} - {obj.object_repr}"
|
||||||
|
|
||||||
|
search_help_text = "Search by resource, changes, or user."
|
||||||
|
|
||||||
|
change_form_template = "admin/change_form_no_submit.html"
|
||||||
|
add_form_template = "admin/change_form_no_submit.html"
|
||||||
|
|
||||||
|
|
||||||
class AuditedAdmin(admin.ModelAdmin, AdminSortFields):
|
class AuditedAdmin(admin.ModelAdmin, AdminSortFields):
|
||||||
"""Custom admin to make auditing easier."""
|
"""Custom admin to make auditing easier."""
|
||||||
|
|
||||||
|
@ -91,14 +117,12 @@ class ListHeaderAdmin(AuditedAdmin):
|
||||||
|
|
||||||
|
|
||||||
class UserContactInline(admin.StackedInline):
|
class UserContactInline(admin.StackedInline):
|
||||||
|
|
||||||
"""Edit a user's profile on the user page."""
|
"""Edit a user's profile on the user page."""
|
||||||
|
|
||||||
model = models.Contact
|
model = models.Contact
|
||||||
|
|
||||||
|
|
||||||
class MyUserAdmin(BaseUserAdmin):
|
class MyUserAdmin(BaseUserAdmin):
|
||||||
|
|
||||||
"""Custom user admin class to use our inlines."""
|
"""Custom user admin class to use our inlines."""
|
||||||
|
|
||||||
inlines = [UserContactInline]
|
inlines = [UserContactInline]
|
||||||
|
@ -152,59 +176,212 @@ class MyUserAdmin(BaseUserAdmin):
|
||||||
|
|
||||||
|
|
||||||
class HostIPInline(admin.StackedInline):
|
class HostIPInline(admin.StackedInline):
|
||||||
|
|
||||||
"""Edit an ip address on the host page."""
|
"""Edit an ip address on the host page."""
|
||||||
|
|
||||||
model = models.HostIP
|
model = models.HostIP
|
||||||
|
|
||||||
|
|
||||||
class MyHostAdmin(AuditedAdmin):
|
class MyHostAdmin(AuditedAdmin):
|
||||||
|
|
||||||
"""Custom host admin class to use our inlines."""
|
"""Custom host admin class to use our inlines."""
|
||||||
|
|
||||||
inlines = [HostIPInline]
|
inlines = [HostIPInline]
|
||||||
|
|
||||||
|
|
||||||
class DomainAdmin(ListHeaderAdmin):
|
class DomainAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
"""Custom domain admin class to add extra buttons."""
|
"""Custom domain admin class to add extra buttons."""
|
||||||
|
|
||||||
|
# Columns
|
||||||
|
list_display = [
|
||||||
|
"name",
|
||||||
|
"organization_type",
|
||||||
|
"state",
|
||||||
|
]
|
||||||
|
|
||||||
|
def organization_type(self, obj):
|
||||||
|
return obj.domain_info.organization_type
|
||||||
|
|
||||||
|
organization_type.admin_order_field = ( # type: ignore
|
||||||
|
"domain_info__organization_type"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filters
|
||||||
|
list_filter = ["domain_info__organization_type"]
|
||||||
|
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
search_help_text = "Search by domain name."
|
search_help_text = "Search by domain name."
|
||||||
change_form_template = "django/admin/domain_change_form.html"
|
change_form_template = "django/admin/domain_change_form.html"
|
||||||
readonly_fields = ["state"]
|
readonly_fields = ["state"]
|
||||||
|
|
||||||
def response_change(self, request, obj):
|
def response_change(self, request, obj):
|
||||||
ACTION_BUTTON = "_place_client_hold"
|
# Create dictionary of action functions
|
||||||
if ACTION_BUTTON in request.POST:
|
ACTION_FUNCTIONS = {
|
||||||
try:
|
"_place_client_hold": self.do_place_client_hold,
|
||||||
obj.place_client_hold()
|
"_remove_client_hold": self.do_remove_client_hold,
|
||||||
except Exception as err:
|
"_edit_domain": self.do_edit_domain,
|
||||||
self.message_user(request, err, messages.ERROR)
|
}
|
||||||
else:
|
|
||||||
self.message_user(
|
|
||||||
request,
|
|
||||||
(
|
|
||||||
"%s is in client hold. This domain is no longer accessible on"
|
|
||||||
" the public internet."
|
|
||||||
)
|
|
||||||
% obj.name,
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(".")
|
|
||||||
|
|
||||||
|
# Check which action button was pressed and call the corresponding function
|
||||||
|
for action, function in ACTION_FUNCTIONS.items():
|
||||||
|
if action in request.POST:
|
||||||
|
return function(request, obj)
|
||||||
|
|
||||||
|
# If no matching action button is found, return the super method
|
||||||
return super().response_change(request, obj)
|
return super().response_change(request, obj)
|
||||||
|
|
||||||
|
def do_place_client_hold(self, request, obj):
|
||||||
|
try:
|
||||||
|
obj.place_client_hold()
|
||||||
|
obj.save()
|
||||||
|
except Exception as err:
|
||||||
|
self.message_user(request, err, messages.ERROR)
|
||||||
|
else:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
(
|
||||||
|
"%s is in client hold. This domain is no longer accessible on"
|
||||||
|
" the public internet."
|
||||||
|
)
|
||||||
|
% obj.name,
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(".")
|
||||||
|
|
||||||
|
def do_remove_client_hold(self, request, obj):
|
||||||
|
try:
|
||||||
|
obj.remove_client_hold()
|
||||||
|
obj.save()
|
||||||
|
except Exception as err:
|
||||||
|
self.message_user(request, err, messages.ERROR)
|
||||||
|
else:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
("%s is ready. This domain is accessible on the public internet.")
|
||||||
|
% obj.name,
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(".")
|
||||||
|
|
||||||
|
def do_edit_domain(self, request, obj):
|
||||||
|
# We want to know, globally, when an edit action occurs
|
||||||
|
request.session["analyst_action"] = "edit"
|
||||||
|
# Restricts this action to this domain (pk) only
|
||||||
|
request.session["analyst_action_location"] = obj.id
|
||||||
|
return HttpResponseRedirect(reverse("domain", args=(obj.id,)))
|
||||||
|
|
||||||
|
def change_view(self, request, object_id):
|
||||||
|
# If the analyst was recently editing a domain page,
|
||||||
|
# delete any associated session values
|
||||||
|
if "analyst_action" in request.session:
|
||||||
|
del request.session["analyst_action"]
|
||||||
|
del request.session["analyst_action_location"]
|
||||||
|
return super().change_view(request, object_id)
|
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None):
|
||||||
|
# Fixes a bug wherein users which are only is_staff
|
||||||
|
# can access 'change' when GET,
|
||||||
|
# but cannot access this page when it is a request of type POST.
|
||||||
|
if request.user.is_staff:
|
||||||
|
return True
|
||||||
|
return super().has_change_permission(request, obj)
|
||||||
|
|
||||||
|
|
||||||
class ContactAdmin(ListHeaderAdmin):
|
class ContactAdmin(ListHeaderAdmin):
|
||||||
"""Custom contact admin class to add search."""
|
"""Custom contact admin class to add search."""
|
||||||
|
|
||||||
search_fields = ["email", "first_name", "last_name"]
|
search_fields = ["email", "first_name", "last_name"]
|
||||||
search_help_text = "Search by firstname, lastname or email."
|
search_help_text = "Search by firstname, lastname or email."
|
||||||
|
list_display = [
|
||||||
|
"contact",
|
||||||
|
"email",
|
||||||
|
]
|
||||||
|
|
||||||
|
# We name the custom prop 'contact' because linter
|
||||||
|
# is not allowing a short_description attr on it
|
||||||
|
# This gets around the linter limitation, for now.
|
||||||
|
def contact(self, obj: models.Contact):
|
||||||
|
"""Duplicate the contact _str_"""
|
||||||
|
if obj.first_name or obj.last_name:
|
||||||
|
return obj.get_formatted_name()
|
||||||
|
elif obj.email:
|
||||||
|
return obj.email
|
||||||
|
elif obj.pk:
|
||||||
|
return str(obj.pk)
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
contact.admin_order_field = "first_name" # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class WebsiteAdmin(ListHeaderAdmin):
|
||||||
|
"""Custom website admin class."""
|
||||||
|
|
||||||
|
# Search
|
||||||
|
search_fields = [
|
||||||
|
"website",
|
||||||
|
]
|
||||||
|
search_help_text = "Search by website."
|
||||||
|
|
||||||
|
|
||||||
|
class UserDomainRoleAdmin(ListHeaderAdmin):
|
||||||
|
"""Custom domain role admin class."""
|
||||||
|
|
||||||
|
# Columns
|
||||||
|
list_display = [
|
||||||
|
"user",
|
||||||
|
"domain",
|
||||||
|
"role",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Search
|
||||||
|
search_fields = [
|
||||||
|
"user__first_name",
|
||||||
|
"user__last_name",
|
||||||
|
"domain__name",
|
||||||
|
"role",
|
||||||
|
]
|
||||||
|
search_help_text = "Search by user, domain, or role."
|
||||||
|
|
||||||
|
|
||||||
|
class DomainInvitationAdmin(ListHeaderAdmin):
|
||||||
|
"""Custom domain invitation admin class."""
|
||||||
|
|
||||||
|
# Columns
|
||||||
|
list_display = [
|
||||||
|
"email",
|
||||||
|
"domain",
|
||||||
|
"status",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Search
|
||||||
|
search_fields = [
|
||||||
|
"email",
|
||||||
|
"domain__name",
|
||||||
|
]
|
||||||
|
search_help_text = "Search by email or domain."
|
||||||
|
|
||||||
|
|
||||||
|
class DomainInformationAdmin(ListHeaderAdmin):
|
||||||
|
"""Customize domain information admin class."""
|
||||||
|
|
||||||
|
# Columns
|
||||||
|
list_display = [
|
||||||
|
"domain",
|
||||||
|
"organization_type",
|
||||||
|
"created_at",
|
||||||
|
"submitter",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Filters
|
||||||
|
list_filter = ["organization_type"]
|
||||||
|
|
||||||
|
# Search
|
||||||
|
search_fields = [
|
||||||
|
"domain__name",
|
||||||
|
]
|
||||||
|
search_help_text = "Search by domain."
|
||||||
|
|
||||||
|
|
||||||
class DomainApplicationAdmin(ListHeaderAdmin):
|
class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
"""Customize the applications listing view."""
|
"""Custom domain applications admin class."""
|
||||||
|
|
||||||
# Set multi-selects 'read-only' (hide selects and show data)
|
# Set multi-selects 'read-only' (hide selects and show data)
|
||||||
# based on user perms and application creator's status
|
# based on user perms and application creator's status
|
||||||
|
@ -376,13 +553,16 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
return super().change_view(request, object_id, form_url, extra_context)
|
return super().change_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.unregister(LogEntry) # Unregister the default registration
|
||||||
|
admin.site.register(LogEntry, CustomLogEntryAdmin)
|
||||||
admin.site.register(models.User, MyUserAdmin)
|
admin.site.register(models.User, MyUserAdmin)
|
||||||
admin.site.register(models.UserDomainRole, AuditedAdmin)
|
admin.site.register(models.UserDomainRole, UserDomainRoleAdmin)
|
||||||
admin.site.register(models.Contact, ContactAdmin)
|
admin.site.register(models.Contact, ContactAdmin)
|
||||||
admin.site.register(models.DomainInvitation, AuditedAdmin)
|
admin.site.register(models.DomainInvitation, DomainInvitationAdmin)
|
||||||
admin.site.register(models.DomainInformation, AuditedAdmin)
|
admin.site.register(models.DomainInformation, DomainInformationAdmin)
|
||||||
admin.site.register(models.Domain, DomainAdmin)
|
admin.site.register(models.Domain, DomainAdmin)
|
||||||
admin.site.register(models.Host, MyHostAdmin)
|
admin.site.register(models.Host, MyHostAdmin)
|
||||||
admin.site.register(models.Nameserver, MyHostAdmin)
|
admin.site.register(models.Nameserver, MyHostAdmin)
|
||||||
admin.site.register(models.Website, AuditedAdmin)
|
admin.site.register(models.Website, WebsiteAdmin)
|
||||||
admin.site.register(models.DomainApplication, DomainApplicationAdmin)
|
admin.site.register(models.DomainApplication, DomainApplicationAdmin)
|
||||||
|
admin.site.register(models.TransitionDomain, AuditedAdmin)
|
||||||
|
|
50
src/registrar/assets/js/get-gov-admin.js
Normal file
50
src/registrar/assets/js/get-gov-admin.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* @file get-gov-admin.js includes custom code for the .gov registrar admin portal.
|
||||||
|
*
|
||||||
|
* Constants and helper functions are at the top.
|
||||||
|
* Event handlers are in the middle.
|
||||||
|
* Initialization (run-on-load) stuff goes at the bottom.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||||
|
// Helper functions.
|
||||||
|
/** Either sets attribute target="_blank" to a given element, or removes it */
|
||||||
|
function openInNewTab(el, removeAttribute = false){
|
||||||
|
if(removeAttribute){
|
||||||
|
el.setAttribute("target", "_blank");
|
||||||
|
}else{
|
||||||
|
el.removeAttribute("target", "_blank");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||||
|
// Event handlers.
|
||||||
|
|
||||||
|
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||||
|
// Initialization code.
|
||||||
|
|
||||||
|
/** An IIFE for pages in DjangoAdmin which may need custom JS implementation.
|
||||||
|
* Currently only appends target="_blank" to the domain_form object,
|
||||||
|
* but this can be expanded.
|
||||||
|
*/
|
||||||
|
(function (){
|
||||||
|
/*
|
||||||
|
On mouseover, appends target="_blank" on domain_form under the Domain page.
|
||||||
|
The reason for this is that the template has a form that contains multiple buttons.
|
||||||
|
The structure of that template complicates seperating those buttons
|
||||||
|
out of the form (while maintaining the same position on the page).
|
||||||
|
However, if we want to open one of those submit actions to a new tab -
|
||||||
|
such as the manage domain button - we need to dynamically append target.
|
||||||
|
As there is no built-in django method which handles this, we do it here.
|
||||||
|
*/
|
||||||
|
function prepareDjangoAdmin() {
|
||||||
|
let domainFormElement = document.getElementById("domain_form");
|
||||||
|
let domainSubmitButton = document.getElementById("manageDomainSubmitButton");
|
||||||
|
if(domainSubmitButton && domainFormElement){
|
||||||
|
domainSubmitButton.addEventListener("mouseover", () => openInNewTab(domainFormElement, true));
|
||||||
|
domainSubmitButton.addEventListener("mouseout", () => openInNewTab(domainFormElement, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareDjangoAdmin();
|
||||||
|
})();
|
|
@ -31,7 +31,7 @@ html[data-theme="light"] {
|
||||||
|
|
||||||
// #{$theme-link-color} would interpolate to 'primary', so we use the source value instead
|
// #{$theme-link-color} would interpolate to 'primary', so we use the source value instead
|
||||||
--link-fg: #{$theme-color-primary};
|
--link-fg: #{$theme-color-primary};
|
||||||
--link-hover-color: #{$theme-color-primary-darker};
|
--link-hover-color: #{$theme-color-primary};
|
||||||
// $theme-link-visited-color - violet-70v
|
// $theme-link-visited-color - violet-70v
|
||||||
--link-selected-fg: #54278f;
|
--link-selected-fg: #54278f;
|
||||||
|
|
||||||
|
@ -140,11 +140,6 @@ h1, h2, h3 {
|
||||||
font-weight: font-weight('bold');
|
font-weight: font-weight('bold');
|
||||||
}
|
}
|
||||||
|
|
||||||
table > caption > a {
|
|
||||||
font-weight: font-weight('bold');
|
|
||||||
text-transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.change-list {
|
.change-list {
|
||||||
.usa-table--striped tbody tr:nth-child(odd) td,
|
.usa-table--striped tbody tr:nth-child(odd) td,
|
||||||
.usa-table--striped tbody tr:nth-child(odd) th,
|
.usa-table--striped tbody tr:nth-child(odd) th,
|
||||||
|
@ -158,9 +153,12 @@ table > caption > a {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'Delete button' layout bug
|
// Fix django admin button height bugs
|
||||||
.submit-row a.deletelink {
|
.submit-row a.deletelink,
|
||||||
|
.delete-confirmation form .cancel-link,
|
||||||
|
.submit-row a.closelink {
|
||||||
height: auto!important;
|
height: auto!important;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep th from collapsing
|
// Keep th from collapsing
|
||||||
|
@ -170,3 +168,15 @@ table > caption > a {
|
||||||
.min-width-81 {
|
.min-width-81 {
|
||||||
min-width: 81px;
|
min-width: 81px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.primary-th {
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-transform: none;
|
||||||
|
font-weight: font-weight('bold');
|
||||||
|
text-align: left;
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--header-link-color);
|
||||||
|
}
|
|
@ -432,3 +432,21 @@ abbr[title] {
|
||||||
height: units('mobile');
|
height: units('mobile');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fixes some font size disparities with the Figma
|
||||||
|
// for usa-alert alert elements
|
||||||
|
.usa-alert {
|
||||||
|
.usa-alert__heading.larger-font-sizing {
|
||||||
|
font-size: units(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The icon was off center for some reason
|
||||||
|
// Fixes that issue
|
||||||
|
@media (min-width: 64em){
|
||||||
|
.usa-alert--warning{
|
||||||
|
.usa-alert__body::before {
|
||||||
|
left: 1rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ class UserFixture:
|
||||||
"last_name": "Adkinson",
|
"last_name": "Adkinson",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "bb21f687-c773-4df3-9243-111cfd4c0be4",
|
"username": "2bf518c2-485a-4c42-ab1a-f5a8b0a08484",
|
||||||
"first_name": "Paul",
|
"first_name": "Paul",
|
||||||
"last_name": "Kuykendall",
|
"last_name": "Kuykendall",
|
||||||
},
|
},
|
||||||
|
@ -77,6 +77,11 @@ class UserFixture:
|
||||||
"first_name": "David",
|
"first_name": "David",
|
||||||
"last_name": "Kennedy",
|
"last_name": "Kennedy",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "f14433d8-f0e9-41bf-9c72-b99b110e665d",
|
||||||
|
"first_name": "Nicolle",
|
||||||
|
"last_name": "LeClair",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
STAFF = [
|
STAFF = [
|
||||||
|
@ -123,6 +128,12 @@ class UserFixture:
|
||||||
"last_name": "DiSarli-Analyst",
|
"last_name": "DiSarli-Analyst",
|
||||||
"email": "gaby@truss.works",
|
"email": "gaby@truss.works",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "cfe7c2fc-e24a-480e-8b78-28645a1459b3",
|
||||||
|
"first_name": "Nicolle-Analyst",
|
||||||
|
"last_name": "LeClair-Analyst",
|
||||||
|
"email": "nicolle.leclair@ecstech.com",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
STAFF_PERMISSIONS = [
|
STAFF_PERMISSIONS = [
|
||||||
|
|
30
src/registrar/migrations/0031_alter_domain_state.py
Normal file
30
src/registrar/migrations/0031_alter_domain_state.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 4.2.1 on 2023-09-07 17:53
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import django_fsm
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0030_alter_user_status"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domain",
|
||||||
|
name="state",
|
||||||
|
field=django_fsm.FSMField(
|
||||||
|
choices=[
|
||||||
|
("created", "Created"),
|
||||||
|
("deleted", "Deleted"),
|
||||||
|
("unknown", "Unknown"),
|
||||||
|
("ready", "Ready"),
|
||||||
|
("onhold", "Onhold"),
|
||||||
|
],
|
||||||
|
default="unknown",
|
||||||
|
help_text="Very basic info about the lifecycle of this domain object",
|
||||||
|
max_length=21,
|
||||||
|
protected=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
60
src/registrar/migrations/0031_transitiondomain.py
Normal file
60
src/registrar/migrations/0031_transitiondomain.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
# Generated by Django 4.2.1 on 2023-09-11 14:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0030_alter_user_status"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="TransitionDomain",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"username",
|
||||||
|
models.TextField(
|
||||||
|
help_text="Username - this will be an email address",
|
||||||
|
verbose_name="Username",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"domain_name",
|
||||||
|
models.TextField(blank=True, null=True, verbose_name="Domain name"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"status",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[("created", "Created"), ("hold", "Hold")],
|
||||||
|
help_text="domain status during the transfer",
|
||||||
|
max_length=255,
|
||||||
|
verbose_name="Status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"email_sent",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="indicates whether email was sent",
|
||||||
|
verbose_name="email sent",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Generated by Django 4.2.1 on 2023-09-12 14:12
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0031_alter_domain_state"),
|
||||||
|
("registrar", "0031_transitiondomain"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
|
@ -13,6 +13,7 @@ from .user_domain_role import UserDomainRole
|
||||||
from .public_contact import PublicContact
|
from .public_contact import PublicContact
|
||||||
from .user import User
|
from .user import User
|
||||||
from .website import Website
|
from .website import Website
|
||||||
|
from .transition_domain import TransitionDomain
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Contact",
|
"Contact",
|
||||||
|
@ -28,6 +29,7 @@ __all__ = [
|
||||||
"PublicContact",
|
"PublicContact",
|
||||||
"User",
|
"User",
|
||||||
"Website",
|
"Website",
|
||||||
|
"TransitionDomain",
|
||||||
]
|
]
|
||||||
|
|
||||||
auditlog.register(Contact)
|
auditlog.register(Contact)
|
||||||
|
@ -42,3 +44,4 @@ auditlog.register(UserDomainRole)
|
||||||
auditlog.register(PublicContact)
|
auditlog.register(PublicContact)
|
||||||
auditlog.register(User)
|
auditlog.register(User)
|
||||||
auditlog.register(Website)
|
auditlog.register(Website)
|
||||||
|
auditlog.register(TransitionDomain)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from string import digits
|
from string import digits
|
||||||
from django_fsm import FSMField # type: ignore
|
from django_fsm import FSMField, transition # type: ignore
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
@ -114,6 +114,12 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
# the state is indeterminate
|
# the state is indeterminate
|
||||||
UNKNOWN = "unknown"
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
# the ready state for a domain object
|
||||||
|
READY = "ready"
|
||||||
|
|
||||||
|
# when a domain is on hold
|
||||||
|
ONHOLD = "onhold"
|
||||||
|
|
||||||
class Cache(property):
|
class Cache(property):
|
||||||
"""
|
"""
|
||||||
Python descriptor to turn class methods into properties.
|
Python descriptor to turn class methods into properties.
|
||||||
|
@ -311,13 +317,17 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
"""Time to renew. Not implemented."""
|
"""Time to renew. Not implemented."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@transition(field="state", source=[State.READY], target=State.ONHOLD)
|
||||||
def place_client_hold(self):
|
def place_client_hold(self):
|
||||||
"""This domain should not be active."""
|
"""This domain should not be active."""
|
||||||
raise NotImplementedError("This is not implemented yet.")
|
# This method is changing the state of the domain in registrar
|
||||||
|
# TODO: implement EPP call
|
||||||
|
|
||||||
|
@transition(field="state", source=[State.ONHOLD], target=State.READY)
|
||||||
def remove_client_hold(self):
|
def remove_client_hold(self):
|
||||||
"""This domain is okay to be active."""
|
"""This domain is okay to be active."""
|
||||||
raise NotImplementedError()
|
# This method is changing the state of the domain in registrar
|
||||||
|
# TODO: implement EPP call
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
42
src/registrar/models/transition_domain.py
Normal file
42
src/registrar/models/transition_domain.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
|
||||||
|
|
||||||
|
class TransitionDomain(TimeStampedModel):
|
||||||
|
"""Transition Domain model stores information about the
|
||||||
|
state of a domain upon transition between registry
|
||||||
|
providers"""
|
||||||
|
|
||||||
|
class StatusChoices(models.TextChoices):
|
||||||
|
CREATED = "created", "Created"
|
||||||
|
HOLD = "hold", "Hold"
|
||||||
|
|
||||||
|
username = models.TextField(
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
verbose_name="Username",
|
||||||
|
help_text="Username - this will be an email address",
|
||||||
|
)
|
||||||
|
domain_name = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Domain name",
|
||||||
|
)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=False,
|
||||||
|
blank=True,
|
||||||
|
choices=StatusChoices.choices,
|
||||||
|
verbose_name="Status",
|
||||||
|
help_text="domain status during the transfer",
|
||||||
|
)
|
||||||
|
email_sent = models.BooleanField(
|
||||||
|
null=False,
|
||||||
|
default=False,
|
||||||
|
verbose_name="email sent",
|
||||||
|
help_text="indicates whether email was sent",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.username
|
|
@ -4,12 +4,20 @@
|
||||||
{% for app in app_list %}
|
{% for app in app_list %}
|
||||||
<div class="app-{{ app.app_label }} module{% if app.app_url in request.path|urlencode %} current-app{% endif %}">
|
<div class="app-{{ app.app_label }} module{% if app.app_url in request.path|urlencode %} current-app{% endif %}">
|
||||||
<table>
|
<table>
|
||||||
<caption>
|
|
||||||
<a href="{{ app.app_url }}" class="section" title="{% blocktranslate with name=app.name %}Models in the {{ name }} application{% endblocktranslate %}">{{ app.name }}</a>
|
|
||||||
</caption>
|
|
||||||
|
|
||||||
{# .gov override #}
|
{# .gov override: add headers #}
|
||||||
<thead>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{% if show_changelinks %}
|
||||||
|
<th colspan="3" class="primary-th">
|
||||||
|
{{ app.name }}
|
||||||
|
</th>
|
||||||
|
{% else %}
|
||||||
|
<th colspan="2" class="primary-th">
|
||||||
|
{{ app.name }}
|
||||||
|
</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Model</th>
|
<th scope="col">Model</th>
|
||||||
<th><span class="display-inline-block min-width-25">Add</span></th>
|
<th><span class="display-inline-block min-width-25">Add</span></th>
|
||||||
|
|
|
@ -2,6 +2,24 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block extrahead %}
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32"
|
||||||
|
href="{% static 'img/registrar/favicons/favicon-32.png' %}"
|
||||||
|
>
|
||||||
|
<link rel="icon" type="image/png" sizes="192x192"
|
||||||
|
href="{% static 'img/registrar/favicons/favicon-192.png' %}"
|
||||||
|
>
|
||||||
|
<link rel="icon" type="image/svg+xml"
|
||||||
|
href="{% static 'img/registrar/favicons/favicon.svg' %}"
|
||||||
|
>
|
||||||
|
<link rel="shortcut icon" type="image/x-icon"
|
||||||
|
href="{% static 'img/registrar/favicons/favicon.ico' %}"
|
||||||
|
>
|
||||||
|
<link rel="apple-touch-icon" size="180x180"
|
||||||
|
href="{% static 'img/registrar/favicons/favicon-180.png' %}"
|
||||||
|
>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
||||||
|
|
||||||
{% block extrastyle %}{{ block.super }}
|
{% block extrastyle %}{{ block.super }}
|
||||||
|
|
|
@ -9,4 +9,4 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
20
src/registrar/templates/admin/change_form_no_submit.html
Normal file
20
src/registrar/templates/admin/change_form_no_submit.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "admin/change_form.html" %}
|
||||||
|
|
||||||
|
{% comment %} Replace the Django ul markup with a div. We'll edit the child markup accordingly in change_form_object_tools {% endcomment %}
|
||||||
|
{% block object-tools %}
|
||||||
|
{% if change and not is_popup %}
|
||||||
|
<div class="object-tools">
|
||||||
|
{% block object-tools-items %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block submit_buttons_top %}
|
||||||
|
{# Do not render the submit buttons #}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block submit_buttons_bottom %}
|
||||||
|
{# Do not render the submit buttons #}
|
||||||
|
{% endblock %}
|
|
@ -60,12 +60,11 @@ Load our custom filters to extract info from the django generated markup.
|
||||||
<tr><td colspan="{{ result|length }}">{{ result.form.non_field_errors }}</td></tr>
|
<tr><td colspan="{{ result|length }}">{{ result.form.non_field_errors }}</td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|
||||||
{% with result_value=result.0|extract_value %}
|
{% with result_value=result.0|extract_value %}
|
||||||
{% with result_label=result.1|extract_a_text %}
|
{% with result_label=result.1|extract_a_text %}
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox" name="_selected_action" value="{{ result_value|default:'value' }}" id="{{ result_label|default:result_value }}" class="action-select">
|
<input type="checkbox" name="_selected_action" value="{{ result_value|default:'value' }}" id="{{ result_value|default:'value' }}-{{ result_label|default:'label' }}" class="action-select">
|
||||||
<label class="usa-sr-only" for="{{ result_label|default:result_value }}">{{ result_label|default:'label' }}</label>
|
<label class="usa-sr-only" for="{{ result_value|default:'value' }}-{{ result_label|default:'label' }}">{{ result_label|default:'label' }}</label>
|
||||||
</td>
|
</td>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
|
@ -6,6 +6,15 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main id="main-content" class="grid-container">
|
<main id="main-content" class="grid-container">
|
||||||
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
||||||
|
<a href="{% url 'home' %}" class="breadcrumb__back">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
||||||
|
Back to manage your domains
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
<h1>Domain request for {{ domainapplication.requested_domain.name }}</h1>
|
<h1>Domain request for {{ domainapplication.requested_domain.name }}</h1>
|
||||||
<div
|
<div
|
||||||
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
{% extends 'admin/change_form.html' %}
|
{% extends 'admin/change_form.html' %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block extrahead %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block field_sets %}
|
{% block field_sets %}
|
||||||
<div class="submit-row">
|
<div class="submit-row">
|
||||||
<input type="submit" value="Place hold" name="_place_client_hold">
|
{% if original.state == original.State.READY %}
|
||||||
|
<input type="submit" value="Place hold" name="_place_client_hold">
|
||||||
|
{% elif original.state == original.State.ONHOLD %}
|
||||||
|
<input type="submit" value="Remove hold" name="_remove_client_hold">
|
||||||
|
{% endif %}
|
||||||
|
<input id="manageDomainSubmitButton" type="submit" value="Manage Domain" name="_edit_domain">
|
||||||
</div>
|
</div>
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -10,8 +10,7 @@
|
||||||
<h1>Authorizing official</h1>
|
<h1>Authorizing official</h1>
|
||||||
|
|
||||||
<p>Your authorizing official is the person within your organization who can
|
<p>Your authorizing official is the person within your organization who can
|
||||||
authorize domain requests. This is generally the highest-ranking or
|
authorize domain requests. This person must be in a role of significant, executive responsibility within the organization. Read more about <a class="usa-link" href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p>
|
||||||
highest-elected official in your organization. Read more about <a class="usa-link" href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p>
|
|
||||||
|
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,14 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="grid-container">
|
<div class="grid-container">
|
||||||
<div class="grid-row">
|
<div class="grid-row">
|
||||||
<p class="font-body-md margin-top-0 margin-bottom-2
|
{% if not is_analyst_or_superuser or not analyst_action or analyst_action_location != domain.pk %}
|
||||||
|
<p class="font-body-md margin-top-0 margin-bottom-2
|
||||||
text-primary-darker text-semibold"
|
text-primary-darker text-semibold"
|
||||||
>
|
>
|
||||||
<span class="usa-sr-only"> Domain name:</span> {{ domain.name }}
|
<span class="usa-sr-only"> Domain name:</span> {{ domain.name }}
|
||||||
</p>
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-row grid-gap">
|
<div class="grid-row grid-gap">
|
||||||
<div class="tablet:grid-col-3">
|
<div class="tablet:grid-col-3">
|
||||||
|
@ -20,15 +22,26 @@
|
||||||
<div class="tablet:grid-col-9">
|
<div class="tablet:grid-col-9">
|
||||||
<main id="main-content" class="grid-container">
|
<main id="main-content" class="grid-container">
|
||||||
|
|
||||||
<a href="{% url 'home' %}" class="breadcrumb__back">
|
{% if is_analyst_or_superuser and analyst_action == 'edit' and analyst_action_location == domain.pk %}
|
||||||
|
<div class="usa-alert usa-alert--warning margin-bottom-2">
|
||||||
|
<div class="usa-alert__body">
|
||||||
|
<h4 class="usa-alert__heading larger-font-sizing">Attention!</h4>
|
||||||
|
<p class="usa-alert__text ">
|
||||||
|
You are making changes to a registrant’s domain. When finished making changes, close this tab and inform the registrant of your updates.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'home' %}" class="breadcrumb__back">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
||||||
Back to manage your domains
|
Back to manage your domains
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
{# messages block is under the back breadcrumb link #}
|
{# messages block is under the back breadcrumb link #}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
|
|
||||||
<h1>Domain contact information</h1>
|
<h1>Domain contact information</h1>
|
||||||
|
|
||||||
<p>If you’d like us to use a different name, email, or phone number you can make those changes below. Changing your contact information here won’t affect your Login.gov account information.</p>
|
<p>If you’d like us to use a different name, email, or phone number you can make those changes below. <strong>Updating your contact information here will update the contact information for all domains in your account.</strong> However, it won’t affect your Login.gov account information.
|
||||||
|
</p>
|
||||||
|
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
|
|
||||||
|
|
|
@ -430,10 +430,16 @@ def create_user():
|
||||||
return User.objects.create_user(
|
return User.objects.create_user(
|
||||||
username="staffuser",
|
username="staffuser",
|
||||||
email="user@example.com",
|
email="user@example.com",
|
||||||
|
is_staff=True,
|
||||||
password=p,
|
password=p,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_ready_domain():
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="city.gov", state=Domain.State.READY)
|
||||||
|
return domain
|
||||||
|
|
||||||
|
|
||||||
def completed_application(
|
def completed_application(
|
||||||
has_other_contacts=True,
|
has_other_contacts=True,
|
||||||
has_current_website=True,
|
has_current_website=True,
|
||||||
|
@ -518,3 +524,11 @@ def multiple_unalphabetical_domain_objects(
|
||||||
application = mock.create_full_dummy_domain_object(domain_type, object_name)
|
application = mock.create_full_dummy_domain_object(domain_type, object_name)
|
||||||
applications.append(application)
|
applications.append(application)
|
||||||
return applications
|
return applications
|
||||||
|
|
||||||
|
|
||||||
|
def generic_domain_object(domain_type, object_name):
|
||||||
|
"""Returns a generic domain object of
|
||||||
|
domain_type 'application', 'information', or 'invitation'"""
|
||||||
|
mock = AuditedAdminMockData()
|
||||||
|
application = mock.create_full_dummy_domain_object(domain_type, object_name)
|
||||||
|
return application
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
from django.test import TestCase, RequestFactory, Client
|
from django.test import TestCase, RequestFactory, Client
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from registrar.admin import (
|
from registrar.admin import (
|
||||||
|
DomainAdmin,
|
||||||
DomainApplicationAdmin,
|
DomainApplicationAdmin,
|
||||||
ListHeaderAdmin,
|
ListHeaderAdmin,
|
||||||
MyUserAdmin,
|
MyUserAdmin,
|
||||||
AuditedAdmin,
|
AuditedAdmin,
|
||||||
)
|
)
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
|
Domain,
|
||||||
DomainApplication,
|
DomainApplication,
|
||||||
DomainInformation,
|
DomainInformation,
|
||||||
User,
|
User,
|
||||||
|
@ -15,14 +18,17 @@ from registrar.models import (
|
||||||
)
|
)
|
||||||
from .common import (
|
from .common import (
|
||||||
completed_application,
|
completed_application,
|
||||||
|
generic_domain_object,
|
||||||
mock_user,
|
mock_user,
|
||||||
create_superuser,
|
create_superuser,
|
||||||
create_user,
|
create_user,
|
||||||
|
create_ready_domain,
|
||||||
multiple_unalphabetical_domain_objects,
|
multiple_unalphabetical_domain_objects,
|
||||||
)
|
)
|
||||||
|
from django.contrib.sessions.backends.db import SessionStore
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
from unittest import skip
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
@ -32,6 +38,60 @@ import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDomainAdmin(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.site = AdminSite()
|
||||||
|
self.admin = DomainAdmin(model=Domain, admin_site=self.site)
|
||||||
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
self.superuser = create_superuser()
|
||||||
|
self.staffuser = create_user()
|
||||||
|
|
||||||
|
def test_place_and_remove_hold(self):
|
||||||
|
domain = create_ready_domain()
|
||||||
|
|
||||||
|
# get admin page and assert Place Hold button
|
||||||
|
p = "userpass"
|
||||||
|
self.client.login(username="staffuser", password=p)
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Place hold")
|
||||||
|
self.assertNotContains(response, "Remove hold")
|
||||||
|
|
||||||
|
# submit place_client_hold and assert Remove Hold button
|
||||||
|
response = self.client.post(
|
||||||
|
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||||
|
{"_place_client_hold": "Place hold", "name": domain.name},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Remove hold")
|
||||||
|
self.assertNotContains(response, "Place hold")
|
||||||
|
|
||||||
|
# submit remove client hold and assert Place hold button
|
||||||
|
response = self.client.post(
|
||||||
|
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||||
|
{"_remove_client_hold": "Remove hold", "name": domain.name},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Place hold")
|
||||||
|
self.assertNotContains(response, "Remove hold")
|
||||||
|
|
||||||
|
@skip("Waiting on epp lib to implement")
|
||||||
|
def test_place_and_remove_hold_epp(self):
|
||||||
|
raise
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
class TestDomainApplicationAdmin(TestCase):
|
class TestDomainApplicationAdmin(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
|
@ -770,3 +830,129 @@ class AuditedAdminTest(TestCase):
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
DomainApplication.objects.all().delete()
|
DomainApplication.objects.all().delete()
|
||||||
DomainInvitation.objects.all().delete()
|
DomainInvitation.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
class DomainSessionVariableTest(TestCase):
|
||||||
|
"""Test cases for session variables in Django Admin"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.admin = DomainAdmin(Domain, None)
|
||||||
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
|
||||||
|
def test_session_vars_set_correctly(self):
|
||||||
|
"""Checks if session variables are being set correctly"""
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
dummy_domain_information = generic_domain_object("information", "session")
|
||||||
|
request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk)
|
||||||
|
self.populate_session_values(request, dummy_domain_information.domain)
|
||||||
|
self.assertEqual(request.session["analyst_action"], "edit")
|
||||||
|
self.assertEqual(
|
||||||
|
request.session["analyst_action_location"],
|
||||||
|
dummy_domain_information.domain.pk,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_session_vars_set_correctly_hardcoded_domain(self):
|
||||||
|
"""Checks if session variables are being set correctly"""
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
dummy_domain_information: Domain = generic_domain_object(
|
||||||
|
"information", "session"
|
||||||
|
)
|
||||||
|
dummy_domain_information.domain.pk = 1
|
||||||
|
|
||||||
|
request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk)
|
||||||
|
self.populate_session_values(request, dummy_domain_information.domain)
|
||||||
|
self.assertEqual(request.session["analyst_action"], "edit")
|
||||||
|
self.assertEqual(request.session["analyst_action_location"], 1)
|
||||||
|
|
||||||
|
def test_session_variables_reset_correctly(self):
|
||||||
|
"""Checks if incorrect session variables get overridden"""
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
dummy_domain_information = generic_domain_object("information", "session")
|
||||||
|
request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk)
|
||||||
|
|
||||||
|
self.populate_session_values(
|
||||||
|
request, dummy_domain_information.domain, preload_bad_data=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(request.session["analyst_action"], "edit")
|
||||||
|
self.assertEqual(
|
||||||
|
request.session["analyst_action_location"],
|
||||||
|
dummy_domain_information.domain.pk,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_session_variables_retain_information(self):
|
||||||
|
"""Checks to see if session variables retain old information"""
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
dummy_domain_information_list = multiple_unalphabetical_domain_objects(
|
||||||
|
"information"
|
||||||
|
)
|
||||||
|
for item in dummy_domain_information_list:
|
||||||
|
request = self.get_factory_post_edit_domain(item.domain.pk)
|
||||||
|
self.populate_session_values(request, item.domain)
|
||||||
|
|
||||||
|
self.assertEqual(request.session["analyst_action"], "edit")
|
||||||
|
self.assertEqual(request.session["analyst_action_location"], item.domain.pk)
|
||||||
|
|
||||||
|
def test_session_variables_concurrent_requests(self):
|
||||||
|
"""Simulates two requests at once"""
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
info_first = generic_domain_object("information", "session")
|
||||||
|
info_second = generic_domain_object("information", "session2")
|
||||||
|
|
||||||
|
request_first = self.get_factory_post_edit_domain(info_first.domain.pk)
|
||||||
|
request_second = self.get_factory_post_edit_domain(info_second.domain.pk)
|
||||||
|
|
||||||
|
self.populate_session_values(request_first, info_first.domain, True)
|
||||||
|
self.populate_session_values(request_second, info_second.domain, True)
|
||||||
|
|
||||||
|
# Check if anything got nulled out
|
||||||
|
self.assertNotEqual(request_first.session["analyst_action"], None)
|
||||||
|
self.assertNotEqual(request_second.session["analyst_action"], None)
|
||||||
|
self.assertNotEqual(request_first.session["analyst_action_location"], None)
|
||||||
|
self.assertNotEqual(request_second.session["analyst_action_location"], None)
|
||||||
|
|
||||||
|
# Check if they are both the same action 'type'
|
||||||
|
self.assertEqual(request_first.session["analyst_action"], "edit")
|
||||||
|
self.assertEqual(request_second.session["analyst_action"], "edit")
|
||||||
|
|
||||||
|
# Check their locations, and ensure they aren't the same across both
|
||||||
|
self.assertNotEqual(
|
||||||
|
request_first.session["analyst_action_location"],
|
||||||
|
request_second.session["analyst_action_location"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def populate_session_values(self, request, domain_object, preload_bad_data=False):
|
||||||
|
"""Boilerplate for creating mock sessions"""
|
||||||
|
request.user = self.client
|
||||||
|
request.session = SessionStore()
|
||||||
|
request.session.create()
|
||||||
|
if preload_bad_data:
|
||||||
|
request.session["analyst_action"] = "invalid"
|
||||||
|
request.session["analyst_action_location"] = "bad location"
|
||||||
|
self.admin.response_change(request, domain_object)
|
||||||
|
|
||||||
|
def get_factory_post_edit_domain(self, primary_key):
|
||||||
|
"""Posts to registrar domain change
|
||||||
|
with the edit domain button 'clicked',
|
||||||
|
then returns the factory object"""
|
||||||
|
return self.factory.post(
|
||||||
|
reverse("admin:registrar_domain_change", args=(primary_key,)),
|
||||||
|
{"_edit_domain": "true"},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
|
@ -79,6 +79,7 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin):
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request, "The organization name and mailing address has been updated."
|
self.request, "The organization name and mailing address has been updated."
|
||||||
)
|
)
|
||||||
|
|
||||||
# superclass has the redirect
|
# superclass has the redirect
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
@ -121,6 +122,7 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin):
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request, "The authorizing official for this domain has been updated."
|
self.request, "The authorizing official for this domain has been updated."
|
||||||
)
|
)
|
||||||
|
|
||||||
# superclass has the redirect
|
# superclass has the redirect
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
@ -187,6 +189,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request, "The name servers for this domain have been updated."
|
self.request, "The name servers for this domain have been updated."
|
||||||
)
|
)
|
||||||
|
|
||||||
# superclass has the redirect
|
# superclass has the redirect
|
||||||
return super().form_valid(formset)
|
return super().form_valid(formset)
|
||||||
|
|
||||||
|
@ -227,6 +230,7 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin):
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request, "Your contact information for this domain has been updated."
|
self.request, "Your contact information for this domain has been updated."
|
||||||
)
|
)
|
||||||
|
|
||||||
# superclass has the redirect
|
# superclass has the redirect
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
@ -272,6 +276,7 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request, "The security email for this domain have been updated."
|
self.request, "The security email for this domain have been updated."
|
||||||
)
|
)
|
||||||
|
|
||||||
# superclass has the redirect
|
# superclass has the redirect
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
@ -347,6 +352,7 @@ class DomainAddUserView(DomainPermissionView, FormMixin):
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request, f"Invited {email_address} to this domain."
|
self.request, f"Invited {email_address} to this domain."
|
||||||
)
|
)
|
||||||
|
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
@ -368,6 +374,7 @@ class DomainAddUserView(DomainPermissionView, FormMixin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
messages.success(self.request, f"Added user {requested_email}.")
|
messages.success(self.request, f"Added user {requested_email}.")
|
||||||
|
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,16 @@
|
||||||
|
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
|
||||||
from registrar.models import UserDomainRole, DomainApplication, DomainInvitation
|
from registrar.models import (
|
||||||
|
DomainApplication,
|
||||||
|
DomainInvitation,
|
||||||
|
DomainInformation,
|
||||||
|
UserDomainRole,
|
||||||
|
)
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PermissionsLoginMixin(PermissionRequiredMixin):
|
class PermissionsLoginMixin(PermissionRequiredMixin):
|
||||||
|
@ -25,27 +34,80 @@ class DomainPermission(PermissionsLoginMixin):
|
||||||
up from the domain's primary key in self.kwargs["pk"]
|
up from the domain's primary key in self.kwargs["pk"]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# ticket 806
|
|
||||||
# if self.request.user is staff or admin and
|
|
||||||
# domain.application__status = 'approved' or 'rejected' or 'action needed'
|
|
||||||
# return True
|
|
||||||
|
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# user needs to have a role on the domain
|
|
||||||
if not UserDomainRole.objects.filter(
|
|
||||||
user=self.request.user, domain__id=self.kwargs["pk"]
|
|
||||||
).exists():
|
|
||||||
return False
|
|
||||||
|
|
||||||
# The user has an ineligible flag
|
|
||||||
if self.request.user.is_restricted():
|
if self.request.user.is_restricted():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
pk = self.kwargs["pk"]
|
||||||
|
# If pk is none then something went very wrong...
|
||||||
|
if pk is None:
|
||||||
|
raise ValueError("Primary key is None")
|
||||||
|
|
||||||
|
if self.can_access_other_user_domains(pk):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# user needs to have a role on the domain
|
||||||
|
if not UserDomainRole.objects.filter(
|
||||||
|
user=self.request.user, domain__id=pk
|
||||||
|
).exists():
|
||||||
|
return False
|
||||||
|
|
||||||
# if we need to check more about the nature of role, do it here.
|
# if we need to check more about the nature of role, do it here.
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def can_access_other_user_domains(self, pk):
|
||||||
|
"""Checks to see if an authorized user (staff or superuser)
|
||||||
|
can access a domain that they did not create or was invited to.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check if the user is permissioned...
|
||||||
|
user_is_analyst_or_superuser = (
|
||||||
|
self.request.user.is_staff or self.request.user.is_superuser
|
||||||
|
)
|
||||||
|
|
||||||
|
if not user_is_analyst_or_superuser:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if the user is attempting a valid edit action.
|
||||||
|
# In other words, if the analyst/admin did not click
|
||||||
|
# the 'Manage Domain' button in /admin,
|
||||||
|
# then they cannot access this page.
|
||||||
|
session = self.request.session
|
||||||
|
can_do_action = (
|
||||||
|
"analyst_action" in session
|
||||||
|
and "analyst_action_location" in session
|
||||||
|
and session["analyst_action_location"] == pk
|
||||||
|
)
|
||||||
|
|
||||||
|
if not can_do_action:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Analysts may manage domains, when they are in these statuses:
|
||||||
|
valid_domain_statuses = [
|
||||||
|
DomainApplication.APPROVED,
|
||||||
|
DomainApplication.IN_REVIEW,
|
||||||
|
DomainApplication.REJECTED,
|
||||||
|
DomainApplication.ACTION_NEEDED,
|
||||||
|
# Edge case - some domains do not have
|
||||||
|
# a status or DomainInformation... aka a status of 'None'.
|
||||||
|
# It is necessary to access those to correct errors.
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
|
||||||
|
requested_domain = None
|
||||||
|
if DomainInformation.objects.filter(id=pk).exists():
|
||||||
|
requested_domain = DomainInformation.objects.get(id=pk)
|
||||||
|
|
||||||
|
if requested_domain.domain_application.status not in valid_domain_statuses:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Valid session keys exist,
|
||||||
|
# the user is permissioned,
|
||||||
|
# and it is in a valid status
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class DomainApplicationPermission(PermissionsLoginMixin):
|
class DomainApplicationPermission(PermissionsLoginMixin):
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import abc # abstract base class
|
import abc # abstract base class
|
||||||
|
|
||||||
from django.views.generic import DetailView, DeleteView, TemplateView
|
from django.views.generic import DetailView, DeleteView, TemplateView
|
||||||
|
|
||||||
from registrar.models import Domain, DomainApplication, DomainInvitation
|
from registrar.models import Domain, DomainApplication, DomainInvitation
|
||||||
|
|
||||||
from .mixins import (
|
from .mixins import (
|
||||||
|
@ -12,6 +11,9 @@ from .mixins import (
|
||||||
DomainInvitationPermission,
|
DomainInvitationPermission,
|
||||||
ApplicationWizardPermission,
|
ApplicationWizardPermission,
|
||||||
)
|
)
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
|
class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
|
||||||
|
@ -27,6 +29,22 @@ class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
|
||||||
# variable name in template context for the model object
|
# variable name in template context for the model object
|
||||||
context_object_name = "domain"
|
context_object_name = "domain"
|
||||||
|
|
||||||
|
# Adds context information for user permissions
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
user = self.request.user
|
||||||
|
context["is_analyst_or_superuser"] = user.is_staff or user.is_superuser
|
||||||
|
# Stored in a variable for the linter
|
||||||
|
action = "analyst_action"
|
||||||
|
action_location = "analyst_action_location"
|
||||||
|
# Flag to see if an analyst is attempting to make edits
|
||||||
|
if action in self.request.session:
|
||||||
|
context[action] = self.request.session[action]
|
||||||
|
if action_location in self.request.session:
|
||||||
|
context[action_location] = self.request.session[action_location]
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
# Abstract property enforces NotImplementedError on an attribute.
|
# Abstract property enforces NotImplementedError on an attribute.
|
||||||
@property
|
@property
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue