mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-06 07:16:25 -06:00
Merge branch 'main' into feature
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
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:
@@ -114,6 +114,8 @@ class CSVMultipleContentTypeField(forms.ModelMultipleChoiceField):
|
||||
|
||||
# TODO: Improve validation of selected ContentTypes
|
||||
def prepare_value(self, value):
|
||||
if not value:
|
||||
return None
|
||||
if type(value) is str:
|
||||
ct_filter = Q()
|
||||
for name in value.split(','):
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<a href="{{ url }}" class="btn btn-primary" role="button">
|
||||
<a href="{{ url }}{% if return_url %}?return_url={{ return_url }}{% endif %}" class="btn btn-primary" role="button">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {{ label }}
|
||||
</a>
|
||||
|
||||
@@ -164,7 +164,7 @@ def sync_button(instance):
|
||||
|
||||
# TODO: Remove in NetBox v4.7
|
||||
@register.inclusion_tag('buttons/add.html')
|
||||
def add_button(model, action='add'):
|
||||
def add_button(model, action='add', return_url=None):
|
||||
try:
|
||||
url = get_action_url(model, action=action)
|
||||
except NoReverseMatch:
|
||||
@@ -173,6 +173,7 @@ def add_button(model, action='add'):
|
||||
return {
|
||||
'url': url,
|
||||
'label': _('Add'),
|
||||
'return_url': return_url,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user