diff --git a/src/registrar/admin.py b/src/registrar/admin.py index feb3b1883..b1b3d8adb 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -11,6 +11,7 @@ from django.db.models import ( Value, When, ) + from django.db.models.functions import Concat, Coalesce from django.http import HttpResponseRedirect from registrar.models.federal_agency import FederalAgency @@ -24,7 +25,7 @@ from registrar.utility.admin_helpers import ( from django.conf import settings from django.contrib.messages import get_messages from django.contrib.admin.helpers import AdminForm -from django.shortcuts import redirect +from django.shortcuts import redirect, get_object_or_404 from django_fsm import get_available_FIELD_transitions, FSMField from registrar.models import DomainInformation, Portfolio, UserPortfolioPermission, DomainInvitation from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices @@ -1478,26 +1479,6 @@ class BaseInvitationAdmin(ListHeaderAdmin): return response -# class DomainInvitationAdminForm(forms.ModelForm): -# """Custom form for DomainInvitation in admin to only allow cancellations.""" - -# STATUS_CHOICES = [ -# ("", "------"), # no action -# ("canceled", "Canceled"), -# ] - -# status = forms.ChoiceField(choices=STATUS_CHOICES, required=False, label="Status") - -# class Meta: -# model = models.DomainInvitation -# fields = "__all__" - -# def clean_status(self): -# # Clean status - we purposely dont edit anything so we dont mess with the state -# status = self.cleaned_data.get("status") -# return status - - class DomainInvitationAdmin(BaseInvitationAdmin): """Custom domain invitation admin class.""" @@ -1527,18 +1508,29 @@ class DomainInvitationAdmin(BaseInvitationAdmin): search_help_text = "Search by email or domain." - # # Mark the FSM field 'status' as readonly - # # to allow admin users to create Domain Invitations - # # without triggering the FSM Transition Not Allowed - # # error. - # readonly_fields = ["status"] - - readonly_fields = [] + # Mark the FSM field 'status' as readonly + # to allow admin users to create Domain Invitations + # without triggering the FSM Transition Not Allowed + # error. + readonly_fields = ["status"] autocomplete_fields = ["domain"] change_form_template = "django/admin/domain_invitation_change_form.html" + def change_view(self, request, object_id, form_url="", extra_context=None): + """Override the change_view to add the invitation obj for the + change_form_object_tools template""" + + if extra_context is None: + extra_context = {} + + # Get the domain invitation object + invitation = get_object_or_404(DomainInvitation, id=object_id) + extra_context["invitation"] = invitation + + return super().change_view(request, object_id, form_url, extra_context) + def save_model(self, request, obj, form, change): """ Override the save_model method. diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index 4f75fd2fb..64b979f2b 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -498,6 +498,22 @@ input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-too font-size: 13px; } +.object-tools li button { + font-family: Source Sans Pro Web, Helvetica Neue, Helvetica, Roboto, Arial, sans-serif; + text-transform: none !important; + font-size: 14px !important; + display: block; + float: left; + padding: 3px 12px; + background: var(--object-tools-bg) !important; + color: var(--object-tools-fg); + font-weight: 400; + font-size: 0.6875rem; + text-transform: uppercase; + letter-spacing: 0.5px; + border-radius: 15px; +} + .module--custom { a { font-size: 13px; diff --git a/src/registrar/assets/src/sass/_theme/_base.scss b/src/registrar/assets/src/sass/_theme/_base.scss index be3b89baf..4f00f4b79 100644 --- a/src/registrar/assets/src/sass/_theme/_base.scss +++ b/src/registrar/assets/src/sass/_theme/_base.scss @@ -46,7 +46,6 @@ body { background-color: color('gray-1'); } - .section-outlined { background-color: color('white'); border: 1px solid color('base-lighter'); diff --git a/src/registrar/models/domain_invitation.py b/src/registrar/models/domain_invitation.py index 3954dea7e..28089dcb5 100644 --- a/src/registrar/models/domain_invitation.py +++ b/src/registrar/models/domain_invitation.py @@ -78,10 +78,6 @@ class DomainInvitation(TimeStampedModel): @transition(field="status", source=DomainInvitationStatus.INVITED, target=DomainInvitationStatus.CANCELED) def cancel_invitation(self): """When an invitation is canceled, change the status to canceled""" - # print("***** IN CANCEL_INVITATION SECTION") - # logger.info(f"Invitation for {self.email} to {self.domain} has been canceled.") - # print("WHEN INVITATION IS CANCELED > CHANGE STATUS TO CANCELED") - # Send email here maybe? pass @transition(field="status", source=DomainInvitationStatus.CANCELED, target=DomainInvitationStatus.INVITED) diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index 66011a3c4..541f4d162 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -15,13 +15,28 @@ {% else %} {% endif %} -{% endblock %} - +{% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/admin/status_with_clipboard.html b/src/registrar/templates/admin/status_with_clipboard.html deleted file mode 100644 index a62ca5055..000000000 --- a/src/registrar/templates/admin/status_with_clipboard.html +++ /dev/null @@ -1,22 +0,0 @@ -{% load static %} - -
- {{ field.value | capfirst }} - - - -
- diff --git a/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html b/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html index 4c0e63d66..7907b5180 100644 --- a/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html +++ b/src/registrar/templates/django/admin/includes/email_clipboard_fieldset.html @@ -7,8 +7,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% block field_other %} {% if field.field.name == "email" %} {% include "admin/input_with_clipboard.html" with field=field.field %} - {% elif field.field.name == "status" %} - {% include "admin/status_with_clipboard.html" with field=field.field %} {% else %} {{ block.super }} {% endif %} diff --git a/src/registrar/tests/test_admin_domain.py b/src/registrar/tests/test_admin_domain.py index 867bf1b82..07940e202 100644 --- a/src/registrar/tests/test_admin_domain.py +++ b/src/registrar/tests/test_admin_domain.py @@ -12,6 +12,7 @@ from registrar.models import ( Domain, DomainRequest, DomainInformation, + DomainInvitation, User, Host, Portfolio, @@ -30,6 +31,9 @@ from .common import ( ) from unittest.mock import ANY, call, patch +from django.contrib.messages import get_messages + + import boto3_mocking # type: ignore import logging @@ -495,6 +499,63 @@ class TestDomainInformationInline(MockEppLib): self.assertIn("poopy@gov.gov", domain_managers) +class DomainInvitationAdminTest(TestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.staffuser = create_user(email="staffdomainmanager@meoward.com", is_staff=True) + cls.site = AdminSite() + cls.admin = DomainAdmin(model=Domain, admin_site=cls.site) + cls.factory = RequestFactory() + + def setUp(self): + self.client = Client(HTTP_HOST="localhost:8080") + self.client.force_login(self.staffuser) + super().setUp() + + def test_cancel_invitation_flow_in_admin(self): + """Testing canceling a domain invitation in Django Admin.""" + + # 1. Create a domain and assign staff user role + domain manager + domain = Domain.objects.create(name="cancelinvitationflowviaadmin.gov") + UserDomainRole.objects.create(user=self.staffuser, domain=domain, role="manager") + + # 2. Invite a domain manager to the above domain + invitation = DomainInvitation.objects.create( + email="inviteddomainmanager@meoward.com", + domain=domain, + status=DomainInvitation.DomainInvitationStatus.INVITED, + ) + + # 3. Go to the Domain Invitations list in /admin + domain_invitation_list_url = reverse("admin:registrar_domaininvitation_changelist") + response = self.client.get(domain_invitation_list_url) + self.assertEqual(response.status_code, 200) + + # 4. Go to the change view of that invitation and make sure you can see the button + domain_invitation_change_url = reverse("admin:registrar_domaininvitation_change", args=[invitation.id]) + response = self.client.get(domain_invitation_change_url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Cancel invitation") + + # 5. Click the "Cancel invitation" button (a POST) + cancel_invitation_url = reverse("invitation-cancel", args=[invitation.id]) + response = self.client.post(cancel_invitation_url, follow=True) + + # 6.Confirm we're redirected to the domain managers page for the domain + expected_redirect_url = reverse("domain-users", args=[domain.id]) + self.assertRedirects(response, expected_redirect_url) + + # 7. Get the messages + messages = list(get_messages(response.wsgi_request)) + message_texts = [str(message) for message in messages] + + # 8. Check that the success banner text is in the messages + expected_message = f"Canceled invitation to {invitation.email}." + self.assertIn(expected_message, message_texts) + + class TestDomainAdminWithClient(TestCase): """Test DomainAdmin class as super user.