diff --git a/src/epplibwrapper/cert.py b/src/epplibwrapper/cert.py index 15ff16c06..589736a04 100644 --- a/src/epplibwrapper/cert.py +++ b/src/epplibwrapper/cert.py @@ -1,7 +1,7 @@ import os import tempfile -from django.conf import settings +from django.conf import settings # type: ignore class Cert: @@ -12,7 +12,7 @@ class Cert: variable but Python's ssl library requires a file. """ - def __init__(self, data=settings.SECRET_REGISTRY_CERT) -> None: + def __init__(self, data=settings.SECRET_REGISTRY_CERT) -> None: # type: ignore self.filename = self._write(data) def __del__(self): @@ -31,4 +31,4 @@ class Key(Cert): """Location of private key as written to disk.""" def __init__(self) -> None: - super().__init__(data=settings.SECRET_REGISTRY_KEY) + super().__init__(data=settings.SECRET_REGISTRY_KEY) # type: ignore diff --git a/src/registrar/admin.py b/src/registrar/admin.py index e3bd5c9f7..fb830378c 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -963,7 +963,9 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin): domain_ids = user_domain_roles.values_list("domain_id", flat=True) domains = Domain.objects.filter(id__in=domain_ids).exclude(state=Domain.State.DELETED) - extra_context = {"domain_requests": domain_requests, "domains": domains} + portfolio_ids = obj.get_portfolios().values_list("portfolio", flat=True) + portfolios = models.Portfolio.objects.filter(id__in=portfolio_ids) + extra_context = {"domain_requests": domain_requests, "domains": domains, "portfolios": portfolios} return super().change_view(request, object_id, form_url, extra_context) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 7965424bc..96740a15c 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -23,6 +23,9 @@ from cfenv import AppEnv # type: ignore from pathlib import Path from typing import Final from botocore.config import Config +import json +import logging +from django.utils.log import ServerFormatter # # # ### # Setup code goes here # @@ -57,7 +60,7 @@ env_db_url = env.dj_db_url("DATABASE_URL") env_debug = env.bool("DJANGO_DEBUG", default=False) env_is_production = env.bool("IS_PRODUCTION", default=False) env_log_level = env.str("DJANGO_LOG_LEVEL", "DEBUG") -env_base_url = env.str("DJANGO_BASE_URL") +env_base_url: str = env.str("DJANGO_BASE_URL") env_getgov_public_site_url = env.str("GETGOV_PUBLIC_SITE_URL", "") env_oidc_active_provider = env.str("OIDC_ACTIVE_PROVIDER", "identity sandbox") @@ -192,7 +195,7 @@ MIDDLEWARE = [ "registrar.registrar_middleware.CheckPortfolioMiddleware", ] -# application object used by Django’s built-in servers (e.g. `runserver`) +# application object used by Django's built-in servers (e.g. `runserver`) WSGI_APPLICATION = "registrar.config.wsgi.application" # endregion @@ -415,7 +418,7 @@ LANGUAGE_COOKIE_SECURE = True # and to interpret datetimes entered in forms TIME_ZONE = "UTC" -# enable Django’s translation system +# enable Django's translation system USE_I18N = True # enable localized formatting of numbers and dates @@ -450,6 +453,40 @@ PHONENUMBER_DEFAULT_REGION = "US" # logger.error("Can't do this important task. Something is very wrong.") # logger.critical("Going to crash now.") + +class JsonFormatter(logging.Formatter): + """Formats logs into JSON for better parsing""" + + def __init__(self): + super().__init__(datefmt="%d/%b/%Y %H:%M:%S") + + def format(self, record): + log_record = { + "timestamp": self.formatTime(record, self.datefmt), + "level": record.levelname, + "name": record.name, + "lineno": record.lineno, + "message": record.getMessage(), + } + return json.dumps(log_record) + + +class JsonServerFormatter(ServerFormatter): + """Formats server logs into JSON for better parsing""" + + def format(self, record): + formatted_record = super().format(record) + log_entry = {"server_time": record.server_time, "level": record.levelname, "message": formatted_record} + return json.dumps(log_entry) + + +# default to json formatted logs +server_formatter, console_formatter = "json.server", "json" + +# don't use json format locally, it makes logs hard to read in console +if "localhost" in env_base_url: + server_formatter, console_formatter = "django.server", "verbose" + LOGGING = { "version": 1, # Don't import Django's existing loggers @@ -469,6 +506,12 @@ LOGGING = { "format": "[{server_time}] {message}", "style": "{", }, + "json.server": { + "()": JsonServerFormatter, + }, + "json": { + "()": JsonFormatter, + }, }, # define where log messages will be sent; # each logger can have one or more handlers @@ -476,12 +519,12 @@ LOGGING = { "console": { "level": env_log_level, "class": "logging.StreamHandler", - "formatter": "verbose", + "formatter": console_formatter, }, "django.server": { "level": "INFO", "class": "logging.StreamHandler", - "formatter": "django.server", + "formatter": server_formatter, }, # No file logger is configured, # because containerized apps diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 8f754937c..929a63525 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -306,6 +306,9 @@ class User(AbstractUser): return roles + def get_portfolios(self): + return self.portfolio_permissions.all() + @classmethod def needs_identity_verification(cls, email, uuid): """A method used by our oidc classes to test whether a user needs email/uuid verification diff --git a/src/registrar/templates/django/admin/user_change_form.html b/src/registrar/templates/django/admin/user_change_form.html index c0ddd8caf..736f12ba4 100644 --- a/src/registrar/templates/django/admin/user_change_form.html +++ b/src/registrar/templates/django/admin/user_change_form.html @@ -17,6 +17,26 @@ {% endblock %} {% block after_related_objects %} + {% if portfolios %} +
+

Portfolio information

+
+
+

Portfolios

+ +
+
+
+ {% endif %} +

Associated requests and domains

diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 25d7e5fd2..83114b3b3 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -2,6 +2,7 @@ from datetime import datetime from django.utils import timezone from django.test import TestCase, RequestFactory, Client from django.contrib.admin.sites import AdminSite +from django_webtest import WebTest # type: ignore from api.tests.common import less_console_noise_decorator from django.urls import reverse from registrar.admin import ( @@ -41,13 +42,12 @@ from registrar.models import ( TransitionDomain, Portfolio, Suborganization, + UserPortfolioPermission, + UserDomainRole, + SeniorOfficial, + PortfolioInvitation, + VerifiedByStaff, ) -from registrar.models.portfolio_invitation import PortfolioInvitation -from registrar.models.senior_official import SeniorOfficial -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 registrar.models.verified_by_staff import VerifiedByStaff from .common import ( MockDbForSharedTests, AuditedAdminMockData, @@ -60,10 +60,11 @@ from .common import ( multiple_unalphabetical_domain_objects, GenericTestHelper, ) +from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from django.contrib.sessions.backends.db import SessionStore from django.contrib.auth import get_user_model from unittest.mock import ANY, patch, Mock -from django_webtest import WebTest # type: ignore + import logging @@ -973,7 +974,7 @@ class TestListHeaderAdmin(TestCase): ) -class TestMyUserAdmin(MockDbForSharedTests): +class TestMyUserAdmin(MockDbForSharedTests, WebTest): """Tests for the MyUserAdmin class as super or staff user Notes: @@ -993,6 +994,7 @@ class TestMyUserAdmin(MockDbForSharedTests): def setUp(self): super().setUp() + self.app.set_user(self.superuser.username) self.client = Client(HTTP_HOST="localhost:8080") def tearDown(self): @@ -1227,6 +1229,20 @@ class TestMyUserAdmin(MockDbForSharedTests): self.assertNotContains(response, "Portfolio roles:") self.assertNotContains(response, "Portfolio additional permissions:") + @less_console_noise_decorator + def test_user_can_see_related_portfolios(self): + """Tests if a user can see the portfolios they are associated with on the user page""" + portfolio, _ = Portfolio.objects.get_or_create(organization_name="test", creator=self.superuser) + permission, _ = UserPortfolioPermission.objects.get_or_create( + user=self.superuser, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN] + ) + response = self.app.get(reverse("admin:registrar_user_change", args=[self.superuser.pk])) + expected_href = reverse("admin:registrar_portfolio_change", args=[portfolio.pk]) + self.assertContains(response, expected_href) + self.assertContains(response, str(portfolio)) + permission.delete() + portfolio.delete() + class AuditedAdminTest(TestCase):