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""" """Class method to return a readable string for domain request permissions"""
# Tracks if they can view, create requests, or not do anything # Tracks if they can view, create requests, or not do anything
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, additional_permissions) all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, additional_permissions)
all_domain_perms = [UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS, UserPortfolioPermissionChoices.EDIT_REQUESTS] all_domain_perms = [
if (all(perm in all_permissions for perm in 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" return "Viewer Requester"
elif (UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions): elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions:
return "Viewer" return "Viewer"
else: else:
return "None" return "None"
@ -136,9 +139,9 @@ class UserPortfolioPermission(TimeStampedModel):
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, additional_permissions) 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. # 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" return "Manager"
elif (UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions): elif UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions:
return "Viewer" return "Viewer"
else: else:
return "None" return "None"

View file

@ -1,6 +1,8 @@
from django.db.models.expressions import Func from django.db.models.expressions import Func
class ArrayRemove(Func): class ArrayRemove(Func):
"""Custom Func to use array_remove to remove null values""" """Custom Func to use array_remove to remove null values"""
function = "array_remove" function = "array_remove"
template = "%(function)s(%(expressions)s, NULL)" template = "%(function)s(%(expressions)s, NULL)"

View file

@ -36,7 +36,7 @@
</form> </form>
</section> </section>
</div> </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 %}"> <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"> <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"> <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> </a>
</section> </section>
</div> </div>
{% comment %} {% endif %} {% endcomment %} {% endif %}
</div> </div>
<!-- ---------- MAIN TABLE ---------- --> <!-- ---------- MAIN TABLE ---------- -->

View file

@ -11,7 +11,21 @@ from registrar.models import (
PublicContact, PublicContact,
UserDomainRole, 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.db.models.functions import Cast
from django.utils import timezone from django.utils import timezone
from django.db.models.functions import Concat, Coalesce 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 registrar.utility.enums import DefaultEmail
from django.contrib.postgres.aggregates import ArrayAgg 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__) logger = logging.getLogger(__name__)
@ -134,6 +152,7 @@ class BaseExport(BaseModelAnnotation):
""" """
pass pass
class MemberExport(BaseExport): class MemberExport(BaseExport):
@classmethod @classmethod
@ -166,8 +185,12 @@ class MemberExport(BaseExport):
"invitation_date", "invitation_date",
"invited_by", "invited_by",
] ]
permissions = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns) permissions = UserPortfolioPermissionModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values(
invitations = PortfolioInvitationModelAnnotation.get_annotated_queryset(portfolio, csv_report=True).values(*shared_columns) *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) queryset_dict = convert_queryset_to_dict(permissions.union(invitations), is_model=False)
return queryset_dict return queryset_dict
@ -199,7 +222,9 @@ class MemberExport(BaseExport):
roles = model.get("roles") roles = model.get("roles")
additional_permissions = model.get("additional_permissions_display") additional_permissions = model.get("additional_permissions_display")
is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (roles or []) 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) member_perm_display = UserPortfolioPermission.get_member_permission_display(roles, additional_permissions)
user_managed_domains = model.get("domain_info", []) user_managed_domains = model.get("domain_info", [])
managed_domains_as_csv = ",".join(user_managed_domains) managed_domains_as_csv = ",".join(user_managed_domains)
@ -231,6 +256,7 @@ class MemberExport(BaseExport):
row = [FIELDS.get(column, "") for column in columns] row = [FIELDS.get(column, "") for column in columns]
return row return row
class DomainExport(BaseExport): class DomainExport(BaseExport):
""" """
A collection of functions which return csv files regarding Domains. Although class is A collection of functions which return csv files regarding Domains. Although class is
@ -1521,4 +1547,3 @@ class DomainRequestDataFull(DomainRequestExport):
distinct=True, distinct=True,
) )
return query return query

View file

@ -20,12 +20,26 @@ Example:
permissions = UserPortfolioPermissionAnnotation.get_annotated_queryset(portfolio, csv_report=True) permissions = UserPortfolioPermissionAnnotation.get_annotated_queryset(portfolio, csv_report=True)
# Returns same fields but formatted for CSV export # 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 (
DomainInvitation, DomainInvitation,
PortfolioInvitation, 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 django.db.models.functions import Concat, Coalesce, Cast
from registrar.models.user_portfolio_permission import UserPortfolioPermission 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
@ -174,8 +188,7 @@ class BaseModelAnnotation(ABC):
model_queryset = ( model_queryset = (
cls.model() cls.model()
.objects .objects.select_related(*select_related)
.select_related(*select_related)
.prefetch_related(*prefetch_related) .prefetch_related(*prefetch_related)
.filter(filter_conditions) .filter(filter_conditions)
.exclude(exclusions) .exclude(exclusions)
@ -183,9 +196,7 @@ class BaseModelAnnotation(ABC):
.order_by(*sort_fields) .order_by(*sort_fields)
.distinct() .distinct()
) )
return cls.annotate_and_retrieve_fields( return cls.annotate_and_retrieve_fields(model_queryset, annotated_fields, related_table_fields, **kwargs)
model_queryset, annotated_fields, related_table_fields, **kwargs
)
@classmethod @classmethod
def update_queryset(cls, queryset, **kwargs): def update_queryset(cls, queryset, **kwargs):
@ -205,6 +216,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation):
Handles formatting of user details, permissions, and related domain information Handles formatting of user details, permissions, and related domain information
for both UI display and CSV export. 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
@ -247,10 +259,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation):
if csv_report: if csv_report:
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("YYYY-MM-DD"), function="to_char", output_field=TextField()
Value("YYYY-MM-DD"),
function="to_char",
output_field=TextField()
) )
else: else:
domain_query = Concat( domain_query = Concat(
@ -272,10 +281,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation):
), ),
"additional_permissions_display": F("additional_permissions"), "additional_permissions_display": F("additional_permissions"),
"member_display": Case( "member_display": Case(
When( When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")),
Q(user__email__isnull=False) & ~Q(user__email=""),
then=F("user__email")
),
When( When(
Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False), Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False),
then=Concat( then=Concat(
@ -295,12 +301,7 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation):
), ),
"source": Value("permission", output_field=CharField()), "source": Value("permission", output_field=CharField()),
"invitation_date": Coalesce( "invitation_date": Coalesce(
Func( Func(F("invitation__created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField()),
F("invitation__created_at"),
Value("YYYY-MM-DD"),
function="to_char",
output_field=TextField()
),
Value("Invalid date"), Value("Invalid date"),
output_field=TextField(), output_field=TextField(),
), ),
@ -311,12 +312,16 @@ class UserPortfolioPermissionModelAnnotation(BaseModelAnnotation):
Subquery( Subquery(
LogEntry.objects.filter( LogEntry.objects.filter(
content_type=ContentType.objects.get_for_model(PortfolioInvitation), content_type=ContentType.objects.get_for_model(PortfolioInvitation),
object_id=Cast(OuterRef("invitation__id"), output_field=TextField()), # Look up the invitation's ID object_id=Cast(
action_flag=ADDITION OuterRef("invitation__id"), output_field=TextField()
).order_by("action_time").values("user__email")[:1] ), # Look up the invitation's ID
action_flag=ADDITION,
)
.order_by("action_time")
.values("user__email")[:1]
), ),
Value("Unknown"), Value("Unknown"),
output_field=CharField() output_field=CharField(),
), ),
} }
@ -394,12 +399,7 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation):
), ),
"source": Value("invitation", output_field=CharField()), "source": Value("invitation", output_field=CharField()),
"invitation_date": Coalesce( "invitation_date": Coalesce(
Func( Func(F("created_at"), Value("YYYY-MM-DD"), function="to_char", output_field=TextField()),
F("created_at"),
Value("YYYY-MM-DD"),
function="to_char",
output_field=TextField()
),
Value("Invalid date"), Value("Invalid date"),
output_field=TextField(), output_field=TextField(),
), ),
@ -412,11 +412,13 @@ class PortfolioInvitationModelAnnotation(BaseModelAnnotation):
content_type=ContentType.objects.get_for_model(PortfolioInvitation), 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. # 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()), object_id=Cast(OuterRef("id"), output_field=TextField()),
action_flag=ADDITION action_flag=ADDITION,
).order_by("action_time").values("user__email")[:1] )
.order_by("action_time")
.values("user__email")[:1]
), ),
Value("Unknown"), 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 import UserPortfolioPermission
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices 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 from registrar.views.utility.mixins import PortfolioMembersPermission

View file

@ -387,6 +387,21 @@ class PortfolioMembersView(PortfolioMembersPermissionView, View):
"""Add additional context data to the template.""" """Add additional context data to the template."""
return render(request, "portfolio_members.html") 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): class NewMemberView(PortfolioMembersPermissionView, FormMixin):