mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-22 18:56:15 +02:00
lint + add invitation script
This commit is contained in:
parent
9374d91c4b
commit
7075b72533
8 changed files with 133 additions and 56 deletions
|
@ -0,0 +1,27 @@
|
|||
import logging
|
||||
from django.core.management import BaseCommand
|
||||
from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors, TerminalHelper
|
||||
from registrar.models import UserPortfolioPermission, PortfolioInvitation
|
||||
from auditlog.models import LogEntry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand, PopulateScriptTemplate):
|
||||
help = "Loops through each UserPortfolioPermission object and populates the invitation field"
|
||||
|
||||
def handle(self, **kwargs):
|
||||
"""Loops through each DomainRequest object and populates
|
||||
its last_status_update and first_submitted_date values"""
|
||||
self.existing_invitations = PortfolioInvitation.objects.filter(portfolio__isnull=False, email__isnull=False).select_related('portfolio')
|
||||
filter_condition = {"invitation__isnull": True, "portfolio__isnull": False, "user__email__isnull": False}
|
||||
self.mass_update_records(UserPortfolioPermission, filter_condition, fields_to_update=["invitation"])
|
||||
|
||||
def update_record(self, record: UserPortfolioPermission):
|
||||
"""Associate the invitation to the right object"""
|
||||
record.invitation = self.existing_invitations.filter(email=record.user.email, portfolio=record.portfolio).first()
|
||||
TerminalHelper.colorful_logger("INFO", "OKCYAN", f"{TerminalColors.OKCYAN}Adding invitation to {record}")
|
||||
|
||||
def should_skip_record(self, record) -> bool:
|
||||
"""There is nothing to add if no invitation exists"""
|
||||
return not record or not self.existing_invitations.filter(email=record.user.email, portfolio=record.portfolio).exists()
|
|
@ -115,16 +115,19 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
if additional_permissions:
|
||||
portfolio_permissions.update(additional_permissions)
|
||||
return list(portfolio_permissions)
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_domain_request_permission_display(cls, roles, additional_permissions):
|
||||
"""Class method to return a readable string for domain request permissions"""
|
||||
# Tracks if they can view, create requests, or not do anything
|
||||
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, additional_permissions)
|
||||
all_domain_perms = [UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, UserPortfolioPermissionChoices.EDIT_REQUESTS]
|
||||
if (all(perm in all_permissions for perm in all_domain_perms)):
|
||||
all_domain_perms = [
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
]
|
||||
if all(perm in all_permissions for perm in all_domain_perms):
|
||||
return "Viewer Requester"
|
||||
elif (UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions):
|
||||
elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions:
|
||||
return "Viewer"
|
||||
else:
|
||||
return "None"
|
||||
|
@ -134,11 +137,11 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
"""Class method to return a readable string for member permissions"""
|
||||
# Tracks if they can view, create requests, or not do anything
|
||||
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, additional_permissions)
|
||||
# Note for reviewers: the reason why this isn't checking on "all" is because
|
||||
# Note for reviewers: the reason why this isn't checking on "all" is because
|
||||
# the way perms work for members is different than requests. We need to consolidate this.
|
||||
if (UserPortfolioPermissionChoices.EDIT_MEMBERS in all_permissions):
|
||||
if UserPortfolioPermissionChoices.EDIT_MEMBERS in all_permissions:
|
||||
return "Manager"
|
||||
elif (UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions):
|
||||
elif UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions:
|
||||
return "Viewer"
|
||||
else:
|
||||
return "None"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from django.db.models.expressions import Func
|
||||
|
||||
|
||||
class ArrayRemove(Func):
|
||||
"""Custom Func to use array_remove to remove null values"""
|
||||
|
||||
function = "array_remove"
|
||||
template = "%(function)s(%(expressions)s, NULL)"
|
||||
template = "%(function)s(%(expressions)s, NULL)"
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% comment %} {% if user_domain_count and user_domain_count > 0 %} {% endcomment %}
|
||||
{% if member_count and member_count > 0 %}
|
||||
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}">
|
||||
<section aria-label="Domains report component" class="margin-top-205">
|
||||
<a href="{% url 'export_members_portfolio' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" role="button">
|
||||
|
@ -46,7 +46,7 @@
|
|||
</a>
|
||||
</section>
|
||||
</div>
|
||||
{% comment %} {% endif %} {% endcomment %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- ---------- MAIN TABLE ---------- -->
|
||||
|
|
|
@ -11,7 +11,21 @@ from registrar.models import (
|
|||
PublicContact,
|
||||
UserDomainRole,
|
||||
)
|
||||
from django.db.models import Case, CharField, Count, DateField, F, ManyToManyField, Q, QuerySet, Value, When, TextField, OuterRef, Subquery
|
||||
from django.db.models import (
|
||||
Case,
|
||||
CharField,
|
||||
Count,
|
||||
DateField,
|
||||
F,
|
||||
ManyToManyField,
|
||||
Q,
|
||||
QuerySet,
|
||||
Value,
|
||||
When,
|
||||
TextField,
|
||||
OuterRef,
|
||||
Subquery,
|
||||
)
|
||||
from django.db.models.functions import Cast
|
||||
from django.utils import timezone
|
||||
from django.db.models.functions import Concat, Coalesce
|
||||
|
@ -25,7 +39,11 @@ from registrar.utility.constants import BranchChoices
|
|||
from registrar.utility.enums import DefaultEmail
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
|
||||
from registrar.utility.model_annotations import BaseModelAnnotation, PortfolioInvitationModelAnnotation, UserPortfolioPermissionModelAnnotation
|
||||
from registrar.utility.model_annotations import (
|
||||
BaseModelAnnotation,
|
||||
PortfolioInvitationModelAnnotation,
|
||||
UserPortfolioPermissionModelAnnotation,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -134,6 +152,7 @@ class BaseExport(BaseModelAnnotation):
|
|||
"""
|
||||
pass
|
||||
|
||||
|
||||
class MemberExport(BaseExport):
|
||||
|
||||
@classmethod
|
||||
|
@ -143,7 +162,7 @@ class MemberExport(BaseExport):
|
|||
This is a special edge case, but the base report requires this to be defined.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_model_annotation_dict(cls, request=None, **kwargs):
|
||||
portfolio = request.session.get("portfolio")
|
||||
|
@ -166,8 +185,12 @@ class MemberExport(BaseExport):
|
|||
"invitation_date",
|
||||
"invited_by",
|
||||
]
|
||||
permissions = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns)
|
||||
invitations = PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns)
|
||||
permissions = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values(
|
||||
*shared_columns
|
||||
)
|
||||
invitations = PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values(
|
||||
*shared_columns
|
||||
)
|
||||
queryset_dict = convert_queryset_to_dict(permissions.union(invitations), is_model=False)
|
||||
return queryset_dict
|
||||
|
||||
|
@ -199,7 +222,9 @@ class MemberExport(BaseExport):
|
|||
roles = model.get("roles")
|
||||
additional_permissions = model.get("additional_permissions_display")
|
||||
is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (roles or [])
|
||||
domain_request_display = UserPortfolioPermission.get_domain_request_permission_display(roles, additional_permissions)
|
||||
domain_request_display = UserPortfolioPermission.get_domain_request_permission_display(
|
||||
roles, additional_permissions
|
||||
)
|
||||
member_perm_display = UserPortfolioPermission.get_member_permission_display(roles, additional_permissions)
|
||||
user_managed_domains = model.get("domain_info", [])
|
||||
managed_domains_as_csv = ",".join(user_managed_domains)
|
||||
|
@ -231,6 +256,7 @@ class MemberExport(BaseExport):
|
|||
row = [FIELDS.get(column, "") for column in columns]
|
||||
return row
|
||||
|
||||
|
||||
class DomainExport(BaseExport):
|
||||
"""
|
||||
A collection of functions which return csv files regarding Domains. Although class is
|
||||
|
@ -1521,4 +1547,3 @@ class DomainRequestDataFull(DomainRequestExport):
|
|||
distinct=True,
|
||||
)
|
||||
return query
|
||||
|
||||
|
|
|
@ -20,12 +20,26 @@ Example:
|
|||
permissions = UserPortfolioPermissionAnnotation.get_annotated_queryset(portfolio, csv_report=True)
|
||||
# Returns same fields but formatted for CSV export
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from registrar.models import (
|
||||
DomainInvitation,
|
||||
PortfolioInvitation,
|
||||
)
|
||||
from django.db.models import CharField, F, ManyToManyField, Q, QuerySet, Value, TextField, OuterRef, Subquery, Func, Case, When
|
||||
from django.db.models import (
|
||||
CharField,
|
||||
F,
|
||||
ManyToManyField,
|
||||
Q,
|
||||
QuerySet,
|
||||
Value,
|
||||
TextField,
|
||||
OuterRef,
|
||||
Subquery,
|
||||
Func,
|
||||
Case,
|
||||
When,
|
||||
)
|
||||
from django.db.models.functions import Concat, Coalesce, Cast
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
from registrar.models.utility.generic_helper import convert_queryset_to_dict
|
||||
|
@ -39,9 +53,9 @@ 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.
|
||||
common queryset building logic.
|
||||
Intended ensure consistent data presentation across both table UI components and CSV exports.
|
||||
"""
|
||||
|
||||
|
@ -118,7 +132,7 @@ class BaseModelAnnotation(ABC):
|
|||
Get a list of fields from related tables.
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
@classmethod
|
||||
def annotate_and_retrieve_fields(
|
||||
cls, initial_queryset, annotated_fields, related_table_fields=None, include_many_to_many=False, **kwargs
|
||||
|
@ -174,8 +188,7 @@ class BaseModelAnnotation(ABC):
|
|||
|
||||
model_queryset = (
|
||||
cls.model()
|
||||
.objects
|
||||
.select_related(*select_related)
|
||||
.objects.select_related(*select_related)
|
||||
.prefetch_related(*prefetch_related)
|
||||
.filter(filter_conditions)
|
||||
.exclude(exclusions)
|
||||
|
@ -183,9 +196,7 @@ class BaseModelAnnotation(ABC):
|
|||
.order_by(*sort_fields)
|
||||
.distinct()
|
||||
)
|
||||
return cls.annotate_and_retrieve_fields(
|
||||
model_queryset, annotated_fields, related_table_fields, **kwargs
|
||||
)
|
||||
return cls.annotate_and_retrieve_fields(model_queryset, annotated_fields, related_table_fields, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def update_queryset(cls, queryset, **kwargs):
|
||||
|
@ -193,7 +204,7 @@ class BaseModelAnnotation(ABC):
|
|||
Returns an updated queryset. Override in subclass to update queryset.
|
||||
"""
|
||||
return queryset
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_model_annotation_dict(cls, **kwargs):
|
||||
return convert_queryset_to_dict(cls.get_annotated_queryset(**kwargs), is_model=False)
|
||||
|
@ -205,6 +216,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation):
|
|||
Handles formatting of user details, permissions, and related domain information
|
||||
for both UI display and CSV export.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def model(cls):
|
||||
# Return the model class that this export handles
|
||||
|
@ -247,10 +259,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation):
|
|||
if csv_report:
|
||||
domain_query = F("user__permissions__domain__name")
|
||||
last_active_query = Func(
|
||||
F("user__last_login"),
|
||||
Value("YYYY-MM-DD"),
|
||||
function="to_char",
|
||||
output_field=TextField()
|
||||
F("user__last_login"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField()
|
||||
)
|
||||
else:
|
||||
domain_query = Concat(
|
||||
|
@ -272,10 +281,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation):
|
|||
),
|
||||
"additional_permissions_display": F("additional_permissions"),
|
||||
"member_display": Case(
|
||||
When(
|
||||
Q(user__email__isnull=False) & ~Q(user__email=""),
|
||||
then=F("user__email")
|
||||
),
|
||||
When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")),
|
||||
When(
|
||||
Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False),
|
||||
then=Concat(
|
||||
|
@ -290,17 +296,12 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation):
|
|||
"domain_info": ArrayAgg(
|
||||
domain_query,
|
||||
distinct=True,
|
||||
filter=Q(user__permissions__domain__isnull=False)
|
||||
filter=Q(user__permissions__domain__isnull=False)
|
||||
& Q(user__permissions__domain__domain_info__portfolio=portfolio),
|
||||
),
|
||||
"source": Value("permission", output_field=CharField()),
|
||||
"invitation_date": Coalesce(
|
||||
Func(
|
||||
F("invitation__created_at"),
|
||||
Value("YYYY-MM-DD"),
|
||||
function="to_char",
|
||||
output_field=TextField()
|
||||
),
|
||||
Func(F("invitation__created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField()),
|
||||
Value("Invalid date"),
|
||||
output_field=TextField(),
|
||||
),
|
||||
|
@ -311,12 +312,16 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation):
|
|||
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]
|
||||
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()
|
||||
output_field=CharField(),
|
||||
),
|
||||
}
|
||||
|
||||
|
@ -394,12 +399,7 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation):
|
|||
),
|
||||
"source": Value("invitation", output_field=CharField()),
|
||||
"invitation_date": Coalesce(
|
||||
Func(
|
||||
F("created_at"),
|
||||
Value("YYYY-MM-DD"),
|
||||
function="to_char",
|
||||
output_field=TextField()
|
||||
),
|
||||
Func(F("created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField()),
|
||||
Value("Invalid date"),
|
||||
output_field=TextField(),
|
||||
),
|
||||
|
@ -412,11 +412,13 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation):
|
|||
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]
|
||||
action_flag=ADDITION,
|
||||
)
|
||||
.order_by("action_time")
|
||||
.values("user__email")[:1]
|
||||
),
|
||||
Value("Unknown"),
|
||||
output_field=CharField()
|
||||
output_field=CharField(),
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,10 @@ from django.views import View
|
|||
|
||||
from registrar.models import UserPortfolioPermission
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from registrar.utility.model_annotations import PortfolioInvitationModelAnnotation, UserPortfolioPermissionModelAnnotation
|
||||
from registrar.utility.model_annotations import (
|
||||
PortfolioInvitationModelAnnotation,
|
||||
UserPortfolioPermissionModelAnnotation,
|
||||
)
|
||||
from registrar.views.utility.mixins import PortfolioMembersPermission
|
||||
|
||||
|
||||
|
|
|
@ -386,6 +386,21 @@ class PortfolioMembersView(PortfolioMembersPermissionView, View):
|
|||
def get(self, request):
|
||||
"""Add additional context data to the template."""
|
||||
return render(request, "portfolio_members.html")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add additional context data to the template."""
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
portfolio = self.request.session.get("portfolio")
|
||||
user_count = portfolio.portfolio_users.count()
|
||||
invitation_count = PortfolioInvitation.objects.filter(
|
||||
portfolio=portfolio
|
||||
).count()
|
||||
context["member_count"] = user_count + invitation_count
|
||||
|
||||
# check if any portfolio invitations exist 4 portfolio
|
||||
# check if any userportfolioroles exist 4 portfolio
|
||||
return context
|
||||
|
||||
|
||||
class NewMemberView(PortfolioMembersPermissionView, FormMixin):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue