Create invitations and show/delete from user management view

This commit is contained in:
Neil Martinsen-Burrell 2023-03-24 13:37:50 -05:00
parent c12aa1dd75
commit 6f67080e6e
No known key found for this signature in database
GPG key ID: 6A3C818CC10D0184
9 changed files with 81 additions and 28 deletions

View file

@ -68,6 +68,11 @@ urlpatterns = [
views.DomainAddUserView.as_view(),
name="domain-users-add",
),
path(
"invitation/<int:pk>/delete",
views.DomainInvitationDeleteView.as_view(http_method_names=["post"]),
name="invitation-delete",
),
]

View file

@ -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

View file

@ -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",
),
),

View file

@ -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

View file

@ -54,6 +54,7 @@
<th data-sortable scope="col" role="columnheader">Email</th>
<th data-sortable scope="col" role="columnheader">Date created</th>
<th data-sortable scope="col" role="columnheader">Status</th>
<th scope="col" role="columnheader"><span class="sr-only">Action</span></th>
</tr>
</thead>
<tbody>
@ -64,6 +65,10 @@
</th>
<td data-sort-value="{{ invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.created_at|date }} </td>
<td data-label="Status">{{ invitation.status|title }}</td>
<td><form method="POST" action="{% url "invitation-delete" pk=invitation.id %}">
{% csrf_token %}<input type="submit" class="usa-button--unstyled" value="Cancel">
</form>
</td>
</tr>
{% endfor %}
</tbody>

View file

@ -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):

View file

@ -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)

View file

@ -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 *

View file

@ -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}."