mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-20 09:46:06 +02:00
Merge pull request #480 from cisagov/nmb/approved-users
Simple user management
This commit is contained in:
commit
7a8f77dde9
17 changed files with 463 additions and 65 deletions
|
@ -223,7 +223,17 @@ section.dashboard {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usa-table {
|
@include at-media(mobile-lg) {
|
||||||
|
margin-top: units(5);
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: units(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.dotgov-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -234,16 +244,20 @@ section.dashboard {
|
||||||
&:visited {
|
&:visited {
|
||||||
color: color('primary');
|
color: color('primary');
|
||||||
}
|
}
|
||||||
|
|
||||||
.usa-icon {
|
.usa-icon {
|
||||||
// align icon with x height
|
// align icon with x height
|
||||||
margin-top: units(0.5);
|
margin-top: units(0.5);
|
||||||
margin-right: units(0.5);
|
margin-right: units(0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th[data-sortable]:not([aria-sort]) .usa-table__header__button {
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table on small mobile
|
.dotgov-table--stacked {
|
||||||
.usa-table--stacked {
|
|
||||||
td, th {
|
td, th {
|
||||||
padding: units(1) units(2) units(2px) 0;
|
padding: units(1) units(2) units(2px) 0;
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -271,17 +285,12 @@ section.dashboard {
|
||||||
}
|
}
|
||||||
|
|
||||||
@include at-media(mobile-lg) {
|
@include at-media(mobile-lg) {
|
||||||
margin-top: units(5);
|
|
||||||
|
|
||||||
h2 {
|
.dotgov-table {
|
||||||
margin-bottom: units(3);
|
tr {
|
||||||
}
|
|
||||||
|
|
||||||
.usa-table tr {
|
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usa-table {
|
|
||||||
td, th {
|
td, th {
|
||||||
border-bottom: 1px solid color('base-light');
|
border-bottom: 1px solid color('base-light');
|
||||||
}
|
}
|
||||||
|
@ -297,8 +306,13 @@ section.dashboard {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
td, th {
|
tbody th {
|
||||||
padding: units(2);
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
td, th,
|
||||||
|
.usa-tabel th{
|
||||||
|
padding: units(2) units(2) units(2) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
th:first-of-type {
|
th:first-of-type {
|
||||||
|
@ -310,7 +324,6 @@ section.dashboard {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#wrapper {
|
#wrapper {
|
||||||
|
|
|
@ -62,6 +62,12 @@ urlpatterns = [
|
||||||
name="todo",
|
name="todo",
|
||||||
),
|
),
|
||||||
path("domain/<int:pk>", views.DomainView.as_view(), name="domain"),
|
path("domain/<int:pk>", views.DomainView.as_view(), name="domain"),
|
||||||
|
path("domain/<int:pk>/users", views.DomainUsersView.as_view(), name="domain-users"),
|
||||||
|
path(
|
||||||
|
"domain/<int:pk>/users/add",
|
||||||
|
views.DomainAddUserView.as_view(),
|
||||||
|
name="domain-users-add",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -107,6 +107,10 @@ class DomainApplicationFixture:
|
||||||
"status": "investigating",
|
"status": "investigating",
|
||||||
"organization_name": "Example - In Investigation",
|
"organization_name": "Example - In Investigation",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"status": "investigating",
|
||||||
|
"organization_name": "Example - Approved",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -249,3 +253,25 @@ class DomainApplicationFixture:
|
||||||
cls._set_many_to_many_relations(da, app)
|
cls._set_many_to_many_relations(da, app)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
|
|
||||||
|
|
||||||
|
class DomainFixture(DomainApplicationFixture):
|
||||||
|
|
||||||
|
"""Create one domain and permissions on it for each user."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls):
|
||||||
|
try:
|
||||||
|
users = list(User.objects.all()) # force evaluation to catch db errors
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
# approve one of each users investigating status domains
|
||||||
|
application = DomainApplication.objects.filter(
|
||||||
|
creator=user, status=DomainApplication.INVESTIGATING
|
||||||
|
).last()
|
||||||
|
logger.debug(f"Approving {application} for {user}")
|
||||||
|
application.approve()
|
||||||
|
application.save()
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
from .application_wizard import *
|
from .application_wizard import *
|
||||||
|
from .domain import DomainAddUserForm
|
||||||
|
|
21
src/registrar/forms/domain.py
Normal file
21
src/registrar/forms/domain.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
"""Forms for domain management."""
|
||||||
|
|
||||||
|
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
|
|
@ -3,7 +3,7 @@ import logging
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from auditlog.context import disable_auditlog # type: ignore
|
from auditlog.context import disable_auditlog # type: ignore
|
||||||
|
|
||||||
from registrar.fixtures import UserFixture, DomainApplicationFixture
|
from registrar.fixtures import UserFixture, DomainApplicationFixture, DomainFixture
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -15,4 +15,5 @@ class Command(BaseCommand):
|
||||||
with disable_auditlog():
|
with disable_auditlog():
|
||||||
UserFixture.load()
|
UserFixture.load()
|
||||||
DomainApplicationFixture.load()
|
DomainApplicationFixture.load()
|
||||||
|
DomainFixture.load()
|
||||||
logger.info("All fixtures loaded.")
|
logger.info("All fixtures loaded.")
|
||||||
|
|
31
src/registrar/templates/domain_add_user.html
Normal file
31
src/registrar/templates/domain_add_user.html
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{% extends "domain_base.html" %}
|
||||||
|
{% load static field_helpers %}
|
||||||
|
|
||||||
|
{% block title %}Add another user{% endblock %}
|
||||||
|
|
||||||
|
{% block domain_content %}
|
||||||
|
<p><a href="{% url "domain-users" pk=domain.id %}">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
||||||
|
</svg>
|
||||||
|
Back to user management
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<h1>Add another user</h1>
|
||||||
|
|
||||||
|
<p>You can add another user to help manage your domain. They will need to sign
|
||||||
|
into the .gov registrar with their Login.gov account.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form class="usa-form usa-form--large" method="post" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% input_with_errors form.email %}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="usa-button"
|
||||||
|
>Add user</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %} {# domain_content #}
|
42
src/registrar/templates/domain_base.html
Normal file
42
src/registrar/templates/domain_base.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Domain {{ domain.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid-container">
|
||||||
|
<div class="grid-row grid-gap">
|
||||||
|
<div class="grid-col-3">
|
||||||
|
{% include 'domain_sidebar.html' %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-col-9">
|
||||||
|
<main id="main-content" class="grid-container">
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2">
|
||||||
|
<div class="usa-alert__body">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% block domain_content %}
|
||||||
|
<p><a href="{% url 'home' %}">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
||||||
|
</svg>
|
||||||
|
Back to manage your domains
|
||||||
|
</a></p>
|
||||||
|
|
||||||
|
<h1>Domain {{ domain.name }}</h1>
|
||||||
|
|
||||||
|
{% endblock %} {# domain_content #}
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %} {# content #}
|
|
@ -1,14 +1,6 @@
|
||||||
{% extends "dashboard_base.html" %}
|
{% extends "domain_base.html" %}
|
||||||
|
|
||||||
|
|
||||||
{% block title %}Domain {{ domain.name }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<main id="main-content" class="grid-container">
|
|
||||||
<div class="tablet:grid-offset-1 desktop:grid-offset-2">
|
|
||||||
<h1>{{ domain.name }}</h1>
|
|
||||||
|
|
||||||
|
{% block domain_content %}
|
||||||
|
{{ block.super }}
|
||||||
<p>Active: {% if domain.is_active %}Yes{% else %}No{% endif %}</p>
|
<p>Active: {% if domain.is_active %}Yes{% else %}No{% endif %}</p>
|
||||||
</div>
|
{% endblock %} {# domain_content #}
|
||||||
</main>
|
|
||||||
{% endblock %} {# content #}
|
|
||||||
|
|
61
src/registrar/templates/domain_sidebar.html
Normal file
61
src/registrar/templates/domain_sidebar.html
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{% load static url_helpers %}
|
||||||
|
|
||||||
|
<div class="margin-bottom-4 tablet:margin-bottom-0">
|
||||||
|
<nav aria-label="Domain sections">
|
||||||
|
<ul class="usa-sidenav">
|
||||||
|
<li class="usa-sidenav__item">
|
||||||
|
{% url 'domain' pk=domain.id as url %}
|
||||||
|
<a href="{{ url }}"
|
||||||
|
{% if request.path == url %}class="usa-current"{% endif %}
|
||||||
|
>
|
||||||
|
Domain Overview
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="usa-sidenav__item">
|
||||||
|
{% url 'todo' as url %}
|
||||||
|
<a href="{{ url }}"
|
||||||
|
{% if request.path == url %}class="usa-current"{% endif %}
|
||||||
|
>
|
||||||
|
DNS name servers
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="usa-sidenav__item">
|
||||||
|
{% url 'todo' as url %}
|
||||||
|
<a href="{{ url }}"
|
||||||
|
{% if request.path == url %}class="usa-current"{% endif %}
|
||||||
|
>
|
||||||
|
Authorizing official
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="usa-sidenav__item">
|
||||||
|
{% url 'todo' as url %}
|
||||||
|
<a href="{{ url }}"
|
||||||
|
{% if request.path == url %}class="usa-current"{% endif %}
|
||||||
|
>
|
||||||
|
Your contact information
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="usa-sidenav__item">
|
||||||
|
{% url 'todo' as url %}
|
||||||
|
<a href="{{ url }}"
|
||||||
|
{% if request.path == url %}class="usa-current"{% endif %}
|
||||||
|
>
|
||||||
|
Security email
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="usa-sidenav__item">
|
||||||
|
{% url 'domain-users' pk=domain.id as url %}
|
||||||
|
<a href="{{ url }}"
|
||||||
|
{% if request.path|startswith:url %}class="usa-current"{% endif %}
|
||||||
|
>
|
||||||
|
User management
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
48
src/registrar/templates/domain_users.html
Normal file
48
src/registrar/templates/domain_users.html
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
{% extends "domain_base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}User management{% endblock %}
|
||||||
|
|
||||||
|
{% block domain_content %}
|
||||||
|
<p><a href="{% url 'home' %}">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
||||||
|
</svg>
|
||||||
|
Back to manage your domains
|
||||||
|
</a></p>
|
||||||
|
|
||||||
|
<h1>User management</h1>
|
||||||
|
|
||||||
|
{% if domain.permissions %}
|
||||||
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
|
||||||
|
<caption class="sr-only">Domain users</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sortable scope="col" role="columnheader">Email</th>
|
||||||
|
<th data-sortable scope="col" role="columnheader">Role</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for permission in domain.permissions.all %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row" role="rowheader" data-sort-value="{{ user.email }}" data-label="Domain name">
|
||||||
|
{{ permission.user.email }}
|
||||||
|
</th>
|
||||||
|
<td data-label="Role">{{ permission.role|title }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div
|
||||||
|
class="usa-sr-only usa-table__announcement-region"
|
||||||
|
aria-live="polite"
|
||||||
|
></div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a class="usa-button usa-button--unstyled" href="{% url 'domain-users-add' pk=domain.id %}">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
|
</svg><span class="margin-left-05">Add another user</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% endblock %} {# domain_content #}
|
|
@ -17,7 +17,7 @@
|
||||||
<section class="dashboard tablet:grid-col-11 desktop:grid-col-10">
|
<section class="dashboard tablet:grid-col-11 desktop:grid-col-10">
|
||||||
<h2>Registered domains</h2>
|
<h2>Registered domains</h2>
|
||||||
{% if domains %}
|
{% if domains %}
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||||
<caption class="sr-only">Your domain applications</caption>
|
<caption class="sr-only">Your domain applications</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
<section class="dashboard tablet:grid-col-11 desktop:grid-col-10">
|
<section class="dashboard tablet:grid-col-11 desktop:grid-col-10">
|
||||||
<h2>Active domain requests</h2>
|
<h2>Active domain requests</h2>
|
||||||
{% if domain_applications %}
|
{% if domain_applications %}
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||||
<caption class="sr-only">Your domain applications</caption>
|
<caption class="sr-only">Your domain applications</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -111,7 +111,7 @@
|
||||||
|
|
||||||
<section class="tablet:grid-col-11 desktop:grid-col-10">
|
<section class="tablet:grid-col-11 desktop:grid-col-10">
|
||||||
<h2 class="padding-top-1 mobile-lg:padding-top-3"> Export domains</h2>
|
<h2 class="padding-top-1 mobile-lg:padding-top-3"> Export domains</h2>
|
||||||
<p>If you would like to analyze your list of domains further, you can download the list of domains and their statuses as csv file</p>
|
<p>Download a list of your domains and their statuses as a csv file.</p>
|
||||||
<a href="{% url 'todo' %}" class="usa-button usa-button--outline">
|
<a href="{% url 'todo' %}" class="usa-button usa-button--outline">
|
||||||
Export domains as csv
|
Export domains as csv
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -8,3 +8,10 @@ register = template.Library()
|
||||||
def namespaced_url(namespace, name="", **kwargs):
|
def namespaced_url(namespace, name="", **kwargs):
|
||||||
"""Get a URL, given its Django namespace and name."""
|
"""Get a URL, given its Django namespace and name."""
|
||||||
return reverse(f"{namespace}:{name}", kwargs=kwargs)
|
return reverse(f"{namespace}:{name}", kwargs=kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter("startswith")
|
||||||
|
def startswith(text, starts):
|
||||||
|
if isinstance(text, str):
|
||||||
|
return text.startswith(starts)
|
||||||
|
return False
|
||||||
|
|
|
@ -1018,7 +1018,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
# self.assertNotContains(page, "VALUE")
|
# self.assertNotContains(page, "VALUE")
|
||||||
|
|
||||||
|
|
||||||
class TestDomainPermissions(TestWithUser):
|
class TestWithDomainPermissions(TestWithUser):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
|
@ -1034,24 +1034,50 @@ class TestDomainPermissions(TestWithUser):
|
||||||
pass
|
pass
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
|
|
||||||
|
class TestDomainPermissions(TestWithDomainPermissions):
|
||||||
def test_not_logged_in(self):
|
def test_not_logged_in(self):
|
||||||
"""Not logged in gets a redirect to Login."""
|
"""Not logged in gets a redirect to Login."""
|
||||||
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("domain-users", kwargs={"pk": self.domain.id})
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("domain-users-add", kwargs={"pk": self.domain.id})
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
def test_no_domain_role(self):
|
def test_no_domain_role(self):
|
||||||
"""Logged in but no role gets 403 Forbidden."""
|
"""Logged in but no role gets 403 Forbidden."""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
self.role.delete() # user no longer has a role on this domain
|
self.role.delete() # user no longer has a role on this domain
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("domain-users", kwargs={"pk": self.domain.id})
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
class TestDomainDetail(TestDomainPermissions, WebTest):
|
with less_console_noise():
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("domain-users-add", kwargs={"pk": self.domain.id})
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDomainDetail(TestWithDomainPermissions, WebTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
def test_domain_detail_link_works(self):
|
def test_domain_detail_link_works(self):
|
||||||
home_page = self.app.get("/")
|
home_page = self.app.get("/")
|
||||||
|
@ -1059,3 +1085,48 @@ class TestDomainDetail(TestDomainPermissions, WebTest):
|
||||||
# click the "Edit" link
|
# click the "Edit" link
|
||||||
detail_page = home_page.click("Edit")
|
detail_page = home_page.click("Edit")
|
||||||
self.assertContains(detail_page, "igorville.gov")
|
self.assertContains(detail_page, "igorville.gov")
|
||||||
|
|
||||||
|
def test_domain_user_management(self):
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("domain-users", kwargs={"pk": self.domain.id})
|
||||||
|
)
|
||||||
|
self.assertContains(response, "User management")
|
||||||
|
|
||||||
|
def test_domain_user_management_add_link(self):
|
||||||
|
"""Button to get to user add page works."""
|
||||||
|
management_page = self.app.get(
|
||||||
|
reverse("domain-users", kwargs={"pk": self.domain.id})
|
||||||
|
)
|
||||||
|
add_page = management_page.click("Add another user")
|
||||||
|
self.assertContains(add_page, "Add another user")
|
||||||
|
|
||||||
|
def test_domain_user_add(self):
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("domain-users-add", kwargs={"pk": self.domain.id})
|
||||||
|
)
|
||||||
|
self.assertContains(response, "Add another user")
|
||||||
|
|
||||||
|
def test_domain_user_add_form(self):
|
||||||
|
"""Adding a user works."""
|
||||||
|
other_user, _ = get_user_model().objects.get_or_create(
|
||||||
|
email="mayor@igorville.gov"
|
||||||
|
)
|
||||||
|
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"] = "mayor@igorville.gov"
|
||||||
|
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
success_result = add_page.form.submit()
|
||||||
|
|
||||||
|
self.assertEqual(success_result.status_code, 302)
|
||||||
|
self.assertEqual(
|
||||||
|
success_result["Location"],
|
||||||
|
reverse("domain-users", kwargs={"pk": self.domain.id}),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
success_page = success_result.follow()
|
||||||
|
self.assertContains(success_page, "mayor@igorville.gov")
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from .application import *
|
from .application import *
|
||||||
from .domain import *
|
from .domain import DomainView, DomainUsersView, DomainAddUserView
|
||||||
from .health import *
|
from .health import *
|
||||||
from .index import *
|
from .index import *
|
||||||
from .whoami import *
|
from .whoami import *
|
||||||
|
|
|
@ -1,13 +1,89 @@
|
||||||
"""View for a single Domain."""
|
"""View for a single Domain."""
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.contrib import messages
|
||||||
|
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 import DetailView
|
||||||
|
from django.views.generic.edit import FormMixin
|
||||||
|
|
||||||
from registrar.models import Domain
|
from registrar.models import Domain, User, UserDomainRole
|
||||||
|
|
||||||
from .utility import DomainPermission
|
from .utility import DomainPermission
|
||||||
|
|
||||||
|
|
||||||
class DomainView(DomainPermission, DetailView):
|
class DomainView(DomainPermission, DetailView):
|
||||||
|
|
||||||
|
"""Domain detail overview page."""
|
||||||
|
|
||||||
model = Domain
|
model = Domain
|
||||||
template_name = "domain_detail.html"
|
template_name = "domain_detail.html"
|
||||||
context_object_name = "domain"
|
context_object_name = "domain"
|
||||||
|
|
||||||
|
|
||||||
|
class DomainUsersView(DomainPermission, DetailView):
|
||||||
|
|
||||||
|
"""User management page in the domain details."""
|
||||||
|
|
||||||
|
model = Domain
|
||||||
|
template_name = "domain_users.html"
|
||||||
|
context_object_name = "domain"
|
||||||
|
|
||||||
|
|
||||||
|
class DomainAddUserForm(DomainPermission, 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
|
||||||
|
|
||||||
|
|
||||||
|
class DomainAddUserView(DomainPermission, FormMixin, DetailView):
|
||||||
|
|
||||||
|
"""Inside of a domain's user management, a form for adding users.
|
||||||
|
|
||||||
|
Multiple inheritance is used here for permissions, form handling, and
|
||||||
|
details of the individual domain.
|
||||||
|
"""
|
||||||
|
|
||||||
|
template_name = "domain_add_user.html"
|
||||||
|
model = Domain
|
||||||
|
form_class = DomainAddUserForm
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse("domain-users", kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
form = self.get_form()
|
||||||
|
if form.is_valid():
|
||||||
|
return self.form_valid(form)
|
||||||
|
else:
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""Add the specified user on this domain."""
|
||||||
|
requested_email = form.cleaned_data["email"]
|
||||||
|
# look up a user with that email
|
||||||
|
# they should exist because we checked in clean_email
|
||||||
|
requested_user = User.objects.get(email=requested_email)
|
||||||
|
|
||||||
|
try:
|
||||||
|
UserDomainRole.objects.create(
|
||||||
|
user=requested_user, domain=self.object, role=UserDomainRole.Roles.ADMIN
|
||||||
|
)
|
||||||
|
except IntegrityError:
|
||||||
|
# User already has the desired role! Do nothing??
|
||||||
|
pass
|
||||||
|
|
||||||
|
messages.success(self.request, f"Added user {requested_email}.")
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
|
|
@ -49,6 +49,8 @@
|
||||||
10038 OUTOFSCOPE http://app:8080/public/css/.*
|
10038 OUTOFSCOPE http://app:8080/public/css/.*
|
||||||
10038 OUTOFSCOPE http://app:8080/public/js/.*
|
10038 OUTOFSCOPE http://app:8080/public/js/.*
|
||||||
10038 OUTOFSCOPE http://app:8080/(robots.txt|sitemap.xml|TODO|edit/)
|
10038 OUTOFSCOPE http://app:8080/(robots.txt|sitemap.xml|TODO|edit/)
|
||||||
|
10038 OUTOFSCOPE http://app:8080/users
|
||||||
|
10038 OUTOFSCOPE http://app:8080/users/add
|
||||||
# This URL always returns 404, so include it as well.
|
# This URL always returns 404, so include it as well.
|
||||||
10038 OUTOFSCOPE http://app:8080/todo
|
10038 OUTOFSCOPE http://app:8080/todo
|
||||||
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
|
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue