From 6f67080e6e8e204ce5643317dd67776dcc543c8d Mon Sep 17 00:00:00 2001 From: Neil Martinsen-Burrell Date: Fri, 24 Mar 2023 13:37:50 -0500 Subject: [PATCH] Create invitations and show/delete from user management view --- src/registrar/config/urls.py | 5 +++ src/registrar/forms/domain.py | 11 ------ .../migrations/0016_domaininvitation.py | 5 ++- src/registrar/models/domain_invitation.py | 2 +- src/registrar/templates/domain_users.html | 5 +++ src/registrar/tests/test_models.py | 4 +- src/registrar/tests/test_views.py | 38 ++++++++++++++++++- src/registrar/views/__init__.py | 7 +++- src/registrar/views/domain.py | 32 ++++++++++------ 9 files changed, 81 insertions(+), 28 deletions(-) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 0d0ec89f5..8674348c8 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -68,6 +68,11 @@ urlpatterns = [ views.DomainAddUserView.as_view(), name="domain-users-add", ), + path( + "invitation//delete", + views.DomainInvitationDeleteView.as_view(http_method_names=["post"]), + name="invitation-delete", + ), ] diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 6a2229961..3d5941eed 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -2,20 +2,9 @@ from django import forms -from registrar.models import User - class DomainAddUserForm(forms.Form): """Form for adding a user to a domain.""" email = forms.EmailField(label="Email") - - def clean_email(self): - requested_email = self.cleaned_data["email"] - try: - User.objects.get(email=requested_email) - except User.DoesNotExist: - # TODO: send an invitation email to a non-existent user - raise forms.ValidationError("That user does not exist in this system.") - return requested_email diff --git a/src/registrar/migrations/0016_domaininvitation.py b/src/registrar/migrations/0016_domaininvitation.py index 8299044fe..f7756ef1d 100644 --- a/src/registrar/migrations/0016_domaininvitation.py +++ b/src/registrar/migrations/0016_domaininvitation.py @@ -1,8 +1,8 @@ -# Generated by Django 4.1.6 on 2023-03-22 19:55 +# Generated by Django 4.1.6 on 2023-03-24 16:56 from django.db import migrations, models import django.db.models.deletion -import django_fsm +import django_fsm # type: ignore class Migration(migrations.Migration): @@ -39,6 +39,7 @@ class Migration(migrations.Migration): "domain", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, + related_name="invitations", to="registrar.domain", ), ), diff --git a/src/registrar/models/domain_invitation.py b/src/registrar/models/domain_invitation.py index 80fed0486..ba9b6ac03 100644 --- a/src/registrar/models/domain_invitation.py +++ b/src/registrar/models/domain_invitation.py @@ -3,7 +3,7 @@ from django.contrib.auth import get_user_model from django.db import models, IntegrityError -from django_fsm import FSMField, transition +from django_fsm import FSMField, transition # type: ignore from .utility.time_stamped_model import TimeStampedModel from .user_domain_role import UserDomainRole diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 9b30e7923..976a64a07 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -54,6 +54,7 @@ Email Date created Status + Action @@ -64,6 +65,10 @@ {{ invitation.created_at|date }} {{ invitation.status|title }} +
+ {% csrf_token %} +
+ {% endfor %} diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 75a43e6d6..74298c85a 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -172,7 +172,9 @@ class TestInvitations(TestCase): def setUp(self): self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") self.email = "mayor@igorville.gov" - self.invitation, _ = DomainInvitation.objects.get_or_create(email=self.email, domain=self.domain) + self.invitation, _ = DomainInvitation.objects.get_or_create( + email=self.email, domain=self.domain + ) self.user, _ = User.objects.get_or_create(email=self.email) def test_retrieval_creates_role(self): diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index c621cf986..78427e6b2 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -9,7 +9,15 @@ from django_webtest import WebTest # type: ignore import boto3_mocking # type: ignore -from registrar.models import DomainApplication, Domain, Contact, Website, UserDomainRole +from registrar.models import ( + DomainApplication, + Domain, + DomainInvitation, + Contact, + Website, + UserDomainRole, + User, +) from registrar.views.application import ApplicationWizard, Step from .common import less_console_noise @@ -1130,3 +1138,31 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) success_page = success_result.follow() self.assertContains(success_page, "mayor@igorville.gov") + + def test_domain_invitation_created(self): + """Add user on a nonexistent email creates an invitation.""" + # make sure there is no user with this email + EMAIL = "mayor@igorville.gov" + User.objects.filter(email=EMAIL).delete() + + add_page = self.app.get( + reverse("domain-users-add", kwargs={"pk": self.domain.id}) + ) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = EMAIL + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_result = add_page.form.submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_page = success_result.follow() + + self.assertContains(success_page, EMAIL) + self.assertContains(success_page, "Cancel") # link to cancel invitation + self.assertTrue(DomainInvitation.objects.filter(email=EMAIL).exists()) + + def test_domain_invitation_cancel(self): + """Posting to the delete view deletes an invitation.""" + EMAIL = "mayor@igorville.gov" + invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=EMAIL) + self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) + with self.assertRaises(DomainInvitation.DoesNotExist): + DomainInvitation.objects.get(id=invitation.id) diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index e1ae2cc32..0776f70fe 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -1,5 +1,10 @@ from .application import * -from .domain import DomainView, DomainUsersView, DomainAddUserView +from .domain import ( + DomainView, + DomainUsersView, + DomainAddUserView, + DomainInvitationDeleteView, +) from .health import * from .index import * from .whoami import * diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 0627d92df..c8acded23 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -1,15 +1,16 @@ """View for a single Domain.""" -from django import forms from django.contrib import messages +from django.contrib.messages.views import SuccessMessageMixin from django.db import IntegrityError from django.shortcuts import redirect from django.urls import reverse from django.views.generic import DetailView -from django.views.generic.edit import FormMixin +from django.views.generic.edit import DeleteView, FormMixin from registrar.models import Domain, DomainInvitation, User, UserDomainRole +from ..forms import DomainAddUserForm from .utility import DomainPermission @@ -31,13 +32,6 @@ class DomainUsersView(DomainPermission, DetailView): context_object_name = "domain" -class DomainAddUserForm(DomainPermission, forms.Form): - - """Form for adding a user to a domain.""" - - email = forms.EmailField(label="Email") - - class DomainAddUserView(DomainPermission, FormMixin, DetailView): """Inside of a domain's user management, a form for adding users. @@ -64,10 +58,15 @@ class DomainAddUserView(DomainPermission, FormMixin, DetailView): def _make_invitation(self, email_address): """Make a Domain invitation for this email and redirect with a message.""" - invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object) + invitation, created = DomainInvitation.objects.get_or_create( + email=email_address, domain=self.object + ) if not created: # that invitation already existed - messages.warning(self.request, f"{email_address} has already been invited to this domain.") + messages.warning( + self.request, + f"{email_address} has already been invited to this domain.", + ) else: messages.success(self.request, f"Invited {email_address} to this domain.") return redirect(self.get_success_url()) @@ -92,3 +91,14 @@ class DomainAddUserView(DomainPermission, FormMixin, DetailView): messages.success(self.request, f"Added user {requested_email}.") return redirect(self.get_success_url()) + + +class DomainInvitationDeleteView(SuccessMessageMixin, DeleteView): + model = DomainInvitation + object: DomainInvitation # workaround for type mismatch in DeleteView + + def get_success_url(self): + return reverse("domain-users", kwargs={"pk": self.object.domain.id}) + + def get_success_message(self, cleaned_data): + return f"Successfully canceled invitation for {self.object.email}."