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(), views.DomainAddUserView.as_view(),
name="domain-users-add", 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 django import forms
from registrar.models import User
class DomainAddUserForm(forms.Form): class DomainAddUserForm(forms.Form):
"""Form for adding a user to a domain.""" """Form for adding a user to a domain."""
email = forms.EmailField(label="Email") 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 from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import django_fsm import django_fsm # type: ignore
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -39,6 +39,7 @@ class Migration(migrations.Migration):
"domain", "domain",
models.ForeignKey( models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
related_name="invitations",
to="registrar.domain", to="registrar.domain",
), ),
), ),

View file

@ -3,7 +3,7 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models, IntegrityError 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 .utility.time_stamped_model import TimeStampedModel
from .user_domain_role import UserDomainRole 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">Email</th>
<th data-sortable scope="col" role="columnheader">Date created</th> <th data-sortable scope="col" role="columnheader">Date created</th>
<th data-sortable scope="col" role="columnheader">Status</th> <th data-sortable scope="col" role="columnheader">Status</th>
<th scope="col" role="columnheader"><span class="sr-only">Action</span></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -64,6 +65,10 @@
</th> </th>
<td data-sort-value="{{ invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.created_at|date }} </td> <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 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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View file

@ -172,7 +172,9 @@ class TestInvitations(TestCase):
def setUp(self): def setUp(self):
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
self.email = "mayor@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) self.user, _ = User.objects.get_or_create(email=self.email)
def test_retrieval_creates_role(self): def test_retrieval_creates_role(self):

View file

@ -9,7 +9,15 @@ from django_webtest import WebTest # type: ignore
import boto3_mocking # 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 registrar.views.application import ApplicationWizard, Step
from .common import less_console_noise from .common import less_console_noise
@ -1130,3 +1138,31 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_page = success_result.follow() success_page = success_result.follow()
self.assertContains(success_page, "mayor@igorville.gov") 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 .application import *
from .domain import DomainView, DomainUsersView, DomainAddUserView from .domain import (
DomainView,
DomainUsersView,
DomainAddUserView,
DomainInvitationDeleteView,
)
from .health import * from .health import *
from .index import * from .index import *
from .whoami import * from .whoami import *

View file

@ -1,15 +1,16 @@
"""View for a single Domain.""" """View for a single Domain."""
from django import forms
from django.contrib import messages from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.db import IntegrityError from django.db import IntegrityError
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
from django.views.generic import DetailView 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 registrar.models import Domain, DomainInvitation, User, UserDomainRole
from ..forms import DomainAddUserForm
from .utility import DomainPermission from .utility import DomainPermission
@ -31,13 +32,6 @@ class DomainUsersView(DomainPermission, DetailView):
context_object_name = "domain" 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): class DomainAddUserView(DomainPermission, FormMixin, DetailView):
"""Inside of a domain's user management, a form for adding users. """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): def _make_invitation(self, email_address):
"""Make a Domain invitation for this email and redirect with a message.""" """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: if not created:
# that invitation already existed # 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: else:
messages.success(self.request, f"Invited {email_address} to this domain.") messages.success(self.request, f"Invited {email_address} to this domain.")
return redirect(self.get_success_url()) return redirect(self.get_success_url())
@ -92,3 +91,14 @@ class DomainAddUserView(DomainPermission, FormMixin, DetailView):
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())
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}."