mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-16 17:47:02 +02:00
portfolio invitation model
This commit is contained in:
parent
152e4c0a2c
commit
8f84f33756
10 changed files with 465 additions and 42 deletions
|
@ -11,6 +11,7 @@ from django.shortcuts import redirect
|
|||
from django_fsm import get_available_FIELD_transitions, FSMField
|
||||
from registrar.models.domain_group import DomainGroup
|
||||
from registrar.models.suborganization import Suborganization
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from waffle.decorators import flag_is_active
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
|
@ -131,12 +132,12 @@ class MyUserAdminForm(UserChangeForm):
|
|||
"groups": NoAutocompleteFilteredSelectMultiple("groups", False),
|
||||
"user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False),
|
||||
"portfolio_roles": FilteredSelectMultipleArrayWidget(
|
||||
"portfolio_roles", is_stacked=False, choices=User.UserPortfolioRoleChoices.choices
|
||||
"portfolio_roles", is_stacked=False, choices=UserPortfolioRoleChoices.choices
|
||||
),
|
||||
"portfolio_additional_permissions": FilteredSelectMultipleArrayWidget(
|
||||
"portfolio_additional_permissions",
|
||||
is_stacked=False,
|
||||
choices=User.UserPortfolioPermissionChoices.choices,
|
||||
choices=UserPortfolioPermissionChoices.choices,
|
||||
),
|
||||
}
|
||||
|
||||
|
@ -169,6 +170,24 @@ class MyUserAdminForm(UserChangeForm):
|
|||
)
|
||||
|
||||
|
||||
class PortfolioInvitationAdminForm(UserChangeForm):
|
||||
"""This form utilizes the custom widget for its class's ManyToMany UIs."""
|
||||
|
||||
class Meta:
|
||||
model = models.PortfolioInvitation
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"portfolio_roles": FilteredSelectMultipleArrayWidget(
|
||||
"portfolio_roles", is_stacked=False, choices=UserPortfolioRoleChoices.choices
|
||||
),
|
||||
"portfolio_additional_permissions": FilteredSelectMultipleArrayWidget(
|
||||
"portfolio_additional_permissions",
|
||||
is_stacked=False,
|
||||
choices=UserPortfolioPermissionChoices.choices,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DomainInformationAdminForm(forms.ModelForm):
|
||||
"""This form utilizes the custom widget for its class's ManyToMany UIs."""
|
||||
|
||||
|
@ -1299,6 +1318,56 @@ class DomainInvitationAdmin(ListHeaderAdmin):
|
|||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
class PortfolioInvitationAdmin(ListHeaderAdmin):
|
||||
"""Custom portfolio invitation admin class."""
|
||||
|
||||
form = PortfolioInvitationAdminForm
|
||||
|
||||
class Meta:
|
||||
model = models.PortfolioInvitation
|
||||
fields = "__all__"
|
||||
|
||||
_meta = Meta()
|
||||
|
||||
# Columns
|
||||
list_display = [
|
||||
"email",
|
||||
"portfolio",
|
||||
"portfolio_roles",
|
||||
"portfolio_additional_permissions",
|
||||
"status",
|
||||
]
|
||||
|
||||
# Search
|
||||
search_fields = [
|
||||
"email",
|
||||
"portfolio__name",
|
||||
]
|
||||
|
||||
# Filters
|
||||
list_filter = ("status",)
|
||||
|
||||
search_help_text = "Search by email or portfolio."
|
||||
|
||||
# Mark the FSM field 'status' as readonly
|
||||
# to allow admin users to create Domain Invitations
|
||||
# without triggering the FSM Transition Not Allowed
|
||||
# error.
|
||||
readonly_fields = ["status"]
|
||||
|
||||
autocomplete_fields = ["portfolio"]
|
||||
|
||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||
|
||||
# Select portfolio invitations to change -> Portfolio invitations
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Portfolio invitations"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
class DomainInformationResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
import/export file"""
|
||||
|
@ -2900,6 +2969,7 @@ admin.site.register(models.PublicContact, PublicContactAdmin)
|
|||
admin.site.register(models.DomainRequest, DomainRequestAdmin)
|
||||
admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
|
||||
admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
|
||||
admin.site.register(models.PortfolioInvitation, PortfolioInvitationAdmin)
|
||||
admin.site.register(models.Portfolio, PortfolioAdmin)
|
||||
admin.site.register(models.DomainGroup, DomainGroupAdmin)
|
||||
admin.site.register(models.Suborganization, SuborganizationAdmin)
|
||||
|
|
84
src/registrar/migrations/0114_portfolioinvitation.py
Normal file
84
src/registrar/migrations/0114_portfolioinvitation.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
# Generated by Django 4.2.10 on 2024-07-31 22:49
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django_fsm
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0113_user_portfolio_user_portfolio_additional_permissions_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="PortfolioInvitation",
|
||||
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)),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
(
|
||||
"portfolio_roles",
|
||||
django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(
|
||||
choices=[
|
||||
("organization_admin", "Admin"),
|
||||
("organization_admin_read_only", "Admin read only"),
|
||||
("organization_member", "Member"),
|
||||
],
|
||||
max_length=50,
|
||||
),
|
||||
blank=True,
|
||||
help_text="Select one or more roles.",
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
(
|
||||
"portfolio_additional_permissions",
|
||||
django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(
|
||||
choices=[
|
||||
("view_all_domains", "View all domains and domain reports"),
|
||||
("view_managed_domains", "View managed domains"),
|
||||
("edit_domains", "User is a manager on a domain"),
|
||||
("view_member", "View members"),
|
||||
("edit_member", "Create and edit members"),
|
||||
("view_all_requests", "View all requests"),
|
||||
("view_created_requests", "View created requests"),
|
||||
("edit_requests", "Create and edit requests"),
|
||||
("view_portfolio", "View organization"),
|
||||
("edit_portfolio", "Edit organization"),
|
||||
],
|
||||
max_length=50,
|
||||
),
|
||||
blank=True,
|
||||
help_text="Select one or more additional permissions.",
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
(
|
||||
"status",
|
||||
django_fsm.FSMField(
|
||||
choices=[("invited", "Invited"), ("retrieved", "Retrieved")],
|
||||
default="invited",
|
||||
max_length=50,
|
||||
protected=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"portfolio",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name="portfolios", to="registrar.portfolio"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"indexes": [models.Index(fields=["status"], name="registrar_p_status_aa4218_idx")],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,4 +1,4 @@
|
|||
from auditlog.registry import auditlog # type: ignore
|
||||
from auditlog.registry import auditlog
|
||||
from .contact import Contact
|
||||
from .domain_request import DomainRequest
|
||||
from .domain_information import DomainInformation
|
||||
|
@ -16,6 +16,7 @@ from .website import Website
|
|||
from .transition_domain import TransitionDomain
|
||||
from .verified_by_staff import VerifiedByStaff
|
||||
from .waffle_flag import WaffleFlag
|
||||
from .portfolio_invitation import PortfolioInvitation
|
||||
from .portfolio import Portfolio
|
||||
from .domain_group import DomainGroup
|
||||
from .suborganization import Suborganization
|
||||
|
@ -40,6 +41,7 @@ __all__ = [
|
|||
"TransitionDomain",
|
||||
"VerifiedByStaff",
|
||||
"WaffleFlag",
|
||||
"PortfolioInvitation",
|
||||
"Portfolio",
|
||||
"DomainGroup",
|
||||
"Suborganization",
|
||||
|
@ -63,6 +65,7 @@ auditlog.register(Website)
|
|||
auditlog.register(TransitionDomain)
|
||||
auditlog.register(VerifiedByStaff)
|
||||
auditlog.register(WaffleFlag)
|
||||
auditlog.register(PortfolioInvitation)
|
||||
auditlog.register(Portfolio)
|
||||
auditlog.register(DomainGroup)
|
||||
auditlog.register(Suborganization)
|
||||
|
|
95
src/registrar/models/portfolio_invitation.py
Normal file
95
src/registrar/models/portfolio_invitation.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
"""People are invited by email to administer domains."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
|
||||
from django_fsm import FSMField, transition
|
||||
from .utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices # type: ignore
|
||||
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PortfolioInvitation(TimeStampedModel):
|
||||
class Meta:
|
||||
"""Contains meta information about this class"""
|
||||
|
||||
indexes = [
|
||||
models.Index(fields=["status"]),
|
||||
]
|
||||
|
||||
# Constants for status field
|
||||
class PortfolioInvitationStatus(models.TextChoices):
|
||||
INVITED = "invited", "Invited"
|
||||
RETRIEVED = "retrieved", "Retrieved"
|
||||
|
||||
email = models.EmailField(
|
||||
null=False,
|
||||
blank=False,
|
||||
)
|
||||
|
||||
portfolio = models.ForeignKey(
|
||||
"registrar.Portfolio",
|
||||
on_delete=models.CASCADE, # delete portfolio, then get rid of invitations
|
||||
null=False,
|
||||
related_name="portfolios",
|
||||
)
|
||||
|
||||
portfolio_roles = ArrayField(
|
||||
models.CharField(
|
||||
max_length=50,
|
||||
choices=UserPortfolioRoleChoices.choices,
|
||||
),
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Select one or more roles.",
|
||||
)
|
||||
|
||||
portfolio_additional_permissions = ArrayField(
|
||||
models.CharField(
|
||||
max_length=50,
|
||||
choices=UserPortfolioPermissionChoices.choices,
|
||||
),
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Select one or more additional permissions.",
|
||||
)
|
||||
|
||||
status = FSMField(
|
||||
choices=PortfolioInvitationStatus.choices,
|
||||
default=PortfolioInvitationStatus.INVITED,
|
||||
protected=True, # can't alter state except through transition methods!
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"Invitation for {self.email} on {self.portfolio} is {self.status}"
|
||||
|
||||
@transition(field="status", source=PortfolioInvitationStatus.INVITED, target=PortfolioInvitationStatus.RETRIEVED)
|
||||
def retrieve(self):
|
||||
"""When an invitation is retrieved, create the corresponding permission.
|
||||
|
||||
Raises:
|
||||
RuntimeError if no matching user can be found.
|
||||
"""
|
||||
|
||||
# get a user with this email address
|
||||
User = get_user_model()
|
||||
try:
|
||||
user = User.objects.get(email=self.email)
|
||||
except User.DoesNotExist:
|
||||
# should not happen because a matching user should exist before
|
||||
# we retrieve this invitation
|
||||
raise RuntimeError("Cannot find the user to retrieve this portfolio invitation.")
|
||||
|
||||
# and create a role for that user on this portfolio
|
||||
user.portfolio = self.portfolio
|
||||
if self.portfolio_roles and len(self.portfolio_roles) > 0:
|
||||
user.portfolio_roles = self.portfolio_roles
|
||||
if self.portfolio_additional_permissions and len(self.portfolio_additional_permissions) > 0:
|
||||
user.portfolio_additional_permissions = self.portfolio_additional_permissions
|
||||
user.save()
|
|
@ -5,8 +5,10 @@ from django.db import models
|
|||
from django.db.models import Q
|
||||
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
|
||||
from .domain_invitation import DomainInvitation
|
||||
from .portfolio_invitation import PortfolioInvitation
|
||||
from .transition_domain import TransitionDomain
|
||||
from .verified_by_staff import VerifiedByStaff
|
||||
from .domain import Domain
|
||||
|
@ -62,36 +64,6 @@ class User(AbstractUser):
|
|||
# after they login.
|
||||
FIXTURE_USER = "fixture_user", "Created by fixtures"
|
||||
|
||||
class UserPortfolioRoleChoices(models.TextChoices):
|
||||
"""
|
||||
Roles make it easier for admins to look at
|
||||
"""
|
||||
|
||||
ORGANIZATION_ADMIN = "organization_admin", "Admin"
|
||||
ORGANIZATION_ADMIN_READ_ONLY = "organization_admin_read_only", "Admin read only"
|
||||
ORGANIZATION_MEMBER = "organization_member", "Member"
|
||||
|
||||
class UserPortfolioPermissionChoices(models.TextChoices):
|
||||
""" """
|
||||
|
||||
VIEW_ALL_DOMAINS = "view_all_domains", "View all domains and domain reports"
|
||||
VIEW_MANAGED_DOMAINS = "view_managed_domains", "View managed domains"
|
||||
# EDIT_DOMAINS is really self.domains. We add is hear and leverage it in has_permission
|
||||
# so we have one way to test for portfolio and domain edit permissions
|
||||
# Do we need to check for portfolio domains specifically?
|
||||
# NOTE: A user on an org can currently invite a user outside the org
|
||||
EDIT_DOMAINS = "edit_domains", "User is a manager on a domain"
|
||||
|
||||
VIEW_MEMBER = "view_member", "View members"
|
||||
EDIT_MEMBER = "edit_member", "Create and edit members"
|
||||
|
||||
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
|
||||
VIEW_CREATED_REQUESTS = "view_created_requests", "View created requests"
|
||||
EDIT_REQUESTS = "edit_requests", "Create and edit requests"
|
||||
|
||||
VIEW_PORTFOLIO = "view_portfolio", "View organization"
|
||||
EDIT_PORTFOLIO = "edit_portfolio", "Edit organization"
|
||||
|
||||
PORTFOLIO_ROLE_PERMISSIONS = {
|
||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||
|
@ -270,7 +242,7 @@ class User(AbstractUser):
|
|||
|
||||
# EDIT_DOMAINS === user is a manager on a domain (has UserDomainRole)
|
||||
# NOTE: Should we check whether the domain is in the portfolio?
|
||||
if portfolio_permission == self.UserPortfolioPermissionChoices.EDIT_DOMAINS and self.domains.exists():
|
||||
if portfolio_permission == UserPortfolioPermissionChoices.EDIT_DOMAINS and self.domains.exists():
|
||||
return True
|
||||
|
||||
if not self.portfolio:
|
||||
|
@ -283,22 +255,22 @@ class User(AbstractUser):
|
|||
# the methods below are checks for individual portfolio permissions. they are defined here
|
||||
# to make them easier to call elsewhere throughout the application
|
||||
def has_base_portfolio_permission(self):
|
||||
return self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO)
|
||||
return self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_PORTFOLIO)
|
||||
|
||||
def has_domains_portfolio_permission(self):
|
||||
return (
|
||||
self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
||||
or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
|
||||
self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
||||
or self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
|
||||
# or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_DOMAINS)
|
||||
)
|
||||
|
||||
def has_edit_domains_portfolio_permission(self):
|
||||
return self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_DOMAINS)
|
||||
return self._has_portfolio_permission(UserPortfolioPermissionChoices.EDIT_DOMAINS)
|
||||
|
||||
def has_domain_requests_portfolio_permission(self):
|
||||
return (
|
||||
self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
|
||||
or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
|
||||
self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
|
||||
or self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
|
||||
# or self._has_portfolio_permission(User.UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||
)
|
||||
|
||||
|
@ -409,6 +381,24 @@ class User(AbstractUser):
|
|||
new_domain_invitation = DomainInvitation(email=transition_domain_email.lower(), domain=new_domain)
|
||||
new_domain_invitation.save()
|
||||
|
||||
def check_portfolio_invitations_on_login(self):
|
||||
"""When a user first arrives on the site, we need to retrieve any portfolio
|
||||
invitations that match their email address."""
|
||||
for invitation in PortfolioInvitation.objects.filter(
|
||||
email__iexact=self.email, status=PortfolioInvitation.PortfolioInvitationStatus.INVITED
|
||||
):
|
||||
if self.portfolio is None:
|
||||
try:
|
||||
invitation.retrieve()
|
||||
invitation.save()
|
||||
except RuntimeError:
|
||||
# retrieving should not fail because of a missing user, but
|
||||
# if it does fail, log the error so a new user can continue
|
||||
# logging in
|
||||
logger.warn("Failed to retrieve invitation %s", invitation, exc_info=True)
|
||||
else:
|
||||
logger.warn("User already has a portfolio, did not retrieve invitation %s", invitation, exc_info=True)
|
||||
|
||||
def on_each_login(self):
|
||||
"""Callback each time the user is authenticated.
|
||||
|
||||
|
@ -420,6 +410,7 @@ class User(AbstractUser):
|
|||
"""
|
||||
|
||||
self.check_domain_invitations_on_login()
|
||||
self.check_portfolio_invitations_on_login()
|
||||
|
||||
def is_org_user(self, request):
|
||||
has_organization_feature_flag = flag_is_active(request, "organization_feature")
|
||||
|
|
33
src/registrar/models/utility/portfolio_helper.py
Normal file
33
src/registrar/models/utility/portfolio_helper.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class UserPortfolioRoleChoices(models.TextChoices):
|
||||
"""
|
||||
Roles make it easier for admins to look at
|
||||
"""
|
||||
|
||||
ORGANIZATION_ADMIN = "organization_admin", "Admin"
|
||||
ORGANIZATION_ADMIN_READ_ONLY = "organization_admin_read_only", "Admin read only"
|
||||
ORGANIZATION_MEMBER = "organization_member", "Member"
|
||||
|
||||
|
||||
class UserPortfolioPermissionChoices(models.TextChoices):
|
||||
""" """
|
||||
|
||||
VIEW_ALL_DOMAINS = "view_all_domains", "View all domains and domain reports"
|
||||
VIEW_MANAGED_DOMAINS = "view_managed_domains", "View managed domains"
|
||||
# EDIT_DOMAINS is really self.domains. We add is hear and leverage it in has_permission
|
||||
# so we have one way to test for portfolio and domain edit permissions
|
||||
# Do we need to check for portfolio domains specifically?
|
||||
# NOTE: A user on an org can currently invite a user outside the org
|
||||
EDIT_DOMAINS = "edit_domains", "User is a manager on a domain"
|
||||
|
||||
VIEW_MEMBER = "view_member", "View members"
|
||||
EDIT_MEMBER = "edit_member", "Create and edit members"
|
||||
|
||||
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
|
||||
VIEW_CREATED_REQUESTS = "view_created_requests", "View created requests"
|
||||
EDIT_REQUESTS = "edit_requests", "Create and edit requests"
|
||||
|
||||
VIEW_PORTFOLIO = "view_portfolio", "View organization"
|
||||
EDIT_PORTFOLIO = "edit_portfolio", "Edit organization"
|
|
@ -30,6 +30,8 @@
|
|||
{% include "django/admin/includes/descriptions/verified_by_staff_description.html" %}
|
||||
{% elif opts.model_name == 'website' %}
|
||||
{% include "django/admin/includes/descriptions/website_description.html" %}
|
||||
{% elif opts.model_name == 'portfolioinvitation' %}
|
||||
{% include "django/admin/includes/descriptions/portfolio_invitation_description.html" %}
|
||||
{% else %}
|
||||
<p>This table does not have a description yet.</p>
|
||||
{% endif %}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<p>
|
||||
Portfolio invitations contain all individuals who have been invited to become members of an organization.
|
||||
Invitations are sent via email, and the recipient must log in to the registrar to officially
|
||||
accept and become a member.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
An “invited” status indicates that the recipient has not logged in to the registrar since the invitation was sent
|
||||
or that the recipient has logged in but is already a member of an organization.
|
||||
A “received” status indicates that the recipient has logged in.
|
||||
</p>
|
|
@ -13,6 +13,7 @@ from registrar.admin import (
|
|||
ContactAdmin,
|
||||
DomainInformationAdmin,
|
||||
MyHostAdmin,
|
||||
PortfolioInvitationAdmin,
|
||||
UserDomainRoleAdmin,
|
||||
VerifiedByStaffAdmin,
|
||||
FsmModelResource,
|
||||
|
@ -38,6 +39,7 @@ from registrar.models import (
|
|||
UserGroup,
|
||||
TransitionDomain,
|
||||
)
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.senior_official import SeniorOfficial
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from registrar.models.verified_by_staff import VerifiedByStaff
|
||||
|
@ -177,6 +179,77 @@ class TestDomainInvitationAdmin(TestCase):
|
|||
self.assertContains(response, retrieved_html, count=1)
|
||||
|
||||
|
||||
class TestPortfolioInvitationAdmin(TestCase):
|
||||
"""Tests for the PortfolioInvitationAdmin class as super user
|
||||
|
||||
Notes:
|
||||
all tests share superuser; do not change this model in tests
|
||||
tests have available superuser, client, and admin
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.factory = RequestFactory()
|
||||
cls.admin = ListHeaderAdmin(model=PortfolioInvitationAdmin, admin_site=AdminSite())
|
||||
cls.superuser = create_superuser()
|
||||
|
||||
def setUp(self):
|
||||
"""Create a client object"""
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
|
||||
def tearDown(self):
|
||||
"""Delete all DomainInvitation objects"""
|
||||
PortfolioInvitation.objects.all().delete()
|
||||
Contact.objects.all().delete()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(self):
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_has_model_description(self):
|
||||
"""Tests if this model has a model description on the table view"""
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/portfolioinvitation/",
|
||||
follow=True,
|
||||
)
|
||||
|
||||
# Make sure that the page is loaded correctly
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Test for a description snippet
|
||||
self.assertContains(
|
||||
response,
|
||||
"Portfolio invitations contain all individuals who have been invited to become members of an organization.",
|
||||
)
|
||||
self.assertContains(response, "Show more")
|
||||
|
||||
def test_get_filters(self):
|
||||
"""Ensures that our filters are displaying correctly"""
|
||||
with less_console_noise():
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
response = self.client.get(
|
||||
"/admin/registrar/portfolioinvitation/",
|
||||
{},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
# Assert that the filters are added
|
||||
self.assertContains(response, "invited", count=4)
|
||||
self.assertContains(response, "Invited", count=2)
|
||||
self.assertContains(response, "retrieved", count=2)
|
||||
self.assertContains(response, "Retrieved", count=2)
|
||||
|
||||
# Check for the HTML context specificially
|
||||
invited_html = '<a href="?status__exact=invited">Invited</a>'
|
||||
retrieved_html = '<a href="?status__exact=retrieved">Retrieved</a>'
|
||||
|
||||
self.assertContains(response, invited_html, count=1)
|
||||
self.assertContains(response, retrieved_html, count=1)
|
||||
|
||||
|
||||
class TestHostAdmin(TestCase):
|
||||
"""Tests for the HostAdmin class as super user
|
||||
|
||||
|
|
|
@ -20,7 +20,9 @@ from registrar.models import (
|
|||
|
||||
import boto3_mocking
|
||||
from registrar.models.portfolio import Portfolio
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.transition_domain import TransitionDomain
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from registrar.models.verified_by_staff import VerifiedByStaff # type: ignore
|
||||
from registrar.utility.constants import BranchChoices
|
||||
|
||||
|
@ -1071,8 +1073,8 @@ class TestDomainInformation(TestCase):
|
|||
return {k: v for k, v in dict_obj.items() if k not in bad_fields}
|
||||
|
||||
|
||||
class TestInvitations(TestCase):
|
||||
"""Test the retrieval of invitations."""
|
||||
class TestDomainInvitations(TestCase):
|
||||
"""Test the retrieval of domain invitations."""
|
||||
|
||||
@less_console_noise_decorator
|
||||
def setUp(self):
|
||||
|
@ -1116,6 +1118,65 @@ class TestInvitations(TestCase):
|
|||
self.assertTrue(UserDomainRole.objects.get(user=self.user, domain=self.domain))
|
||||
|
||||
|
||||
class TestPortfolioInvitations(TestCase):
|
||||
"""Test the retrieval of portfolio invitations."""
|
||||
|
||||
@less_console_noise_decorator
|
||||
def setUp(self):
|
||||
self.email = "mayor@igorville.gov"
|
||||
self.email2 = "creator@igorville.gov"
|
||||
self.user, _ = User.objects.get_or_create(email=self.email)
|
||||
self.user2, _ = User.objects.get_or_create(email=self.email2, username="creator")
|
||||
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user2, organization_name="Hotel California")
|
||||
self.portfolio_role_base = UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
||||
self.portfolio_role_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
||||
self.portfolio_permission_1 = UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS
|
||||
self.portfolio_permission_2 = UserPortfolioPermissionChoices.EDIT_REQUESTS
|
||||
self.invitation, _ = PortfolioInvitation.objects.get_or_create(
|
||||
email=self.email,
|
||||
portfolio=self.portfolio,
|
||||
portfolio_roles=[self.portfolio_role_base, self.portfolio_role_admin],
|
||||
portfolio_additional_permissions=[self.portfolio_permission_1, self.portfolio_permission_2],
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
Portfolio.objects.all().delete()
|
||||
PortfolioInvitation.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_retrieval(self):
|
||||
self.assertFalse(self.user.portfolio)
|
||||
self.invitation.retrieve()
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.portfolio.organization_name, "Hotel California")
|
||||
self.assertEqual(self.user.portfolio_roles, [self.portfolio_role_base, self.portfolio_role_admin])
|
||||
self.assertEqual(
|
||||
self.user.portfolio_additional_permissions, [self.portfolio_permission_1, self.portfolio_permission_2]
|
||||
)
|
||||
self.assertEqual(self.invitation.status, PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_retrieve_missing_user_error(self):
|
||||
# get rid of matching users
|
||||
User.objects.filter(email=self.email).delete()
|
||||
with self.assertRaises(RuntimeError):
|
||||
self.invitation.retrieve()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_retrieve_user_already_member_error(self):
|
||||
self.assertFalse(self.user.portfolio)
|
||||
portfolio2, _ = Portfolio.objects.get_or_create(creator=self.user2, organization_name="Tokyo Hotel")
|
||||
self.user.portfolio = portfolio2
|
||||
self.assertEqual(self.user.portfolio.organization_name, "Tokyo Hotel")
|
||||
self.user.save()
|
||||
self.user.check_portfolio_invitations_on_login()
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.portfolio.organization_name, "Tokyo Hotel")
|
||||
self.assertEqual(self.invitation.status, PortfolioInvitation.PortfolioInvitationStatus.INVITED)
|
||||
|
||||
|
||||
class TestUser(TestCase):
|
||||
"""Test actions that occur on user login,
|
||||
test class method that controls how users get validated."""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue