mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-27 04:58:42 +02:00
Merge pull request #1745 from cisagov/za/1602-extend-expirations-easily
(getgov-backup) Ticket #1602: Extend expiration date button
This commit is contained in:
commit
2817a3ed5b
8 changed files with 464 additions and 30 deletions
|
@ -1,17 +1,18 @@
|
||||||
|
from datetime import date
|
||||||
import logging
|
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
|
||||||
from django.http import HttpResponse
|
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
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.http.response import HttpResponseRedirect
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from dateutil.relativedelta import relativedelta # type: ignore
|
||||||
from epplibwrapper.errors import ErrorCode, RegistryError
|
from epplibwrapper.errors import ErrorCode, RegistryError
|
||||||
from registrar.models import Contact, Domain, DomainApplication, DraftDomain, User, Website
|
from registrar.models import Contact, Domain, DomainApplication, DraftDomain, User, Website
|
||||||
from registrar.utility import csv_export
|
from registrar.utility import csv_export
|
||||||
|
@ -24,6 +25,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1148,6 +1150,26 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
# Table ordering
|
# Table ordering
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
|
||||||
|
"""Custom changeform implementation to pass in context information"""
|
||||||
|
if extra_context is None:
|
||||||
|
extra_context = {}
|
||||||
|
|
||||||
|
# Pass in what the an extended expiration date would be for the expiration date modal
|
||||||
|
if object_id is not None:
|
||||||
|
domain = Domain.objects.get(pk=object_id)
|
||||||
|
years_to_extend_by = self._get_calculated_years_for_exp_date(domain)
|
||||||
|
curr_exp_date = domain.registry_expiration_date
|
||||||
|
if curr_exp_date < date.today():
|
||||||
|
extra_context["extended_expiration_date"] = date.today() + relativedelta(years=years_to_extend_by)
|
||||||
|
else:
|
||||||
|
new_date = domain.registry_expiration_date + relativedelta(years=years_to_extend_by)
|
||||||
|
extra_context["extended_expiration_date"] = new_date
|
||||||
|
else:
|
||||||
|
extra_context["extended_expiration_date"] = None
|
||||||
|
|
||||||
|
return super().changeform_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
def export_data_type(self, request):
|
def export_data_type(self, request):
|
||||||
# match the CSV example with all the fields
|
# match the CSV example with all the fields
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
@ -1206,6 +1228,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
"_edit_domain": self.do_edit_domain,
|
"_edit_domain": self.do_edit_domain,
|
||||||
"_delete_domain": self.do_delete_domain,
|
"_delete_domain": self.do_delete_domain,
|
||||||
"_get_status": self.do_get_status,
|
"_get_status": self.do_get_status,
|
||||||
|
"_extend_expiration_date": self.do_extend_expiration_date,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check which action button was pressed and call the corresponding function
|
# Check which action button was pressed and call the corresponding function
|
||||||
|
@ -1216,6 +1239,81 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
# If no matching action button is found, return the super method
|
# 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_extend_expiration_date(self, request, obj):
|
||||||
|
"""Extends a domains expiration date by one year from the current date"""
|
||||||
|
|
||||||
|
# Make sure we're dealing with a Domain
|
||||||
|
if not isinstance(obj, Domain):
|
||||||
|
self.message_user(request, "Object is not of type Domain.", messages.ERROR)
|
||||||
|
return None
|
||||||
|
|
||||||
|
years = self._get_calculated_years_for_exp_date(obj)
|
||||||
|
|
||||||
|
# Renew the domain.
|
||||||
|
try:
|
||||||
|
obj.renew_domain(length=years)
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"Successfully extended the expiration date.",
|
||||||
|
)
|
||||||
|
except RegistryError as err:
|
||||||
|
if err.is_connection_error():
|
||||||
|
error_message = "Error connecting to the registry."
|
||||||
|
else:
|
||||||
|
error_message = f"Error extending this domain: {err}."
|
||||||
|
self.message_user(request, error_message, messages.ERROR)
|
||||||
|
except KeyError:
|
||||||
|
# In normal code flow, a keyerror can only occur when
|
||||||
|
# fresh data can't be pulled from the registry, and thus there is no cache.
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"Error connecting to the registry. No expiration date was found.",
|
||||||
|
messages.ERROR,
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(err, stack_info=True)
|
||||||
|
self.message_user(request, "Could not delete: An unspecified error occured", messages.ERROR)
|
||||||
|
|
||||||
|
return HttpResponseRedirect(".")
|
||||||
|
|
||||||
|
def _get_calculated_years_for_exp_date(self, obj, extension_period: int = 1):
|
||||||
|
"""Given the current date, an extension period, and a registry_expiration_date
|
||||||
|
on the domain object, calculate the number of years needed to extend the
|
||||||
|
current expiration date by the extension period.
|
||||||
|
"""
|
||||||
|
# Get the date we want to update to
|
||||||
|
desired_date = self._get_current_date() + relativedelta(years=extension_period)
|
||||||
|
|
||||||
|
# Grab the current expiration date
|
||||||
|
try:
|
||||||
|
exp_date = obj.registry_expiration_date
|
||||||
|
except KeyError:
|
||||||
|
# if no expiration date from registry, set it to today
|
||||||
|
logger.warning("current expiration date not set; setting to today")
|
||||||
|
exp_date = self._get_current_date()
|
||||||
|
|
||||||
|
# If the expiration date is super old (2020, for example), we need to
|
||||||
|
# "catch up" to the current year, so we add the difference.
|
||||||
|
# If both years match, then lets just proceed as normal.
|
||||||
|
calculated_exp_date = exp_date + relativedelta(years=extension_period)
|
||||||
|
|
||||||
|
year_difference = desired_date.year - exp_date.year
|
||||||
|
|
||||||
|
years = extension_period
|
||||||
|
if desired_date > calculated_exp_date:
|
||||||
|
# Max probably isn't needed here (no code flow), but it guards against negative and 0.
|
||||||
|
# In both of those cases, we just want to extend by the extension_period.
|
||||||
|
years = max(extension_period, year_difference)
|
||||||
|
|
||||||
|
return years
|
||||||
|
|
||||||
|
# Workaround for unit tests, as we cannot mock date directly.
|
||||||
|
# it is immutable. Rather than dealing with a convoluted workaround,
|
||||||
|
# lets wrap this in a function.
|
||||||
|
def _get_current_date(self):
|
||||||
|
"""Gets the current date"""
|
||||||
|
return date.today()
|
||||||
|
|
||||||
def do_delete_domain(self, request, obj):
|
def do_delete_domain(self, request, obj):
|
||||||
if not isinstance(obj, Domain):
|
if not isinstance(obj, Domain):
|
||||||
# Could be problematic if the type is similar,
|
# Could be problematic if the type is similar,
|
||||||
|
|
|
@ -23,6 +23,33 @@ function openInNewTab(el, removeAttribute = false){
|
||||||
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||||
// Initialization code.
|
// Initialization code.
|
||||||
|
|
||||||
|
/** An IIFE for pages in DjangoAdmin that use modals.
|
||||||
|
* Dja strips out form elements, and modals generate their content outside
|
||||||
|
* of the current form scope, so we need to "inject" these inputs.
|
||||||
|
*/
|
||||||
|
(function (){
|
||||||
|
function createPhantomModalFormButtons(){
|
||||||
|
let submitButtons = document.querySelectorAll('.usa-modal button[type="submit"]');
|
||||||
|
form = document.querySelector("form")
|
||||||
|
submitButtons.forEach((button) => {
|
||||||
|
|
||||||
|
let input = document.createElement("input");
|
||||||
|
input.type = "submit";
|
||||||
|
input.name = button.name;
|
||||||
|
input.value = button.value;
|
||||||
|
input.style.display = "none"
|
||||||
|
|
||||||
|
// Add the hidden input to the form
|
||||||
|
form.appendChild(input);
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
console.log("clicking")
|
||||||
|
input.click();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
createPhantomModalFormButtons();
|
||||||
|
})();
|
||||||
/** An IIFE for pages in DjangoAdmin which may need custom JS implementation.
|
/** An IIFE for pages in DjangoAdmin which may need custom JS implementation.
|
||||||
* Currently only appends target="_blank" to the domain_form object,
|
* Currently only appends target="_blank" to the domain_form object,
|
||||||
* but this can be expanded.
|
* but this can be expanded.
|
||||||
|
@ -41,8 +68,8 @@ function openInNewTab(el, removeAttribute = false){
|
||||||
let domainFormElement = document.getElementById("domain_form");
|
let domainFormElement = document.getElementById("domain_form");
|
||||||
let domainSubmitButton = document.getElementById("manageDomainSubmitButton");
|
let domainSubmitButton = document.getElementById("manageDomainSubmitButton");
|
||||||
if(domainSubmitButton && domainFormElement){
|
if(domainSubmitButton && domainFormElement){
|
||||||
domainSubmitButton.addEventListener("mouseover", () => openInNewTab(domainFormElement, true));
|
domainSubmitButton.addEventListener("mouseover", () => openInNewTab(domainFormElement, true));
|
||||||
domainSubmitButton.addEventListener("mouseout", () => openInNewTab(domainFormElement, false));
|
domainSubmitButton.addEventListener("mouseout", () => openInNewTab(domainFormElement, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -183,6 +183,10 @@ h1, h2, h3,
|
||||||
.submit-row div.spacer {
|
.submit-row div.spacer {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
.submit-row .mini-spacer{
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
.submit-row span {
|
.submit-row span {
|
||||||
margin-top: units(1);
|
margin-top: units(1);
|
||||||
}
|
}
|
||||||
|
@ -280,3 +284,21 @@ h1, h2, h3,
|
||||||
.select2-dropdown {
|
.select2-dropdown {
|
||||||
display: inline-grid !important;
|
display: inline-grid !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.admin-confirm-button {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button groups in /admin incorrectly have bullets.
|
||||||
|
// Remove that!
|
||||||
|
.usa-modal__footer .usa-button-group__item {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// USWDS media checks are overzealous in this situation,
|
||||||
|
// we should manually define this behaviour.
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.button-list-mobile {
|
||||||
|
display: contents !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -265,15 +265,17 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
|
|
||||||
Default length and unit of time are 1 year.
|
Default length and unit of time are 1 year.
|
||||||
"""
|
"""
|
||||||
# if no expiration date from registry, set to today
|
|
||||||
|
# If no date is specified, grab the registry_expiration_date
|
||||||
try:
|
try:
|
||||||
cur_exp_date = self.registry_expiration_date
|
exp_date = self.registry_expiration_date
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
# if no expiration date from registry, set it to today
|
||||||
logger.warning("current expiration date not set; setting to today")
|
logger.warning("current expiration date not set; setting to today")
|
||||||
cur_exp_date = date.today()
|
exp_date = date.today()
|
||||||
|
|
||||||
# create RenewDomain request
|
# create RenewDomain request
|
||||||
request = commands.RenewDomain(name=self.name, cur_exp_date=cur_exp_date, period=epp.Period(length, unit))
|
request = commands.RenewDomain(name=self.name, cur_exp_date=exp_date, period=epp.Period(length, unit))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# update expiration date in registry, and set the updated
|
# update expiration date in registry, and set the updated
|
||||||
|
|
|
@ -18,11 +18,12 @@
|
||||||
<link rel="apple-touch-icon" size="180x180"
|
<link rel="apple-touch-icon" size="180x180"
|
||||||
href="{% static 'img/registrar/favicons/favicon-180.png' %}"
|
href="{% static 'img/registrar/favicons/favicon-180.png' %}"
|
||||||
>
|
>
|
||||||
|
<script src="{% static 'js/uswds-init.min.js' %}" defer></script>
|
||||||
|
<script src="{% static 'js/uswds.min.js' %}" defer></script>
|
||||||
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
||||||
{% endblock %}
|
{% 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 }}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}" />
|
<link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -2,21 +2,117 @@
|
||||||
{% load i18n static %}
|
{% load i18n static %}
|
||||||
|
|
||||||
{% block field_sets %}
|
{% block field_sets %}
|
||||||
<div class="submit-row">
|
<div class="display-flex flex-row flex-justify submit-row">
|
||||||
<input id="manageDomainSubmitButton" type="submit" value="Manage domain" name="_edit_domain">
|
<div class="flex-align-self-start button-list-mobile">
|
||||||
<input type="submit" value="Get registry status" name="_get_status">
|
<input id="manageDomainSubmitButton" type="submit" value="Manage domain" name="_edit_domain">
|
||||||
<div class="spacer"></div>
|
{# Dja has margin styles defined on inputs as is. Lets work with it, rather than fight it. #}
|
||||||
{% if original.state == original.State.READY %}
|
<span class="mini-spacer"></span>
|
||||||
<input type="submit" value="Place hold" name="_place_client_hold" class="custom-link-button">
|
<input type="submit" value="Get registry status" name="_get_status">
|
||||||
{% elif original.state == original.State.ON_HOLD %}
|
</div>
|
||||||
<input type="submit" value="Remove hold" name="_remove_client_hold" class="custom-link-button">
|
<div class="desktop:flex-align-self-end">
|
||||||
{% endif %}
|
{% if original.state != original.State.DELETED %}
|
||||||
{% if original.state == original.State.READY or original.state == original.State.ON_HOLD %}
|
<a
|
||||||
<span> | </span>
|
class="text-middle"
|
||||||
{% endif %}
|
href="#toggle-extend-expiration-alert"
|
||||||
{% if original.state != original.State.DELETED %}
|
aria-controls="toggle-extend-expiration-alert"
|
||||||
<input type="submit" value="Remove from registry" name="_delete_domain" class="custom-link-button">
|
data-open-modal
|
||||||
|
>
|
||||||
|
Extend expiration date
|
||||||
|
</a>
|
||||||
|
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
|
||||||
|
{% endif %}
|
||||||
|
{% if original.state == original.State.READY %}
|
||||||
|
<input type="submit" value="Place hold" name="_place_client_hold" class="custom-link-button">
|
||||||
|
{% elif original.state == original.State.ON_HOLD %}
|
||||||
|
<input type="submit" value="Remove hold" name="_remove_client_hold" class="custom-link-button">
|
||||||
|
{% endif %}
|
||||||
|
{% if original.state == original.State.READY or original.state == original.State.ON_HOLD %}
|
||||||
|
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
|
||||||
|
{% endif %}
|
||||||
|
{% if original.state != original.State.DELETED %}
|
||||||
|
<input type="submit" value="Remove from registry" name="_delete_domain" class="custom-link-button">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block submit_buttons_bottom %}
|
||||||
|
{% comment %}
|
||||||
|
Modals behave very weirdly in django admin.
|
||||||
|
They tend to "strip out" any injected form elements, leaving only the main form.
|
||||||
|
In addition, USWDS handles modals by first destroying the element, then repopulating it toward the end of the page.
|
||||||
|
In effect, this means that the modal is not, and cannot, be surrounded by any form element at compile time.
|
||||||
|
|
||||||
|
The current workaround for this is to use javascript to inject a hidden input, and bind submit of that
|
||||||
|
element to the click of the confirmation button within this modal.
|
||||||
|
|
||||||
|
This is controlled by the class `dja-form-placeholder` on the button.
|
||||||
|
|
||||||
|
In addition, the modal element MUST be placed low in the DOM. The script loads slower on DJA than on other portions
|
||||||
|
of the application, so this means that it will briefly "populate", causing unintended visual effects.
|
||||||
|
{% endcomment %}
|
||||||
|
<div
|
||||||
|
class="usa-modal"
|
||||||
|
id="toggle-extend-expiration-alert"
|
||||||
|
aria-labelledby="Are you sure you want to extend the expiration date?"
|
||||||
|
aria-describedby="This expiration date will be extended."
|
||||||
|
>
|
||||||
|
<div class="usa-modal__content">
|
||||||
|
<div class="usa-modal__main">
|
||||||
|
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||||
|
Are you sure you want to extend the expiration date?
|
||||||
|
</h2>
|
||||||
|
<div class="usa-prose">
|
||||||
|
<p>
|
||||||
|
This will extend the expiration date by one year.
|
||||||
|
{# Acts as a <br> #}
|
||||||
|
<div class="display-inline"></div>
|
||||||
|
This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Domain: <b>{{ original.name }}</b>
|
||||||
|
{# Acts as a <br> #}
|
||||||
|
<div class="display-inline"></div>
|
||||||
|
New expiration date: <b>{{ extended_expiration_date }}</b>
|
||||||
|
{{test}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usa-modal__footer">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="usa-button dja-form-placeholder"
|
||||||
|
name="_extend_expiration_date"
|
||||||
|
>
|
||||||
|
Yes, extend date
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-modal__close"
|
||||||
|
aria-label="Close this window"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
|
@ -922,6 +922,11 @@ class MockEppLib(TestCase):
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=datetime.date(2023, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mockButtonRenewedDomainExpDate = fakedEppObject(
|
||||||
|
"fake.gov",
|
||||||
|
ex_date=datetime.date(2025, 5, 25),
|
||||||
|
)
|
||||||
|
|
||||||
mockDnsNeededRenewedDomainExpDate = fakedEppObject(
|
mockDnsNeededRenewedDomainExpDate = fakedEppObject(
|
||||||
"fakeneeded.gov",
|
"fakeneeded.gov",
|
||||||
ex_date=datetime.date(2023, 2, 15),
|
ex_date=datetime.date(2023, 2, 15),
|
||||||
|
@ -1050,11 +1055,19 @@ class MockEppLib(TestCase):
|
||||||
res_data=[self.mockMaximumRenewedDomainExpDate],
|
res_data=[self.mockMaximumRenewedDomainExpDate],
|
||||||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||||
)
|
)
|
||||||
else:
|
elif getattr(_request, "name", None) == "fake.gov":
|
||||||
return MagicMock(
|
period = getattr(_request, "period", None)
|
||||||
res_data=[self.mockRenewedDomainExpDate],
|
extension_period = getattr(period, "length", None)
|
||||||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
if extension_period == 2:
|
||||||
)
|
return MagicMock(
|
||||||
|
res_data=[self.mockButtonRenewedDomainExpDate],
|
||||||
|
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return MagicMock(
|
||||||
|
res_data=[self.mockRenewedDomainExpDate],
|
||||||
|
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||||
|
)
|
||||||
|
|
||||||
def mockInfoDomainCommands(self, _request, cleaned):
|
def mockInfoDomainCommands(self, _request, cleaned):
|
||||||
request_name = getattr(_request, "name", None)
|
request_name = getattr(_request, "name", None)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
from datetime import date
|
||||||
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 contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
|
from django_webtest import WebTest # type: ignore
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from registrar.admin import (
|
from registrar.admin import (
|
||||||
|
@ -35,7 +37,7 @@ from .common import (
|
||||||
)
|
)
|
||||||
from django.contrib.sessions.backends.db import SessionStore
|
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 ANY, call, patch
|
||||||
from unittest import skip
|
from unittest import skip
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -45,7 +47,11 @@ import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TestDomainAdmin(MockEppLib):
|
class TestDomainAdmin(MockEppLib, WebTest):
|
||||||
|
# csrf checks do not work with WebTest.
|
||||||
|
# We disable them here. TODO for another ticket.
|
||||||
|
csrf_checks = False
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.admin = DomainAdmin(model=Domain, admin_site=self.site)
|
self.admin = DomainAdmin(model=Domain, admin_site=self.site)
|
||||||
|
@ -53,8 +59,177 @@ class TestDomainAdmin(MockEppLib):
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
self.staffuser = create_user()
|
self.staffuser = create_user()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
self.app.set_user(self.superuser.username)
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
|
@skip("TODO for another ticket. This test case is grabbing old db data.")
|
||||||
|
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
|
||||||
|
def test_extend_expiration_date_button(self, mock_date_today):
|
||||||
|
"""
|
||||||
|
Tests if extend_expiration_date button extends correctly
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a ready domain with a preset expiration date
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||||
|
|
||||||
|
response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
|
||||||
|
|
||||||
|
# Make sure the ex date is what we expect it to be
|
||||||
|
domain_ex_date = Domain.objects.get(id=domain.id).expiration_date
|
||||||
|
self.assertEqual(domain_ex_date, date(2023, 5, 25))
|
||||||
|
|
||||||
|
# Make sure that the page is loading as expected
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Extend expiration date")
|
||||||
|
|
||||||
|
# Grab the form to submit
|
||||||
|
form = response.forms["domain_form"]
|
||||||
|
|
||||||
|
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||||
|
# Submit the form
|
||||||
|
response = form.submit("_extend_expiration_date")
|
||||||
|
|
||||||
|
# Follow the response
|
||||||
|
response = response.follow()
|
||||||
|
|
||||||
|
# refresh_from_db() does not work for objects with protected=True.
|
||||||
|
# https://github.com/viewflow/django-fsm/issues/89
|
||||||
|
new_domain = Domain.objects.get(id=domain.id)
|
||||||
|
|
||||||
|
# Check that the current expiration date is what we expect
|
||||||
|
self.assertEqual(new_domain.expiration_date, date(2025, 5, 25))
|
||||||
|
|
||||||
|
# Assert that everything on the page looks correct
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Extend expiration date")
|
||||||
|
|
||||||
|
# Ensure the message we recieve is in line with what we expect
|
||||||
|
expected_message = "Successfully extended the expiration date."
|
||||||
|
expected_call = call(
|
||||||
|
# The WGSI request doesn't need to be tested
|
||||||
|
ANY,
|
||||||
|
messages.INFO,
|
||||||
|
expected_message,
|
||||||
|
extra_tags="",
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
mock_add_message.assert_has_calls([expected_call], 1)
|
||||||
|
|
||||||
|
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2024, 1, 1))
|
||||||
|
def test_extend_expiration_date_button_epp(self, mock_date_today):
|
||||||
|
"""
|
||||||
|
Tests if extend_expiration_date button sends the right epp command
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a ready domain with a preset expiration date
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||||
|
|
||||||
|
response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
|
||||||
|
|
||||||
|
# Make sure that the page is loading as expected
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Extend expiration date")
|
||||||
|
|
||||||
|
# Grab the form to submit
|
||||||
|
form = response.forms["domain_form"]
|
||||||
|
|
||||||
|
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||||
|
with patch("registrar.models.Domain.renew_domain") as renew_mock:
|
||||||
|
# Submit the form
|
||||||
|
response = form.submit("_extend_expiration_date")
|
||||||
|
|
||||||
|
# Follow the response
|
||||||
|
response = response.follow()
|
||||||
|
|
||||||
|
# This value is based off of the current year - the expiration date.
|
||||||
|
# We "freeze" time to 2024, so 2024 - 2023 will always result in an
|
||||||
|
# "extension" of 2, as that will be one year of extension from that date.
|
||||||
|
extension_length = 2
|
||||||
|
|
||||||
|
# Assert that it is calling the function with the right extension length.
|
||||||
|
# We only need to test the value that EPP sends, as we can assume the other
|
||||||
|
# test cases cover the "renew" function.
|
||||||
|
renew_mock.assert_has_calls([call(length=extension_length)], any_order=False)
|
||||||
|
|
||||||
|
# We should not make duplicate calls
|
||||||
|
self.assertEqual(renew_mock.call_count, 1)
|
||||||
|
|
||||||
|
# Assert that everything on the page looks correct
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Extend expiration date")
|
||||||
|
|
||||||
|
# Ensure the message we recieve is in line with what we expect
|
||||||
|
expected_message = "Successfully extended the expiration date."
|
||||||
|
expected_call = call(
|
||||||
|
# The WGSI request doesn't need to be tested
|
||||||
|
ANY,
|
||||||
|
messages.INFO,
|
||||||
|
expected_message,
|
||||||
|
extra_tags="",
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
mock_add_message.assert_has_calls([expected_call], 1)
|
||||||
|
|
||||||
|
@patch("registrar.admin.DomainAdmin._get_current_date", return_value=date(2023, 1, 1))
|
||||||
|
def test_extend_expiration_date_button_date_matches_epp(self, mock_date_today):
|
||||||
|
"""
|
||||||
|
Tests if extend_expiration_date button sends the right epp command
|
||||||
|
when the current year matches the expiration date
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a ready domain with a preset expiration date
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||||
|
|
||||||
|
response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
|
||||||
|
|
||||||
|
# Make sure that the page is loading as expected
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Extend expiration date")
|
||||||
|
|
||||||
|
# Grab the form to submit
|
||||||
|
form = response.forms["domain_form"]
|
||||||
|
|
||||||
|
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||||
|
with patch("registrar.models.Domain.renew_domain") as renew_mock:
|
||||||
|
# Submit the form
|
||||||
|
response = form.submit("_extend_expiration_date")
|
||||||
|
|
||||||
|
# Follow the response
|
||||||
|
response = response.follow()
|
||||||
|
|
||||||
|
extension_length = 1
|
||||||
|
|
||||||
|
# Assert that it is calling the function with the right extension length.
|
||||||
|
# We only need to test the value that EPP sends, as we can assume the other
|
||||||
|
# test cases cover the "renew" function.
|
||||||
|
renew_mock.assert_has_calls([call(length=extension_length)], any_order=False)
|
||||||
|
|
||||||
|
# We should not make duplicate calls
|
||||||
|
self.assertEqual(renew_mock.call_count, 1)
|
||||||
|
|
||||||
|
# Assert that everything on the page looks correct
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, domain.name)
|
||||||
|
self.assertContains(response, "Extend expiration date")
|
||||||
|
|
||||||
|
# Ensure the message we recieve is in line with what we expect
|
||||||
|
expected_message = "Successfully extended the expiration date."
|
||||||
|
expected_call = call(
|
||||||
|
# The WGSI request doesn't need to be tested
|
||||||
|
ANY,
|
||||||
|
messages.INFO,
|
||||||
|
expected_message,
|
||||||
|
extra_tags="",
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
mock_add_message.assert_has_calls([expected_call], 1)
|
||||||
|
|
||||||
def test_short_org_name_in_domains_list(self):
|
def test_short_org_name_in_domains_list(self):
|
||||||
"""
|
"""
|
||||||
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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue