mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-23 11:16:07 +02:00
Create invitations and show/delete from user management view
This commit is contained in:
parent
c12aa1dd75
commit
6f67080e6e
9 changed files with 81 additions and 28 deletions
|
@ -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",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 *
|
||||||
|
|
|
@ -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}."
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue