From b7606052c786723694ecd893a9743443e7354695 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 18 Oct 2024 15:35:50 -0400 Subject: [PATCH 01/24] component structure and wrapping UI --- src/registrar/assets/sass/_theme/_search.scss | 11 ++ src/registrar/assets/sass/_theme/styles.scss | 1 + src/registrar/config/urls.py | 10 ++ .../includes/member_domains_table.html | 78 ++++++++++ src/registrar/templates/portfolio_member.html | 6 +- .../templates/portfolio_member_domains.html | 64 ++++++++ src/registrar/views/member_domains_json.py | 142 ++++++++++++++++++ src/registrar/views/portfolios.py | 42 +++++- src/registrar/views/utility/mixins.py | 33 +--- .../views/utility/permission_views.py | 18 +-- 10 files changed, 357 insertions(+), 48 deletions(-) create mode 100644 src/registrar/assets/sass/_theme/_search.scss create mode 100644 src/registrar/templates/includes/member_domains_table.html create mode 100644 src/registrar/templates/portfolio_member_domains.html create mode 100644 src/registrar/views/member_domains_json.py diff --git a/src/registrar/assets/sass/_theme/_search.scss b/src/registrar/assets/sass/_theme/_search.scss new file mode 100644 index 000000000..2bd7832e2 --- /dev/null +++ b/src/registrar/assets/sass/_theme/_search.scss @@ -0,0 +1,11 @@ +@use "base" as *; + +.usa-search--show-label { + flex-wrap: wrap; + label { + width: 100%; + } + .usa-search--show-label__input-wrapper { + flex: 1; + } +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/styles.scss b/src/registrar/assets/sass/_theme/styles.scss index 8c38ae0b4..78d27b2e0 100644 --- a/src/registrar/assets/sass/_theme/styles.scss +++ b/src/registrar/assets/sass/_theme/styles.scss @@ -15,6 +15,7 @@ @forward "buttons"; @forward "pagination"; @forward "forms"; +@forward "search"; @forward "tooltips"; @forward "fieldsets"; @forward "alerts"; diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index ee923aac6..016a834d2 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -96,6 +96,11 @@ urlpatterns = [ views.PortfolioMemberEditView.as_view(), name="member-permissions", ), + path( + "member//domains", + views.PortfolioMemberDomainsView.as_view(), + name="member-domains", + ), path( "invitedmember/", views.PortfolioInvitedMemberView.as_view(), @@ -106,6 +111,11 @@ urlpatterns = [ views.PortfolioInvitedMemberEditView.as_view(), name="invitedmember-permissions", ), + path( + "invitedmember//domains", + views.PortfolioInvitedMemberDomainsView.as_view(), + name="invitedmember-domains", + ), # path( # "no-organization-members/", # views.PortfolioNoMembersView.as_view(), diff --git a/src/registrar/templates/includes/member_domains_table.html b/src/registrar/templates/includes/member_domains_table.html new file mode 100644 index 000000000..7d9a37308 --- /dev/null +++ b/src/registrar/templates/includes/member_domains_table.html @@ -0,0 +1,78 @@ +{% load static %} + +
+

+ Domains assigned to {{ email }} +

+ +
+ + +
+ + + + + +
+ diff --git a/src/registrar/templates/portfolio_member.html b/src/registrar/templates/portfolio_member.html index f2ee8f4c5..c0611f854 100644 --- a/src/registrar/templates/portfolio_member.html +++ b/src/registrar/templates/portfolio_member.html @@ -126,11 +126,9 @@ {% comment %}view_button is passed below as true in all cases. This is because manage_button logic will trump view_button logic; ie. if manage_button is true, view_button will never be looked at{% endcomment %} {% if portfolio_permission %} - {% include "includes/summary_item.html" with title='Domain management' domain_mgmt=True value=portfolio_permission.get_managed_domains_count edit_link='#' editable=True manage_button=has_edit_members_portfolio_permission view_button=True %} + {% include "includes/summary_item.html" with title='Domain management' domain_mgmt=True value=portfolio_permission.get_managed_domains_count edit_link=domains_url editable=True manage_button=has_edit_members_portfolio_permission view_button=True %} {% elif portfolio_invitation %} - {% include "includes/summary_item.html" with title='Domain management' domain_mgmt=True value=portfolio_invitation.get_managed_domains_count edit_link='#' editable=True manage_button=has_edit_members_portfolio_permission view_button=True %} - {% else %} - {% include "includes/summary_item.html" with title='Domain management' domain_mgmt=True value=0 edit_link='#' editable=True manage_button=has_edit_members_portfolio_permission view_button=True %} + {% include "includes/summary_item.html" with title='Domain management' domain_mgmt=True value=portfolio_invitation.get_managed_domains_count edit_link=domains_url editable=True manage_button=has_edit_members_portfolio_permission view_button=True %} {% endif %} diff --git a/src/registrar/templates/portfolio_member_domains.html b/src/registrar/templates/portfolio_member_domains.html new file mode 100644 index 000000000..c51c3464e --- /dev/null +++ b/src/registrar/templates/portfolio_member_domains.html @@ -0,0 +1,64 @@ +{% extends 'portfolio_base.html' %} +{% load static field_helpers%} + +{% block title %}Organization member domains {% endblock %} + +{% load static %} + +{% block portfolio_content %} +
+ + {% url 'members' as url %} + {% if portfolio_permission %} + {% url 'member' pk=portfolio_permission.id as url2 %} + {% else %} + {% url 'invitedmember' pk=portfolio_invitation.id as url2 %} + {% endif %} + + +
+
+

Domain assignments

+
+ {% if has_edit_members_portfolio_permission %} + + {% endif %} +
+ +

+ A domain manager can be assigned to any domain across the organization. Domain managers can change domain information, adjust DNS settings, and invite or assign other domain managers to their assigned domains. +

+ + {% if member %} + {% with member.email as email %} + {% include "includes/member_domains_table.html" with email=email %} + {% endwith %} + {% else %} + {% with portfolio_invitation.email as email %} + {% include "includes/member_domains_table.html" with email=email %} + {% endwith %} + {% endif %} + + + +
+{% endblock %} diff --git a/src/registrar/views/member_domains_json.py b/src/registrar/views/member_domains_json.py new file mode 100644 index 000000000..f7c8b4637 --- /dev/null +++ b/src/registrar/views/member_domains_json.py @@ -0,0 +1,142 @@ +import logging +from django.http import JsonResponse +from django.core.paginator import Paginator +from registrar.models import UserDomainRole, Domain, DomainInformation, User +from django.contrib.auth.decorators import login_required +from django.urls import reverse +from django.db.models import Q + +logger = logging.getLogger(__name__) + + +@login_required +def get_domains_json(request): + """Given the current request, + get all domains that are associated with the UserDomainRole object""" + + domain_ids = get_domain_ids_from_request(request) + + objects = Domain.objects.filter(id__in=domain_ids).select_related("domain_info__sub_organization") + unfiltered_total = objects.count() + + objects = apply_search(objects, request) + objects = apply_state_filter(objects, request) + objects = apply_sorting(objects, request) + + paginator = Paginator(objects, 10) + page_number = request.GET.get("page") + page_obj = paginator.get_page(page_number) + + domains = [serialize_domain(domain, request.user) for domain in page_obj.object_list] + + return JsonResponse( + { + "domains": domains, + "page": page_obj.number, + "num_pages": paginator.num_pages, + "has_previous": page_obj.has_previous(), + "has_next": page_obj.has_next(), + "total": paginator.count, + "unfiltered_total": unfiltered_total, + } + ) + + +def get_domain_ids_from_request(request): + """Get domain ids from request. + + If portfolio specified, return domain ids associated with portfolio. + Otherwise, return domain ids associated with request.user. + """ + portfolio = request.GET.get("portfolio") + if portfolio: + current_user: User = request.user + if current_user.is_org_user(request) and current_user.has_view_all_domains_portfolio_permission(portfolio): + domain_infos = DomainInformation.objects.filter(portfolio=portfolio) + return domain_infos.values_list("domain_id", flat=True) + else: + domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True) + user_domain_roles = UserDomainRole.objects.filter(user=request.user).values_list("domain_id", flat=True) + return domain_info_ids.intersection(user_domain_roles) + user_domain_roles = UserDomainRole.objects.filter(user=request.user) + return user_domain_roles.values_list("domain_id", flat=True) + + +def apply_search(queryset, request): + search_term = request.GET.get("search_term") + if search_term: + queryset = queryset.filter(Q(name__icontains=search_term)) + return queryset + + +def apply_state_filter(queryset, request): + status_param = request.GET.get("status") + if status_param: + status_list = status_param.split(",") + # if unknown is in status_list, append 'dns needed' since both + # unknown and dns needed display as DNS Needed, and both are + # searchable via state parameter of 'unknown' + if "unknown" in status_list: + status_list.append("dns needed") + # Split the status list into normal states and custom states + normal_states = [state for state in status_list if state in Domain.State.values] + custom_states = [state for state in status_list if state == "expired"] + # Construct Q objects for normal states that can be queried through ORM + state_query = Q() + if normal_states: + state_query |= Q(state__in=normal_states) + # Handle custom states in Python, as expired can not be queried through ORM + if "expired" in custom_states: + expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"] + state_query |= Q(id__in=expired_domain_ids) + # Apply the combined query + queryset = queryset.filter(state_query) + # If there are filtered states, and expired is not one of them, domains with + # state_display of 'Expired' must be removed + if "expired" not in custom_states: + expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"] + queryset = queryset.exclude(id__in=expired_domain_ids) + + return queryset + + +def apply_sorting(queryset, request): + sort_by = request.GET.get("sort_by", "id") + order = request.GET.get("order", "asc") + if sort_by == "state_display": + objects = list(queryset) + objects.sort(key=lambda domain: domain.state_display(), reverse=(order == "desc")) + return objects + else: + if order == "desc": + sort_by = f"-{sort_by}" + return queryset.order_by(sort_by) + + +def serialize_domain(domain, user): + suborganization_name = None + try: + domain_info = domain.domain_info + if domain_info: + suborganization = domain_info.sub_organization + if suborganization: + suborganization_name = suborganization.name + except Domain.domain_info.RelatedObjectDoesNotExist: + domain_info = None + logger.debug(f"Issue in domains_json: We could not find domain_info for {domain}") + + # Check if there is a UserDomainRole for this domain and user + user_domain_role_exists = UserDomainRole.objects.filter(domain_id=domain.id, user=user).exists() + view_only = not user_domain_role_exists or domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] + return { + "id": domain.id, + "name": domain.name, + "expiration_date": domain.expiration_date, + "state": domain.state, + "state_display": domain.state_display(), + "get_state_help_text": domain.get_state_help_text(), + "action_url": reverse("domain", kwargs={"pk": domain.id}), + "action_label": ("View" if view_only else "Manage"), + "svg_icon": ("visibility" if view_only else "settings"), + "domain_info__sub_organization": suborganization_name, + } diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index cc1a09b25..b587ea9c9 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -18,8 +18,7 @@ from registrar.views.utility.permission_views import ( PortfolioDomainsPermissionView, PortfolioBasePermissionView, NoPortfolioDomainsPermissionView, - PortfolioInvitedMemberEditPermissionView, - PortfolioInvitedMemberPermissionView, + PortfolioMemberDomainsPermissionView, PortfolioMemberEditPermissionView, PortfolioMemberPermissionView, PortfolioMembersPermissionView, @@ -88,6 +87,7 @@ class PortfolioMemberView(PortfolioMemberPermissionView, View): self.template_name, { "edit_url": reverse("member-permissions", args=[pk]), + "domains_url": reverse("member-domains", args=[pk]), "portfolio_permission": portfolio_permission, "member": member, "member_has_view_all_requests_portfolio_permission": member_has_view_all_requests_portfolio_permission, @@ -136,9 +136,27 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View): "member": user, # Pass the user object again to the template }, ) + +class PortfolioMemberDomainsView(PortfolioMemberDomainsPermissionView, View): + + template_name = "portfolio_member_domains.html" + + def get(self, request, pk): + portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk) + member = portfolio_permission.user + + return render( + request, + self.template_name, + { + "portfolio_permission": portfolio_permission, + "member": member, + }, + ) -class PortfolioInvitedMemberView(PortfolioInvitedMemberPermissionView, View): + +class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View): template_name = "portfolio_member.html" # form_class = PortfolioInvitedMemberForm @@ -166,6 +184,7 @@ class PortfolioInvitedMemberView(PortfolioInvitedMemberPermissionView, View): self.template_name, { "edit_url": reverse("invitedmember-permissions", args=[pk]), + "domains_url": reverse("invitedmember-domains", args=[pk]), "portfolio_invitation": portfolio_invitation, "member_has_view_all_requests_portfolio_permission": member_has_view_all_requests_portfolio_permission, "member_has_edit_request_portfolio_permission": member_has_edit_request_portfolio_permission, @@ -175,7 +194,7 @@ class PortfolioInvitedMemberView(PortfolioInvitedMemberPermissionView, View): ) -class PortfolioInvitedMemberEditView(PortfolioInvitedMemberEditPermissionView, View): +class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View): template_name = "portfolio_member_permissions.html" form_class = PortfolioInvitedMemberForm @@ -208,6 +227,21 @@ class PortfolioInvitedMemberEditView(PortfolioInvitedMemberEditPermissionView, V "invitation": portfolio_invitation, # Pass the user object again to the template }, ) + +class PortfolioInvitedMemberDomainsView(PortfolioMemberDomainsPermissionView, View): + + template_name = "portfolio_member_domains.html" + + def get(self, request, pk): + portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk) + + return render( + request, + self.template_name, + { + "portfolio_invitation": portfolio_invitation, + }, + ) class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View): diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 9cee2f61a..c1cf97d82 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -521,11 +521,11 @@ class PortfolioMembersPermission(PortfolioBasePermission): class PortfolioMemberPermission(PortfolioBasePermission): - """Permission mixin that allows access to portfolio member pages if user + """Permission mixin that allows access to portfolio member or invited member pages if user has access, otherwise 403""" def has_permission(self): - """Check if this user has access to members for this portfolio. + """Check if this user has access to members or invited members for this portfolio. The user is in self.request.user and the portfolio can be looked up from the portfolio's primary key in self.kwargs["pk"]""" @@ -540,11 +540,11 @@ class PortfolioMemberPermission(PortfolioBasePermission): class PortfolioMemberEditPermission(PortfolioBasePermission): - """Permission mixin that allows access to portfolio member pages if user + """Permission mixin that allows access to portfolio member or invited member pages if user has access to edit, otherwise 403""" def has_permission(self): - """Check if this user has access to members for this portfolio. + """Check if this user has access to members or invited members for this portfolio. The user is in self.request.user and the portfolio can be looked up from the portfolio's primary key in self.kwargs["pk"]""" @@ -556,12 +556,12 @@ class PortfolioMemberEditPermission(PortfolioBasePermission): return super().has_permission() -class PortfolioInvitedMemberPermission(PortfolioBasePermission): - """Permission mixin that allows access to portfolio invited member pages if user - has access, otherwise 403""" +class PortfolioMemberDomainsPermission(PortfolioBasePermission): + """Permission mixin that allows access to portfolio member or invited member domains pages if user + has access to edit, otherwise 403""" def has_permission(self): - """Check if this user has access to members for this portfolio. + """Check if this user has access to member or invited member domains for this portfolio. The user is in self.request.user and the portfolio can be looked up from the portfolio's primary key in self.kwargs["pk"]""" @@ -573,20 +573,3 @@ class PortfolioInvitedMemberPermission(PortfolioBasePermission): return False return super().has_permission() - - -class PortfolioInvitedMemberEditPermission(PortfolioBasePermission): - """Permission mixin that allows access to portfolio invited member pages if user - has access to edit, otherwise 403""" - - def has_permission(self): - """Check if this user has access to members for this portfolio. - - The user is in self.request.user and the portfolio can be looked - up from the portfolio's primary key in self.kwargs["pk"]""" - - portfolio = self.request.session.get("portfolio") - if not self.request.user.has_edit_members_portfolio_permission(portfolio): - return False - - return super().has_permission() diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index c1d25d691..5c05a3a20 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -15,8 +15,7 @@ from .mixins import ( DomainRequestWizardPermission, PortfolioDomainRequestsPermission, PortfolioDomainsPermission, - PortfolioInvitedMemberEditPermission, - PortfolioInvitedMemberPermission, + PortfolioMemberDomainsPermission, PortfolioMemberEditPermission, UserDeleteDomainRolePermission, UserProfilePermission, @@ -279,19 +278,8 @@ class PortfolioMemberEditPermissionView(PortfolioMemberEditPermission, Portfolio `template_name`. """ - -class PortfolioInvitedMemberPermissionView(PortfolioInvitedMemberPermission, PortfolioBasePermissionView, abc.ABC): - """Abstract base view for portfolio member views that enforces permissions. - - This abstract view cannot be instantiated. Actual views must specify - `template_name`. - """ - - -class PortfolioInvitedMemberEditPermissionView( - PortfolioInvitedMemberEditPermission, PortfolioBasePermissionView, abc.ABC -): - """Abstract base view for portfolio member edit views that enforces permissions. +class PortfolioMemberDomainsPermissionView(PortfolioMemberDomainsPermission, PortfolioBasePermissionView, abc.ABC): + """Abstract base view for portfolio member domains views that enforces permissions. This abstract view cannot be instantiated. Actual views must specify `template_name`. From 71db456200b08ecc04dd4bd60498f725cd706928 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 18 Oct 2024 16:48:41 -0400 Subject: [PATCH 02/24] json as class rather than method, implemented base member_domains_json view --- src/registrar/config/urls.py | 4 +- src/registrar/views/__init__.py | 2 + src/registrar/views/member_domains_json.py | 249 ++++++++-------- src/registrar/views/portfolio_members_json.py | 265 +++++++++--------- 4 files changed, 271 insertions(+), 249 deletions(-) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 016a834d2..c493f1a08 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -26,7 +26,6 @@ from registrar.views.report_views import ( # --jsons from registrar.views.domain_requests_json import get_domain_requests_json from registrar.views.domains_json import get_domains_json -from registrar.views.portfolio_members_json import get_portfolio_members_json from registrar.views.utility.api_views import ( get_senior_official_from_federal_agency_json, get_federal_and_portfolio_types_from_federal_agency_json, @@ -338,7 +337,8 @@ urlpatterns = [ ), path("get-domains-json/", get_domains_json, name="get_domains_json"), path("get-domain-requests-json/", get_domain_requests_json, name="get_domain_requests_json"), - path("get-portfolio-members-json/", get_portfolio_members_json, name="get_portfolio_members_json"), + path("get-portfolio-members-json/", views.PortfolioMembersJson.as_view(), name="get_portfolio_members_json"), + path('get-member-domains-json/', views.PortfolioMemberDomainsJson.as_view(), name="get_member_domains_json"), ] # Djangooidc strips out context data from that context, so we define a custom error diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index c4cb03192..cd3f74fc8 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -19,3 +19,5 @@ from .health import * from .index import * from .portfolios import * from .transfer_user import TransferUserView +from .member_domains_json import PortfolioMemberDomainsJson +from .portfolio_members_json import PortfolioMembersJson diff --git a/src/registrar/views/member_domains_json.py b/src/registrar/views/member_domains_json.py index f7c8b4637..4125fe345 100644 --- a/src/registrar/views/member_domains_json.py +++ b/src/registrar/views/member_domains_json.py @@ -1,142 +1,159 @@ import logging from django.http import JsonResponse from django.core.paginator import Paginator +from django.shortcuts import get_object_or_404 +from django.views import View from registrar.models import UserDomainRole, Domain, DomainInformation, User from django.contrib.auth.decorators import login_required from django.urls import reverse from django.db.models import Q +from registrar.models.domain_invitation import DomainInvitation +from registrar.views.utility.mixins import PortfolioMemberDomainsPermission + logger = logging.getLogger(__name__) -@login_required -def get_domains_json(request): - """Given the current request, - get all domains that are associated with the UserDomainRole object""" +class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View): - domain_ids = get_domain_ids_from_request(request) + def get(self, request): + """Given the current request, + get all domains that are associated with the portfolio, or + associated with the member/invited member""" - objects = Domain.objects.filter(id__in=domain_ids).select_related("domain_info__sub_organization") - unfiltered_total = objects.count() + domain_ids = self.get_domain_ids_from_request(request) - objects = apply_search(objects, request) - objects = apply_state_filter(objects, request) - objects = apply_sorting(objects, request) + objects = Domain.objects.filter(id__in=domain_ids).select_related("domain_info__sub_organization") + unfiltered_total = objects.count() - paginator = Paginator(objects, 10) - page_number = request.GET.get("page") - page_obj = paginator.get_page(page_number) + objects = self.apply_search(objects, request) + objects = self.apply_state_filter(objects, request) + objects = self.apply_sorting(objects, request) - domains = [serialize_domain(domain, request.user) for domain in page_obj.object_list] + paginator = Paginator(objects, 10) + page_number = request.GET.get("page") + page_obj = paginator.get_page(page_number) - return JsonResponse( - { - "domains": domains, - "page": page_obj.number, - "num_pages": paginator.num_pages, - "has_previous": page_obj.has_previous(), - "has_next": page_obj.has_next(), - "total": paginator.count, - "unfiltered_total": unfiltered_total, - } - ) + domains = [self.serialize_domain(domain, request.user) for domain in page_obj.object_list] + + return JsonResponse( + { + "domains": domains, + "page": page_obj.number, + "num_pages": paginator.num_pages, + "has_previous": page_obj.has_previous(), + "has_next": page_obj.has_next(), + "total": paginator.count, + "unfiltered_total": unfiltered_total, + } + ) -def get_domain_ids_from_request(request): - """Get domain ids from request. + def get_domain_ids_from_request(self, request): + """Get domain ids from request. - If portfolio specified, return domain ids associated with portfolio. - Otherwise, return domain ids associated with request.user. - """ - portfolio = request.GET.get("portfolio") - if portfolio: - current_user: User = request.user - if current_user.is_org_user(request) and current_user.has_view_all_domains_portfolio_permission(portfolio): + request.get.email - email address of invited member + request.get.member - member id of member + request.get.portfolio - portfolio id of portfolio + request.get.member_only - whether to return only domains associated with member + or to return all domains in the portfolio + """ + portfolio = request.GET.get("portfolio") + email = request.GET.get("email") + member_id = request.GET.get("member") + member_only = request.GET.get("member_only", "false").lower() in ["true", "1"] + if member_only: + if member_id: + member = get_object_or_404(User, pk=member_id) + domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True) + user_domain_roles = UserDomainRole.objects.filter(user=member).values_list("domain_id", flat=True) + return domain_info_ids.intersection(user_domain_roles) + elif email: + domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True) + domain_invitations = DomainInvitation.objects.filter(email=email).values_list("domain_id", flat=True) + return domain_info_ids.intersection(domain_invitations) + else: domain_infos = DomainInformation.objects.filter(portfolio=portfolio) return domain_infos.values_list("domain_id", flat=True) + logger.warning("Invalid search criteria, returning empty results list") + return [] + + + def apply_search(self, queryset, request): + search_term = request.GET.get("search_term") + if search_term: + queryset = queryset.filter(Q(name__icontains=search_term)) + return queryset + + + def apply_state_filter(self, queryset, request): + status_param = request.GET.get("status") + if status_param: + status_list = status_param.split(",") + # if unknown is in status_list, append 'dns needed' since both + # unknown and dns needed display as DNS Needed, and both are + # searchable via state parameter of 'unknown' + if "unknown" in status_list: + status_list.append("dns needed") + # Split the status list into normal states and custom states + normal_states = [state for state in status_list if state in Domain.State.values] + custom_states = [state for state in status_list if state == "expired"] + # Construct Q objects for normal states that can be queried through ORM + state_query = Q() + if normal_states: + state_query |= Q(state__in=normal_states) + # Handle custom states in Python, as expired can not be queried through ORM + if "expired" in custom_states: + expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"] + state_query |= Q(id__in=expired_domain_ids) + # Apply the combined query + queryset = queryset.filter(state_query) + # If there are filtered states, and expired is not one of them, domains with + # state_display of 'Expired' must be removed + if "expired" not in custom_states: + expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"] + queryset = queryset.exclude(id__in=expired_domain_ids) + + return queryset + + + def apply_sorting(self, queryset, request): + sort_by = request.GET.get("sort_by", "id") + order = request.GET.get("order", "asc") + if sort_by == "state_display": + objects = list(queryset) + objects.sort(key=lambda domain: domain.state_display(), reverse=(order == "desc")) + return objects else: - domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True) - user_domain_roles = UserDomainRole.objects.filter(user=request.user).values_list("domain_id", flat=True) - return domain_info_ids.intersection(user_domain_roles) - user_domain_roles = UserDomainRole.objects.filter(user=request.user) - return user_domain_roles.values_list("domain_id", flat=True) + if order == "desc": + sort_by = f"-{sort_by}" + return queryset.order_by(sort_by) -def apply_search(queryset, request): - search_term = request.GET.get("search_term") - if search_term: - queryset = queryset.filter(Q(name__icontains=search_term)) - return queryset + def serialize_domain(self, domain, user): + suborganization_name = None + try: + domain_info = domain.domain_info + if domain_info: + suborganization = domain_info.sub_organization + if suborganization: + suborganization_name = suborganization.name + except Domain.domain_info.RelatedObjectDoesNotExist: + domain_info = None + logger.debug(f"Issue in domains_json: We could not find domain_info for {domain}") - -def apply_state_filter(queryset, request): - status_param = request.GET.get("status") - if status_param: - status_list = status_param.split(",") - # if unknown is in status_list, append 'dns needed' since both - # unknown and dns needed display as DNS Needed, and both are - # searchable via state parameter of 'unknown' - if "unknown" in status_list: - status_list.append("dns needed") - # Split the status list into normal states and custom states - normal_states = [state for state in status_list if state in Domain.State.values] - custom_states = [state for state in status_list if state == "expired"] - # Construct Q objects for normal states that can be queried through ORM - state_query = Q() - if normal_states: - state_query |= Q(state__in=normal_states) - # Handle custom states in Python, as expired can not be queried through ORM - if "expired" in custom_states: - expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"] - state_query |= Q(id__in=expired_domain_ids) - # Apply the combined query - queryset = queryset.filter(state_query) - # If there are filtered states, and expired is not one of them, domains with - # state_display of 'Expired' must be removed - if "expired" not in custom_states: - expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"] - queryset = queryset.exclude(id__in=expired_domain_ids) - - return queryset - - -def apply_sorting(queryset, request): - sort_by = request.GET.get("sort_by", "id") - order = request.GET.get("order", "asc") - if sort_by == "state_display": - objects = list(queryset) - objects.sort(key=lambda domain: domain.state_display(), reverse=(order == "desc")) - return objects - else: - if order == "desc": - sort_by = f"-{sort_by}" - return queryset.order_by(sort_by) - - -def serialize_domain(domain, user): - suborganization_name = None - try: - domain_info = domain.domain_info - if domain_info: - suborganization = domain_info.sub_organization - if suborganization: - suborganization_name = suborganization.name - except Domain.domain_info.RelatedObjectDoesNotExist: - domain_info = None - logger.debug(f"Issue in domains_json: We could not find domain_info for {domain}") - - # Check if there is a UserDomainRole for this domain and user - user_domain_role_exists = UserDomainRole.objects.filter(domain_id=domain.id, user=user).exists() - view_only = not user_domain_role_exists or domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] - return { - "id": domain.id, - "name": domain.name, - "expiration_date": domain.expiration_date, - "state": domain.state, - "state_display": domain.state_display(), - "get_state_help_text": domain.get_state_help_text(), - "action_url": reverse("domain", kwargs={"pk": domain.id}), - "action_label": ("View" if view_only else "Manage"), - "svg_icon": ("visibility" if view_only else "settings"), - "domain_info__sub_organization": suborganization_name, - } + # Check if there is a UserDomainRole for this domain and user + user_domain_role_exists = UserDomainRole.objects.filter(domain_id=domain.id, user=user).exists() + view_only = not user_domain_role_exists or domain.state in [Domain.State.DELETED, Domain.State.ON_HOLD] + return { + "id": domain.id, + "name": domain.name, + "expiration_date": domain.expiration_date, + "state": domain.state, + "state_display": domain.state_display(), + "get_state_help_text": domain.get_state_help_text(), + "action_url": reverse("domain", kwargs={"pk": domain.id}), + "action_label": ("View" if view_only else "Manage"), + "svg_icon": ("visibility" if view_only else "settings"), + "domain_info__sub_organization": suborganization_name, + } diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index d2f2276cf..94ca9ac69 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -5,81 +5,110 @@ from django.db.models import Value, F, CharField, TextField, Q, Case, When from django.db.models.functions import Concat, Coalesce from django.urls import reverse from django.db.models.functions import Cast +from django.views import View from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices +from registrar.views.utility.mixins import PortfolioMembersPermission -@login_required -def get_portfolio_members_json(request): - """Fetch members (permissions and invitations) for the given portfolio.""" +class PortfolioMembersJson(PortfolioMembersPermission, View): + + def get(self, request): + """Fetch members (permissions and invitations) for the given portfolio.""" - portfolio = request.GET.get("portfolio") + portfolio = request.GET.get("portfolio") - # Two initial querysets which will be combined - permissions = initial_permissions_search(portfolio) - invitations = initial_invitations_search(portfolio) + # Two initial querysets which will be combined + permissions = self.initial_permissions_search(portfolio) + invitations = self.initial_invitations_search(portfolio) - # Get total across both querysets before applying filters - unfiltered_total = permissions.count() + invitations.count() + # Get total across both querysets before applying filters + unfiltered_total = permissions.count() + invitations.count() - permissions = apply_search_term(permissions, request) - invitations = apply_search_term(invitations, request) + permissions = self.apply_search_term(permissions, request) + invitations = self.apply_search_term(invitations, request) - # Union the two querysets - objects = permissions.union(invitations) - objects = apply_sorting(objects, request) + # Union the two querysets + objects = permissions.union(invitations) + objects = self.apply_sorting(objects, request) - paginator = Paginator(objects, 10) - page_number = request.GET.get("page", 1) - page_obj = paginator.get_page(page_number) + paginator = Paginator(objects, 10) + page_number = request.GET.get("page", 1) + page_obj = paginator.get_page(page_number) - members = [serialize_members(request, portfolio, item, request.user) for item in page_obj.object_list] + members = [self.serialize_members(request, portfolio, item, request.user) for item in page_obj.object_list] - return JsonResponse( - { - "members": members, - "page": page_obj.number, - "num_pages": paginator.num_pages, - "has_previous": page_obj.has_previous(), - "has_next": page_obj.has_next(), - "total": paginator.count, - "unfiltered_total": unfiltered_total, - } - ) - - -def initial_permissions_search(portfolio): - """Perform initial search for permissions before applying any filters.""" - permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio) - permissions = ( - permissions.select_related("user") - .annotate( - first_name=F("user__first_name"), - last_name=F("user__last_name"), - email_display=F("user__email"), - last_active=Cast(F("user__last_login"), output_field=TextField()), # Cast last_login to text - additional_permissions_display=F("additional_permissions"), - member_display=Case( - # If email is present and not blank, use email - When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")), - # If first name or last name is present, use concatenation of first_name + " " + last_name - When( - Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False), - then=Concat( - Coalesce(F("user__first_name"), Value("")), - Value(" "), - Coalesce(F("user__last_name"), Value("")), - ), - ), - # If neither, use an empty string - default=Value(""), - output_field=CharField(), - ), - source=Value("permission", output_field=CharField()), + return JsonResponse( + { + "members": members, + "page": page_obj.number, + "num_pages": paginator.num_pages, + "has_previous": page_obj.has_previous(), + "has_next": page_obj.has_next(), + "total": paginator.count, + "unfiltered_total": unfiltered_total, + } ) - .values( + + + def initial_permissions_search(self, portfolio): + """Perform initial search for permissions before applying any filters.""" + permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio) + permissions = ( + permissions.select_related("user") + .annotate( + first_name=F("user__first_name"), + last_name=F("user__last_name"), + email_display=F("user__email"), + last_active=Cast(F("user__last_login"), output_field=TextField()), # Cast last_login to text + additional_permissions_display=F("additional_permissions"), + member_display=Case( + # If email is present and not blank, use email + When(Q(user__email__isnull=False) & ~Q(user__email=""), then=F("user__email")), + # If first name or last name is present, use concatenation of first_name + " " + last_name + When( + Q(user__first_name__isnull=False) | Q(user__last_name__isnull=False), + then=Concat( + Coalesce(F("user__first_name"), Value("")), + Value(" "), + Coalesce(F("user__last_name"), Value("")), + ), + ), + # If neither, use an empty string + default=Value(""), + output_field=CharField(), + ), + source=Value("permission", output_field=CharField()), + ) + .values( + "id", + "first_name", + "last_name", + "email_display", + "last_active", + "roles", + "additional_permissions_display", + "member_display", + "source", + ) + ) + return permissions + + + def initial_invitations_search(self, portfolio): + """Perform initial invitations search before applying any filters.""" + invitations = PortfolioInvitation.objects.filter(portfolio=portfolio) + invitations = invitations.annotate( + first_name=Value(None, output_field=CharField()), + last_name=Value(None, output_field=CharField()), + email_display=F("email"), + last_active=Value("Invited", output_field=TextField()), + additional_permissions_display=F("additional_permissions"), + member_display=F("email"), + source=Value("invitation", output_field=CharField()), + ).values( "id", "first_name", "last_name", @@ -90,82 +119,56 @@ def initial_permissions_search(portfolio): "member_display", "source", ) - ) - return permissions + return invitations -def initial_invitations_search(portfolio): - """Perform initial invitations search before applying any filters.""" - invitations = PortfolioInvitation.objects.filter(portfolio=portfolio) - invitations = invitations.annotate( - first_name=Value(None, output_field=CharField()), - last_name=Value(None, output_field=CharField()), - email_display=F("email"), - last_active=Value("Invited", output_field=TextField()), - additional_permissions_display=F("additional_permissions"), - member_display=F("email"), - source=Value("invitation", output_field=CharField()), - ).values( - "id", - "first_name", - "last_name", - "email_display", - "last_active", - "roles", - "additional_permissions_display", - "member_display", - "source", - ) - return invitations + def apply_search_term(self, queryset, request): + """Apply search term to the queryset.""" + search_term = request.GET.get("search_term", "").lower() + if search_term: + queryset = queryset.filter( + Q(first_name__icontains=search_term) + | Q(last_name__icontains=search_term) + | Q(email_display__icontains=search_term) + ) + return queryset -def apply_search_term(queryset, request): - """Apply search term to the queryset.""" - search_term = request.GET.get("search_term", "").lower() - if search_term: - queryset = queryset.filter( - Q(first_name__icontains=search_term) - | Q(last_name__icontains=search_term) - | Q(email_display__icontains=search_term) + def apply_sorting(self, queryset, request): + """Apply sorting to the queryset.""" + sort_by = request.GET.get("sort_by", "id") # Default to 'id' + order = request.GET.get("order", "asc") # Default to 'asc' + # Adjust sort_by to match the annotated fields in the unioned queryset + if sort_by == "member": + sort_by = "member_display" + if order == "desc": + queryset = queryset.order_by(F(sort_by).desc()) + else: + queryset = queryset.order_by(sort_by) + return queryset + + + def serialize_members(self, request, portfolio, item, user): + # Check if the user can edit other users + user_can_edit_other_users = any( + user.has_perm(perm) for perm in ["registrar.full_access_permission", "registrar.change_user"] ) - return queryset + view_only = not user.has_edit_members_portfolio_permission(portfolio) or not user_can_edit_other_users -def apply_sorting(queryset, request): - """Apply sorting to the queryset.""" - sort_by = request.GET.get("sort_by", "id") # Default to 'id' - order = request.GET.get("order", "asc") # Default to 'asc' - # Adjust sort_by to match the annotated fields in the unioned queryset - if sort_by == "member": - sort_by = "member_display" - if order == "desc": - queryset = queryset.order_by(F(sort_by).desc()) - else: - queryset = queryset.order_by(sort_by) - return queryset + is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (item.get("roles") or []) + action_url = reverse("member" if item["source"] == "permission" else "invitedmember", kwargs={"pk": item["id"]}) - -def serialize_members(request, portfolio, item, user): - # Check if the user can edit other users - user_can_edit_other_users = any( - user.has_perm(perm) for perm in ["registrar.full_access_permission", "registrar.change_user"] - ) - - view_only = not user.has_edit_members_portfolio_permission(portfolio) or not user_can_edit_other_users - - is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (item.get("roles") or []) - action_url = reverse("member" if item["source"] == "permission" else "invitedmember", kwargs={"pk": item["id"]}) - - # Serialize member data - member_json = { - "id": item.get("id", ""), - "name": " ".join(filter(None, [item.get("first_name", ""), item.get("last_name", "")])), - "email": item.get("email_display", ""), - "member_display": item.get("member_display", ""), - "is_admin": is_admin, - "last_active": item.get("last_active", ""), - "action_url": action_url, - "action_label": ("View" if view_only else "Manage"), - "svg_icon": ("visibility" if view_only else "settings"), - } - return member_json + # Serialize member data + member_json = { + "id": item.get("id", ""), + "name": " ".join(filter(None, [item.get("first_name", ""), item.get("last_name", "")])), + "email": item.get("email_display", ""), + "member_display": item.get("member_display", ""), + "is_admin": is_admin, + "last_active": item.get("last_active", ""), + "action_url": action_url, + "action_label": ("View" if view_only else "Manage"), + "svg_icon": ("visibility" if view_only else "settings"), + } + return member_json From a8fec4fccac7c5c8fc11575a9ee881181dbc2e4e Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 18 Oct 2024 18:22:07 -0400 Subject: [PATCH 03/24] JS --- src/registrar/assets/js/get-gov.js | 136 +++++++++++++++++- .../includes/member_domains_table.html | 37 ++++- .../templates/portfolio_member_domains.html | 12 +- src/registrar/views/member_domains_json.py | 49 +------ 4 files changed, 169 insertions(+), 65 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 8281aa50a..efb8c6f11 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1899,7 +1899,7 @@ class MembersTable extends LoadTableBase { // --------- FETCH DATA - // fetch json of page of domais, given params + // fetch json of page of domains, given params let baseUrl = document.getElementById("get_members_json_url"); if (!baseUrl) { return; @@ -2014,14 +2014,120 @@ class MembersTable extends LoadTableBase { } } +class MemberDomainsTable extends LoadTableBase { + + constructor() { + super('.member-domains__table', '.member-domains__table-wrapper', '#member-domains__search-field', '#member-domains__search-field-submit', '.member-domains__reset-search', '.member-domains__reset-filters', '.member-domains__no-data', '.member-domains__no-search-results'); + } + /** + * Loads rows in the members list, as well as updates pagination around the members list + * based on the supplied attributes. + * @param {*} page - the page number of the results (starts with 1) + * @param {*} sortBy - the sort column option + * @param {*} order - the sort order {asc, desc} + * @param {*} scroll - control for the scrollToElement functionality + * @param {*} searchTerm - the search term + * @param {*} portfolio - the portfolio id + */ + loadTable(page, sortBy = 'name', order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { + + // --------- SEARCH + let searchParams = new URLSearchParams( + { + "page": page, + "sort_by": sortBy, + "order": order, + "search_term": searchTerm, + } + ); + + let emailValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-email') : null; + let memberIdValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-id') : null; + let memberOnly = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-only') : null; + + if (portfolio) + searchParams.append("portfolio", portfolio) + if (emailValue) + searchParams.append("email", emailValue) + if (memberIdValue) + searchParams.append("member_id", memberIdValue) + if (memberOnly) + searchParams.append("member_only", memberOnly) + + + // --------- FETCH DATA + // fetch json of page of domais, given params + let baseUrl = document.getElementById("get_member_domains_json_url"); + if (!baseUrl) { + return; + } + + let baseUrlValue = baseUrl.innerHTML; + if (!baseUrlValue) { + return; + } + + let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function + fetch(url) + .then(response => response.json()) + .then(data => { + if (data.error) { + console.error('Error in AJAX call: ' + data.error); + return; + } + + // handle the display of proper messaging in the event that no members exist in the list or search returns no results + this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); + + // identify the DOM element where the domain list will be inserted into the DOM + const memberDomainsList = document.querySelector('.member-domains__table tbody'); + memberDomainsList.innerHTML = ''; + + + data.domains.forEach(domain => { + const row = document.createElement('tr'); + + row.innerHTML = ` + + ${domain.name} + + `; + memberDomainsList.appendChild(row); + }); + + // Do not scroll on first page load + if (scroll) + ScrollToElement('class', 'member-domains'); + this.scrollToTable = true; + + // update pagination + this.updatePagination( + 'member domain', + '#member-domains-pagination', + '#member-domains-pagination .usa-pagination__counter', + '#member-domains', + data.page, + data.num_pages, + data.has_previous, + data.has_next, + data.total, + ); + this.currentSortBy = sortBy; + this.currentOrder = order; + this.currentSearchTerm = searchTerm; + }) + .catch(error => console.error('Error fetching domains:', error)); + } +} + /** * An IIFE that listens for DOM Content to be loaded, then executes. This function - * initializes the domains list and associated functionality on the home page of the app. + * initializes the domains list and associated functionality. * */ document.addEventListener('DOMContentLoaded', function() { - const isDomainsPage = document.querySelector("#domains") + const isDomainsPage = document.getElementById("domains") if (isDomainsPage){ const domainsTable = new DomainsTable(); if (domainsTable.tableWrapper) { @@ -2033,11 +2139,11 @@ document.addEventListener('DOMContentLoaded', function() { /** * An IIFE that listens for DOM Content to be loaded, then executes. This function - * initializes the domain requests list and associated functionality on the home page of the app. + * initializes the domain requests list and associated functionality. * */ document.addEventListener('DOMContentLoaded', function() { - const domainRequestsSectionWrapper = document.querySelector('.domain-requests'); + const domainRequestsSectionWrapper = document.getElementById('domain-requests'); if (domainRequestsSectionWrapper) { const domainRequestsTable = new DomainRequestsTable(); if (domainRequestsTable.tableWrapper) { @@ -2090,11 +2196,11 @@ const utcDateString = (dateString) => { /** * An IIFE that listens for DOM Content to be loaded, then executes. This function - * initializes the domains list and associated functionality on the home page of the app. + * initializes the members list and associated functionality. * */ document.addEventListener('DOMContentLoaded', function() { - const isMembersPage = document.querySelector("#members") + const isMembersPage = document.getElementById("members") if (isMembersPage){ const membersTable = new MembersTable(); if (membersTable.tableWrapper) { @@ -2104,6 +2210,22 @@ document.addEventListener('DOMContentLoaded', function() { } }); +/** + * An IIFE that listens for DOM Content to be loaded, then executes. This function + * initializes the member domains list and associated functionality. + * + */ +document.addEventListener('DOMContentLoaded', function() { + const isMemberDomainsPage = document.getElementById("member-domains") + if (isMemberDomainsPage){ + const memberDomainsTable = new MemberDomainsTable(); + if (memberDomainsTable.tableWrapper) { + // Initial load + memberDomainsTable.loadTable(1); + } + } +}); + /** * An IIFE that displays confirmation modal on the user profile page */ diff --git a/src/registrar/templates/includes/member_domains_table.html b/src/registrar/templates/includes/member_domains_table.html index 7d9a37308..29ce042d6 100644 --- a/src/registrar/templates/includes/member_domains_table.html +++ b/src/registrar/templates/includes/member_domains_table.html @@ -1,8 +1,37 @@ {% load static %} -
+{% if member %} + +{% else %} + +{% endif %} + +{% comment %} Stores the json endpoint in a url for easier access {% endcomment %} +{% url 'get_member_domains_json' as url %} + +
+

- Domains assigned to {{ email }} + Domains assigned to + {% if member %} + {{ member.email }} + {% else %} + {{ portfolio_invitation.email }} + {% endif %}

@@ -25,7 +54,7 @@ @@ -49,7 +78,7 @@ member domains - Domains + Domains diff --git a/src/registrar/templates/portfolio_member_domains.html b/src/registrar/templates/portfolio_member_domains.html index c51c3464e..1f811c707 100644 --- a/src/registrar/templates/portfolio_member_domains.html +++ b/src/registrar/templates/portfolio_member_domains.html @@ -48,17 +48,7 @@ A domain manager can be assigned to any domain across the organization. Domain managers can change domain information, adjust DNS settings, and invite or assign other domain managers to their assigned domains.

- {% if member %} - {% with member.email as email %} - {% include "includes/member_domains_table.html" with email=email %} - {% endwith %} - {% else %} - {% with portfolio_invitation.email as email %} - {% include "includes/member_domains_table.html" with email=email %} - {% endwith %} - {% endif %} + {% include "includes/member_domains_table.html" %} - -
{% endblock %} diff --git a/src/registrar/views/member_domains_json.py b/src/registrar/views/member_domains_json.py index 4125fe345..7dcec6bef 100644 --- a/src/registrar/views/member_domains_json.py +++ b/src/registrar/views/member_domains_json.py @@ -27,7 +27,6 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View): unfiltered_total = objects.count() objects = self.apply_search(objects, request) - objects = self.apply_state_filter(objects, request) objects = self.apply_sorting(objects, request) paginator = Paginator(objects, 10) @@ -53,14 +52,14 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View): """Get domain ids from request. request.get.email - email address of invited member - request.get.member - member id of member + request.get.member_id - member id of member request.get.portfolio - portfolio id of portfolio request.get.member_only - whether to return only domains associated with member or to return all domains in the portfolio """ portfolio = request.GET.get("portfolio") email = request.GET.get("email") - member_id = request.GET.get("member") + member_id = request.GET.get("member_id") member_only = request.GET.get("member_only", "false").lower() in ["true", "1"] if member_only: if member_id: @@ -86,48 +85,12 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View): return queryset - def apply_state_filter(self, queryset, request): - status_param = request.GET.get("status") - if status_param: - status_list = status_param.split(",") - # if unknown is in status_list, append 'dns needed' since both - # unknown and dns needed display as DNS Needed, and both are - # searchable via state parameter of 'unknown' - if "unknown" in status_list: - status_list.append("dns needed") - # Split the status list into normal states and custom states - normal_states = [state for state in status_list if state in Domain.State.values] - custom_states = [state for state in status_list if state == "expired"] - # Construct Q objects for normal states that can be queried through ORM - state_query = Q() - if normal_states: - state_query |= Q(state__in=normal_states) - # Handle custom states in Python, as expired can not be queried through ORM - if "expired" in custom_states: - expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"] - state_query |= Q(id__in=expired_domain_ids) - # Apply the combined query - queryset = queryset.filter(state_query) - # If there are filtered states, and expired is not one of them, domains with - # state_display of 'Expired' must be removed - if "expired" not in custom_states: - expired_domain_ids = [domain.id for domain in queryset if domain.state_display() == "Expired"] - queryset = queryset.exclude(id__in=expired_domain_ids) - - return queryset - - def apply_sorting(self, queryset, request): - sort_by = request.GET.get("sort_by", "id") + sort_by = request.GET.get("sort_by", "name") order = request.GET.get("order", "asc") - if sort_by == "state_display": - objects = list(queryset) - objects.sort(key=lambda domain: domain.state_display(), reverse=(order == "desc")) - return objects - else: - if order == "desc": - sort_by = f"-{sort_by}" - return queryset.order_by(sort_by) + if order == "desc": + sort_by = f"-{sort_by}" + return queryset.order_by(sort_by) def serialize_domain(self, domain, user): From ebd4b17d8080dc4fc115fe5c4a3ccbcda2676db7 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 21 Oct 2024 15:34:25 -0400 Subject: [PATCH 04/24] JS refactor --- src/registrar/assets/js/get-gov.js | 48 +-- .../includes/domain_requests_table.html | 32 +- .../templates/includes/domains_table.html | 374 +++++++++--------- .../includes/member_domains_table.html | 27 +- .../templates/includes/members_table.html | 14 +- 5 files changed, 252 insertions(+), 243 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index efb8c6f11..0850afd41 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1003,25 +1003,24 @@ function unloadModals() { } class LoadTableBase { - constructor(tableSelector, tableWrapperSelector, searchFieldId, searchSubmitId, resetSearchBtn, resetFiltersBtn, noDataDisplay, noSearchresultsDisplay) { - this.tableWrapper = document.querySelector(tableWrapperSelector); - this.tableHeaders = document.querySelectorAll(`${tableSelector} th[data-sortable]`); + constructor(sectionSelector) { + this.tableWrapper = document.getElementById(`${sectionSelector}__table-wrapper`); + this.tableHeaders = document.querySelectorAll(`#${sectionSelector} th[data-sortable]`); this.currentSortBy = 'id'; this.currentOrder = 'asc'; this.currentStatus = []; this.currentSearchTerm = ''; this.scrollToTable = false; - this.searchInput = document.querySelector(searchFieldId); - this.searchSubmit = document.querySelector(searchSubmitId); - this.tableAnnouncementRegion = document.querySelector(`${tableWrapperSelector} .usa-table__announcement-region`); - this.resetSearchButton = document.querySelector(resetSearchBtn); - this.resetFiltersButton = document.querySelector(resetFiltersBtn); - // NOTE: these 3 can't be used if filters are active on a page with more than 1 table - this.statusCheckboxes = document.querySelectorAll('input[name="filter-status"]'); - this.statusIndicator = document.querySelector('.filter-indicator'); - this.statusToggle = document.querySelector('.usa-button--filter'); - this.noTableWrapper = document.querySelector(noDataDisplay); - this.noSearchResultsWrapper = document.querySelector(noSearchresultsDisplay); + this.searchInput = document.getElementById(`${sectionSelector}__search-field`); + this.searchSubmit = document.getElementById(`${sectionSelector}__search-field-submit`); + this.tableAnnouncementRegion = document.getElementById(`${sectionSelector}__usa-table__announcement-region`); + this.resetSearchButton = document.getElementById(`${sectionSelector}__reset-search`); + this.resetFiltersButton = document.getElementById(`${sectionSelector}__reset-filters`); + this.statusCheckboxes = document.querySelectorAll(`.${sectionSelector} input[name="filter-status"]`); + this.statusIndicator = document.getElementById(`${sectionSelector}__filter-indicator`); + this.statusToggle = document.getElementById(`${sectionSelector}__usa-button--filter`); + this.noTableWrapper = document.getElementById(`${sectionSelector}__no-data`); + this.noSearchResultsWrapper = document.getElementById(`${sectionSelector}__no-search-results`); this.portfolioElement = document.getElementById('portfolio-js-value'); this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null; this.initializeTableHeaders(); @@ -1363,7 +1362,7 @@ class LoadTableBase { class DomainsTable extends LoadTableBase { constructor() { - super('.domains__table', '.domains__table-wrapper', '#domains__search-field', '#domains__search-field-submit', '.domains__reset-search', '.domains__reset-filters', '.domains__no-data', '.domains__no-search-results'); + super('domains'); } /** * Loads rows in the domains list, as well as updates pagination around the domains list @@ -1415,7 +1414,7 @@ class DomainsTable extends LoadTableBase { this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); // identify the DOM element where the domain list will be inserted into the DOM - const domainList = document.querySelector('.domains__table tbody'); + const domainList = document.querySelector('#domains tbody'); domainList.innerHTML = ''; data.domains.forEach(domain => { @@ -1501,7 +1500,7 @@ class DomainsTable extends LoadTableBase { class DomainRequestsTable extends LoadTableBase { constructor() { - super('.domain-requests__table', '.domain-requests__table-wrapper', '#domain-requests__search-field', '#domain-requests__search-field-submit', '.domain-requests__reset-search', '.domain-requests__reset-filters', '.domain-requests__no-data', '.domain-requests__no-search-results'); + super('domain-requests'); } toggleExportButton(requests) { @@ -1567,7 +1566,7 @@ class DomainRequestsTable extends LoadTableBase { this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); // identify the DOM element where the domain request list will be inserted into the DOM - const tbody = document.querySelector('.domain-requests__table tbody'); + const tbody = document.querySelector('#domain-requests tbody'); tbody.innerHTML = ''; // Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases @@ -1599,7 +1598,7 @@ class DomainRequestsTable extends LoadTableBase { delheader.setAttribute('class', 'delete-header'); delheader.innerHTML = ` Delete Action`; - let tableHeaderRow = document.querySelector('.domain-requests__table thead tr'); + let tableHeaderRow = document.querySelector('#domain-requests thead tr'); tableHeaderRow.appendChild(delheader); } } @@ -1871,7 +1870,7 @@ class DomainRequestsTable extends LoadTableBase { class MembersTable extends LoadTableBase { constructor() { - super('.members__table', '.members__table-wrapper', '#members__search-field', '#members__search-field-submit', '.members__reset-search', '.members__reset-filters', '.members__no-data', '.members__no-search-results'); + super('members'); } /** * Loads rows in the members list, as well as updates pagination around the members list @@ -1923,7 +1922,7 @@ class MembersTable extends LoadTableBase { this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); // identify the DOM element where the domain list will be inserted into the DOM - const memberList = document.querySelector('.members__table tbody'); + const memberList = document.querySelector('#members tbody'); memberList.innerHTML = ''; const invited = 'Invited'; @@ -2017,7 +2016,8 @@ class MembersTable extends LoadTableBase { class MemberDomainsTable extends LoadTableBase { constructor() { - super('.member-domains__table', '.member-domains__table-wrapper', '#member-domains__search-field', '#member-domains__search-field-submit', '.member-domains__reset-search', '.member-domains__reset-filters', '.member-domains__no-data', '.member-domains__no-search-results'); + super('member-domains'); + this.currentSortBy = 'name'; } /** * Loads rows in the members list, as well as updates pagination around the members list @@ -2029,7 +2029,7 @@ class MemberDomainsTable extends LoadTableBase { * @param {*} searchTerm - the search term * @param {*} portfolio - the portfolio id */ - loadTable(page, sortBy = 'name', order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { + loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { // --------- SEARCH let searchParams = new URLSearchParams( @@ -2080,7 +2080,7 @@ class MemberDomainsTable extends LoadTableBase { this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); // identify the DOM element where the domain list will be inserted into the DOM - const memberDomainsList = document.querySelector('.member-domains__table tbody'); + const memberDomainsList = document.querySelector('#member-domains tbody'); memberDomainsList.innerHTML = ''; diff --git a/src/registrar/templates/includes/domain_requests_table.html b/src/registrar/templates/includes/domain_requests_table.html index 4bef23870..5b7604222 100644 --- a/src/registrar/templates/includes/domain_requests_table.html +++ b/src/registrar/templates/includes/domain_requests_table.html @@ -17,22 +17,24 @@
diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html index 11d3ac945..731249a3e 100644 --- a/src/registrar/templates/includes/domains_table.html +++ b/src/registrar/templates/includes/domains_table.html @@ -7,195 +7,197 @@
- {% if not portfolio %} -

Domains

- {% else %} - - - {% endif %} - - {% if user_domain_count and user_domain_count > 0 %} - - {% endif %} -
- {% if portfolio %} -
- Filter by -
-
- -
-
-

Status

-
- Select to apply status filter -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- + + + + +
+ + {% if user_domain_count and user_domain_count > 0 %} + {% endif %} - - + {% if portfolio %} +
+ Filter by +
+ +
+

Status

+
+ Select to apply status filter +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
- -
- + + + {% endif %} + + + +
+ diff --git a/src/registrar/templates/includes/member_domains_table.html b/src/registrar/templates/includes/member_domains_table.html index 29ce042d6..ca8ade7a7 100644 --- a/src/registrar/templates/includes/member_domains_table.html +++ b/src/registrar/templates/includes/member_domains_table.html @@ -40,13 +40,15 @@
diff --git a/src/registrar/templates/includes/members_table.html b/src/registrar/templates/includes/members_table.html index 529d2629d..d14be09d8 100644 --- a/src/registrar/templates/includes/members_table.html +++ b/src/registrar/templates/includes/members_table.html @@ -12,7 +12,7 @@
{% csrf_token %} -
From e2fc43831f245f1be562b68bfcdc9d89965129d1 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 21 Oct 2024 18:38:02 -0400 Subject: [PATCH 05/24] updated tests --- .../tests/test_views_member_domains_json.py | 338 ++++++++++++++++++ .../tests/test_views_members_json.py | 26 ++ 2 files changed, 364 insertions(+) create mode 100644 src/registrar/tests/test_views_member_domains_json.py diff --git a/src/registrar/tests/test_views_member_domains_json.py b/src/registrar/tests/test_views_member_domains_json.py new file mode 100644 index 000000000..11b77576e --- /dev/null +++ b/src/registrar/tests/test_views_member_domains_json.py @@ -0,0 +1,338 @@ +from django.urls import reverse + +from api.tests.common import less_console_noise_decorator +from registrar.models.domain import Domain +from registrar.models.domain_information import DomainInformation +from registrar.models.domain_invitation import DomainInvitation +from registrar.models.portfolio import Portfolio +from registrar.models.portfolio_invitation import PortfolioInvitation +from registrar.models.user import User +from registrar.models.user_domain_role import UserDomainRole +from registrar.models.user_portfolio_permission import UserPortfolioPermission +from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices +from waffle.testutils import override_flag +from .test_views import TestWithUser +from django_webtest import WebTest # type: ignore + + +class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Create test member + cls.user_member = User.objects.create( + username="test_member", + first_name="Second", + last_name="User", + email="second@example.com", + phone="8003112345", + title="Member", + ) + + # Create test user with no perms + cls.user_no_perms = User.objects.create( + username="test_user_no_perms", + first_name="No", + last_name="Permissions", + email="user_no_perms@example.com", + phone="8003112345", + title="No Permissions", + ) + + # Create Portfolio + cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Test Portfolio") + + # Assign permissions to the user making requests + UserPortfolioPermission.objects.create( + user=cls.user, + portfolio=cls.portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], + additional_permissions=[ + UserPortfolioPermissionChoices.VIEW_MEMBERS, + UserPortfolioPermissionChoices.EDIT_MEMBERS, + ], + ) + + # Assign some domains + cls.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-03-01", state="ready") + cls.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-03-01", state="ready") + cls.domain3 = Domain.objects.create(name="example3.com", expiration_date="2024-03-01", state="ready") + # Add domain1 and domain2 to portfolio + DomainInformation.objects.create(creator=cls.user, domain=cls.domain1, portfolio=cls.portfolio) + DomainInformation.objects.create(creator=cls.user, domain=cls.domain2, portfolio=cls.portfolio) + DomainInformation.objects.create(creator=cls.user, domain=cls.domain3, portfolio=cls.portfolio) + + # Assign user_member to view all domains + UserPortfolioPermission.objects.create( + user=cls.user_member, + portfolio=cls.portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], + ) + # Add user_member as manager of domains + UserDomainRole.objects.create(user=cls.user_member, domain=cls.domain1) + UserDomainRole.objects.create(user=cls.user_member, domain=cls.domain2) + + # Add an invited member who has been invited to manage domains + cls.invited_member_email = "invited@example.com" + PortfolioInvitation.objects.create( + email=cls.invited_member_email, + portfolio=cls.portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER], + additional_permissions=[ + UserPortfolioPermissionChoices.VIEW_MEMBERS, + ], + ) + DomainInvitation.objects.create(email=cls.invited_member_email, domain=cls.domain1, status=DomainInvitation.DomainInvitationStatus.INVITED) + DomainInvitation.objects.create(email=cls.invited_member_email, domain=cls.domain2, status=DomainInvitation.DomainInvitationStatus.INVITED) + + @classmethod + def tearDownClass(cls): + PortfolioInvitation.objects.all().delete() + UserPortfolioPermission.objects.all().delete() + UserDomainRole.objects.all().delete() + DomainInvitation.objects.all().delete() + DomainInformation.objects.all().delete() + Domain.objects.all().delete() + Portfolio.objects.all().delete() + User.objects.all().delete() + super().tearDownClass() + + def setUp(self): + super().setUp() + self.app.set_user(self.user.username) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_get_portfolio_member_domains_json_authenticated(self): + """Test that portfolio member's domains are returned properly for an authenticated user.""" + response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "true"}) + self.assertEqual(response.status_code, 200) + data = response.json + + # Check pagination info + self.assertEqual(data["page"], 1) + self.assertFalse(data["has_previous"]) + self.assertFalse(data["has_next"]) + self.assertEqual(data["num_pages"], 1) + self.assertEqual(data["total"], 2) + self.assertEqual(data["unfiltered_total"], 2) + + # Check the number of domains + self.assertEqual(len(data["domains"]), 2) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_get_portfolio_invitedmember_domains_json_authenticated(self): + """Test that portfolio invitedmember's domains are returned properly for an authenticated user.""" + response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "email": self.invited_member_email, "member_only": "true"}) + self.assertEqual(response.status_code, 200) + data = response.json + + # Check pagination info + self.assertEqual(data["page"], 1) + self.assertFalse(data["has_previous"]) + self.assertFalse(data["has_next"]) + self.assertEqual(data["num_pages"], 1) + self.assertEqual(data["total"], 2) + self.assertEqual(data["unfiltered_total"], 2) + + # Check the number of domains + self.assertEqual(len(data["domains"]), 2) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_get_portfolio_member_domains_json_authenticated_include_all_domains(self): + """Test that all portfolio domains are returned properly for an authenticated user.""" + response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "false"}) + self.assertEqual(response.status_code, 200) + data = response.json + + # Check pagination info + self.assertEqual(data["page"], 1) + self.assertFalse(data["has_previous"]) + self.assertFalse(data["has_next"]) + self.assertEqual(data["num_pages"], 1) + self.assertEqual(data["total"], 3) + self.assertEqual(data["unfiltered_total"], 3) + + # Check the number of domains + self.assertEqual(len(data["domains"]), 3) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_get_portfolio_invitedmember_domains_json_authenticated_include_all_domains(self): + """Test that all portfolio domains are returned properly for an authenticated user.""" + response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "email": self.invited_member_email, "member_only": "false"}) + self.assertEqual(response.status_code, 200) + data = response.json + + # Check pagination info + self.assertEqual(data["page"], 1) + self.assertFalse(data["has_previous"]) + self.assertFalse(data["has_next"]) + self.assertEqual(data["num_pages"], 1) + self.assertEqual(data["total"], 3) + self.assertEqual(data["unfiltered_total"], 3) + + # Check the number of domains + self.assertEqual(len(data["domains"]), 3) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_get_portfolio_member_domains_json_authenticated_search(self): + """Test that search_term yields correct domain.""" + response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "false", "search_term": "example1"}) + self.assertEqual(response.status_code, 200) + data = response.json + + # Check pagination info + self.assertEqual(data["page"], 1) + self.assertFalse(data["has_previous"]) + self.assertFalse(data["has_next"]) + self.assertEqual(data["num_pages"], 1) + self.assertEqual(data["total"], 1) + self.assertEqual(data["unfiltered_total"], 3) + + # Check the number of domains + self.assertEqual(len(data["domains"]), 1) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_get_portfolio_invitedmember_domains_json_authenticated_search(self): + """Test that search_term yields correct domain.""" + response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "email": self.invited_member_email, "member_only": "false", "search_term": "example1"}) + self.assertEqual(response.status_code, 200) + data = response.json + + # Check pagination info + self.assertEqual(data["page"], 1) + self.assertFalse(data["has_previous"]) + self.assertFalse(data["has_next"]) + self.assertEqual(data["num_pages"], 1) + self.assertEqual(data["total"], 1) + self.assertEqual(data["unfiltered_total"], 3) + + # Check the number of domains + self.assertEqual(len(data["domains"]), 1) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_get_portfolio_member_domains_json_authenticated_sort(self): + """Test that sort returns results in correct order.""" + # Test by name in ascending order + response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "false", "sort_by": "name", "order":"asc"}) + self.assertEqual(response.status_code, 200) + data = response.json + + # Check pagination info + self.assertEqual(data["page"], 1) + self.assertFalse(data["has_previous"]) + self.assertFalse(data["has_next"]) + self.assertEqual(data["num_pages"], 1) + self.assertEqual(data["total"], 3) + self.assertEqual(data["unfiltered_total"], 3) + + # Check the number of domains + self.assertEqual(len(data["domains"]), 3) + + # Check the name of the first domain is example1.com + self.assertEqual(data["domains"][0]["name"], "example1.com") + + # Test by name in descending order + response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "false", "sort_by": "name", "order":"desc"}) + self.assertEqual(response.status_code, 200) + data = response.json + + # Check pagination info + self.assertEqual(data["page"], 1) + self.assertFalse(data["has_previous"]) + self.assertFalse(data["has_next"]) + self.assertEqual(data["num_pages"], 1) + self.assertEqual(data["total"], 3) + self.assertEqual(data["unfiltered_total"], 3) + + # Check the number of domains + self.assertEqual(len(data["domains"]), 3) + + # Check the name of the first domain is example1.com + self.assertEqual(data["domains"][0]["name"], "example3.com") + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_get_portfolio_invitedmember_domains_json_authenticated_sort(self): + """Test that sort returns results in correct order.""" + # Test by name in ascending order + response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "email": self.invited_member_email, "member_only": "false", "sort_by": "name", "order":"asc"}) + self.assertEqual(response.status_code, 200) + data = response.json + + # Check pagination info + self.assertEqual(data["page"], 1) + self.assertFalse(data["has_previous"]) + self.assertFalse(data["has_next"]) + self.assertEqual(data["num_pages"], 1) + self.assertEqual(data["total"], 3) + self.assertEqual(data["unfiltered_total"], 3) + + # Check the number of domains + self.assertEqual(len(data["domains"]), 3) + + # Check the name of the first domain is example1.com + self.assertEqual(data["domains"][0]["name"], "example1.com") + + # Test by name in descending order + response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "email": self.invited_member_email, "member_only": "false", "sort_by": "name", "order":"desc"}) + self.assertEqual(response.status_code, 200) + data = response.json + + # Check pagination info + self.assertEqual(data["page"], 1) + self.assertFalse(data["has_previous"]) + self.assertFalse(data["has_next"]) + self.assertEqual(data["num_pages"], 1) + self.assertEqual(data["total"], 3) + self.assertEqual(data["unfiltered_total"], 3) + + # Check the number of domains + self.assertEqual(len(data["domains"]), 3) + + # Check the name of the first domain is example1.com + self.assertEqual(data["domains"][0]["name"], "example3.com") + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_get_portfolio_members_json_restricted_user(self): + """Test that an restricted user is denied access.""" + # set user to a user with no permissions + self.app.set_user(self.user_no_perms) + + # Try to access the portfolio members without being authenticated + response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "true"}, expect_errors=True) + + # Assert that the response is a 403 + self.assertEqual(response.status_code, 403) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_get_portfolio_members_json_unauthenticated(self): + """Test that an unauthenticated user is redirected to login.""" + # set app to unauthenticated + self.app.set_user(None) + + # Try to access the portfolio members without being authenticated + response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "true"}, expect_errors=True) + + # Assert that the response is a redirect to openid login + self.assertEqual(response.status_code, 302) + self.assertIn("/openid/login", response.location) diff --git a/src/registrar/tests/test_views_members_json.py b/src/registrar/tests/test_views_members_json.py index 9cd4e823c..1ae4aadf5 100644 --- a/src/registrar/tests/test_views_members_json.py +++ b/src/registrar/tests/test_views_members_json.py @@ -1,10 +1,12 @@ from django.urls import reverse +from api.tests.common import less_console_noise_decorator from registrar.models.portfolio import Portfolio from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.user import User from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices +from waffle.testutils import override_flag from .test_views import TestWithUser from django_webtest import WebTest # type: ignore @@ -91,6 +93,9 @@ class GetPortfolioMembersJsonTest(TestWithUser, WebTest): super().setUp() self.app.set_user(self.user.username) + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) def test_get_portfolio_members_json_authenticated(self): """Test that portfolio members are returned properly for an authenticated user.""" response = self.app.get(reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id}) @@ -120,6 +125,24 @@ class GetPortfolioMembersJsonTest(TestWithUser, WebTest): actual_emails = {member["email"] for member in data["members"]} self.assertEqual(expected_emails, actual_emails) + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_get_portfolio_members_json_unauthenticated(self): + """Test that an unauthenticated user is redirected or denied access.""" + # Log out the user by setting the user to None + self.app.set_user(None) + + # Try to access the portfolio members without being authenticated + response = self.app.get(reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id}, expect_errors=True) + + # Assert that the response is a redirect to the login page + self.assertEqual(response.status_code, 302) # Redirect to openid login + self.assertIn("/openid/login", response.location) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) def test_pagination(self): """Test that pagination works properly when there are more members than page size.""" # Create additional members to exceed page size of 10 @@ -170,6 +193,9 @@ class GetPortfolioMembersJsonTest(TestWithUser, WebTest): # Check the number of members on page 2 self.assertEqual(len(data["members"]), 5) + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) def test_search(self): """Test search functionality for portfolio members.""" # Search by name From 08d878b427432f8a8d5bbc99ccfc0711aca41066 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 21 Oct 2024 19:45:37 -0400 Subject: [PATCH 06/24] more unit tests --- src/registrar/tests/test_views_portfolio.py | 210 ++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 13173565c..3ac4ac3dd 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -14,6 +14,7 @@ from registrar.models.portfolio_invitation import PortfolioInvitation from registrar.models.user_group import UserGroup from registrar.models.user_portfolio_permission import UserPortfolioPermission from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices +from registrar.tests.test_views import TestWithUser from .common import MockSESClient, completed_domain_request, create_test_user from waffle.testutils import override_flag from django.contrib.sessions.middleware import SessionMiddleware @@ -1387,3 +1388,212 @@ class TestPortfolio(WebTest): # Check that the domain request still exists self.assertTrue(DomainRequest.objects.filter(pk=domain_request.pk).exists()) domain_request.delete() + + +class TestPortfolioMemberDomainsView(TestWithUser, WebTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Create test member + cls.user_member = User.objects.create( + username="test_member", + first_name="Second", + last_name="User", + email="second@example.com", + phone="8003112345", + title="Member", + ) + + # Create test user with no perms + cls.user_no_perms = User.objects.create( + username="test_user_no_perms", + first_name="No", + last_name="Permissions", + email="user_no_perms@example.com", + phone="8003112345", + title="No Permissions", + ) + + # Create Portfolio + cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Test Portfolio") + + # Assign permissions to the user making requests + UserPortfolioPermission.objects.create( + user=cls.user, + portfolio=cls.portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], + additional_permissions=[ + UserPortfolioPermissionChoices.VIEW_MEMBERS, + UserPortfolioPermissionChoices.EDIT_MEMBERS, + ], + ) + cls.permission = UserPortfolioPermission.objects.create( + user=cls.user_member, + portfolio=cls.portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], + additional_permissions=[ + UserPortfolioPermissionChoices.VIEW_MEMBERS, + UserPortfolioPermissionChoices.EDIT_MEMBERS, + ], + ) + + @classmethod + def tearDownClass(cls): + UserPortfolioPermission.objects.all().delete() + Portfolio.objects.all().delete() + User.objects.all().delete() + super().tearDownClass() + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_member_domains_authenticated(self): + """Tests that the portfolio member domains view is accessible.""" + self.client.force_login(self.user) + + response = self.client.get(reverse("member-domains", kwargs={"pk": self.permission.id})) + + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, self.user_member.email) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_member_domains_no_perms(self): + """Tests that the portfolio member domains view is not accessible to user with no perms.""" + self.client.force_login(self.user_no_perms) + + response = self.client.get(reverse("member-domains", kwargs={"pk": self.permission.id})) + + # Make sure the request returns forbidden + self.assertEqual(response.status_code, 403) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_member_domains_unauthenticated(self): + """Tests that the portfolio member domains view is not accessible when no authenticated user.""" + self.client.logout() + + response = self.client.get(reverse("member-domains", kwargs={"pk": self.permission.id})) + + # Make sure the request returns redirect to openid login + self.assertEqual(response.status_code, 302) # Redirect to openid login + self.assertIn("/openid/login", response.url) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_member_domains_not_found(self): + """Tests that the portfolio member domains view returns not found if user portfolio permission not found.""" + self.client.force_login(self.user) + + response = self.client.get(reverse("member-domains", kwargs={"pk": "0"})) + + + # Make sure the response is not found + self.assertEqual(response.status_code, 404) + + +class TestPortfolioInvitedMemberDomainsView(TestWithUser, WebTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.user_no_perms = User.objects.create( + username="test_user_no_perms", + first_name="No", + last_name="Permissions", + email="user_no_perms@example.com", + phone="8003112345", + title="No Permissions", + ) + + # Create Portfolio + cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Test Portfolio") + + # Add an invited member who has been invited to manage domains + cls.invited_member_email = "invited@example.com" + cls.invitation = PortfolioInvitation.objects.create( + email=cls.invited_member_email, + portfolio=cls.portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER], + additional_permissions=[ + UserPortfolioPermissionChoices.VIEW_MEMBERS, + ], + ) + + # Assign permissions to the user making requests + UserPortfolioPermission.objects.create( + user=cls.user, + portfolio=cls.portfolio, + roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], + additional_permissions=[ + UserPortfolioPermissionChoices.VIEW_MEMBERS, + UserPortfolioPermissionChoices.EDIT_MEMBERS, + ], + ) + + @classmethod + def tearDownClass(cls): + PortfolioInvitation.objects.all().delete() + UserPortfolioPermission.objects.all().delete() + Portfolio.objects.all().delete() + User.objects.all().delete() + super().tearDownClass() + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_invitedmember_domains_authenticated(self): + """Tests that the portfolio invited member domains view is accessible.""" + self.client.force_login(self.user) + + response = self.client.get(reverse("invitedmember-domains", kwargs={"pk": self.invitation.id})) + + + # Make sure the page loaded, and that we're on the right page + self.assertEqual(response.status_code, 200) + self.assertContains(response, self.invited_member_email) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_invitedmember_domains_no_perms(self): + """Tests that the portfolio invited member domains view is not accessible to user with no perms.""" + self.client.force_login(self.user_no_perms) + + response = self.client.get(reverse("invitedmember-domains", kwargs={"pk": self.invitation.id})) + + # Make sure the request returns forbidden + self.assertEqual(response.status_code, 403) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_invitedmember_domains_unauthenticated(self): + """Tests that the portfolio invited member domains view is not accessible when no authenticated user.""" + self.client.logout() + + response = self.client.get(reverse("invitedmember-domains", kwargs={"pk": self.invitation.id})) + + # Make sure the request returns redirect to openid login + self.assertEqual(response.status_code, 302) # Redirect to openid login + self.assertIn("/openid/login", response.url) + + @less_console_noise_decorator + @override_flag("organization_feature", active=True) + @override_flag("organization_members", active=True) + def test_member_domains_not_found(self): + """Tests that the portfolio invited member domains view returns not found if user is not a member.""" + self.client.force_login(self.user) + + response = self.client.get(reverse("invitedmember-domains", kwargs={"pk": "0"})) + + + # Make sure the response is not found + self.assertEqual(response.status_code, 404) + \ No newline at end of file From dbf208c2bb1b8a924aed5616c86ea58583e94f4b Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Mon, 21 Oct 2024 19:55:05 -0400 Subject: [PATCH 07/24] formatted for code readability --- src/registrar/config/urls.py | 2 +- .../tests/test_views_member_domains_json.py | 114 +++++++++++++++--- .../tests/test_views_members_json.py | 8 +- src/registrar/tests/test_views_portfolio.py | 11 +- src/registrar/views/member_domains_json.py | 15 ++- src/registrar/views/portfolio_members_json.py | 8 +- src/registrar/views/portfolios.py | 7 +- .../views/utility/permission_views.py | 1 + 8 files changed, 117 insertions(+), 49 deletions(-) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index c493f1a08..f61e31e54 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -338,7 +338,7 @@ urlpatterns = [ path("get-domains-json/", get_domains_json, name="get_domains_json"), path("get-domain-requests-json/", get_domain_requests_json, name="get_domain_requests_json"), path("get-portfolio-members-json/", views.PortfolioMembersJson.as_view(), name="get_portfolio_members_json"), - path('get-member-domains-json/', views.PortfolioMemberDomainsJson.as_view(), name="get_member_domains_json"), + path("get-member-domains-json/", views.PortfolioMemberDomainsJson.as_view(), name="get_member_domains_json"), ] # Djangooidc strips out context data from that context, so we define a custom error diff --git a/src/registrar/tests/test_views_member_domains_json.py b/src/registrar/tests/test_views_member_domains_json.py index 11b77576e..68b4f4de2 100644 --- a/src/registrar/tests/test_views_member_domains_json.py +++ b/src/registrar/tests/test_views_member_domains_json.py @@ -53,7 +53,7 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): UserPortfolioPermissionChoices.EDIT_MEMBERS, ], ) - + # Assign some domains cls.domain1 = Domain.objects.create(name="example1.com", expiration_date="2024-03-01", state="ready") cls.domain2 = Domain.objects.create(name="example2.com", expiration_date="2024-03-01", state="ready") @@ -83,8 +83,12 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): UserPortfolioPermissionChoices.VIEW_MEMBERS, ], ) - DomainInvitation.objects.create(email=cls.invited_member_email, domain=cls.domain1, status=DomainInvitation.DomainInvitationStatus.INVITED) - DomainInvitation.objects.create(email=cls.invited_member_email, domain=cls.domain2, status=DomainInvitation.DomainInvitationStatus.INVITED) + DomainInvitation.objects.create( + email=cls.invited_member_email, domain=cls.domain1, status=DomainInvitation.DomainInvitationStatus.INVITED + ) + DomainInvitation.objects.create( + email=cls.invited_member_email, domain=cls.domain2, status=DomainInvitation.DomainInvitationStatus.INVITED + ) @classmethod def tearDownClass(cls): @@ -107,7 +111,10 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): @override_flag("organization_members", active=True) def test_get_portfolio_member_domains_json_authenticated(self): """Test that portfolio member's domains are returned properly for an authenticated user.""" - response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "true"}) + response = self.app.get( + reverse("get_member_domains_json"), + params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "true"}, + ) self.assertEqual(response.status_code, 200) data = response.json @@ -127,7 +134,10 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): @override_flag("organization_members", active=True) def test_get_portfolio_invitedmember_domains_json_authenticated(self): """Test that portfolio invitedmember's domains are returned properly for an authenticated user.""" - response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "email": self.invited_member_email, "member_only": "true"}) + response = self.app.get( + reverse("get_member_domains_json"), + params={"portfolio": self.portfolio.id, "email": self.invited_member_email, "member_only": "true"}, + ) self.assertEqual(response.status_code, 200) data = response.json @@ -147,7 +157,10 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): @override_flag("organization_members", active=True) def test_get_portfolio_member_domains_json_authenticated_include_all_domains(self): """Test that all portfolio domains are returned properly for an authenticated user.""" - response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "false"}) + response = self.app.get( + reverse("get_member_domains_json"), + params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "false"}, + ) self.assertEqual(response.status_code, 200) data = response.json @@ -167,7 +180,10 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): @override_flag("organization_members", active=True) def test_get_portfolio_invitedmember_domains_json_authenticated_include_all_domains(self): """Test that all portfolio domains are returned properly for an authenticated user.""" - response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "email": self.invited_member_email, "member_only": "false"}) + response = self.app.get( + reverse("get_member_domains_json"), + params={"portfolio": self.portfolio.id, "email": self.invited_member_email, "member_only": "false"}, + ) self.assertEqual(response.status_code, 200) data = response.json @@ -187,7 +203,15 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): @override_flag("organization_members", active=True) def test_get_portfolio_member_domains_json_authenticated_search(self): """Test that search_term yields correct domain.""" - response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "false", "search_term": "example1"}) + response = self.app.get( + reverse("get_member_domains_json"), + params={ + "portfolio": self.portfolio.id, + "member_id": self.user_member.id, + "member_only": "false", + "search_term": "example1", + }, + ) self.assertEqual(response.status_code, 200) data = response.json @@ -207,7 +231,15 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): @override_flag("organization_members", active=True) def test_get_portfolio_invitedmember_domains_json_authenticated_search(self): """Test that search_term yields correct domain.""" - response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "email": self.invited_member_email, "member_only": "false", "search_term": "example1"}) + response = self.app.get( + reverse("get_member_domains_json"), + params={ + "portfolio": self.portfolio.id, + "email": self.invited_member_email, + "member_only": "false", + "search_term": "example1", + }, + ) self.assertEqual(response.status_code, 200) data = response.json @@ -228,7 +260,16 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): def test_get_portfolio_member_domains_json_authenticated_sort(self): """Test that sort returns results in correct order.""" # Test by name in ascending order - response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "false", "sort_by": "name", "order":"asc"}) + response = self.app.get( + reverse("get_member_domains_json"), + params={ + "portfolio": self.portfolio.id, + "member_id": self.user_member.id, + "member_only": "false", + "sort_by": "name", + "order": "asc", + }, + ) self.assertEqual(response.status_code, 200) data = response.json @@ -247,7 +288,16 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): self.assertEqual(data["domains"][0]["name"], "example1.com") # Test by name in descending order - response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "false", "sort_by": "name", "order":"desc"}) + response = self.app.get( + reverse("get_member_domains_json"), + params={ + "portfolio": self.portfolio.id, + "member_id": self.user_member.id, + "member_only": "false", + "sort_by": "name", + "order": "desc", + }, + ) self.assertEqual(response.status_code, 200) data = response.json @@ -271,7 +321,16 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): def test_get_portfolio_invitedmember_domains_json_authenticated_sort(self): """Test that sort returns results in correct order.""" # Test by name in ascending order - response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "email": self.invited_member_email, "member_only": "false", "sort_by": "name", "order":"asc"}) + response = self.app.get( + reverse("get_member_domains_json"), + params={ + "portfolio": self.portfolio.id, + "email": self.invited_member_email, + "member_only": "false", + "sort_by": "name", + "order": "asc", + }, + ) self.assertEqual(response.status_code, 200) data = response.json @@ -290,7 +349,16 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): self.assertEqual(data["domains"][0]["name"], "example1.com") # Test by name in descending order - response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "email": self.invited_member_email, "member_only": "false", "sort_by": "name", "order":"desc"}) + response = self.app.get( + reverse("get_member_domains_json"), + params={ + "portfolio": self.portfolio.id, + "email": self.invited_member_email, + "member_only": "false", + "sort_by": "name", + "order": "desc", + }, + ) self.assertEqual(response.status_code, 200) data = response.json @@ -315,10 +383,14 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): """Test that an restricted user is denied access.""" # set user to a user with no permissions self.app.set_user(self.user_no_perms) - + # Try to access the portfolio members without being authenticated - response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "true"}, expect_errors=True) - + response = self.app.get( + reverse("get_member_domains_json"), + params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "true"}, + expect_errors=True, + ) + # Assert that the response is a 403 self.assertEqual(response.status_code, 403) @@ -329,10 +401,14 @@ class GetPortfolioMemberDomainsJsonTest(TestWithUser, WebTest): """Test that an unauthenticated user is redirected to login.""" # set app to unauthenticated self.app.set_user(None) - + # Try to access the portfolio members without being authenticated - response = self.app.get(reverse("get_member_domains_json"), params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "true"}, expect_errors=True) - + response = self.app.get( + reverse("get_member_domains_json"), + params={"portfolio": self.portfolio.id, "member_id": self.user_member.id, "member_only": "true"}, + expect_errors=True, + ) + # Assert that the response is a redirect to openid login self.assertEqual(response.status_code, 302) self.assertIn("/openid/login", response.location) diff --git a/src/registrar/tests/test_views_members_json.py b/src/registrar/tests/test_views_members_json.py index 1ae4aadf5..47dbb79d0 100644 --- a/src/registrar/tests/test_views_members_json.py +++ b/src/registrar/tests/test_views_members_json.py @@ -132,10 +132,12 @@ class GetPortfolioMembersJsonTest(TestWithUser, WebTest): """Test that an unauthenticated user is redirected or denied access.""" # Log out the user by setting the user to None self.app.set_user(None) - + # Try to access the portfolio members without being authenticated - response = self.app.get(reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id}, expect_errors=True) - + response = self.app.get( + reverse("get_portfolio_members_json"), params={"portfolio": self.portfolio.id}, expect_errors=True + ) + # Assert that the response is a redirect to the login page self.assertEqual(response.status_code, 302) # Redirect to openid login self.assertIn("/openid/login", response.location) diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 3ac4ac3dd..2213d6339 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1437,7 +1437,7 @@ class TestPortfolioMemberDomainsView(TestWithUser, WebTest): UserPortfolioPermissionChoices.EDIT_MEMBERS, ], ) - + @classmethod def tearDownClass(cls): UserPortfolioPermission.objects.all().delete() @@ -1454,7 +1454,6 @@ class TestPortfolioMemberDomainsView(TestWithUser, WebTest): response = self.client.get(reverse("member-domains", kwargs={"pk": self.permission.id})) - # Make sure the page loaded, and that we're on the right page self.assertEqual(response.status_code, 200) self.assertContains(response, self.user_member.email) @@ -1493,10 +1492,9 @@ class TestPortfolioMemberDomainsView(TestWithUser, WebTest): response = self.client.get(reverse("member-domains", kwargs={"pk": "0"})) - # Make sure the response is not found self.assertEqual(response.status_code, 404) - + class TestPortfolioInvitedMemberDomainsView(TestWithUser, WebTest): @classmethod @@ -1536,7 +1534,7 @@ class TestPortfolioInvitedMemberDomainsView(TestWithUser, WebTest): UserPortfolioPermissionChoices.EDIT_MEMBERS, ], ) - + @classmethod def tearDownClass(cls): PortfolioInvitation.objects.all().delete() @@ -1554,7 +1552,6 @@ class TestPortfolioInvitedMemberDomainsView(TestWithUser, WebTest): response = self.client.get(reverse("invitedmember-domains", kwargs={"pk": self.invitation.id})) - # Make sure the page loaded, and that we're on the right page self.assertEqual(response.status_code, 200) self.assertContains(response, self.invited_member_email) @@ -1593,7 +1590,5 @@ class TestPortfolioInvitedMemberDomainsView(TestWithUser, WebTest): response = self.client.get(reverse("invitedmember-domains", kwargs={"pk": "0"})) - # Make sure the response is not found self.assertEqual(response.status_code, 404) - \ No newline at end of file diff --git a/src/registrar/views/member_domains_json.py b/src/registrar/views/member_domains_json.py index 7dcec6bef..f37afdff0 100644 --- a/src/registrar/views/member_domains_json.py +++ b/src/registrar/views/member_domains_json.py @@ -4,7 +4,6 @@ from django.core.paginator import Paginator from django.shortcuts import get_object_or_404 from django.views import View from registrar.models import UserDomainRole, Domain, DomainInformation, User -from django.contrib.auth.decorators import login_required from django.urls import reverse from django.db.models import Q @@ -47,7 +46,6 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View): } ) - def get_domain_ids_from_request(self, request): """Get domain ids from request. @@ -64,27 +62,29 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View): if member_only: if member_id: member = get_object_or_404(User, pk=member_id) - domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True) + domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list( + "domain_id", flat=True + ) user_domain_roles = UserDomainRole.objects.filter(user=member).values_list("domain_id", flat=True) return domain_info_ids.intersection(user_domain_roles) elif email: - domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True) + domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list( + "domain_id", flat=True + ) domain_invitations = DomainInvitation.objects.filter(email=email).values_list("domain_id", flat=True) return domain_info_ids.intersection(domain_invitations) else: domain_infos = DomainInformation.objects.filter(portfolio=portfolio) return domain_infos.values_list("domain_id", flat=True) - logger.warning("Invalid search criteria, returning empty results list") + logger.warning("Invalid search criteria, returning empty results list") return [] - def apply_search(self, queryset, request): search_term = request.GET.get("search_term") if search_term: queryset = queryset.filter(Q(name__icontains=search_term)) return queryset - def apply_sorting(self, queryset, request): sort_by = request.GET.get("sort_by", "name") order = request.GET.get("order", "asc") @@ -92,7 +92,6 @@ class PortfolioMemberDomainsJson(PortfolioMemberDomainsPermission, View): sort_by = f"-{sort_by}" return queryset.order_by(sort_by) - def serialize_domain(self, domain, user): suborganization_name = None try: diff --git a/src/registrar/views/portfolio_members_json.py b/src/registrar/views/portfolio_members_json.py index 94ca9ac69..f7df4f30a 100644 --- a/src/registrar/views/portfolio_members_json.py +++ b/src/registrar/views/portfolio_members_json.py @@ -1,6 +1,5 @@ from django.http import JsonResponse from django.core.paginator import Paginator -from django.contrib.auth.decorators import login_required from django.db.models import Value, F, CharField, TextField, Q, Case, When from django.db.models.functions import Concat, Coalesce from django.urls import reverse @@ -14,7 +13,7 @@ from registrar.views.utility.mixins import PortfolioMembersPermission class PortfolioMembersJson(PortfolioMembersPermission, View): - + def get(self, request): """Fetch members (permissions and invitations) for the given portfolio.""" @@ -52,7 +51,6 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): } ) - def initial_permissions_search(self, portfolio): """Perform initial search for permissions before applying any filters.""" permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio) @@ -96,7 +94,6 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): ) return permissions - def initial_invitations_search(self, portfolio): """Perform initial invitations search before applying any filters.""" invitations = PortfolioInvitation.objects.filter(portfolio=portfolio) @@ -121,7 +118,6 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): ) return invitations - def apply_search_term(self, queryset, request): """Apply search term to the queryset.""" search_term = request.GET.get("search_term", "").lower() @@ -133,7 +129,6 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): ) return queryset - def apply_sorting(self, queryset, request): """Apply sorting to the queryset.""" sort_by = request.GET.get("sort_by", "id") # Default to 'id' @@ -147,7 +142,6 @@ class PortfolioMembersJson(PortfolioMembersPermission, View): queryset = queryset.order_by(sort_by) return queryset - def serialize_members(self, request, portfolio, item, user): # Check if the user can edit other users user_can_edit_other_users = any( diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index b587ea9c9..6fb976d5c 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -136,7 +136,8 @@ class PortfolioMemberEditView(PortfolioMemberEditPermissionView, View): "member": user, # Pass the user object again to the template }, ) - + + class PortfolioMemberDomainsView(PortfolioMemberDomainsPermissionView, View): template_name = "portfolio_member_domains.html" @@ -155,7 +156,6 @@ class PortfolioMemberDomainsView(PortfolioMemberDomainsPermissionView, View): ) - class PortfolioInvitedMemberView(PortfolioMemberPermissionView, View): template_name = "portfolio_member.html" @@ -227,7 +227,8 @@ class PortfolioInvitedMemberEditView(PortfolioMemberEditPermissionView, View): "invitation": portfolio_invitation, # Pass the user object again to the template }, ) - + + class PortfolioInvitedMemberDomainsView(PortfolioMemberDomainsPermissionView, View): template_name = "portfolio_member_domains.html" diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index 5c05a3a20..1b6db24de 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -278,6 +278,7 @@ class PortfolioMemberEditPermissionView(PortfolioMemberEditPermission, Portfolio `template_name`. """ + class PortfolioMemberDomainsPermissionView(PortfolioMemberDomainsPermission, PortfolioBasePermissionView, abc.ABC): """Abstract base view for portfolio member domains views that enforces permissions. From 0952c9d216a8a9f356332a879a5f6a85b115964a Mon Sep 17 00:00:00 2001 From: asaki222 Date: Wed, 23 Oct 2024 23:01:27 -0400 Subject: [PATCH 08/24] made text changes --- src/registrar/templates/domain_request_dotgov_domain.html | 7 +------ .../portfolio_domain_request_additional_details.html | 3 ++- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/registrar/templates/domain_request_dotgov_domain.html b/src/registrar/templates/domain_request_dotgov_domain.html index 764154254..1456a61c2 100644 --- a/src/registrar/templates/domain_request_dotgov_domain.html +++ b/src/registrar/templates/domain_request_dotgov_domain.html @@ -6,7 +6,7 @@
  • Be available
  • Relate to your organization’s name, location, and/or services
  • -
  • Be unlikely to mislead or confuse the general public (even if your domain is only intended for a specific audience)
  • +
  • Be clear to the general public. Your domain name must not be easily confused with other organizations.

@@ -19,11 +19,6 @@

Note that only federal agencies can request generic terms like vote.gov.

- -

Domain examples for your type of organization

-
- {% include "includes/domain_example.html" %} -
{% endblock %} diff --git a/src/registrar/templates/portfolio_domain_request_additional_details.html b/src/registrar/templates/portfolio_domain_request_additional_details.html index 3b004354f..4ea830f0b 100644 --- a/src/registrar/templates/portfolio_domain_request_additional_details.html +++ b/src/registrar/templates/portfolio_domain_request_additional_details.html @@ -9,12 +9,13 @@
+

Required fields are marked with an asterisk (*).

Is there anything else you’d like us to know about your domain request?

-

Provide details below. *

+

Provide details below. *

{% with attr_maxlength=2000 add_label_class="usa-sr-only" %} {% input_with_errors forms.0.anything_else %} {% endwith %} From 337e6e5705c203887a7ef9b04484160d8493d3cc Mon Sep 17 00:00:00 2001 From: asaki222 Date: Thu, 24 Oct 2024 13:39:33 -0400 Subject: [PATCH 09/24] updated with org model flag --- .../templates/domain_request_dotgov_domain.html | 10 ++++++++++ .../portfolio_domain_request_additional_details.html | 11 ++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/domain_request_dotgov_domain.html b/src/registrar/templates/domain_request_dotgov_domain.html index 1456a61c2..b81a59662 100644 --- a/src/registrar/templates/domain_request_dotgov_domain.html +++ b/src/registrar/templates/domain_request_dotgov_domain.html @@ -6,7 +6,11 @@
  • Be available
  • Relate to your organization’s name, location, and/or services
  • + {% if has_organization_feature_flag %}
  • Be clear to the general public. Your domain name must not be easily confused with other organizations.
  • + {% else %} +
  • Be unlikely to mislead or confuse the general public (even if your domain is only intended for a specific audience)
  • + {% end %}

@@ -19,6 +23,12 @@

Note that only federal agencies can request generic terms like vote.gov.

+ {% if not has_organization_feature_flag%} +

Domain examples for your type of organization

+
+ {% include "includes/domain_example.html" %} +
+ {% endif %} {% endblock %} diff --git a/src/registrar/templates/portfolio_domain_request_additional_details.html b/src/registrar/templates/portfolio_domain_request_additional_details.html index 4ea830f0b..f04fedef2 100644 --- a/src/registrar/templates/portfolio_domain_request_additional_details.html +++ b/src/registrar/templates/portfolio_domain_request_additional_details.html @@ -7,15 +7,20 @@ {% block form_fields %} -
- -

Required fields are marked with an asterisk (*).

+
+ {% if has_organization_feature_flag %} +

Required fields are marked with an asterisk (*).

+ {% endif %}

Is there anything else you’d like us to know about your domain request?

+ {% if has_organization_feature_flag %}

Provide details below. *

+ {% else %} +

Provide details below. *

+ {% end %} {% with attr_maxlength=2000 add_label_class="usa-sr-only" %} {% input_with_errors forms.0.anything_else %} {% endwith %} From dc666e4699c68e628e12083c3cd941098f304ca3 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Thu, 24 Oct 2024 14:34:44 -0400 Subject: [PATCH 10/24] fixed issues --- src/registrar/templates/domain_request_dotgov_domain.html | 2 +- .../portfolio_domain_request_additional_details.html | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/registrar/templates/domain_request_dotgov_domain.html b/src/registrar/templates/domain_request_dotgov_domain.html index b81a59662..9765edb11 100644 --- a/src/registrar/templates/domain_request_dotgov_domain.html +++ b/src/registrar/templates/domain_request_dotgov_domain.html @@ -10,7 +10,7 @@
  • Be clear to the general public. Your domain name must not be easily confused with other organizations.
  • {% else %}
  • Be unlikely to mislead or confuse the general public (even if your domain is only intended for a specific audience)
  • - {% end %} + {% endif %}

    diff --git a/src/registrar/templates/portfolio_domain_request_additional_details.html b/src/registrar/templates/portfolio_domain_request_additional_details.html index f04fedef2..49097fbe9 100644 --- a/src/registrar/templates/portfolio_domain_request_additional_details.html +++ b/src/registrar/templates/portfolio_domain_request_additional_details.html @@ -16,11 +16,7 @@
    - {% if has_organization_feature_flag %}

    Provide details below. *

    - {% else %} -

    Provide details below. *

    - {% end %} {% with attr_maxlength=2000 add_label_class="usa-sr-only" %} {% input_with_errors forms.0.anything_else %} {% endwith %} From 5c9f0bdc23b3f318679117330a591e9dd1e91fb4 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Thu, 24 Oct 2024 14:47:37 -0400 Subject: [PATCH 11/24] fixed issues --- .../portfolio_domain_request_additional_details.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/registrar/templates/portfolio_domain_request_additional_details.html b/src/registrar/templates/portfolio_domain_request_additional_details.html index 49097fbe9..3c5b50d6b 100644 --- a/src/registrar/templates/portfolio_domain_request_additional_details.html +++ b/src/registrar/templates/portfolio_domain_request_additional_details.html @@ -7,10 +7,7 @@ {% block form_fields %} -
    - {% if has_organization_feature_flag %} -

    Required fields are marked with an asterisk (*).

    - {% endif %} +

    Is there anything else you’d like us to know about your domain request?

    From 7edd0448282dde5e08bc331814216e44a5b10768 Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Fri, 25 Oct 2024 11:01:37 -0500 Subject: [PATCH 12/24] revert --- .github/workflows/clone-staging.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clone-staging.yaml b/.github/workflows/clone-staging.yaml index ebb0a1a82..4fbb2ccb7 100644 --- a/.github/workflows/clone-staging.yaml +++ b/.github/workflows/clone-staging.yaml @@ -20,7 +20,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - - name: Install Cloud Foundry CLI + - name: Clone uses: cloud-gov/cg-cli-tools@main - name: Clone From 027870e60a0110c9dcbcace07e207c9dce196605 Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Fri, 25 Oct 2024 15:16:17 -0500 Subject: [PATCH 13/24] refactor clone job --- .github/workflows/clone-staging.yaml | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/workflows/clone-staging.yaml b/.github/workflows/clone-staging.yaml index 4fbb2ccb7..c82d1fa70 100644 --- a/.github/workflows/clone-staging.yaml +++ b/.github/workflows/clone-staging.yaml @@ -12,41 +12,41 @@ on: env: DESTINATION_ENVIRONMENT: ms SOURCE_ENVIRONMENT: staging + jobs: clone-database: runs-on: ubuntu-latest + env: + CF_USERNAME: CF_${{ env.SOURCE_ENVIRONMENT }}_USERNAME + CF_PASSWORD: CF_${{ env.SOURCE_ENVIRONMENT }}_PASSWORD steps: - name: Checkout repository uses: actions/checkout@v3 - - name: Clone + - name: Share DB Service uses: cloud-gov/cg-cli-tools@main + with: + cf_username: ${{ secrets[env.CF_USERNAME] }} + cf_password: ${{ secrets[env.CF_PASSWORD] }} + cf_org: cisa-dotgov + cf_space: ${{ env.DESTINATION_ENVIRONMENT }} + cf_command: share-service getgov-${{ env.DESTINATION_ENVIRONMENT }}-database -s ${{ env.SOURCE_ENVIRONMENT }} - - name: Clone - env: - CF_USERNAME: CF_${{ env.DESTINATION_ENVIRONMENT }}_USERNAME - CF_PASSWORD: CF_${{ env.DESTINATION_ENVIRONMENT }}_PASSWORD - run: | - # login to cf cli - cf login -a api.fr.cloud.gov -u $CF_USERNAME -p $CF_PASSWORD -o cisa-dotgov -s ${{ env.DESTINATION_ENVIRONMENT }} - - # install cg-manage-rds tool - pip install git+https://github.com/cloud-gov/cg-manage-rds.git - - # share the sandbox db with the Staging space - cf share-service getgov-${{ env.DESTINATION_ENVIRONMENT }}-database -s ${{ env.SOURCE_ENVIRONMENT }} - - # target the Staging space - cf target -s ${{ env.SOURCE_ENVIRONMENT }} - - # clone from staging to the sandbox - cg-manage-rds clone getgov-${{ env.SOURCE_ENVIRONMENT }}-database getgov-${{ env.DESTINATION_ENVIRONMENT }}-database - - rm db_backup.sql - - # switch to the target sandbox space - cf target -s ${{ env.DESTINATION_ENVIRONMENT }} - - # un-share the sandbox from Staging - cf unshare-service getgov-${{ env.DESTINATION_ENVIRONMENT }}-database -s ${{ env.SOURCE_ENVIRONMENT }} + - name: Clone Database + uses: cloud-gov/cg-cli-tools@main + with: + cf_username: ${{ secrets.CF_MS_USERNAM }} + cf_password: ${{ secrets.CF_MS_PASSWORD }} + cf_org: cisa-dotgov + cf_space: ${{ env.SOURCE_ENVIRONMENT }} + command: cg-manage-rds clone getgov-${{ env.SOURCE_ENVIRONMENT }}-database getgov-${{ env.DESTINATION_ENVIRONMENT }}-database + + - name: Unshare DB Service + uses: cloud-gov/cg-cli-tools@main + with: + cf_username: ${{ secrets.CF_MS_USERNAM }} + cf_password: ${{ secrets.CF_MS_PASSWORD }} + cf_org: cisa-dotgov + cf_space: ${{ env.SOURCE_ENVIRONMENT }} + cf_command: unshare-service getgov-${{ env.DESTINATION_ENVIRONMENT }}-database -s ${{ env.SOURCE_ENVIRONMENT }} \ No newline at end of file From 5727c15b3a2ee0e1e27eef252b84804933429935 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 25 Oct 2024 16:23:30 -0400 Subject: [PATCH 14/24] fix search label, fix all domains link --- src/registrar/assets/js/get-gov.js | 6 +++--- src/registrar/templates/includes/member_domains_table.html | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 8d9335630..27fcbd604 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1972,7 +1972,7 @@ class MembersTable extends LoadTableBase { * @param {Array} domain_urls - An array of corresponding domain URLs. * @returns {string} - A string of HTML displaying the domains assigned to the member. */ - generateDomainsHTML(num_domains, domain_names, domain_urls) { + generateDomainsHTML(num_domains, domain_names, domain_urls, member_id) { // Initialize an empty string for the HTML let domainsHTML = ''; @@ -1992,7 +1992,7 @@ class MembersTable extends LoadTableBase { // If there are more than 6 domains, display a "View assigned domains" link if (num_domains >= 6) { - domainsHTML += "

    View assigned domains

    "; + domainsHTML += `

    View assigned domains

    `; } domainsHTML += "
    "; @@ -2143,7 +2143,7 @@ class MembersTable extends LoadTableBase { admin_tagHTML = `Admin` // generate html blocks for domains and permissions for the member - let domainsHTML = this.generateDomainsHTML(num_domains, domain_names, domain_urls); + let domainsHTML = this.generateDomainsHTML(num_domains, domain_names, domain_urls, member_id); let permissionsHTML = this.generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices); // domainsHTML block and permissionsHTML block need to be wrapped with hide/show toggle, Expand diff --git a/src/registrar/templates/includes/member_domains_table.html b/src/registrar/templates/includes/member_domains_table.html index ca8ade7a7..77d9b9891 100644 --- a/src/registrar/templates/includes/member_domains_table.html +++ b/src/registrar/templates/includes/member_domains_table.html @@ -44,7 +44,12 @@ {% if has_edit_members_portfolio_permission %} Search all domains {% else %} - Search domains assigned to ovietta.evans@gsa.gov + Search domains assigned to + {% if member %} + {{ member.email }} + {% else %} + {{ portfolio_invitation.email }} + {% endif %} {% endif %}
    From 599dcb59ae8b30b335cc542c880e76eafcbd27b2 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 25 Oct 2024 16:44:00 -0400 Subject: [PATCH 15/24] fixtures error handling update --- src/registrar/fixtures/fixtures_domains.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/registrar/fixtures/fixtures_domains.py b/src/registrar/fixtures/fixtures_domains.py index 98f13cd43..77fb62dad 100644 --- a/src/registrar/fixtures/fixtures_domains.py +++ b/src/registrar/fixtures/fixtures_domains.py @@ -89,12 +89,18 @@ class DomainFixture(DomainRequestFixture): # Approve the current domain request if domain_request: - cls._approve_request(domain_request, users) + try: + cls._approve_request(domain_request, users) + except Exception as err: + logger.warning(f'Cannot approve domain request in fixtures: {err}') domain_requests_to_update.append(domain_request) # Approve the expired domain request if domain_request_expired: - cls._approve_request(domain_request_expired, users) + try: + cls._approve_request(domain_request_expired, users) + except Exception as err: + logger.warning(f'Cannot approve domain request (expired) in fixtures: {err}') domain_requests_to_update.append(domain_request_expired) expired_requests.append(domain_request_expired) From f35f149542c249c1c1afc1187fd347e9e535b3c5 Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Fri, 25 Oct 2024 16:08:48 -0500 Subject: [PATCH 16/24] remove domains in dns_needed state from current csv reports --- src/registrar/utility/csv_export.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index ce710ef53..2e5ee4d91 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -746,7 +746,6 @@ class DomainDataFull(DomainExport): return Q( domain__state__in=[ Domain.State.READY, - Domain.State.DNS_NEEDED, Domain.State.ON_HOLD, ], ) @@ -842,7 +841,6 @@ class DomainDataFederal(DomainExport): organization_type__icontains="federal", domain__state__in=[ Domain.State.READY, - Domain.State.DNS_NEEDED, Domain.State.ON_HOLD, ], ) From 0d89fd6edb03243f6bee65194a0da70a3f59a5ea Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 25 Oct 2024 17:36:39 -0400 Subject: [PATCH 17/24] lint --- src/registrar/assets/js/get-gov.js | 2 +- src/registrar/fixtures/fixtures_domains.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 27fcbd604..fd19b6c30 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1992,7 +1992,7 @@ class MembersTable extends LoadTableBase { // If there are more than 6 domains, display a "View assigned domains" link if (num_domains >= 6) { - domainsHTML += `

    View assigned domains

    `; + domainsHTML += `

    View assigned domains

    `; } domainsHTML += "
    "; diff --git a/src/registrar/fixtures/fixtures_domains.py b/src/registrar/fixtures/fixtures_domains.py index 77fb62dad..4606024d0 100644 --- a/src/registrar/fixtures/fixtures_domains.py +++ b/src/registrar/fixtures/fixtures_domains.py @@ -92,7 +92,7 @@ class DomainFixture(DomainRequestFixture): try: cls._approve_request(domain_request, users) except Exception as err: - logger.warning(f'Cannot approve domain request in fixtures: {err}') + logger.warning(f"Cannot approve domain request in fixtures: {err}") domain_requests_to_update.append(domain_request) # Approve the expired domain request @@ -100,7 +100,7 @@ class DomainFixture(DomainRequestFixture): try: cls._approve_request(domain_request_expired, users) except Exception as err: - logger.warning(f'Cannot approve domain request (expired) in fixtures: {err}') + logger.warning(f"Cannot approve domain request (expired) in fixtures: {err}") domain_requests_to_update.append(domain_request_expired) expired_requests.append(domain_request_expired) From 6032123e5a36d2ae4abe08343709c3270a955fe9 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 25 Oct 2024 17:45:20 -0400 Subject: [PATCH 18/24] fix passed id to generateDomainsHTML --- src/registrar/assets/js/get-gov.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index fd19b6c30..3ab86b55b 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1972,7 +1972,7 @@ class MembersTable extends LoadTableBase { * @param {Array} domain_urls - An array of corresponding domain URLs. * @returns {string} - A string of HTML displaying the domains assigned to the member. */ - generateDomainsHTML(num_domains, domain_names, domain_urls, member_id) { + generateDomainsHTML(num_domains, domain_names, domain_urls, id) { // Initialize an empty string for the HTML let domainsHTML = ''; @@ -1992,7 +1992,7 @@ class MembersTable extends LoadTableBase { // If there are more than 6 domains, display a "View assigned domains" link if (num_domains >= 6) { - domainsHTML += `

    View assigned domains

    `; + domainsHTML += `

    View assigned domains

    `; } domainsHTML += "
    "; @@ -2143,7 +2143,7 @@ class MembersTable extends LoadTableBase { admin_tagHTML = `Admin` // generate html blocks for domains and permissions for the member - let domainsHTML = this.generateDomainsHTML(num_domains, domain_names, domain_urls, member_id); + let domainsHTML = this.generateDomainsHTML(num_domains, domain_names, domain_urls, member.id); let permissionsHTML = this.generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices); // domainsHTML block and permissionsHTML block need to be wrapped with hide/show toggle, Expand From 337c10908689798cfca45df3727414841223fafa Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 25 Oct 2024 17:51:37 -0400 Subject: [PATCH 19/24] revise to use action_url --- src/registrar/assets/js/get-gov.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 3ab86b55b..fac25d2b0 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1972,7 +1972,7 @@ class MembersTable extends LoadTableBase { * @param {Array} domain_urls - An array of corresponding domain URLs. * @returns {string} - A string of HTML displaying the domains assigned to the member. */ - generateDomainsHTML(num_domains, domain_names, domain_urls, id) { + generateDomainsHTML(num_domains, domain_names, domain_urls, action_url) { // Initialize an empty string for the HTML let domainsHTML = ''; @@ -1992,7 +1992,7 @@ class MembersTable extends LoadTableBase { // If there are more than 6 domains, display a "View assigned domains" link if (num_domains >= 6) { - domainsHTML += `

    View assigned domains

    `; + domainsHTML += `

    View assigned domains

    `; } domainsHTML += ""; @@ -2143,7 +2143,7 @@ class MembersTable extends LoadTableBase { admin_tagHTML = `Admin` // generate html blocks for domains and permissions for the member - let domainsHTML = this.generateDomainsHTML(num_domains, domain_names, domain_urls, member.id); + let domainsHTML = this.generateDomainsHTML(num_domains, domain_names, domain_urls, action_url); let permissionsHTML = this.generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices); // domainsHTML block and permissionsHTML block need to be wrapped with hide/show toggle, Expand From 8a24194a545a38e6c9d232a3dd0920bd4cd1e684 Mon Sep 17 00:00:00 2001 From: asaki222 Date: Mon, 28 Oct 2024 11:13:37 -0400 Subject: [PATCH 20/24] adjusted to portfolio check instead of organization feature flag --- src/registrar/templates/domain_request_dotgov_domain.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/domain_request_dotgov_domain.html b/src/registrar/templates/domain_request_dotgov_domain.html index 9765edb11..18e04f305 100644 --- a/src/registrar/templates/domain_request_dotgov_domain.html +++ b/src/registrar/templates/domain_request_dotgov_domain.html @@ -6,7 +6,7 @@
    • Be available
    • Relate to your organization’s name, location, and/or services
    • - {% if has_organization_feature_flag %} + {% if portfolio %}
    • Be clear to the general public. Your domain name must not be easily confused with other organizations.
    • {% else %}
    • Be unlikely to mislead or confuse the general public (even if your domain is only intended for a specific audience)
    • @@ -23,7 +23,7 @@

      Note that only federal agencies can request generic terms like vote.gov.

      - {% if not has_organization_feature_flag%} + {% if not portfolio %}

      Domain examples for your type of organization

      {% include "includes/domain_example.html" %} From d23c5d15df90fabffc9dc2340c3a0d363775a77d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:42:46 -0600 Subject: [PATCH 21/24] update fields --- ...135_alter_federalagency_agency_and_more.py | 30 +++++++++++++++++++ src/registrar/models/federal_agency.py | 6 ++-- 2 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 src/registrar/migrations/0135_alter_federalagency_agency_and_more.py diff --git a/src/registrar/migrations/0135_alter_federalagency_agency_and_more.py b/src/registrar/migrations/0135_alter_federalagency_agency_and_more.py new file mode 100644 index 000000000..bcf8398dc --- /dev/null +++ b/src/registrar/migrations/0135_alter_federalagency_agency_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.10 on 2024-10-28 16:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "registrar", + "0134_rename_portfolio_additional_permissions_portfolioinvitation_additional_permissions_and_more", + ), + ] + + operations = [ + migrations.AlterField( + model_name="federalagency", + name="agency", + field=models.CharField(null=True, verbose_name="Federal agency"), + ), + migrations.AlterField( + model_name="federalagency", + name="federal_type", + field=models.CharField( + choices=[("executive", "Executive"), ("judicial", "Judicial"), ("legislative", "Legislative")], + max_length=20, + null=True, + ), + ), + ] diff --git a/src/registrar/models/federal_agency.py b/src/registrar/models/federal_agency.py index aeeebac8c..19502e861 100644 --- a/src/registrar/models/federal_agency.py +++ b/src/registrar/models/federal_agency.py @@ -13,15 +13,15 @@ class FederalAgency(TimeStampedModel): agency = models.CharField( null=True, - blank=True, - help_text="Federal agency", + blank=False, + verbose_name="Federal agency", ) federal_type = models.CharField( max_length=20, choices=BranchChoices.choices, null=True, - blank=True, + blank=False, ) acronym = models.CharField( From f0e7ad4a454f543af2a181ca8c13ced4622ba9ef Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Mon, 28 Oct 2024 12:01:08 -0500 Subject: [PATCH 22/24] fix tests --- src/registrar/tests/test_reports.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index eebb11422..b7f1653d3 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -89,7 +89,6 @@ class CsvReportsTest(MockDbForSharedTests): call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\r\n"), call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"), call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,(blank)\r\n"), - call("adomain2.gov,Interstate,,,,,(blank)\r\n"), call("zdomain12.gov,Interstate,,,,,(blank)\r\n"), ] # We don't actually want to write anything for a test case, @@ -470,8 +469,6 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib): # Invoke setter self.domain_1.security_contact # Invoke setter - self.domain_2.security_contact - # Invoke setter self.domain_3.security_contact # Add a first ready date on the first domain. Leaving the others blank. self.domain_1.first_ready = get_default_start_date() @@ -492,7 +489,6 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib): "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,(blank)\n" "adomain10.gov,Federal,Armed Forces Retirement Home,,,,(blank)\n" "ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n" - "adomain2.gov,Interstate,,,,,(blank)\n" "zdomain12.gov,Interstate,,,,,(blank)\n" ) # Normalize line endings and remove commas, From 18ca5f70ae2f2952583b6aaa41eb63ccef3ecf3e Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Tue, 29 Oct 2024 09:57:11 -0500 Subject: [PATCH 23/24] hardcode env vars --- .github/workflows/clone-staging.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/clone-staging.yaml b/.github/workflows/clone-staging.yaml index c82d1fa70..704d80b36 100644 --- a/.github/workflows/clone-staging.yaml +++ b/.github/workflows/clone-staging.yaml @@ -18,8 +18,8 @@ jobs: clone-database: runs-on: ubuntu-latest env: - CF_USERNAME: CF_${{ env.SOURCE_ENVIRONMENT }}_USERNAME - CF_PASSWORD: CF_${{ env.SOURCE_ENVIRONMENT }}_PASSWORD + CF_USERNAME: CF_MS_USERNAME + CF_PASSWORD: CF_STAGING_PASSWORD steps: - name: Checkout repository uses: actions/checkout@v3 From a1567cf235afc54ef4b1888d3faa5063f9a7e8be Mon Sep 17 00:00:00 2001 From: matthewswspence Date: Tue, 29 Oct 2024 10:16:39 -0500 Subject: [PATCH 24/24] fix minor bug in clone workflow --- .github/workflows/clone-staging.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clone-staging.yaml b/.github/workflows/clone-staging.yaml index 704d80b36..2a6c33410 100644 --- a/.github/workflows/clone-staging.yaml +++ b/.github/workflows/clone-staging.yaml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest env: CF_USERNAME: CF_MS_USERNAME - CF_PASSWORD: CF_STAGING_PASSWORD + CF_PASSWORD: CF_MS_PASSWORD steps: - name: Checkout repository uses: actions/checkout@v3