lint + add invitation script

This commit is contained in:
zandercymatics 2024-11-15 13:30:36 -07:00
parent 9374d91c4b
commit 7075b72533
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
8 changed files with 133 additions and 56 deletions

View file

@ -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()

View file

@ -121,10 +121,13 @@ class UserPortfolioPermission(TimeStampedModel):
"""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"
@ -136,9 +139,9 @@ class UserPortfolioPermission(TimeStampedModel):
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, additional_permissions)
# 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"

View file

@ -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)"

View file

@ -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 ---------- -->

View file

@ -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
@ -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

View file

@ -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
@ -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):
@ -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(
@ -295,12 +301,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation):
),
"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(),
),
}

View file

@ -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

View file

@ -387,6 +387,21 @@ class PortfolioMembersView(PortfolioMembersPermissionView, View):
"""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):