mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-06-11 06:54:48 +02:00
Add UserDomainRole table and helpers
This commit is contained in:
parent
22eb49c004
commit
49b4f078e8
9 changed files with 211 additions and 7 deletions
|
@ -0,0 +1,66 @@
|
||||||
|
# Generated by Django 4.1.6 on 2023-03-06 17:26
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0011_remove_domainapplication_security_email"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="domain",
|
||||||
|
name="owners",
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserDomainRole",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
("role", models.TextField(choices=[("admin", "Admin")])),
|
||||||
|
(
|
||||||
|
"domain",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="permissions",
|
||||||
|
to="registrar.domain",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="permissions",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="domains",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
related_name="users",
|
||||||
|
through="registrar.UserDomainRole",
|
||||||
|
to="registrar.domain",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="userdomainrole",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("user", "domain"), name="unique_user_domain_role"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -6,6 +6,7 @@ from .domain import Domain
|
||||||
from .host_ip import HostIP
|
from .host_ip import HostIP
|
||||||
from .host import Host
|
from .host import Host
|
||||||
from .nameserver import Nameserver
|
from .nameserver import Nameserver
|
||||||
|
from .user_domain_role import UserDomainRole
|
||||||
from .user_profile import UserProfile
|
from .user_profile import UserProfile
|
||||||
from .user import User
|
from .user import User
|
||||||
from .website import Website
|
from .website import Website
|
||||||
|
@ -17,6 +18,7 @@ __all__ = [
|
||||||
"HostIP",
|
"HostIP",
|
||||||
"Host",
|
"Host",
|
||||||
"Nameserver",
|
"Nameserver",
|
||||||
|
"UserDomainRole",
|
||||||
"UserProfile",
|
"UserProfile",
|
||||||
"User",
|
"User",
|
||||||
"Website",
|
"Website",
|
||||||
|
@ -28,6 +30,7 @@ auditlog.register(Domain)
|
||||||
auditlog.register(HostIP)
|
auditlog.register(HostIP)
|
||||||
auditlog.register(Host)
|
auditlog.register(Host)
|
||||||
auditlog.register(Nameserver)
|
auditlog.register(Nameserver)
|
||||||
|
auditlog.register(UserDomainRole)
|
||||||
auditlog.register(UserProfile)
|
auditlog.register(UserProfile)
|
||||||
auditlog.register(User)
|
auditlog.register(User)
|
||||||
auditlog.register(Website)
|
auditlog.register(Website)
|
||||||
|
|
|
@ -276,9 +276,8 @@ class Domain(TimeStampedModel):
|
||||||
help_text="Domain is live in the registry",
|
help_text="Domain is live in the registry",
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: determine the relationship between this field
|
# ForeignKey on UserDomainRole creates a "permissions" member for
|
||||||
# and the domain application's `creator` and `submitter`
|
# all of the user-roles that are in place for this domain
|
||||||
owners = models.ManyToManyField(
|
|
||||||
"registrar.User",
|
# ManyToManyField on User creates a "users" member for all of the
|
||||||
help_text="",
|
# users who have some role on this domain
|
||||||
)
|
|
||||||
|
|
|
@ -502,6 +502,25 @@ class DomainApplication(TimeStampedModel):
|
||||||
# This is a side-effect of the state transition
|
# This is a side-effect of the state transition
|
||||||
self._send_confirmation_email()
|
self._send_confirmation_email()
|
||||||
|
|
||||||
|
@transition(field="status", source=[SUBMITTED, INVESTIGATING], target=APPROVED)
|
||||||
|
def approve(self):
|
||||||
|
"""Approve an application that has been submitted.
|
||||||
|
|
||||||
|
This has substantial side-effects because it creates another database
|
||||||
|
object for the approved Domain and makes the user who created the
|
||||||
|
application into an admin on that domain.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# create the domain if it doesn't exist
|
||||||
|
Domain = apps.get_model("registrar.Domain")
|
||||||
|
created_domain, _ = Domain.objects.get_or_create(name=self.requested_domain)
|
||||||
|
|
||||||
|
# create the permission for the user
|
||||||
|
UserDomainRole = apps.get_model("registrar.UserDomainRole")
|
||||||
|
UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.creator, domain=created_domain, role=UserDomainRole.Roles.ADMIN
|
||||||
|
)
|
||||||
|
|
||||||
# ## Form policies ###
|
# ## Form policies ###
|
||||||
#
|
#
|
||||||
# These methods control what questions need to be answered by applicants
|
# These methods control what questions need to be answered by applicants
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
|
@ -7,6 +8,12 @@ class User(AbstractUser):
|
||||||
but can be customized later.
|
but can be customized later.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
domains = models.ManyToManyField(
|
||||||
|
"registrar.Domain",
|
||||||
|
through="registrar.UserDomainRole",
|
||||||
|
related_name="users",
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
# this info is pulled from Login.gov
|
# this info is pulled from Login.gov
|
||||||
if self.first_name or self.last_name:
|
if self.first_name or self.last_name:
|
||||||
|
|
49
src/registrar/models/user_domain_role.py
Normal file
49
src/registrar/models/user_domain_role.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
|
||||||
|
class UserDomainRole(TimeStampedModel):
|
||||||
|
|
||||||
|
"""This is a linking table that connects a user with a role on a domain."""
|
||||||
|
|
||||||
|
class Roles(models.TextChoices):
|
||||||
|
|
||||||
|
"""The possible roles are listed here.
|
||||||
|
|
||||||
|
Implementation of the named roles for allowing particular operations happens
|
||||||
|
elsewhere.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ADMIN = "admin"
|
||||||
|
|
||||||
|
user = models.ForeignKey(
|
||||||
|
"registrar.User",
|
||||||
|
null=False,
|
||||||
|
on_delete=models.CASCADE, # when a user is deleted, their permissions will be too
|
||||||
|
related_name="permissions",
|
||||||
|
)
|
||||||
|
|
||||||
|
domain = models.ForeignKey(
|
||||||
|
"registrar.Domain",
|
||||||
|
null=False,
|
||||||
|
on_delete=models.CASCADE, # when a domain is deleted, permissions are too
|
||||||
|
related_name="permissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
role = models.TextField(
|
||||||
|
choices=Roles.choices,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "User {} is {} on domain {}".format(self.user, self.role, self.domain)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
# a user can have only one role on a given domain, that is, there can
|
||||||
|
# be only a single row with a certain (user, domain) pair.
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=['user', 'domain'], name='unique_user_domain_role'
|
||||||
|
)
|
||||||
|
]
|
|
@ -16,7 +16,41 @@
|
||||||
|
|
||||||
<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 %}
|
||||||
|
<table class="usa-table usa-table--borderless usa-table--stacked">
|
||||||
|
<caption class="sr-only">Your domain applications</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sortable scope="col" role="columnheader">Domain name</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="usa-sr-only">Action</span></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for domain in domains %}
|
||||||
|
<tr>
|
||||||
|
<th th scope="row" role="rowheader" data-label="Domain name">
|
||||||
|
{{ domain.name }}
|
||||||
|
</th>
|
||||||
|
<td data-sort-value="{{ domain.created_time|date:"U" }}" data-label="Date created">{{ domain.created_time|date }}</td>
|
||||||
|
<td data-label="Status">{{ domain.application_status|title }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url "todo" %}">
|
||||||
|
Edit <span class="usa-sr-only">{{ domain.name }} </span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div
|
||||||
|
class="usa-sr-only usa-table__announcement-region"
|
||||||
|
aria-live="polite"
|
||||||
|
></div>
|
||||||
|
{% else %}
|
||||||
<p>You don't have any registered domains yet</p>
|
<p>You don't have any registered domains yet</p>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="dashboard tablet:grid-col-11 desktop:grid-col-10">
|
<section class="dashboard tablet:grid-col-11 desktop:grid-col-10">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
from registrar.models import Contact, DomainApplication, User, Website, Domain
|
from registrar.models import Contact, DomainApplication, User, Website, Domain, UserDomainRole
|
||||||
from unittest import skip
|
from unittest import skip
|
||||||
|
|
||||||
import boto3_mocking # type: ignore
|
import boto3_mocking # type: ignore
|
||||||
|
@ -139,6 +139,24 @@ class TestDomain(TestCase):
|
||||||
d1.activate()
|
d1.activate()
|
||||||
|
|
||||||
|
|
||||||
|
class TestPermissions(TestCase):
|
||||||
|
|
||||||
|
"""Test the User-Domain-Role connection."""
|
||||||
|
|
||||||
|
def test_approval_creates_role(self):
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
|
user, _ = User.objects.get_or_create()
|
||||||
|
application = DomainApplication.objects.create(creator=user,
|
||||||
|
requested_domain=domain)
|
||||||
|
# skip using the submit method
|
||||||
|
application.status = DomainApplication.SUBMITTED
|
||||||
|
application.approve()
|
||||||
|
|
||||||
|
# should be a role for this user
|
||||||
|
self.assertTrue(UserDomainRole.objects.get(user=user, domain=domain))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@skip("Not implemented yet.")
|
@skip("Not implemented yet.")
|
||||||
class TestDomainApplicationLifeCycle(TestCase):
|
class TestDomainApplicationLifeCycle(TestCase):
|
||||||
def test_application_approval(self):
|
def test_application_approval(self):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.db.models import F
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
from registrar.models import DomainApplication
|
from registrar.models import DomainApplication
|
||||||
|
@ -9,4 +10,12 @@ def index(request):
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
applications = DomainApplication.objects.filter(creator=request.user)
|
applications = DomainApplication.objects.filter(creator=request.user)
|
||||||
context["domain_applications"] = applications
|
context["domain_applications"] = applications
|
||||||
|
|
||||||
|
domains = request.user.permissions.values(
|
||||||
|
"role",
|
||||||
|
name=F("domain__name"),
|
||||||
|
created_time=F("domain__created_at"),
|
||||||
|
application_status=F("domain__domain_application__status"),
|
||||||
|
)
|
||||||
|
context["domains"] = domains
|
||||||
return render(request, "home.html", context)
|
return render(request, "home.html", context)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue