mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-22 09:11:04 +02:00
Add some additional data + migration
This commit is contained in:
parent
9ac8a3e8bc
commit
10c01870d7
7 changed files with 184 additions and 28 deletions
|
@ -1275,6 +1275,8 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin):
|
||||||
"get_roles",
|
"get_roles",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
readonly_fields = ["invitation"]
|
||||||
|
|
||||||
autocomplete_fields = ["user", "portfolio"]
|
autocomplete_fields = ["user", "portfolio"]
|
||||||
search_fields = ["user__first_name", "user__last_name", "user__email", "portfolio__organization_name"]
|
search_fields = ["user__first_name", "user__last_name", "user__email", "portfolio__organization_name"]
|
||||||
search_help_text = "Search by first name, last name, email, or portfolio."
|
search_help_text = "Search by first name, last name, email, or portfolio."
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-11-14 17:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0136_domainrequest_requested_suborganization_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="userportfoliopermission",
|
||||||
|
name="invitation",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="created_user_portfolio_permission",
|
||||||
|
to="registrar.portfolioinvitation",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -9,6 +9,8 @@ from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
from .utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices # type: ignore
|
from .utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices # type: ignore
|
||||||
from .utility.time_stamped_model import TimeStampedModel
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.contrib.admin.models import LogEntry, ADDITION
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -65,6 +67,20 @@ class PortfolioInvitation(TimeStampedModel):
|
||||||
protected=True, # can't alter state except through transition methods!
|
protected=True, # can't alter state except through transition methods!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO - replace this with a "creator" field on portfolio invitation. This should be another ticket.
|
||||||
|
@property
|
||||||
|
def creator(self):
|
||||||
|
"""Get the user who created this invitation from the audit log"""
|
||||||
|
content_type = ContentType.objects.get_for_model(self)
|
||||||
|
log_entry = LogEntry.objects.filter(
|
||||||
|
content_type=content_type,
|
||||||
|
object_id=self.pk,
|
||||||
|
action_flag=ADDITION
|
||||||
|
).order_by("action_time").first()
|
||||||
|
|
||||||
|
return log_entry.user if log_entry else None
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Invitation for {self.email} on {self.portfolio} is {self.status}"
|
return f"Invitation for {self.email} on {self.portfolio} is {self.status}"
|
||||||
|
|
||||||
|
@ -101,7 +117,7 @@ class PortfolioInvitation(TimeStampedModel):
|
||||||
|
|
||||||
# and create a role for that user on this portfolio
|
# and create a role for that user on this portfolio
|
||||||
user_portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
|
user_portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
|
||||||
portfolio=self.portfolio, user=user
|
portfolio=self.portfolio, user=user, invitation=self
|
||||||
)
|
)
|
||||||
if self.roles and len(self.roles) > 0:
|
if self.roles and len(self.roles) > 0:
|
||||||
user_portfolio_permission.roles = self.roles
|
user_portfolio_permission.roles = self.roles
|
||||||
|
|
|
@ -65,6 +65,16 @@ class UserPortfolioPermission(TimeStampedModel):
|
||||||
help_text="Select one or more additional permissions.",
|
help_text="Select one or more additional permissions.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO - this needs a small script to update existing values
|
||||||
|
invitation = models.ForeignKey(
|
||||||
|
"registrar.PortfolioInvitation",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
# We don't want to accidentally delete invitations
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="created_user_portfolio_permission",
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
readable_roles = []
|
readable_roles = []
|
||||||
if self.roles:
|
if self.roles:
|
||||||
|
|
|
@ -26,7 +26,7 @@ from registrar.utility.constants import BranchChoices
|
||||||
from registrar.utility.enums import DefaultEmail
|
from registrar.utility.enums import DefaultEmail
|
||||||
from django.contrib.postgres.aggregates import ArrayAgg
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
|
||||||
from registrar.utility.model_dicts import BaseModelDict, PortfolioInvitationModelDict, UserPortfolioPermissionModelDict
|
from registrar.utility.model_annotations import BaseModelAnnotation, PortfolioInvitationModelAnnotation, UserPortfolioPermissionModelAnnotation
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -58,7 +58,7 @@ def format_end_date(end_date):
|
||||||
return timezone.make_aware(datetime.strptime(end_date, "%Y-%m-%d")) if end_date else get_default_end_date()
|
return timezone.make_aware(datetime.strptime(end_date, "%Y-%m-%d")) if end_date else get_default_end_date()
|
||||||
|
|
||||||
|
|
||||||
class BaseExport(BaseModelDict):
|
class BaseExport(BaseModelAnnotation):
|
||||||
"""
|
"""
|
||||||
A generic class for exporting data which returns a csv file for the given model.
|
A generic class for exporting data which returns a csv file for the given model.
|
||||||
Base class in an inheritance tree of 3.
|
Base class in an inheritance tree of 3.
|
||||||
|
@ -87,13 +87,13 @@ class BaseExport(BaseModelDict):
|
||||||
"""
|
"""
|
||||||
writer = csv.writer(csv_file)
|
writer = csv.writer(csv_file)
|
||||||
columns = cls.get_columns()
|
columns = cls.get_columns()
|
||||||
models_dict = cls.get_models_dict(request=request)
|
model_dict = cls.get_model_dict(request=request)
|
||||||
|
|
||||||
# Write to csv file before the write_csv
|
# Write to csv file before the write_csv
|
||||||
cls.write_csv_before(writer, **export_kwargs)
|
cls.write_csv_before(writer, **export_kwargs)
|
||||||
|
|
||||||
# Write the csv file
|
# Write the csv file
|
||||||
rows = cls.write_csv(writer, columns, models_dict)
|
rows = cls.write_csv(writer, columns, model_dict)
|
||||||
|
|
||||||
# Return rows that for easier parsing and testing
|
# Return rows that for easier parsing and testing
|
||||||
return rows
|
return rows
|
||||||
|
@ -146,7 +146,7 @@ class MemberExport(BaseExport):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_models_dict(cls, request=None):
|
def get_model_dict(cls, request=None):
|
||||||
portfolio = request.session.get("portfolio")
|
portfolio = request.session.get("portfolio")
|
||||||
if not portfolio:
|
if not portfolio:
|
||||||
return {}
|
return {}
|
||||||
|
@ -164,10 +164,13 @@ class MemberExport(BaseExport):
|
||||||
"member_display",
|
"member_display",
|
||||||
"domain_info",
|
"domain_info",
|
||||||
"source",
|
"source",
|
||||||
|
"invitation_date",
|
||||||
|
"invited_by",
|
||||||
]
|
]
|
||||||
permissions = UserPortfolioPermissionModelDict.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns)
|
permissions = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns)
|
||||||
invitations = PortfolioInvitationModelDict.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns)
|
invitations = PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns)
|
||||||
return convert_queryset_to_dict(permissions.union(invitations), is_model=False)
|
queryset_dict = convert_queryset_to_dict(permissions.union(invitations), is_model=False)
|
||||||
|
return queryset_dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_columns(cls):
|
def get_columns(cls):
|
||||||
|
@ -178,6 +181,7 @@ class MemberExport(BaseExport):
|
||||||
"Email",
|
"Email",
|
||||||
"Organization admin",
|
"Organization admin",
|
||||||
"Invited by",
|
"Invited by",
|
||||||
|
"Invitation date",
|
||||||
"Last active",
|
"Last active",
|
||||||
"Domain requests",
|
"Domain requests",
|
||||||
"Member management",
|
"Member management",
|
||||||
|
@ -195,19 +199,26 @@ class MemberExport(BaseExport):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (model.get("roles") or [])
|
is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (model.get("roles") or [])
|
||||||
domains = ",".join(model.get("domain_info")) if model.get("domain_info") else ""
|
# Tracks if they can view, create requests, or not do anything
|
||||||
|
x = model.get("roles")
|
||||||
|
print(f"what are the roles? {x}")
|
||||||
|
domain_request_user_permission = None
|
||||||
|
|
||||||
|
user_managed_domains = model.get("domain_info", [])
|
||||||
|
managed_domains_as_csv = ",".join(user_managed_domains)
|
||||||
|
# Whether they can make domain requests. Tentatively, I think the options as we currently understand would be: None, Viewer, Viewer Requester
|
||||||
FIELDS = {
|
FIELDS = {
|
||||||
"Email": model.get("email_display"),
|
"Email": model.get("email_display"),
|
||||||
"Organization admin": is_admin,
|
"Organization admin": is_admin,
|
||||||
"Invited by": model.get("source"),
|
"Invited by": model.get("invited_by"),
|
||||||
|
"Invitation date": model.get("invitation_date"),
|
||||||
"Last active": model.get("last_active"),
|
"Last active": model.get("last_active"),
|
||||||
"Domain requests": "TODO",
|
"Domain requests": "TODO",
|
||||||
"Member management": "TODO",
|
"Member management": "TODO",
|
||||||
"Domain management": "TODO",
|
"Domain management": "TODO",
|
||||||
"Number of domains": "TODO",
|
"Number of domains": len(user_managed_domains),
|
||||||
# Quote enclose the domain list
|
# TODO - this doesn't quote enclose with one record
|
||||||
# Note: this will only enclose when more than two items exist
|
"Domains": managed_domains_as_csv,
|
||||||
"Domains": domains,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# "id",
|
# "id",
|
||||||
|
|
|
@ -1,5 +1,24 @@
|
||||||
"""
|
"""
|
||||||
TODO: explanation here
|
Model annotation classes. Intended to return django querysets with computed fields for api endpoints and our csv reports.
|
||||||
|
|
||||||
|
Created to manage the complexity of the MembersTable and Members CSV report, as they require complex but common annotations.
|
||||||
|
|
||||||
|
These classes provide consistent, reusable query transformations that:
|
||||||
|
1. Add computed fields via annotations
|
||||||
|
2. Handle related model data
|
||||||
|
3. Format fields for display
|
||||||
|
4. Standardize field names across different contexts
|
||||||
|
|
||||||
|
Used by both API endpoints (e.g. portfolio members JSON) and data exports (e.g. CSV reports).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
# For a JSON table endpoint
|
||||||
|
permissions = UserPortfolioPermissionAnnotation.get_annotated_queryset(portfolio)
|
||||||
|
# Returns queryset with standardized fields for member tables
|
||||||
|
|
||||||
|
# For a CSV export
|
||||||
|
permissions = UserPortfolioPermissionAnnotation.get_annotated_queryset(portfolio, csv_report=True)
|
||||||
|
# Returns same fields but formatted for CSV export
|
||||||
"""
|
"""
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
|
@ -12,9 +31,19 @@ from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
from registrar.models.utility.generic_helper import convert_queryset_to_dict
|
from registrar.models.utility.generic_helper import convert_queryset_to_dict
|
||||||
from registrar.models.utility.orm_helper import ArrayRemove
|
from registrar.models.utility.orm_helper import ArrayRemove
|
||||||
from django.contrib.postgres.aggregates import ArrayAgg
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
from django.contrib.admin.models import LogEntry, ADDITION
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
|
||||||
class BaseModelDict(ABC):
|
class BaseModelAnnotation(ABC):
|
||||||
|
"""
|
||||||
|
Abstract base class that standardizes how models are annotated for csv exports or complex annotation queries.
|
||||||
|
For example, the Members table / csv export.
|
||||||
|
|
||||||
|
Subclasses define model-specific annotations, filters, and field formatting while inheriting
|
||||||
|
common queryset building logic.
|
||||||
|
Intended ensure consistent data presentation across both table UI components and CSV exports.
|
||||||
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
@ -166,14 +195,16 @@ class BaseModelDict(ABC):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_models_dict(cls, **kwargs):
|
def get_model_dict(cls, **kwargs):
|
||||||
request = kwargs.get("request")
|
|
||||||
print(f"get_models_dict => request is: {request}")
|
|
||||||
return convert_queryset_to_dict(cls.get_annotated_queryset(**kwargs), is_model=False)
|
return convert_queryset_to_dict(cls.get_annotated_queryset(**kwargs), is_model=False)
|
||||||
|
|
||||||
|
|
||||||
class UserPortfolioPermissionModelDict(BaseModelDict):
|
class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation):
|
||||||
|
"""
|
||||||
|
Annotates UserPortfolioPermission querysets with computed fields for member tables.
|
||||||
|
Handles formatting of user details, permissions, and related domain information
|
||||||
|
for both UI display and CSV export.
|
||||||
|
"""
|
||||||
@classmethod
|
@classmethod
|
||||||
def model(cls):
|
def model(cls):
|
||||||
# Return the model class that this export handles
|
# Return the model class that this export handles
|
||||||
|
@ -217,7 +248,7 @@ class UserPortfolioPermissionModelDict(BaseModelDict):
|
||||||
domain_query = F("user__permissions__domain__name")
|
domain_query = F("user__permissions__domain__name")
|
||||||
last_active_query = Func(
|
last_active_query = Func(
|
||||||
F("user__last_login"),
|
F("user__last_login"),
|
||||||
Value("FMMonth DD, YYYY"),
|
Value("YYYY-MM-DD"),
|
||||||
function="to_char",
|
function="to_char",
|
||||||
output_field=TextField()
|
output_field=TextField()
|
||||||
)
|
)
|
||||||
|
@ -263,6 +294,30 @@ class UserPortfolioPermissionModelDict(BaseModelDict):
|
||||||
& Q(user__permissions__domain__domain_info__portfolio=portfolio),
|
& Q(user__permissions__domain__domain_info__portfolio=portfolio),
|
||||||
),
|
),
|
||||||
"source": Value("permission", output_field=CharField()),
|
"source": Value("permission", output_field=CharField()),
|
||||||
|
"invitation_date": Coalesce(
|
||||||
|
Func(
|
||||||
|
F("invitation__created_at"),
|
||||||
|
Value("YYYY-MM-DD"),
|
||||||
|
function="to_char",
|
||||||
|
output_field=TextField()
|
||||||
|
),
|
||||||
|
Value("Invalid date"),
|
||||||
|
output_field=TextField(),
|
||||||
|
),
|
||||||
|
# TODO - replace this with a "creator" field on portfolio invitation. This should be another ticket.
|
||||||
|
# Grab the invitation creator from the audit log. This will need to be replaced with a creator field.
|
||||||
|
# When that happens, just replace this with F("invitation__creator")
|
||||||
|
"invited_by": Coalesce(
|
||||||
|
Subquery(
|
||||||
|
LogEntry.objects.filter(
|
||||||
|
content_type=ContentType.objects.get_for_model(PortfolioInvitation),
|
||||||
|
object_id=Cast(OuterRef("invitation__id"), output_field=TextField()), # Look up the invitation's ID
|
||||||
|
action_flag=ADDITION
|
||||||
|
).order_by("action_time").values("user__email")[:1]
|
||||||
|
),
|
||||||
|
Value("Unknown"),
|
||||||
|
output_field=CharField()
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -287,13 +342,25 @@ class UserPortfolioPermissionModelDict(BaseModelDict):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PortfolioInvitationModelDict(BaseModelDict):
|
class PortfolioInvitationModelAnnotation(BaseModelAnnotation):
|
||||||
|
"""
|
||||||
|
Annotates PortfolioInvitation querysets with computed fields for the member table.
|
||||||
|
Handles formatting of user details, permissions, and related domain information
|
||||||
|
for both UI display and CSV export.
|
||||||
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def model(cls):
|
def model(cls):
|
||||||
# Return the model class that this export handles
|
# Return the model class that this export handles
|
||||||
return PortfolioInvitation
|
return PortfolioInvitation
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_exclusions(cls):
|
||||||
|
"""
|
||||||
|
Get a Q object of exclusion conditions to pass to .exclude() when building queryset.
|
||||||
|
"""
|
||||||
|
return Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_filter_conditions(cls, portfolio):
|
def get_filter_conditions(cls, portfolio):
|
||||||
"""
|
"""
|
||||||
|
@ -322,7 +389,7 @@ class PortfolioInvitationModelDict(BaseModelDict):
|
||||||
else:
|
else:
|
||||||
domain_query = Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField())
|
domain_query = Concat(F("domain__id"), Value(":"), F("domain__name"), output_field=CharField())
|
||||||
|
|
||||||
# Get all existing domain invitations and search on that
|
# Get all existing domain invitations and search on that for domains the user exists on
|
||||||
domain_invitations = DomainInvitation.objects.filter(
|
domain_invitations = DomainInvitation.objects.filter(
|
||||||
email=OuterRef("email"), # Check if email matches the OuterRef("email")
|
email=OuterRef("email"), # Check if email matches the OuterRef("email")
|
||||||
domain__domain_info__portfolio=portfolio, # Check if the domain's portfolio matches the given portfolio
|
domain__domain_info__portfolio=portfolio, # Check if the domain's portfolio matches the given portfolio
|
||||||
|
@ -342,6 +409,31 @@ class PortfolioInvitationModelDict(BaseModelDict):
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
"source": Value("invitation", output_field=CharField()),
|
"source": Value("invitation", output_field=CharField()),
|
||||||
|
"invitation_date": Coalesce(
|
||||||
|
Func(
|
||||||
|
F("created_at"),
|
||||||
|
Value("YYYY-MM-DD"),
|
||||||
|
function="to_char",
|
||||||
|
output_field=TextField()
|
||||||
|
),
|
||||||
|
Value("Invalid date"),
|
||||||
|
output_field=TextField(),
|
||||||
|
),
|
||||||
|
# TODO - replace this with a "creator" field on portfolio invitation. This should be another ticket.
|
||||||
|
# Grab the invitation creator from the audit log. This will need to be replaced with a creator field.
|
||||||
|
# When that happens, just replace this with F("invitation__creator")
|
||||||
|
"invited_by": Coalesce(
|
||||||
|
Subquery(
|
||||||
|
LogEntry.objects.filter(
|
||||||
|
content_type=ContentType.objects.get_for_model(PortfolioInvitation),
|
||||||
|
# Look up the invitation's ID. LogEntry expects a string as this it is stored as json.
|
||||||
|
object_id=Cast(OuterRef("id"), output_field=TextField()),
|
||||||
|
action_flag=ADDITION
|
||||||
|
).order_by("action_time").values("user__email")[:1]
|
||||||
|
),
|
||||||
|
Value("Unknown"),
|
||||||
|
output_field=CharField()
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
|
@ -9,7 +9,7 @@ from django.views import View
|
||||||
|
|
||||||
from registrar.models import UserPortfolioPermission
|
from registrar.models import UserPortfolioPermission
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||||
from registrar.utility.model_dicts import PortfolioInvitationModelDict, UserPortfolioPermissionModelDict
|
from registrar.utility.model_annotations import PortfolioInvitationModelAnnotation, UserPortfolioPermissionModelAnnotation
|
||||||
from registrar.views.utility.mixins import PortfolioMembersPermission
|
from registrar.views.utility.mixins import PortfolioMembersPermission
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View):
|
||||||
|
|
||||||
def initial_permissions_search(self, portfolio):
|
def initial_permissions_search(self, portfolio):
|
||||||
"""Perform initial search for permissions before applying any filters."""
|
"""Perform initial search for permissions before applying any filters."""
|
||||||
queryset = UserPortfolioPermissionModelDict.get_annotated_queryset(portfolio)
|
queryset = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio)
|
||||||
return queryset.values(
|
return queryset.values(
|
||||||
"id",
|
"id",
|
||||||
"first_name",
|
"first_name",
|
||||||
|
@ -72,7 +72,7 @@ class PortfolioMembersJson(PortfolioMembersPermission, View):
|
||||||
def initial_invitations_search(self, portfolio):
|
def initial_invitations_search(self, portfolio):
|
||||||
"""Perform initial invitations search and get related DomainInvitation data based on the email."""
|
"""Perform initial invitations search and get related DomainInvitation data based on the email."""
|
||||||
# Get DomainInvitation query for matching email and for the portfolio
|
# Get DomainInvitation query for matching email and for the portfolio
|
||||||
queryset = PortfolioInvitationModelDict.get_annotated_queryset(portfolio)
|
queryset = PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio)
|
||||||
return queryset.values(
|
return queryset.values(
|
||||||
"id",
|
"id",
|
||||||
"first_name",
|
"first_name",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue