-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2526 from cisagov/bob/2367-portfolio-invitation
Issue #2367: Portfolio invitation backend - MIGRATION - [BOB]
- Loading branch information
Showing
11 changed files
with
478 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
# Generated by Django 4.2.10 on 2024-08-01 12:28 | ||
|
||
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", "0114_alter_user_portfolio_additional_permissions"), | ||
] | ||
|
||
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"), | ||
("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")], | ||
}, | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
Oops, something went wrong.