Merge branch 'main' into feature
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run

This commit is contained in:
Jeremy Stretch
2025-09-02 10:50:58 -04:00
66 changed files with 2348 additions and 1987 deletions

View File

@@ -1,16 +1,20 @@
from dataclasses import dataclass
from typing import Iterable
from django.conf import settings
from django.contrib.auth.mixins import AccessMixin
from django.core.exceptions import ImproperlyConfigured
from django.db.models import QuerySet
from django.urls import reverse
from django.urls.exceptions import NoReverseMatch
from django.utils.translation import gettext_lazy as _
from netbox.api.authentication import TokenAuthentication
from netbox.plugins import PluginConfig
from netbox.registry import registry
from utilities.relations import get_related_models
from utilities.request import safe_for_redirect
from utilities.string import title
from .permissions import resolve_permission
__all__ = (
@@ -19,6 +23,7 @@ __all__ = (
'GetRelatedModelsMixin',
'GetReturnURLMixin',
'ObjectPermissionRequiredMixin',
'TokenConditionalLoginRequiredMixin',
'ViewTab',
'get_action_url',
'get_viewname',
@@ -40,6 +45,19 @@ class ConditionalLoginRequiredMixin(AccessMixin):
return super().dispatch(request, *args, **kwargs)
class TokenConditionalLoginRequiredMixin(ConditionalLoginRequiredMixin):
def dispatch(self, request, *args, **kwargs):
# Attempt to authenticate the user using a DRF token, if provided
if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
authenticator = TokenAuthentication()
auth_info = authenticator.authenticate(request)
if auth_info is not None:
request.user = auth_info[0] # User object
request.auth = auth_info[1]
return super().dispatch(request, *args, **kwargs)
class ContentTypePermissionRequiredMixin(ConditionalLoginRequiredMixin):
"""
Similar to Django's built-in PermissionRequiredMixin, but extended to check model-level permission assignments.
@@ -163,8 +181,17 @@ class GetRelatedModelsMixin:
"""
Provides logic for collecting all related models for the currently viewed model.
"""
@dataclass
class RelatedObjectCount:
queryset: QuerySet
filter_param: str
label: str = ''
def get_related_models(self, request, instance, omit=[], extra=[]):
@property
def name(self):
return self.label or title(_(self.queryset.model._meta.verbose_name_plural))
def get_related_models(self, request, instance, omit=None, extra=None):
"""
Get related models of the view's `queryset` model without those listed in `omit`. Will be sorted alphabetical.
@@ -177,6 +204,7 @@ class GetRelatedModelsMixin:
extra: Add extra models to the list of automatically determined related models. Can be used to add indirect
relationships.
"""
omit = omit or []
model = self.queryset.model
related = filter(
lambda m: m[0] is not model and m[0] not in omit,
@@ -184,7 +212,7 @@ class GetRelatedModelsMixin:
)
related_models = [
(
self.RelatedObjectCount(
model.objects.restrict(request.user, 'view').filter(**(
{f'{field}__in': instance}
if isinstance(instance, Iterable)
@@ -194,11 +222,14 @@ class GetRelatedModelsMixin:
)
for model, field in related
]
related_models.extend(extra)
if extra is not None:
related_models.extend([
self.RelatedObjectCount(*attrs) for attrs in extra
])
return sorted(
filter(lambda qs: qs[0].exists(), related_models),
key=lambda qs: qs[0].model._meta.verbose_name.lower(),
filter(lambda roc: roc.queryset.exists(), related_models),
key=lambda roc: roc.name,
)