mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
Merge branch 'feature' into 12237-django-4.2-2
This commit is contained in:
commit
2091fef53b
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.5.3
|
placeholder: v3.5.4
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.5.3
|
placeholder: v3.5.4
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -1,21 +1,31 @@
|
|||||||
# NetBox v3.5
|
# NetBox v3.5
|
||||||
|
|
||||||
## v3.5.4 (FUTURE)
|
## v3.5.5 (FUTURE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.5.4 (2023-06-20)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
* [#12828](https://github.com/netbox-community/netbox/issues/12828) - Define colors for staged change action choices
|
||||||
* [#12847](https://github.com/netbox-community/netbox/issues/12847) - Include "add" button on all device & virtual machine component list views
|
* [#12847](https://github.com/netbox-community/netbox/issues/12847) - Include "add" button on all device & virtual machine component list views
|
||||||
* [#12862](https://github.com/netbox-community/netbox/issues/12862) - Add menu navigation button to add wireless links directly
|
* [#12862](https://github.com/netbox-community/netbox/issues/12862) - Add menu navigation button to add wireless links directly
|
||||||
|
* [#12865](https://github.com/netbox-community/netbox/issues/12865) - Add "add" buttons for reports & scripts to navigation menu
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#12474](https://github.com/netbox-community/netbox/issues/12474) - Update cable terminations when assigning a location to a new site
|
||||||
* [#12622](https://github.com/netbox-community/netbox/issues/12622) - Permit the assignment of non-site VLANs to prefixes assigned to a site
|
* [#12622](https://github.com/netbox-community/netbox/issues/12622) - Permit the assignment of non-site VLANs to prefixes assigned to a site
|
||||||
* [#12682](https://github.com/netbox-community/netbox/issues/12682) - Correct OpenAPI schema for connected device API endpoint
|
* [#12682](https://github.com/netbox-community/netbox/issues/12682) - Correct OpenAPI schema for connected device API endpoint
|
||||||
* [#12687](https://github.com/netbox-community/netbox/issues/12687) - Allow the assignment of all /31 IP addresses to interfaces
|
* [#12687](https://github.com/netbox-community/netbox/issues/12687) - Allow the assignment of all /31 IP addresses to interfaces
|
||||||
* [#12818](https://github.com/netbox-community/netbox/issues/12818) - Fix permissions evaluation when queuing a data sync job
|
* [#12818](https://github.com/netbox-community/netbox/issues/12818) - Fix permissions evaluation when queuing a data sync job
|
||||||
* [#12822](https://github.com/netbox-community/netbox/issues/12822) - Fix encoding of whitespace in custom link URLs
|
* [#12822](https://github.com/netbox-community/netbox/issues/12822) - Fix encoding of whitespace in custom link URLs
|
||||||
* [#12838](https://github.com/netbox-community/netbox/issues/12838) - Correct rounding of rack power utilization values
|
* [#12838](https://github.com/netbox-community/netbox/issues/12838) - Correct rounding of rack power utilization values
|
||||||
|
* [#12845](https://github.com/netbox-community/netbox/issues/12845) - Fix pagination of objects for related IP addresses table
|
||||||
* [#12850](https://github.com/netbox-community/netbox/issues/12850) - Fix table configuration modal for the contact assignments list
|
* [#12850](https://github.com/netbox-community/netbox/issues/12850) - Fix table configuration modal for the contact assignments list
|
||||||
|
* [#12885](https://github.com/netbox-community/netbox/issues/12885) - Permit mounting of devices in rack unit 100
|
||||||
|
* [#12914](https://github.com/netbox-community/netbox/issues/12914) - Clear stored ordering from user config when cleared by request
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
widget=DateTimePicker()
|
widget=DateTimePicker()
|
||||||
)
|
)
|
||||||
user = DynamicModelMultipleChoiceField(
|
user = DynamicModelMultipleChoiceField(
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('User'),
|
label=_('User'),
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
|
@ -5,7 +5,7 @@ import sys
|
|||||||
from django import get_version
|
from django import get_version
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
# Additional objects to include
|
# Additional objects to include
|
||||||
namespace['ContentType'] = ContentType
|
namespace['ContentType'] = ContentType
|
||||||
namespace['User'] = User
|
namespace['User'] = get_user_model()
|
||||||
|
|
||||||
# Load convenience commands
|
# Load convenience commands
|
||||||
namespace.update({
|
namespace.update({
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import django_rq
|
import django_rq
|
||||||
from django.contrib.auth.models import User
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
@ -69,7 +69,7 @@ class Job(models.Model):
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
to=User,
|
to=settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name='+',
|
related_name='+',
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -11,6 +11,7 @@ DEVICETYPE_IMAGE_FORMATS = 'image/bmp,image/gif,image/jpeg,image/png,image/tiff,
|
|||||||
#
|
#
|
||||||
|
|
||||||
RACK_U_HEIGHT_DEFAULT = 42
|
RACK_U_HEIGHT_DEFAULT = 42
|
||||||
|
RACK_U_HEIGHT_MAX = 100
|
||||||
|
|
||||||
RACK_ELEVATION_BORDER_WIDTH = 2
|
RACK_ELEVATION_BORDER_WIDTH = 2
|
||||||
RACK_ELEVATION_DEFAULT_LEGEND_WIDTH = 30
|
RACK_ELEVATION_DEFAULT_LEGEND_WIDTH = 30
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from extras.filtersets import LocalConfigContextFilterSet
|
from extras.filtersets import LocalConfigContextFilterSet
|
||||||
@ -395,12 +395,12 @@ class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
label=_('Location (slug)'),
|
label=_('Location (slug)'),
|
||||||
)
|
)
|
||||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
label=_('User (ID)'),
|
label=_('User (ID)'),
|
||||||
)
|
)
|
||||||
user = django_filters.ModelMultipleChoiceFilter(
|
user = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='user__username',
|
field_name='user__username',
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
to_field_name='username',
|
to_field_name='username',
|
||||||
label=_('User (name)'),
|
label=_('User (name)'),
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from timezone_field import TimeZoneFormField
|
from timezone_field import TimeZoneFormField
|
||||||
|
|
||||||
@ -322,7 +322,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
|
|
||||||
class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
|
class RackReservationBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
user = forms.ModelChoiceField(
|
user = forms.ModelChoiceField(
|
||||||
queryset=User.objects.order_by(
|
queryset=get_user_model().objects.order_by(
|
||||||
'username'
|
'username'
|
||||||
),
|
),
|
||||||
required=False
|
required=False
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
@ -376,7 +376,7 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
label=_('Rack')
|
label=_('Rack')
|
||||||
)
|
)
|
||||||
user_id = DynamicModelMultipleChoiceField(
|
user_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('User'),
|
label=_('User'),
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from timezone_field import TimeZoneFormField
|
from timezone_field import TimeZoneFormField
|
||||||
@ -236,7 +236,7 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
|
|||||||
help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.")
|
help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.")
|
||||||
)
|
)
|
||||||
user = forms.ModelChoiceField(
|
user = forms.ModelChoiceField(
|
||||||
queryset=User.objects.order_by(
|
queryset=get_user_model().objects.order_by(
|
||||||
'username'
|
'username'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -18,6 +18,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='device',
|
model_name='device',
|
||||||
name='position',
|
name='position',
|
||||||
field=models.DecimalField(blank=True, decimal_places=1, max_digits=4, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(99.5)]),
|
field=models.DecimalField(blank=True, decimal_places=1, max_digits=4, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100.5)]),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -555,7 +555,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
|||||||
decimal_places=1,
|
decimal_places=1,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(99.5)],
|
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX + 0.5)],
|
||||||
verbose_name='Position (U)',
|
verbose_name='Position (U)',
|
||||||
help_text=_('The lowest-numbered unit occupied by the device')
|
help_text=_('The lowest-numbered unit occupied by the device')
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import decimal
|
import decimal
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -126,7 +126,7 @@ class Rack(PrimaryModel, WeightMixin):
|
|||||||
u_height = models.PositiveSmallIntegerField(
|
u_height = models.PositiveSmallIntegerField(
|
||||||
default=RACK_U_HEIGHT_DEFAULT,
|
default=RACK_U_HEIGHT_DEFAULT,
|
||||||
verbose_name='Height (U)',
|
verbose_name='Height (U)',
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(100)],
|
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX)],
|
||||||
help_text=_('Height in rack units')
|
help_text=_('Height in rack units')
|
||||||
)
|
)
|
||||||
desc_units = models.BooleanField(
|
desc_units = models.BooleanField(
|
||||||
@ -505,7 +505,7 @@ class RackReservation(PrimaryModel):
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
to=User,
|
to=settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
|
@ -27,6 +27,7 @@ def handle_location_site_change(instance, created, **kwargs):
|
|||||||
Rack.objects.filter(location__in=locations).update(site=instance.site)
|
Rack.objects.filter(location__in=locations).update(site=instance.site)
|
||||||
Device.objects.filter(location__in=locations).update(site=instance.site)
|
Device.objects.filter(location__in=locations).update(site=instance.site)
|
||||||
PowerPanel.objects.filter(location__in=locations).update(site=instance.site)
|
PowerPanel.objects.filter(location__in=locations).update(site=instance.site)
|
||||||
|
CableTermination.objects.filter(_location__in=locations).update(_site=instance.site)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Rack)
|
@receiver(post_save, sender=Rack)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -14,6 +14,9 @@ from wireless.choices import WirelessChannelChoices
|
|||||||
from wireless.models import WirelessLAN
|
from wireless.models import WirelessLAN
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class AppTest(APITestCase):
|
class AppTest(APITestCase):
|
||||||
|
|
||||||
def test_root(self):
|
def test_root(self):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
@ -12,6 +12,9 @@ from virtualization.models import Cluster, ClusterType
|
|||||||
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class DeviceComponentFilterSetTests:
|
class DeviceComponentFilterSetTests:
|
||||||
|
|
||||||
def test_device_type(self):
|
def test_device_type(self):
|
||||||
|
@ -6,7 +6,7 @@ except ImportError:
|
|||||||
from backports.zoneinfo import ZoneInfo
|
from backports.zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -22,6 +22,9 @@ from utilities.testing import ViewTestCases, create_tags, create_test_device, po
|
|||||||
from wireless.models import WirelessLAN
|
from wireless.models import WirelessLAN
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||||
model = Region
|
model = Region
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
@ -256,7 +256,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
|
|||||||
assigned_object = serializers.SerializerMethodField(read_only=True)
|
assigned_object = serializers.SerializerMethodField(read_only=True)
|
||||||
created_by = serializers.PrimaryKeyRelatedField(
|
created_by = serializers.PrimaryKeyRelatedField(
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
default=serializers.CurrentUserDefault()
|
default=serializers.CurrentUserDefault()
|
||||||
)
|
)
|
||||||
|
@ -210,7 +210,7 @@ class ChangeActionChoices(ChoiceSet):
|
|||||||
ACTION_DELETE = 'delete'
|
ACTION_DELETE = 'delete'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(ACTION_CREATE, 'Create'),
|
(ACTION_CREATE, 'Create', 'green'),
|
||||||
(ACTION_UPDATE, 'Update'),
|
(ACTION_UPDATE, 'Update', 'blue'),
|
||||||
(ACTION_DELETE, 'Delete'),
|
(ACTION_DELETE, 'Delete', 'red'),
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
@ -159,12 +159,12 @@ class SavedFilterFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
content_types = ContentTypeFilter()
|
content_types = ContentTypeFilter()
|
||||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
label=_('User (ID)'),
|
label=_('User (ID)'),
|
||||||
)
|
)
|
||||||
user = django_filters.ModelMultipleChoiceFilter(
|
user = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='user__username',
|
field_name='user__username',
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
to_field_name='username',
|
to_field_name='username',
|
||||||
label=_('User (name)'),
|
label=_('User (name)'),
|
||||||
)
|
)
|
||||||
@ -223,12 +223,12 @@ class JournalEntryFilterSet(NetBoxModelFilterSet):
|
|||||||
queryset=ContentType.objects.all()
|
queryset=ContentType.objects.all()
|
||||||
)
|
)
|
||||||
created_by_id = django_filters.ModelMultipleChoiceFilter(
|
created_by_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
label=_('User (ID)'),
|
label=_('User (ID)'),
|
||||||
)
|
)
|
||||||
created_by = django_filters.ModelMultipleChoiceFilter(
|
created_by = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='created_by__username',
|
field_name='created_by__username',
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
to_field_name='username',
|
to_field_name='username',
|
||||||
label=_('User (name)'),
|
label=_('User (name)'),
|
||||||
)
|
)
|
||||||
@ -510,12 +510,12 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
|||||||
queryset=ContentType.objects.all()
|
queryset=ContentType.objects.all()
|
||||||
)
|
)
|
||||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
label=_('User (ID)'),
|
label=_('User (ID)'),
|
||||||
)
|
)
|
||||||
user = django_filters.ModelMultipleChoiceFilter(
|
user = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='user__username',
|
field_name='user__username',
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
to_field_name='username',
|
to_field_name='username',
|
||||||
label=_('User name'),
|
label=_('User name'),
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
@ -385,7 +385,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
|
|||||||
widget=DateTimePicker()
|
widget=DateTimePicker()
|
||||||
)
|
)
|
||||||
created_by_id = DynamicModelMultipleChoiceField(
|
created_by_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('User'),
|
label=_('User'),
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
@ -429,7 +429,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
user_id = DynamicModelMultipleChoiceField(
|
user_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('User'),
|
label=_('User'),
|
||||||
widget=APISelectMultiple(
|
widget=APISelectMultiple(
|
||||||
|
@ -4,7 +4,7 @@ import sys
|
|||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
@ -63,6 +63,8 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
logger.info(f"Script completed in {job.duration}")
|
logger.info(f"Script completed in {job.duration}")
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
# Params
|
# Params
|
||||||
script = options['script']
|
script = options['script']
|
||||||
loglevel = options['loglevel']
|
loglevel = options['loglevel']
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.contrib.auth.models import User
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -24,7 +24,7 @@ class ObjectChange(models.Model):
|
|||||||
db_index=True
|
db_index=True
|
||||||
)
|
)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
to=User,
|
to=settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name='changes',
|
related_name='changes',
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -3,7 +3,7 @@ import urllib.parse
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.models import User
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
@ -419,7 +419,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
to=User,
|
to=settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
@ -560,7 +560,7 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
|
|||||||
fk_field='assigned_object_id'
|
fk_field='assigned_object_id'
|
||||||
)
|
)
|
||||||
created_by = models.ForeignKey(
|
created_by = models.ForeignKey(
|
||||||
to=User,
|
to=settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
|
@ -113,3 +113,6 @@ class StagedChange(ChangeLoggedModel):
|
|||||||
logger.info(f'Deleting {self.model._meta.verbose_name} {instance}')
|
logger.info(f'Deleting {self.model._meta.verbose_name} {instance}')
|
||||||
instance.delete()
|
instance.delete()
|
||||||
apply.alters_data = True
|
apply.alters_data = True
|
||||||
|
|
||||||
|
def get_action_color(self):
|
||||||
|
return ChangeActionChoices.colors.get(self.action)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
@ -15,6 +15,9 @@ from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
|
|||||||
from utilities.testing import APITestCase, APIViewTestCases
|
from utilities.testing import APITestCase, APIViewTestCases
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class AppTest(APITestCase):
|
class AppTest(APITestCase):
|
||||||
|
|
||||||
def test_root(self):
|
def test_root(self):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
@ -18,6 +18,9 @@ from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests, cr
|
|||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldTestCase(TestCase, BaseFilterSetTests):
|
class CustomFieldTestCase(TestCase, BaseFilterSetTests):
|
||||||
queryset = CustomField.objects.all()
|
queryset = CustomField.objects.all()
|
||||||
filterset = CustomFieldFilterSet
|
filterset = CustomFieldFilterSet
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
@ -11,6 +11,9 @@ from extras.models import *
|
|||||||
from utilities.testing import ViewTestCases, TestCase
|
from utilities.testing import ViewTestCases, TestCase
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = CustomField
|
model = CustomField
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ class AvailableASNSerializer(serializers.Serializer):
|
|||||||
Representation of an ASN which does not exist in the database.
|
Representation of an ASN which does not exist in the database.
|
||||||
"""
|
"""
|
||||||
asn = serializers.IntegerField(read_only=True)
|
asn = serializers.IntegerField(read_only=True)
|
||||||
|
description = serializers.CharField(required=False)
|
||||||
|
|
||||||
def to_representation(self, asn):
|
def to_representation(self, asn):
|
||||||
rir = NestedRIRSerializer(self.context['range'].rir, context={
|
rir = NestedRIRSerializer(self.context['range'].rir, context={
|
||||||
@ -432,6 +433,7 @@ class AvailableIPSerializer(serializers.Serializer):
|
|||||||
family = serializers.IntegerField(read_only=True)
|
family = serializers.IntegerField(read_only=True)
|
||||||
address = serializers.CharField(read_only=True)
|
address = serializers.CharField(read_only=True)
|
||||||
vrf = NestedVRFSerializer(read_only=True)
|
vrf = NestedVRFSerializer(read_only=True)
|
||||||
|
description = serializers.CharField(required=False)
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
if self.context.get('vrf'):
|
if self.context.get('vrf'):
|
||||||
|
@ -3,7 +3,9 @@ from django.db import transaction
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django_pglocks import advisory_lock
|
from django_pglocks import advisory_lock
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from netaddr import IPSet
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
@ -12,10 +14,12 @@ from circuits.models import Provider
|
|||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from ipam import filtersets
|
from ipam import filtersets
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
|
from ipam.utils import get_next_available_prefix
|
||||||
from netbox.api.viewsets import NetBoxModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
from netbox.api.viewsets.mixins import ObjectValidationMixin
|
from netbox.api.viewsets.mixins import ObjectValidationMixin
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from netbox.constants import ADVISORY_LOCK_KEYS
|
from netbox.constants import ADVISORY_LOCK_KEYS
|
||||||
|
from utilities.api import get_serializer_for_model
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from ipam.models import L2VPN, L2VPNTermination
|
from ipam.models import L2VPN, L2VPNTermination
|
||||||
@ -207,237 +211,233 @@ def get_results_limit(request):
|
|||||||
return limit
|
return limit
|
||||||
|
|
||||||
|
|
||||||
class AvailableASNsView(ObjectValidationMixin, APIView):
|
class AvailableObjectsView(ObjectValidationMixin, APIView):
|
||||||
queryset = ASN.objects.all()
|
"""
|
||||||
|
Return a list of dicts representing child objects that have not yet been created for a parent object.
|
||||||
|
"""
|
||||||
|
read_serializer_class = None
|
||||||
|
write_serializer_class = None
|
||||||
|
advisory_lock_key = None
|
||||||
|
|
||||||
|
def get_parent(self, request, pk):
|
||||||
|
"""
|
||||||
|
Return the parent object.
|
||||||
|
"""
|
||||||
|
raise NotImplemented()
|
||||||
|
|
||||||
|
def get_available_objects(self, parent, limit=None):
|
||||||
|
"""
|
||||||
|
Return all available objects for the parent.
|
||||||
|
"""
|
||||||
|
raise NotImplemented()
|
||||||
|
|
||||||
|
def get_extra_context(self, parent):
|
||||||
|
"""
|
||||||
|
Return any extra context data for the serializer.
|
||||||
|
"""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def check_sufficient_available(self, requested_objects, available_objects):
|
||||||
|
"""
|
||||||
|
Check if there exist a sufficient number of available objects to satisfy the request.
|
||||||
|
"""
|
||||||
|
return len(requested_objects) <= len(available_objects)
|
||||||
|
|
||||||
|
def prep_object_data(self, requested_objects, available_objects, parent):
|
||||||
|
"""
|
||||||
|
Prepare data by setting any programmatically determined object attributes (e.g. next available VLAN ID)
|
||||||
|
on the request data.
|
||||||
|
"""
|
||||||
|
return requested_objects
|
||||||
|
|
||||||
@extend_schema(methods=["get"], responses={200: serializers.AvailableASNSerializer(many=True)})
|
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
asnrange = get_object_or_404(ASNRange.objects.restrict(request.user), pk=pk)
|
parent = self.get_parent(request, pk)
|
||||||
limit = get_results_limit(request)
|
limit = get_results_limit(request)
|
||||||
|
available_objects = self.get_available_objects(parent, limit)
|
||||||
|
|
||||||
available_asns = asnrange.get_available_asns()[:limit]
|
serializer = self.read_serializer_class(available_objects, many=True, context={
|
||||||
|
|
||||||
serializer = serializers.AvailableASNSerializer(available_asns, many=True, context={
|
|
||||||
'request': request,
|
'request': request,
|
||||||
'range': asnrange,
|
**self.get_extra_context(parent),
|
||||||
})
|
})
|
||||||
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@extend_schema(methods=["post"], responses={201: serializers.ASNSerializer(many=True)})
|
|
||||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-asns'])
|
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
self.queryset = self.queryset.restrict(request.user, 'add')
|
self.queryset = self.queryset.restrict(request.user, 'add')
|
||||||
asnrange = get_object_or_404(ASNRange.objects.restrict(request.user), pk=pk)
|
parent = self.get_parent(request, pk)
|
||||||
|
|
||||||
# Normalize to a list of objects
|
# Normalize request data to a list of objects
|
||||||
requested_asns = request.data if isinstance(request.data, list) else [request.data]
|
requested_objects = request.data if isinstance(request.data, list) else [request.data]
|
||||||
|
|
||||||
# Determine if the requested number of IPs is available
|
# Serialize and validate the request data
|
||||||
available_asns = asnrange.get_available_asns()
|
serializer = self.write_serializer_class(data=requested_objects, many=True, context={
|
||||||
if len(available_asns) < len(requested_asns):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"detail": f"An insufficient number of ASNs are available within {asnrange} "
|
|
||||||
f"({len(requested_asns)} requested, {len(available_asns)} available)"
|
|
||||||
},
|
|
||||||
status=status.HTTP_409_CONFLICT
|
|
||||||
)
|
|
||||||
|
|
||||||
# Assign ASNs from the list of available IPs and copy VRF assignment from the parent
|
|
||||||
for i, requested_asn in enumerate(requested_asns):
|
|
||||||
requested_asn.update({
|
|
||||||
'rir': asnrange.rir.pk,
|
|
||||||
'range': asnrange.pk,
|
|
||||||
'asn': available_asns[i],
|
|
||||||
})
|
|
||||||
|
|
||||||
# Initialize the serializer with a list or a single object depending on what was requested
|
|
||||||
context = {'request': request}
|
|
||||||
if isinstance(request.data, list):
|
|
||||||
serializer = serializers.ASNSerializer(data=requested_asns, many=True, context=context)
|
|
||||||
else:
|
|
||||||
serializer = serializers.ASNSerializer(data=requested_asns[0], context=context)
|
|
||||||
|
|
||||||
# Create the new IP address(es)
|
|
||||||
if serializer.is_valid():
|
|
||||||
try:
|
|
||||||
with transaction.atomic():
|
|
||||||
created = serializer.save()
|
|
||||||
self._validate_objects(created)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
raise PermissionDenied()
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
def get_serializer_class(self):
|
|
||||||
if self.request.method == "GET":
|
|
||||||
return serializers.AvailableASNSerializer
|
|
||||||
|
|
||||||
return serializers.ASNSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class AvailablePrefixesView(ObjectValidationMixin, APIView):
|
|
||||||
queryset = Prefix.objects.all()
|
|
||||||
|
|
||||||
@extend_schema(methods=["get"], responses={200: serializers.AvailablePrefixSerializer(many=True)})
|
|
||||||
def get(self, request, pk):
|
|
||||||
prefix = get_object_or_404(Prefix.objects.restrict(request.user), pk=pk)
|
|
||||||
available_prefixes = prefix.get_available_prefixes()
|
|
||||||
|
|
||||||
serializer = serializers.AvailablePrefixSerializer(available_prefixes.iter_cidrs(), many=True, context={
|
|
||||||
'request': request,
|
'request': request,
|
||||||
'vrf': prefix.vrf,
|
**self.get_extra_context(parent),
|
||||||
})
|
})
|
||||||
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@extend_schema(methods=["post"], responses={201: serializers.PrefixSerializer(many=True)})
|
|
||||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
|
|
||||||
def post(self, request, pk):
|
|
||||||
self.queryset = self.queryset.restrict(request.user, 'add')
|
|
||||||
prefix = get_object_or_404(Prefix.objects.restrict(request.user), pk=pk)
|
|
||||||
available_prefixes = prefix.get_available_prefixes()
|
|
||||||
|
|
||||||
# Validate Requested Prefixes' length
|
|
||||||
serializer = serializers.PrefixLengthSerializer(
|
|
||||||
data=request.data if isinstance(request.data, list) else [request.data],
|
|
||||||
many=True,
|
|
||||||
context={
|
|
||||||
'request': request,
|
|
||||||
'prefix': prefix,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
return Response(
|
return Response(
|
||||||
serializer.errors,
|
serializer.errors,
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
requested_prefixes = serializer.validated_data
|
with advisory_lock(ADVISORY_LOCK_KEYS[self.advisory_lock_key]):
|
||||||
# Allocate prefixes to the requested objects based on availability within the parent
|
available_objects = self.get_available_objects(parent)
|
||||||
for i, requested_prefix in enumerate(requested_prefixes):
|
|
||||||
|
|
||||||
# Find the first available prefix equal to or larger than the requested size
|
# Determine if the requested number of objects is available
|
||||||
for available_prefix in available_prefixes.iter_cidrs():
|
if not self.check_sufficient_available(serializer.validated_data, available_objects):
|
||||||
if requested_prefix['prefix_length'] >= available_prefix.prefixlen:
|
|
||||||
allocated_prefix = '{}/{}'.format(available_prefix.network, requested_prefix['prefix_length'])
|
|
||||||
requested_prefix['prefix'] = allocated_prefix
|
|
||||||
requested_prefix['vrf'] = prefix.vrf.pk if prefix.vrf else None
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{"detail": f"Insufficient resources are available to satisfy the request"},
|
||||||
"detail": "Insufficient space is available to accommodate the requested prefix size(s)"
|
|
||||||
},
|
|
||||||
status=status.HTTP_409_CONFLICT
|
status=status.HTTP_409_CONFLICT
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove the allocated prefix from the list of available prefixes
|
# Prepare object data for deserialization
|
||||||
available_prefixes.remove(allocated_prefix)
|
requested_objects = self.prep_object_data(serializer.validated_data, available_objects, parent)
|
||||||
|
|
||||||
# Initialize the serializer with a list or a single object depending on what was requested
|
# Initialize the serializer with a list or a single object depending on what was requested
|
||||||
|
serializer_class = get_serializer_for_model(self.queryset.model)
|
||||||
context = {'request': request}
|
context = {'request': request}
|
||||||
if isinstance(request.data, list):
|
if isinstance(request.data, list):
|
||||||
serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True, context=context)
|
serializer = serializer_class(data=requested_objects, many=True, context=context)
|
||||||
else:
|
else:
|
||||||
serializer = serializers.PrefixSerializer(data=requested_prefixes[0], context=context)
|
serializer = serializer_class(data=requested_objects[0], context=context)
|
||||||
|
|
||||||
# Create the new Prefix(es)
|
if not serializer.is_valid():
|
||||||
if serializer.is_valid():
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Create the new IP address(es)
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
created = serializer.save()
|
created = serializer.save()
|
||||||
self._validate_objects(created)
|
self._validate_objects(created)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
def get_serializer_class(self):
|
class AvailableASNsView(AvailableObjectsView):
|
||||||
if self.request.method == "GET":
|
queryset = ASN.objects.all()
|
||||||
return serializers.AvailablePrefixSerializer
|
read_serializer_class = serializers.AvailableASNSerializer
|
||||||
|
write_serializer_class = serializers.AvailableASNSerializer
|
||||||
return serializers.PrefixLengthSerializer
|
advisory_lock_key = 'available-asns'
|
||||||
|
|
||||||
|
|
||||||
class AvailableIPAddressesView(ObjectValidationMixin, APIView):
|
|
||||||
queryset = IPAddress.objects.all()
|
|
||||||
|
|
||||||
def get_parent(self, request, pk):
|
def get_parent(self, request, pk):
|
||||||
raise NotImplemented()
|
return get_object_or_404(ASNRange.objects.restrict(request.user), pk=pk)
|
||||||
|
|
||||||
@extend_schema(methods=["get"], responses={200: serializers.AvailableIPSerializer(many=True)})
|
def get_available_objects(self, parent, limit=None):
|
||||||
|
return parent.get_available_asns()[:limit]
|
||||||
|
|
||||||
|
def get_extra_context(self, parent):
|
||||||
|
return {
|
||||||
|
'range': parent,
|
||||||
|
}
|
||||||
|
|
||||||
|
def prep_object_data(self, requested_objects, available_objects, parent):
|
||||||
|
for i, request_data in enumerate(requested_objects):
|
||||||
|
request_data.update({
|
||||||
|
'rir': parent.rir.pk,
|
||||||
|
'range': parent.pk,
|
||||||
|
'asn': available_objects[i],
|
||||||
|
})
|
||||||
|
|
||||||
|
return requested_objects
|
||||||
|
|
||||||
|
@extend_schema(methods=["get"], responses={200: serializers.AvailableASNSerializer(many=True)})
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
parent = self.get_parent(request, pk)
|
return super().get(request, pk)
|
||||||
limit = get_results_limit(request)
|
|
||||||
|
|
||||||
|
@extend_schema(methods=["post"], responses={201: serializers.AvailableASNSerializer(many=True)})
|
||||||
|
def post(self, request, pk):
|
||||||
|
return super().post(request, pk)
|
||||||
|
|
||||||
|
|
||||||
|
class AvailablePrefixesView(AvailableObjectsView):
|
||||||
|
queryset = Prefix.objects.all()
|
||||||
|
read_serializer_class = serializers.AvailablePrefixSerializer
|
||||||
|
write_serializer_class = serializers.PrefixLengthSerializer
|
||||||
|
advisory_lock_key = 'available-prefixes'
|
||||||
|
|
||||||
|
def get_parent(self, request, pk):
|
||||||
|
return get_object_or_404(Prefix.objects.restrict(request.user), pk=pk)
|
||||||
|
|
||||||
|
def get_available_objects(self, parent, limit=None):
|
||||||
|
return parent.get_available_prefixes().iter_cidrs()
|
||||||
|
|
||||||
|
def check_sufficient_available(self, requested_objects, available_objects):
|
||||||
|
available_prefixes = IPSet(available_objects)
|
||||||
|
for requested_object in requested_objects:
|
||||||
|
if not get_next_available_prefix(available_prefixes, requested_object['prefix_length']):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_extra_context(self, parent):
|
||||||
|
return {
|
||||||
|
'prefix': parent,
|
||||||
|
'vrf': parent.vrf,
|
||||||
|
}
|
||||||
|
|
||||||
|
def prep_object_data(self, requested_objects, available_objects, parent):
|
||||||
|
available_prefixes = IPSet(available_objects)
|
||||||
|
for i, request_data in enumerate(requested_objects):
|
||||||
|
|
||||||
|
# Find the first available prefix equal to or larger than the requested size
|
||||||
|
if allocated_prefix := get_next_available_prefix(available_prefixes, request_data['prefix_length']):
|
||||||
|
request_data.update({
|
||||||
|
'prefix': allocated_prefix,
|
||||||
|
'vrf': parent.vrf.pk if parent.vrf else None,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
raise ValidationError("Insufficient space is available to accommodate the requested prefix size(s)")
|
||||||
|
|
||||||
|
return requested_objects
|
||||||
|
|
||||||
|
@extend_schema(methods=["get"], responses={200: serializers.AvailablePrefixSerializer(many=True)})
|
||||||
|
def get(self, request, pk):
|
||||||
|
return super().get(request, pk)
|
||||||
|
|
||||||
|
@extend_schema(methods=["post"], responses={201: serializers.PrefixSerializer(many=True)})
|
||||||
|
def post(self, request, pk):
|
||||||
|
return super().post(request, pk)
|
||||||
|
|
||||||
|
|
||||||
|
class AvailableIPAddressesView(AvailableObjectsView):
|
||||||
|
queryset = IPAddress.objects.all()
|
||||||
|
read_serializer_class = serializers.AvailableIPSerializer
|
||||||
|
write_serializer_class = serializers.AvailableIPSerializer
|
||||||
|
advisory_lock_key = 'available-ips'
|
||||||
|
|
||||||
|
def get_available_objects(self, parent, limit=None):
|
||||||
# Calculate available IPs within the parent
|
# Calculate available IPs within the parent
|
||||||
ip_list = []
|
ip_list = []
|
||||||
for index, ip in enumerate(parent.get_available_ips(), start=1):
|
for index, ip in enumerate(parent.get_available_ips(), start=1):
|
||||||
ip_list.append(ip)
|
ip_list.append(ip)
|
||||||
if index == limit:
|
if index == limit:
|
||||||
break
|
break
|
||||||
serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={
|
return ip_list
|
||||||
'request': request,
|
|
||||||
|
def get_extra_context(self, parent):
|
||||||
|
return {
|
||||||
'parent': parent,
|
'parent': parent,
|
||||||
'vrf': parent.vrf,
|
'vrf': parent.vrf,
|
||||||
|
}
|
||||||
|
|
||||||
|
def prep_object_data(self, requested_objects, available_objects, parent):
|
||||||
|
available_ips = iter(available_objects)
|
||||||
|
for i, request_data in enumerate(requested_objects):
|
||||||
|
request_data.update({
|
||||||
|
'address': f'{next(available_ips)}/{parent.mask_length}',
|
||||||
|
'vrf': parent.vrf.pk if parent.vrf else None,
|
||||||
})
|
})
|
||||||
|
|
||||||
return Response(serializer.data)
|
return requested_objects
|
||||||
|
|
||||||
|
@extend_schema(methods=["get"], responses={200: serializers.AvailableIPSerializer(many=True)})
|
||||||
|
def get(self, request, pk):
|
||||||
|
return super().get(request, pk)
|
||||||
|
|
||||||
@extend_schema(methods=["post"], responses={201: serializers.IPAddressSerializer(many=True)})
|
@extend_schema(methods=["post"], responses={201: serializers.IPAddressSerializer(many=True)})
|
||||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
|
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
self.queryset = self.queryset.restrict(request.user, 'add')
|
return super().post(request, pk)
|
||||||
parent = self.get_parent(request, pk)
|
|
||||||
|
|
||||||
# Normalize to a list of objects
|
|
||||||
requested_ips = request.data if isinstance(request.data, list) else [request.data]
|
|
||||||
|
|
||||||
# Determine if the requested number of IPs is available
|
|
||||||
available_ips = parent.get_available_ips()
|
|
||||||
if available_ips.size < len(requested_ips):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"detail": f"An insufficient number of IP addresses are available within {parent} "
|
|
||||||
f"({len(requested_ips)} requested, {len(available_ips)} available)"
|
|
||||||
},
|
|
||||||
status=status.HTTP_409_CONFLICT
|
|
||||||
)
|
|
||||||
|
|
||||||
# Assign addresses from the list of available IPs and copy VRF assignment from the parent
|
|
||||||
available_ips = iter(available_ips)
|
|
||||||
for requested_ip in requested_ips:
|
|
||||||
requested_ip['address'] = f'{next(available_ips)}/{parent.mask_length}'
|
|
||||||
requested_ip['vrf'] = parent.vrf.pk if parent.vrf else None
|
|
||||||
|
|
||||||
# Initialize the serializer with a list or a single object depending on what was requested
|
|
||||||
context = {'request': request}
|
|
||||||
if isinstance(request.data, list):
|
|
||||||
serializer = serializers.IPAddressSerializer(data=requested_ips, many=True, context=context)
|
|
||||||
else:
|
|
||||||
serializer = serializers.IPAddressSerializer(data=requested_ips[0], context=context)
|
|
||||||
|
|
||||||
# Create the new IP address(es)
|
|
||||||
if serializer.is_valid():
|
|
||||||
try:
|
|
||||||
with transaction.atomic():
|
|
||||||
created = serializer.save()
|
|
||||||
self._validate_objects(created)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
raise PermissionDenied()
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
def get_serializer_class(self):
|
|
||||||
if self.request.method == "GET":
|
|
||||||
return serializers.AvailableIPSerializer
|
|
||||||
|
|
||||||
return serializers.IPAddressSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class PrefixAvailableIPAddressesView(AvailableIPAddressesView):
|
class PrefixAvailableIPAddressesView(AvailableIPAddressesView):
|
||||||
@ -452,77 +452,36 @@ class IPRangeAvailableIPAddressesView(AvailableIPAddressesView):
|
|||||||
return get_object_or_404(IPRange.objects.restrict(request.user), pk=pk)
|
return get_object_or_404(IPRange.objects.restrict(request.user), pk=pk)
|
||||||
|
|
||||||
|
|
||||||
class AvailableVLANsView(ObjectValidationMixin, APIView):
|
class AvailableVLANsView(AvailableObjectsView):
|
||||||
queryset = VLAN.objects.all()
|
queryset = VLAN.objects.all()
|
||||||
|
read_serializer_class = serializers.AvailableVLANSerializer
|
||||||
|
write_serializer_class = serializers.CreateAvailableVLANSerializer
|
||||||
|
advisory_lock_key = 'available-vlans'
|
||||||
|
|
||||||
|
def get_parent(self, request, pk):
|
||||||
|
return get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk)
|
||||||
|
|
||||||
|
def get_available_objects(self, parent, limit=None):
|
||||||
|
return parent.get_available_vids()[:limit]
|
||||||
|
|
||||||
|
def get_extra_context(self, parent):
|
||||||
|
return {
|
||||||
|
'group': parent,
|
||||||
|
}
|
||||||
|
|
||||||
|
def prep_object_data(self, requested_objects, available_objects, parent):
|
||||||
|
for i, request_data in enumerate(requested_objects):
|
||||||
|
request_data.update({
|
||||||
|
'vid': available_objects.pop(0),
|
||||||
|
'group': parent.pk,
|
||||||
|
})
|
||||||
|
|
||||||
|
return requested_objects
|
||||||
|
|
||||||
@extend_schema(methods=["get"], responses={200: serializers.AvailableVLANSerializer(many=True)})
|
@extend_schema(methods=["get"], responses={200: serializers.AvailableVLANSerializer(many=True)})
|
||||||
def get(self, request, pk):
|
def get(self, request, pk):
|
||||||
vlangroup = get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk)
|
return super().get(request, pk)
|
||||||
limit = get_results_limit(request)
|
|
||||||
|
|
||||||
available_vlans = vlangroup.get_available_vids()[:limit]
|
|
||||||
serializer = serializers.AvailableVLANSerializer(available_vlans, many=True, context={
|
|
||||||
'request': request,
|
|
||||||
'group': vlangroup,
|
|
||||||
})
|
|
||||||
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@extend_schema(methods=["post"], responses={201: serializers.VLANSerializer(many=True)})
|
@extend_schema(methods=["post"], responses={201: serializers.VLANSerializer(many=True)})
|
||||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-vlans'])
|
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
self.queryset = self.queryset.restrict(request.user, 'add')
|
return super().post(request, pk)
|
||||||
vlangroup = get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk)
|
|
||||||
available_vlans = vlangroup.get_available_vids()
|
|
||||||
many = isinstance(request.data, list)
|
|
||||||
|
|
||||||
# Validate requested VLANs
|
|
||||||
serializer = serializers.CreateAvailableVLANSerializer(
|
|
||||||
data=request.data if many else [request.data],
|
|
||||||
many=True,
|
|
||||||
context={
|
|
||||||
'request': request,
|
|
||||||
'group': vlangroup,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if not serializer.is_valid():
|
|
||||||
return Response(
|
|
||||||
serializer.errors,
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
requested_vlans = serializer.validated_data
|
|
||||||
|
|
||||||
for i, requested_vlan in enumerate(requested_vlans):
|
|
||||||
try:
|
|
||||||
requested_vlan['vid'] = available_vlans.pop(0)
|
|
||||||
requested_vlan['group'] = vlangroup.pk
|
|
||||||
except IndexError:
|
|
||||||
return Response({
|
|
||||||
"detail": "The requested number of VLANs is not available"
|
|
||||||
}, status=status.HTTP_409_CONFLICT)
|
|
||||||
|
|
||||||
# Initialize the serializer with a list or a single object depending on what was requested
|
|
||||||
context = {'request': request}
|
|
||||||
if many:
|
|
||||||
serializer = serializers.VLANSerializer(data=requested_vlans, many=True, context=context)
|
|
||||||
else:
|
|
||||||
serializer = serializers.VLANSerializer(data=requested_vlans[0], context=context)
|
|
||||||
|
|
||||||
# Create the new VLAN(s)
|
|
||||||
if serializer.is_valid():
|
|
||||||
try:
|
|
||||||
with transaction.atomic():
|
|
||||||
created = serializer.save()
|
|
||||||
self._validate_objects(created)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
raise PermissionDenied()
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
def get_serializer_class(self):
|
|
||||||
if self.request.method == "GET":
|
|
||||||
return serializers.AvailableVLANSerializer
|
|
||||||
|
|
||||||
return serializers.VLANSerializer
|
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .models import ASN, Prefix, VLAN
|
from .models import Prefix, VLAN
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'add_available_ipaddresses',
|
||||||
|
'add_available_vlans',
|
||||||
|
'add_requested_prefixes',
|
||||||
|
'get_next_available_prefix',
|
||||||
|
'rebuild_prefixes',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def add_requested_prefixes(parent, prefix_list, show_available=True, show_assigned=True):
|
def add_requested_prefixes(parent, prefix_list, show_available=True, show_assigned=True):
|
||||||
@ -184,3 +192,15 @@ def rebuild_prefixes(vrf):
|
|||||||
|
|
||||||
# Final flush of any remaining Prefixes
|
# Final flush of any remaining Prefixes
|
||||||
Prefix.objects.bulk_update(update_queue, ['_depth', '_children'])
|
Prefix.objects.bulk_update(update_queue, ['_depth', '_children'])
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_available_prefix(ipset, prefix_size):
|
||||||
|
"""
|
||||||
|
Given a prefix length, allocate the next available prefix from an IPSet.
|
||||||
|
"""
|
||||||
|
for available_prefix in ipset.iter_cidrs():
|
||||||
|
if prefix_size >= available_prefix.prefixlen:
|
||||||
|
allocated_prefix = f"{available_prefix.network}/{prefix_size}"
|
||||||
|
ipset.remove(allocated_prefix)
|
||||||
|
return allocated_prefix
|
||||||
|
return None
|
||||||
|
@ -301,12 +301,14 @@ CUSTOMIZATION_MENU = Menu(
|
|||||||
MenuItem(
|
MenuItem(
|
||||||
link='extras:report_list',
|
link='extras:report_list',
|
||||||
link_text=_('Reports'),
|
link_text=_('Reports'),
|
||||||
permissions=['extras.view_report']
|
permissions=['extras.view_report'],
|
||||||
|
buttons=get_model_buttons('extras', "reportmodule", actions=['add'])
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
link='extras:script_list',
|
link='extras:script_list',
|
||||||
link_text=_('Scripts'),
|
link_text=_('Scripts'),
|
||||||
permissions=['extras.view_script']
|
permissions=['extras.view_script'],
|
||||||
|
buttons=get_model_buttons('extras', "scriptmodule", actions=['add'])
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.5.4-dev'
|
VERSION = '3.5.5-dev'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
@ -140,10 +140,14 @@ class BaseTable(tables.Table):
|
|||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
table_name = self.__class__.__name__
|
table_name = self.__class__.__name__
|
||||||
if self.prefixed_order_by_field in request.GET:
|
if self.prefixed_order_by_field in request.GET:
|
||||||
|
if request.GET[self.prefixed_order_by_field]:
|
||||||
# If an ordering has been specified as a query parameter, save it as the
|
# If an ordering has been specified as a query parameter, save it as the
|
||||||
# user's preferred ordering for this table.
|
# user's preferred ordering for this table.
|
||||||
ordering = request.GET.getlist(self.prefixed_order_by_field)
|
ordering = request.GET.getlist(self.prefixed_order_by_field)
|
||||||
request.user.config.set(f'tables.{table_name}.ordering', ordering, commit=True)
|
request.user.config.set(f'tables.{table_name}.ordering', ordering, commit=True)
|
||||||
|
else:
|
||||||
|
# If the ordering has been set to none (empty), clear any existing preference.
|
||||||
|
request.user.config.clear(f'tables.{table_name}.ordering', commit=True)
|
||||||
elif ordering := request.user.config.get(f'tables.{table_name}.ordering'):
|
elif ordering := request.user.config.get(f'tables.{table_name}.ordering'):
|
||||||
# If no ordering has been specified, set the preferred ordering (if any).
|
# If no ordering has been specified, set the preferred ordering (if any).
|
||||||
self.order_by = ordering
|
self.order_by = ordering
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
@ -16,6 +17,9 @@ from utilities.testing import TestCase
|
|||||||
from utilities.testing.api import APITestCase
|
from utilities.testing.api import APITestCase
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class TokenAuthenticationTestCase(APITestCase):
|
class TokenAuthenticationTestCase(APITestCase):
|
||||||
|
|
||||||
@override_settings(LOGIN_REQUIRED=True, EXEMPT_VIEW_PERMISSIONS=['*'])
|
@override_settings(LOGIN_REQUIRED=True, EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
@ -6,12 +6,12 @@
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body" id="object_list">
|
<div class="card-body htmx-container table-responsive" id="object_list">
|
||||||
{% include 'htmx/table.html' %}
|
{% include 'htmx/table.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modals %}
|
{% block modals %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
@ -28,7 +29,7 @@ class NestedUserSerializer(WritableNestedSerializer):
|
|||||||
url = serializers.HyperlinkedIdentityField(view_name='users-api:user-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='users-api:user-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = get_user_model()
|
||||||
fields = ['id', 'url', 'display', 'username']
|
fields = ['id', 'url', 'display', 'username']
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.STR)
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
@ -30,7 +31,7 @@ class UserSerializer(ValidatedModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = get_user_model()
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'url', 'display', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active',
|
'id', 'url', 'display', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active',
|
||||||
'date_joined', 'groups',
|
'date_joined', 'groups',
|
||||||
@ -124,7 +125,7 @@ class ObjectPermissionSerializer(ValidatedModelSerializer):
|
|||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
users = SerializedPKRelatedField(
|
users = SerializedPKRelatedField(
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
serializer=NestedUserSerializer,
|
serializer=NestedUserSerializer,
|
||||||
required=False,
|
required=False,
|
||||||
many=True
|
many=True
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
@ -32,7 +33,7 @@ class UsersRootView(APIRootView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class UserViewSet(NetBoxModelViewSet):
|
class UserViewSet(NetBoxModelViewSet):
|
||||||
queryset = RestrictedQuerySet(model=User).prefetch_related('groups').order_by('username')
|
queryset = RestrictedQuerySet(model=get_user_model()).prefetch_related('groups').order_by('username')
|
||||||
serializer_class = serializers.UserSerializer
|
serializer_class = serializers.UserSerializer
|
||||||
filterset_class = filtersets.UserFilterSet
|
filterset_class = filtersets.UserFilterSet
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
@ -47,7 +48,7 @@ class UserFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = get_user_model()
|
||||||
fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_staff', 'is_active']
|
fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_staff', 'is_active']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
@ -68,12 +69,12 @@ class TokenFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='user',
|
field_name='user',
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
label=_('User'),
|
label=_('User'),
|
||||||
)
|
)
|
||||||
user = django_filters.ModelMultipleChoiceFilter(
|
user = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='user__username',
|
field_name='user__username',
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
to_field_name='username',
|
to_field_name='username',
|
||||||
label=_('User (name)'),
|
label=_('User (name)'),
|
||||||
)
|
)
|
||||||
@ -116,12 +117,12 @@ class ObjectPermissionFilterSet(BaseFilterSet):
|
|||||||
)
|
)
|
||||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='users',
|
field_name='users',
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
label=_('User'),
|
label=_('User'),
|
||||||
)
|
)
|
||||||
user = django_filters.ModelMultipleChoiceFilter(
|
user = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='users__username',
|
field_name='users__username',
|
||||||
queryset=User.objects.all(),
|
queryset=get_user_model().objects.all(),
|
||||||
to_field_name='username',
|
to_field_name='username',
|
||||||
label=_('User (name)'),
|
label=_('User (name)'),
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
from netbox.graphql.fields import ObjectField, ObjectListField
|
from netbox.graphql.fields import ObjectField, ObjectListField
|
||||||
from .types import *
|
from .types import *
|
||||||
from utilities.graphql_optimizer import gql_query_optimizer
|
from utilities.graphql_optimizer import gql_query_optimizer
|
||||||
@ -17,4 +18,4 @@ class UsersQuery(graphene.ObjectType):
|
|||||||
user_list = ObjectListField(UserType)
|
user_list = ObjectListField(UserType)
|
||||||
|
|
||||||
def resolve_user_list(root, info, **kwargs):
|
def resolve_user_list(root, info, **kwargs):
|
||||||
return gql_query_optimizer(User.objects.all(), info)
|
return gql_query_optimizer(get_user_model().objects.all(), info)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
from users import filtersets
|
from users import filtersets
|
||||||
@ -25,7 +26,7 @@ class GroupType(DjangoObjectType):
|
|||||||
class UserType(DjangoObjectType):
|
class UserType(DjangoObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = get_user_model()
|
||||||
fields = (
|
fields = (
|
||||||
'id', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'date_joined',
|
'id', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'date_joined',
|
||||||
'groups',
|
'groups',
|
||||||
@ -34,4 +35,4 @@ class UserType(DjangoObjectType):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_queryset(cls, queryset, info):
|
def get_queryset(cls, queryset, info):
|
||||||
return RestrictedQuerySet(model=User).restrict(info.context.user, 'view')
|
return RestrictedQuerySet(model=get_user_model()).restrict(info.context.user, 'view')
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
@ -7,6 +8,9 @@ from utilities.testing import APIViewTestCases, APITestCase
|
|||||||
from utilities.utils import deepmerge
|
from utilities.utils import deepmerge
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class AppTest(APITestCase):
|
class AppTest(APITestCase):
|
||||||
|
|
||||||
def test_root(self):
|
def test_root(self):
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
@ -10,6 +11,9 @@ from users.models import ObjectPermission, Token
|
|||||||
from utilities.testing import BaseFilterSetTests
|
from utilities.testing import BaseFilterSetTests
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class UserTestCase(TestCase, BaseFilterSetTests):
|
class UserTestCase(TestCase, BaseFilterSetTests):
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
filterset = filtersets.UserFilterSet
|
filterset = filtersets.UserFilterSet
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class UserConfigTest(TestCase):
|
class UserConfigTest(TestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -16,6 +16,9 @@ DEFAULT_USER_PREFERENCES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class UserPreferencesTest(TestCase):
|
class UserPreferencesTest(TestCase):
|
||||||
user_permissions = ['dcim.view_site']
|
user_permissions = ['dcim.view_site']
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import inspect
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
@ -26,6 +26,9 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# REST/GraphQL API Tests
|
# REST/GraphQL API Tests
|
||||||
#
|
#
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.core.exceptions import FieldDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
@ -27,7 +27,7 @@ class TestCase(_TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
# Create the test user and assign permissions
|
# Create the test user and assign permissions
|
||||||
self.user = User.objects.create_user(username='testuser')
|
self.user = get_user_model().objects.create_user(username='testuser')
|
||||||
self.add_permissions(*self.user_permissions)
|
self.add_permissions(*self.user_permissions)
|
||||||
|
|
||||||
# Initialize the test client
|
# Initialize the test client
|
||||||
|
@ -2,7 +2,8 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission, User
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
||||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||||
@ -63,7 +64,7 @@ def create_test_user(username='testuser', permissions=None):
|
|||||||
"""
|
"""
|
||||||
Create a User with the given permissions.
|
Create a User with the given permissions.
|
||||||
"""
|
"""
|
||||||
user = User.objects.create_user(username=username)
|
user = get_user_model().objects.create_user(username=username)
|
||||||
if permissions is None:
|
if permissions is None:
|
||||||
permissions = ()
|
permissions = ()
|
||||||
for perm_name in permissions:
|
for perm_name in permissions:
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
bleach==6.0.0
|
bleach==6.0.0
|
||||||
boto3==1.26.145
|
boto3==1.26.156
|
||||||
Django==4.2.2
|
Django==4.2.2
|
||||||
django-cors-headers==4.0.0
|
django-cors-headers==4.1.0
|
||||||
django-debug-toolbar==4.1.0
|
django-debug-toolbar==4.1.0
|
||||||
django-filter==23.2
|
django-filter==23.2
|
||||||
django-graphiql-debug-toolbar==0.2.0
|
django-graphiql-debug-toolbar==0.2.0
|
||||||
django-mptt==0.14
|
django-mptt==0.14
|
||||||
django-pglocks==1.0.4
|
django-pglocks==1.0.4
|
||||||
django-prometheus==2.3.1
|
django-prometheus==2.3.1
|
||||||
django-redis==5.2.0
|
django-redis==5.3.0
|
||||||
django-rich==1.5.0
|
django-rich==1.6.0
|
||||||
django-rq==2.8.1
|
django-rq==2.8.1
|
||||||
django-tables2==2.5.3
|
django-tables2==2.5.3
|
||||||
django-taggit==4.0.0
|
django-taggit==4.0.0
|
||||||
django-timezone-field==5.0
|
django-timezone-field==5.1
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.14.0
|
||||||
drf-spectacular==0.26.2
|
drf-spectacular==0.26.2
|
||||||
drf-spectacular-sidecar==2023.6.1
|
drf-spectacular-sidecar==2023.6.1
|
||||||
@ -23,7 +23,7 @@ graphene-django==3.0.0
|
|||||||
gunicorn==20.1.0
|
gunicorn==20.1.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
Markdown==3.3.7
|
Markdown==3.3.7
|
||||||
mkdocs-material==9.1.15
|
mkdocs-material==9.1.16
|
||||||
mkdocstrings[python-legacy]==0.22.0
|
mkdocstrings[python-legacy]==0.22.0
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==9.5.0
|
Pillow==9.5.0
|
||||||
@ -31,9 +31,9 @@ psycopg==3.1.9
|
|||||||
psycopg-binary==3.1.9
|
psycopg-binary==3.1.9
|
||||||
psycopg-pool==3.1.7
|
psycopg-pool==3.1.7
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
sentry-sdk==1.25.0
|
sentry-sdk==1.25.1
|
||||||
social-auth-app-django==5.2.0
|
social-auth-app-django==5.2.0
|
||||||
social-auth-core[openidconnect]==4.4.2
|
social-auth-core[openidconnect]==4.4.2
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
tablib==3.4.0
|
tablib==3.5.0
|
||||||
tzdata==2023.3
|
tzdata==2023.3
|
||||||
|
Loading…
Reference in New Issue
Block a user