From 4243d1929373331b6f222eb2bc466c88dbf3f30c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:29:22 -0600 Subject: [PATCH] stub --- src/registrar/admin.py | 79 ++++++++++++++++++- .../0118_portfolio_federal_type_and_more.py | 44 +++++++++++ src/registrar/models/portfolio.py | 52 ++++++++++++ src/registrar/models/suborganization.py | 1 + src/registrar/models/user.py | 2 +- 5 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 src/registrar/migrations/0118_portfolio_federal_type_and_more.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ca4038d51..56196e1b7 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -6,10 +6,12 @@ from django.template.loader import get_template from django import forms from django.db.models import Value, CharField, Q from django.db.models.functions import Concat, Coalesce +from django.contrib.admin.widgets import RelatedFieldWidgetWrapper from django.http import HttpResponseRedirect from django.shortcuts import redirect from django_fsm import get_available_FIELD_transitions, FSMField from registrar.models.domain_group import DomainGroup +from registrar.models.portfolio import Portfolio from registrar.models.suborganization import Suborganization from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from waffle.decorators import flag_is_active @@ -19,7 +21,7 @@ from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType from django.urls import reverse from epplibwrapper.errors import ErrorCode, RegistryError -from registrar.models.user_domain_role import UserDomainRole +from registrar.models import UserDomainRole, DomainInformation from waffle.admin import FlagAdmin from waffle.models import Sample, Switch from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial @@ -39,7 +41,7 @@ from import_export import resources from import_export.admin import ImportExportModelAdmin from django.core.exceptions import ObjectDoesNotExist from django.contrib.admin.widgets import FilteredSelectMultiple - +from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ logger = logging.getLogger(__name__) @@ -2833,23 +2835,94 @@ class VerifiedByStaffAdmin(ListHeaderAdmin): super().save_model(request, obj, form, change) + class PortfolioAdmin(ListHeaderAdmin): + class SuborganizationInline(admin.StackedInline): + """""" + model = models.Suborganization change_form_template = "django/admin/portfolio_change_form.html" + #inlines = [SuborganizationInline, UserInline] + fieldsets = [ + # TODO - this will need to be reworked + #(None, {"fields": ["organization_name", "federal_agency", "creator", "created_at", "notes"]}), + (None, {"fields": ["organization_name", "creator", "created_at", "notes"]}), + ("Portfolio members", {"fields": ["administrators", "members"]}), + ("Portfolio domains", {"fields": ["domains", "domain_requests"]}), + ("Type of organization", {"fields": ["organization_type", "federal_type"]}), + ("Organization name and mailing address", {"fields": ["federal_agency", "state_territory", "address_line1", "address_line2", "city", "zipcode", "urbanization"]}), + ("Suborganizations", {"fields": ["suborganizations"]}), + ("Senior official", {"fields": ["senior_official"]}), + ("Other", {"fields": ["security_contact_email"]}), + ] + # NOTE: use add_fieldsets to modify that page list_display = ("organization_name", "federal_agency", "creator") search_fields = ["organization_name"] search_help_text = "Search by organization name." readonly_fields = [ - "creator", + "created_at", + + # Custom fields such as these must be defined as readonly, even if they are not. + "administrators", + "members", + "domains", + "domain_requests", + "suborganizations" ] + def suborganizations(self, obj: models.Portfolio): + queryset = obj.get_suborganizations() + return self.get_links_csv(queryset, "suborganization") + suborganizations.short_description = "Suborganizations" + + def domains(self, obj: models.Portfolio): + queryset = obj.get_domains() + return self.get_links_csv(queryset, "domaininformation") + domains.short_description = "Domains" + + def domain_requests(self, obj: models.Portfolio): + queryset = obj.get_domain_requests() + return self.get_links_csv(queryset, "domainrequest") + domain_requests.short_description = "Domain requests" + + def administrators(self, obj: models.Portfolio): + queryset = obj.get_administrators() + return self.get_links_csv(queryset, "user", "get_full_name") + administrators.short_description = "Administrators" + + def members(self, obj: models.Portfolio): + queryset = obj.get_members() + return self.get_links_csv(queryset, "user", "get_full_name") + members.short_description = "Members" + # Creates select2 fields (with search bars) autocomplete_fields = [ "creator", "federal_agency", ] + # TODO change these names + def get_links_csv(self, queryset, model_name, link_text_attribute=None): + links = [] + for item in queryset: + if link_text_attribute: + item_display_value = getattr(item, link_text_attribute) + if callable(item_display_value): + item_display_value = item_display_value() + else: + item_display_value = item + + if item_display_value: + link = self.get_html_change_link(model_name=model_name, object_id=item.pk, text_content=item_display_value) + links.append(link) + return format_html(", ".join(links)) + + def get_html_change_link(self, model_name, object_id, text_content): + change_url = reverse(f"admin:registrar_{model_name}_change", args=[object_id]) + return f'{escape(text_content)}' + + def change_view(self, request, object_id, form_url="", extra_context=None): """Add related suborganizations and domain groups""" obj = self.get_object(request, object_id) diff --git a/src/registrar/migrations/0118_portfolio_federal_type_and_more.py b/src/registrar/migrations/0118_portfolio_federal_type_and_more.py new file mode 100644 index 000000000..2cbcc8f99 --- /dev/null +++ b/src/registrar/migrations/0118_portfolio_federal_type_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2.10 on 2024-08-13 16:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0117_alter_portfolioinvitation_portfolio_additional_permissions_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="portfolio", + name="federal_type", + field=models.CharField( + blank=True, + choices=[("executive", "Executive"), ("judicial", "Judicial"), ("legislative", "Legislative")], + max_length=50, + null=True, + ), + ), + migrations.AlterField( + model_name="suborganization", + name="portfolio", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="portfolio_suborganizations", + to="registrar.portfolio", + ), + ), + migrations.AlterField( + model_name="user", + name="portfolio", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="portfolio_users", + to="registrar.portfolio", + ), + ), + ] diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 06b01e672..5b4ef7c6b 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -2,6 +2,8 @@ from django.db import models from registrar.models.domain_request import DomainRequest from registrar.models.federal_agency import FederalAgency +from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices +from registrar.utility.constants import BranchChoices from .utility.time_stamped_model import TimeStampedModel @@ -39,6 +41,13 @@ class Portfolio(TimeStampedModel): default=FederalAgency.get_non_federal_agency, ) + federal_type = models.CharField( + max_length=50, + choices=BranchChoices.choices, + null=True, + blank=True, + ) + senior_official = models.ForeignKey( "registrar.SeniorOfficial", on_delete=models.PROTECT, @@ -110,3 +119,46 @@ class Portfolio(TimeStampedModel): def __str__(self) -> str: return f"{self.organization_name}" + + # == Getters for domains == # + def get_domains(self): + """Returns all DomainInformations associated with this portfolio""" + return self.information_portfolio.all() + + def get_domain_requests(self): + """Returns all DomainRequests associated with this portfolio""" + return self.DomainRequest_portfolio.all() + + # == Getters for suborganization == # + def get_suborganizations(self): + """Returns all suborganizations associated with this portfolio""" + return self.portfolio_suborganizations.all() + + # == Getters for users == # + def get_users(self): + """Returns all users associated with this portfolio""" + return self.portfolio_users.all() + + def get_administrators(self): + """Returns all administrators associated with this portfolio""" + return self.portfolio_users.filter( + portfolio_roles__overlap=[ + UserPortfolioRoleChoices.ORGANIZATION_ADMIN, + ] + ) + + def get_readonly_administrators(self): + """Returns all readonly_administrators associated with this portfolio""" + return self.portfolio_users.filter( + portfolio_roles__overlap=[ + UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY, + ] + ) + + def get_members(self): + """Returns all members associated with this portfolio""" + return self.portfolio_users.filter( + portfolio_roles__overlap=[ + UserPortfolioRoleChoices.ORGANIZATION_MEMBER, + ] + ) diff --git a/src/registrar/models/suborganization.py b/src/registrar/models/suborganization.py index b1e010953..feeee0669 100644 --- a/src/registrar/models/suborganization.py +++ b/src/registrar/models/suborganization.py @@ -16,6 +16,7 @@ class Suborganization(TimeStampedModel): portfolio = models.ForeignKey( "registrar.Portfolio", on_delete=models.PROTECT, + related_name="portfolio_suborganizations", ) def __str__(self) -> str: diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 0221c2d50..0f75c0499 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -115,7 +115,7 @@ class User(AbstractUser): "registrar.Portfolio", null=True, blank=True, - related_name="user", + related_name="portfolio_users", on_delete=models.SET_NULL, )