Merge branch 'main' into dk/2593-domain-request-search-bar

This commit is contained in:
zandercymatics 2024-09-12 14:27:43 -06:00
commit bda1041ecd
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
6 changed files with 101 additions and 17 deletions

View file

@ -1,7 +1,7 @@
import os import os
import tempfile import tempfile
from django.conf import settings from django.conf import settings # type: ignore
class Cert: class Cert:
@ -12,7 +12,7 @@ class Cert:
variable but Python's ssl library requires a file. 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) self.filename = self._write(data)
def __del__(self): def __del__(self):
@ -31,4 +31,4 @@ class Key(Cert):
"""Location of private key as written to disk.""" """Location of private key as written to disk."""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__(data=settings.SECRET_REGISTRY_KEY) super().__init__(data=settings.SECRET_REGISTRY_KEY) # type: ignore

View file

@ -963,7 +963,9 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
domain_ids = user_domain_roles.values_list("domain_id", flat=True) domain_ids = user_domain_roles.values_list("domain_id", flat=True)
domains = Domain.objects.filter(id__in=domain_ids).exclude(state=Domain.State.DELETED) 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) return super().change_view(request, object_id, form_url, extra_context)

View file

@ -23,6 +23,9 @@ from cfenv import AppEnv # type: ignore
from pathlib import Path from pathlib import Path
from typing import Final from typing import Final
from botocore.config import Config from botocore.config import Config
import json
import logging
from django.utils.log import ServerFormatter
# # # ### # # # ###
# Setup code goes here # # 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_debug = env.bool("DJANGO_DEBUG", default=False)
env_is_production = env.bool("IS_PRODUCTION", default=False) env_is_production = env.bool("IS_PRODUCTION", default=False)
env_log_level = env.str("DJANGO_LOG_LEVEL", "DEBUG") 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_getgov_public_site_url = env.str("GETGOV_PUBLIC_SITE_URL", "")
env_oidc_active_provider = env.str("OIDC_ACTIVE_PROVIDER", "identity sandbox") env_oidc_active_provider = env.str("OIDC_ACTIVE_PROVIDER", "identity sandbox")
@ -192,7 +195,7 @@ MIDDLEWARE = [
"registrar.registrar_middleware.CheckPortfolioMiddleware", "registrar.registrar_middleware.CheckPortfolioMiddleware",
] ]
# application object used by Djangos built-in servers (e.g. `runserver`) # application object used by Django's built-in servers (e.g. `runserver`)
WSGI_APPLICATION = "registrar.config.wsgi.application" WSGI_APPLICATION = "registrar.config.wsgi.application"
# endregion # endregion
@ -415,7 +418,7 @@ LANGUAGE_COOKIE_SECURE = True
# and to interpret datetimes entered in forms # and to interpret datetimes entered in forms
TIME_ZONE = "UTC" TIME_ZONE = "UTC"
# enable Djangos translation system # enable Django's translation system
USE_I18N = True USE_I18N = True
# enable localized formatting of numbers and dates # 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.error("Can't do this important task. Something is very wrong.")
# logger.critical("Going to crash now.") # 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 = { LOGGING = {
"version": 1, "version": 1,
# Don't import Django's existing loggers # Don't import Django's existing loggers
@ -469,6 +506,12 @@ LOGGING = {
"format": "[{server_time}] {message}", "format": "[{server_time}] {message}",
"style": "{", "style": "{",
}, },
"json.server": {
"()": JsonServerFormatter,
},
"json": {
"()": JsonFormatter,
},
}, },
# define where log messages will be sent; # define where log messages will be sent;
# each logger can have one or more handlers # each logger can have one or more handlers
@ -476,12 +519,12 @@ LOGGING = {
"console": { "console": {
"level": env_log_level, "level": env_log_level,
"class": "logging.StreamHandler", "class": "logging.StreamHandler",
"formatter": "verbose", "formatter": console_formatter,
}, },
"django.server": { "django.server": {
"level": "INFO", "level": "INFO",
"class": "logging.StreamHandler", "class": "logging.StreamHandler",
"formatter": "django.server", "formatter": server_formatter,
}, },
# No file logger is configured, # No file logger is configured,
# because containerized apps # because containerized apps

View file

@ -306,6 +306,9 @@ class User(AbstractUser):
return roles return roles
def get_portfolios(self):
return self.portfolio_permissions.all()
@classmethod @classmethod
def needs_identity_verification(cls, email, uuid): def needs_identity_verification(cls, email, uuid):
"""A method used by our oidc classes to test whether a user needs email/uuid verification """A method used by our oidc classes to test whether a user needs email/uuid verification

View file

@ -17,6 +17,26 @@
{% endblock %} {% endblock %}
{% block after_related_objects %} {% block after_related_objects %}
{% if portfolios %}
<div class="module aligned padding-3">
<h2>Portfolio information</h2>
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
<h3>Portfolios</h3>
<ul class="margin-0 padding-0">
{% for portfolio in portfolios %}
<li>
<a href="{% url 'admin:registrar_portfolio_change' portfolio.pk %}">
{{ portfolio }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endif %}
<div class="module aligned padding-3"> <div class="module aligned padding-3">
<h2>Associated requests and domains</h2> <h2>Associated requests and domains</h2>
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4"> <div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">

View file

@ -2,6 +2,7 @@ from datetime import datetime
from django.utils import timezone from django.utils import timezone
from django.test import TestCase, RequestFactory, Client from django.test import TestCase, RequestFactory, Client
from django.contrib.admin.sites import AdminSite from django.contrib.admin.sites import AdminSite
from django_webtest import WebTest # type: ignore
from api.tests.common import less_console_noise_decorator from api.tests.common import less_console_noise_decorator
from django.urls import reverse from django.urls import reverse
from registrar.admin import ( from registrar.admin import (
@ -41,13 +42,12 @@ from registrar.models import (
TransitionDomain, TransitionDomain,
Portfolio, Portfolio,
Suborganization, 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 ( from .common import (
MockDbForSharedTests, MockDbForSharedTests,
AuditedAdminMockData, AuditedAdminMockData,
@ -60,10 +60,11 @@ from .common import (
multiple_unalphabetical_domain_objects, multiple_unalphabetical_domain_objects,
GenericTestHelper, GenericTestHelper,
) )
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from django.contrib.sessions.backends.db import SessionStore from django.contrib.sessions.backends.db import SessionStore
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from unittest.mock import ANY, patch, Mock from unittest.mock import ANY, patch, Mock
from django_webtest import WebTest # type: ignore
import logging 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 """Tests for the MyUserAdmin class as super or staff user
Notes: Notes:
@ -993,6 +994,7 @@ class TestMyUserAdmin(MockDbForSharedTests):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.app.set_user(self.superuser.username)
self.client = Client(HTTP_HOST="localhost:8080") self.client = Client(HTTP_HOST="localhost:8080")
def tearDown(self): def tearDown(self):
@ -1227,6 +1229,20 @@ class TestMyUserAdmin(MockDbForSharedTests):
self.assertNotContains(response, "Portfolio roles:") self.assertNotContains(response, "Portfolio roles:")
self.assertNotContains(response, "Portfolio additional permissions:") 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): class AuditedAdminTest(TestCase):