Sfoglia il codice sorgente

Merge pull request #9706 from netbox-community/develop

Release v3.2.6
Jeremy Stretch 3 anni fa
parent
commit
b72793a85a
51 ha cambiato i file con 438 aggiunte e 185 eliminazioni
  1. 1 1
      .github/ISSUE_TEMPLATE/bug_report.yaml
  2. 1 1
      .github/ISSUE_TEMPLATE/feature_request.yaml
  3. 6 0
      NOTICE
  4. 4 4
      base_requirements.txt
  5. 36 12
      docs/configuration/dynamic-settings.md
  6. 1 1
      docs/models/extras/customfield.md
  7. 24 1
      docs/release-notes/version-3.2.md
  8. 3 4
      netbox/circuits/tables/circuits.py
  9. 3 3
      netbox/circuits/views.py
  10. 9 0
      netbox/dcim/api/nested_serializers.py
  11. 2 2
      netbox/dcim/api/serializers.py
  12. 1 1
      netbox/dcim/api/views.py
  13. 0 9
      netbox/dcim/constants.py
  14. 6 0
      netbox/dcim/filtersets.py
  15. 16 1
      netbox/dcim/forms/models.py
  16. 3 3
      netbox/dcim/migrations/0001_squashed.py
  17. 1 2
      netbox/dcim/models/device_components.py
  18. 4 3
      netbox/dcim/models/power.py
  19. 3 4
      netbox/dcim/tables/cables.py
  20. 5 7
      netbox/dcim/tables/devices.py
  21. 5 7
      netbox/dcim/tables/racks.py
  22. 5 7
      netbox/dcim/tables/sites.py
  23. 5 0
      netbox/dcim/tests/test_filtersets.py
  24. 1 1
      netbox/dcim/views.py
  25. 3 0
      netbox/extras/admin.py
  26. 1 1
      netbox/extras/api/serializers.py
  27. 12 3
      netbox/extras/filtersets.py
  28. 8 6
      netbox/extras/forms/filtersets.py
  29. 2 7
      netbox/extras/models/customfields.py
  30. 64 1
      netbox/extras/tests/test_filtersets.py
  31. 13 1
      netbox/ipam/forms/filtersets.py
  32. 11 16
      netbox/ipam/tables/ip.py
  33. 3 4
      netbox/ipam/tables/vlans.py
  34. 5 7
      netbox/ipam/tables/vrfs.py
  35. 14 11
      netbox/ipam/views.py
  36. 23 0
      netbox/netbox/authentication.py
  37. 25 0
      netbox/netbox/config/parameters.py
  38. 10 10
      netbox/netbox/constants.py
  39. 14 1
      netbox/netbox/settings.py
  40. 2 1
      netbox/netbox/views/generic/bulk_views.py
  41. 0 0
      netbox/project-static/dist/netbox.js
  42. 0 0
      netbox/project-static/dist/netbox.js.map
  43. 1 0
      netbox/project-static/src/select/api/apiSelect.ts
  44. 4 12
      netbox/templates/dcim/device.html
  45. 9 0
      netbox/templates/dcim/device_edit.html
  46. 19 15
      netbox/templates/extras/object_journal.html
  47. 31 0
      netbox/tenancy/tables/columns.py
  48. 7 0
      netbox/utilities/management/commands/__init__.py
  49. 3 5
      netbox/virtualization/tables/clusters.py
  50. 3 4
      netbox/virtualization/tables/virtualmachines.py
  51. 6 6
      requirements.txt

+ 1 - 1
.github/ISSUE_TEMPLATE/bug_report.yaml

@@ -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.2.5
+      placeholder: v3.2.6
     validations:
     validations:
       required: true
       required: true
   - type: dropdown
   - type: dropdown

+ 1 - 1
.github/ISSUE_TEMPLATE/feature_request.yaml

@@ -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.2.5
+      placeholder: v3.2.6
     validations:
     validations:
       required: true
       required: true
   - type: dropdown
   - type: dropdown

+ 6 - 0
NOTICE

@@ -1 +1,7 @@
 Copyrighted and licensed under Apache License 2.0 by DigitalOcean, LLC.
 Copyrighted and licensed under Apache License 2.0 by DigitalOcean, LLC.
+
+This project contains code developed expressly for NetBox, and its reuse in
+other projects may introduce issues affecting performance, data integrity,
+and security.
+
+For more information, please see https://github.com/netbox-community/netbox.

+ 4 - 4
base_requirements.txt

@@ -1,3 +1,7 @@
+# HTML sanitizer
+# https://github.com/mozilla/bleach
+bleach
+
 # The Python web framework on which NetBox is built
 # The Python web framework on which NetBox is built
 # https://github.com/django/django
 # https://github.com/django/django
 Django
 Django
@@ -126,7 +130,3 @@ tablib
 # Timezone data (required by django-timezone-field on Python 3.9+)
 # Timezone data (required by django-timezone-field on Python 3.9+)
 # https://github.com/python/tzdata
 # https://github.com/python/tzdata
 tzdata
 tzdata
-
-# HTML sanitizer
-# https://github.com/mozilla/bleach
-bleach

+ 36 - 12
docs/configuration/dynamic-settings.md

@@ -43,18 +43,6 @@ changes in the database indefinitely.
 
 
 ---
 ---
 
 
-## JOBRESULT_RETENTION
-
-Default: 90
-
-The number of days to retain job results (scripts and reports). Set this to `0` to retain
-job results in the database indefinitely.
-
-!!! warning
-    If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
-
----
-
 ## CUSTOM_VALIDATORS
 ## CUSTOM_VALIDATORS
 
 
 This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:
 This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:
@@ -110,6 +98,18 @@ Setting this to False will disable the GraphQL API.
 
 
 ---
 ---
 
 
+## JOBRESULT_RETENTION
+
+Default: 90
+
+The number of days to retain job results (scripts and reports). Set this to `0` to retain
+job results in the database indefinitely.
+
+!!! warning
+    If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
+
+---
+
 ## MAINTENANCE_MODE
 ## MAINTENANCE_MODE
 
 
 Default: False
 Default: False
@@ -185,6 +185,30 @@ The default maximum number of objects to display per page within each list of ob
 
 
 ---
 ---
 
 
+## POWERFEED_DEFAULT_AMPERAGE
+
+Default: 15
+
+The default value for the `amperage` field when creating new power feeds.
+
+---
+
+## POWERFEED_DEFAULT_MAX_UTILIZATION
+
+Default: 80
+
+The default value (percentage) for the `max_utilization` field when creating new power feeds.
+
+---
+
+## POWERFEED_DEFAULT_VOLTAGE
+
+Default: 120
+
+The default value for the `voltage` field when creating new power feeds.
+
+---
+
 ## PREFER_IPV4
 ## PREFER_IPV4
 
 
 Default: False
 Default: False

+ 1 - 1
docs/models/extras/customfield.md

@@ -10,7 +10,7 @@ Within the database, custom fields are stored as JSON data directly alongside ea
 
 
 Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field:
 Custom fields may be created by navigating to Customization > Custom Fields. NetBox supports six types of custom field:
 
 
-* Text: Free-form text (up to 255 characters)
+* Text: Free-form text (intended for single-line use)
 * Long text: Free-form of any length; supports Markdown rendering
 * Long text: Free-form of any length; supports Markdown rendering
 * Integer: A whole number (positive or negative)
 * Integer: A whole number (positive or negative)
 * Boolean: True or false
 * Boolean: True or false

+ 24 - 1
docs/release-notes/version-3.2.md

@@ -1,5 +1,28 @@
 # NetBox v3.2
 # NetBox v3.2
 
 
+## v3.2.6 (2022-07-11)
+
+### Enhancements
+
+* [#7702](https://github.com/netbox-community/netbox/issues/7702) - Enable dynamic configuration for default powerfeed attributes
+* [#9396](https://github.com/netbox-community/netbox/issues/9396) - Allow filtering modules by bay ID
+* [#9403](https://github.com/netbox-community/netbox/issues/9403) - Enable modifying virtual chassis properties when creating/editing a device
+* [#9540](https://github.com/netbox-community/netbox/issues/9540) - Add filters for assigned device & VM to IP addresses list
+* [#9686](https://github.com/netbox-community/netbox/issues/9686) - Add tenant group column for all object tables with tenant assignments
+
+### Bug Fixes
+
+* [#8854](https://github.com/netbox-community/netbox/issues/8854) - Fix `REMOTE_AUTH_DEFAULT_GROUPS` for social-auth backends
+* [#9575](https://github.com/netbox-community/netbox/issues/9575) - Fix AttributeError exception for FHRP group with an IP address assigned
+* [#9597](https://github.com/netbox-community/netbox/issues/9597) - Include `installed_module` in module bay REST API serializer
+* [#9632](https://github.com/netbox-community/netbox/issues/9632) - Automatically focus on search box when expanding dropdowns
+* [#9657](https://github.com/netbox-community/netbox/issues/9657) - Fix filtering for custom fields and webhooks in the UI
+* [#9682](https://github.com/netbox-community/netbox/issues/9682) - Fix bulk assignment of ASNs to sites
+* [#9687](https://github.com/netbox-community/netbox/issues/9687) - Don't restrict custom text field lengths when entering via UI form
+* [#9704](https://github.com/netbox-community/netbox/issues/9704) - Include `last_updated` field on JournalEntry REST API serializer
+
+---
+
 ## v3.2.5 (2022-06-20)
 ## v3.2.5 (2022-06-20)
 
 
 ### Enhancements
 ### Enhancements
@@ -25,7 +48,7 @@
 * [#9484](https://github.com/netbox-community/netbox/issues/9484) - Include services listening on "all IPs" under IP address view
 * [#9484](https://github.com/netbox-community/netbox/issues/9484) - Include services listening on "all IPs" under IP address view
 * [#9486](https://github.com/netbox-community/netbox/issues/9486) - Fix redirect URL when adding device components from the module view
 * [#9486](https://github.com/netbox-community/netbox/issues/9486) - Fix redirect URL when adding device components from the module view
 * [#9495](https://github.com/netbox-community/netbox/issues/9495) - Correct link to contacts in contact groups table column
 * [#9495](https://github.com/netbox-community/netbox/issues/9495) - Correct link to contacts in contact groups table column
-* [#9503](https://github.com/netbox-community/netbox/issues/9503) - Hyperlinks in ack elevation SVGs must always use absolute URLs
+* [#9503](https://github.com/netbox-community/netbox/issues/9503) - Hyperlinks in rack elevation SVGs must always use absolute URLs
 * [#9512](https://github.com/netbox-community/netbox/issues/9512) - Fix duplicate site results when searching by ASN
 * [#9512](https://github.com/netbox-community/netbox/issues/9512) - Fix duplicate site results when searching by ASN
 * [#9524](https://github.com/netbox-community/netbox/issues/9524) - Correct order of VLAN fields under VM interface creation form
 * [#9524](https://github.com/netbox-community/netbox/issues/9524) - Correct order of VLAN fields under VM interface creation form
 * [#9537](https://github.com/netbox-community/netbox/issues/9537) - Ensure consistent use of placeholder tag throughout UI
 * [#9537](https://github.com/netbox-community/netbox/issues/9537) - Ensure consistent use of placeholder tag throughout UI

+ 3 - 4
netbox/circuits/tables/circuits.py

@@ -2,7 +2,7 @@ import django_tables2 as tables
 
 
 from circuits.models import *
 from circuits.models import *
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin
 from .columns import CommitRateColumn
 from .columns import CommitRateColumn
 
 
 __all__ = (
 __all__ = (
@@ -39,7 +39,7 @@ class CircuitTypeTable(NetBoxTable):
         default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
         default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
 
 
 
 
-class CircuitTable(NetBoxTable):
+class CircuitTable(TenancyColumnsMixin, NetBoxTable):
     cid = tables.Column(
     cid = tables.Column(
         linkify=True,
         linkify=True,
         verbose_name='Circuit ID'
         verbose_name='Circuit ID'
@@ -48,7 +48,6 @@ class CircuitTable(NetBoxTable):
         linkify=True
         linkify=True
     )
     )
     status = columns.ChoiceFieldColumn()
     status = columns.ChoiceFieldColumn()
-    tenant = TenantColumn()
     termination_a = tables.TemplateColumn(
     termination_a = tables.TemplateColumn(
         template_code=CIRCUITTERMINATION_LINK,
         template_code=CIRCUITTERMINATION_LINK,
         verbose_name='Side A'
         verbose_name='Side A'
@@ -69,7 +68,7 @@ class CircuitTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Circuit
         model = Circuit
         fields = (
         fields = (
-            'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date',
+            'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'tenant_group', 'termination_a', 'termination_z', 'install_date',
             'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
             'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
         )
         )
         default_columns = (
         default_columns = (

+ 3 - 3
netbox/circuits/views.py

@@ -30,7 +30,7 @@ class ProviderView(generic.ObjectView):
         circuits = Circuit.objects.restrict(request.user, 'view').filter(
         circuits = Circuit.objects.restrict(request.user, 'view').filter(
             provider=instance
             provider=instance
         ).prefetch_related(
         ).prefetch_related(
-            'type', 'tenant', 'terminations__site'
+            'type', 'tenant', 'tenant__group', 'terminations__site'
         )
         )
         circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('provider',))
         circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('provider',))
         circuits_table.configure(request)
         circuits_table.configure(request)
@@ -91,7 +91,7 @@ class ProviderNetworkView(generic.ObjectView):
             Q(termination_a__provider_network=instance.pk) |
             Q(termination_a__provider_network=instance.pk) |
             Q(termination_z__provider_network=instance.pk)
             Q(termination_z__provider_network=instance.pk)
         ).prefetch_related(
         ).prefetch_related(
-            'type', 'tenant', 'terminations__site'
+            'type', 'tenant', 'tenant__group', 'terminations__site'
         )
         )
         circuits_table = tables.CircuitTable(circuits, user=request.user)
         circuits_table = tables.CircuitTable(circuits, user=request.user)
         circuits_table.configure(request)
         circuits_table.configure(request)
@@ -192,7 +192,7 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
 
 
 class CircuitListView(generic.ObjectListView):
 class CircuitListView(generic.ObjectListView):
     queryset = Circuit.objects.prefetch_related(
     queryset = Circuit.objects.prefetch_related(
-        'provider', 'type', 'tenant', 'termination_a', 'termination_z'
+        'provider', 'type', 'tenant', 'tenant__group', 'termination_a', 'termination_z'
     )
     )
     filterset = filtersets.CircuitFilterSet
     filterset = filtersets.CircuitFilterSet
     filterset_form = forms.CircuitFilterForm
     filterset_form = forms.CircuitFilterForm

+ 9 - 0
netbox/dcim/api/nested_serializers.py

@@ -5,6 +5,7 @@ from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
 
 
 __all__ = [
 __all__ = [
     'ComponentNestedModuleSerializer',
     'ComponentNestedModuleSerializer',
+    'ModuleBayNestedModuleSerializer',
     'NestedCableSerializer',
     'NestedCableSerializer',
     'NestedConsolePortSerializer',
     'NestedConsolePortSerializer',
     'NestedConsolePortTemplateSerializer',
     'NestedConsolePortTemplateSerializer',
@@ -281,6 +282,14 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
         fields = ['id', 'url', 'display', 'name']
         fields = ['id', 'url', 'display', 'name']
 
 
 
 
+class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
+
+    class Meta:
+        model = models.Module
+        fields = ['id', 'url', 'display', 'serial']
+
+
 class ComponentNestedModuleSerializer(WritableNestedSerializer):
 class ComponentNestedModuleSerializer(WritableNestedSerializer):
     """
     """
     Used by device component serializers.
     Used by device component serializers.

+ 2 - 2
netbox/dcim/api/serializers.py

@@ -886,12 +886,12 @@ class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
 class ModuleBaySerializer(NetBoxModelSerializer):
 class ModuleBaySerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
     device = NestedDeviceSerializer()
     device = NestedDeviceSerializer()
-    # installed_module = NestedModuleSerializer(required=False, allow_null=True)
+    installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = ModuleBay
         model = ModuleBay
         fields = [
         fields = [
-            'id', 'url', 'display', 'device', 'name', 'label', 'position', 'description', 'tags', 'custom_fields',
+            'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags', 'custom_fields',
             'created', 'last_updated',
             'created', 'last_updated',
         ]
         ]
 
 

+ 1 - 1
netbox/dcim/api/views.py

@@ -611,7 +611,7 @@ class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
 
 
 
 
 class ModuleBayViewSet(NetBoxModelViewSet):
 class ModuleBayViewSet(NetBoxModelViewSet):
-    queryset = ModuleBay.objects.prefetch_related('tags')
+    queryset = ModuleBay.objects.prefetch_related('tags', 'installed_module')
     serializer_class = serializers.ModuleBaySerializer
     serializer_class = serializers.ModuleBaySerializer
     filterset_class = filtersets.ModuleBayFilterSet
     filterset_class = filtersets.ModuleBayFilterSet
     brief_prefetch_fields = ['device']
     brief_prefetch_fields = ['device']

+ 0 - 9
netbox/dcim/constants.py

@@ -49,15 +49,6 @@ WIRELESS_IFACE_TYPES = [
 NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
 NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
 
 
 
 
-#
-# Power feeds
-#
-
-POWERFEED_VOLTAGE_DEFAULT = 120
-POWERFEED_AMPERAGE_DEFAULT = 20
-POWERFEED_MAX_UTILIZATION_DEFAULT = 80  # Percentage
-
-
 #
 #
 # Device components
 # Device components
 #
 #

+ 6 - 0
netbox/dcim/filtersets.py

@@ -992,6 +992,12 @@ class ModuleFilterSet(NetBoxModelFilterSet):
         to_field_name='model',
         to_field_name='model',
         label='Module type (model)',
         label='Module type (model)',
     )
     )
+    module_bay_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='module_bay',
+        queryset=ModuleBay.objects.all(),
+        to_field_name='id',
+        label='Module Bay (ID)'
+    )
     device_id = django_filters.ModelMultipleChoiceFilter(
     device_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Device.objects.all(),
         queryset=Device.objects.all(),
         label='Device (ID)',
         label='Device (ID)',

+ 16 - 1
netbox/dcim/forms/models.py

@@ -521,13 +521,28 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
         required=False,
         required=False,
         label=''
         label=''
     )
     )
+    virtual_chassis = DynamicModelChoiceField(
+        queryset=VirtualChassis.objects.all(),
+        required=False
+    )
+    vc_position = forms.IntegerField(
+        required=False,
+        label='Position',
+        help_text="The position in the virtual chassis this device is identified by"
+    )
+    vc_priority = forms.IntegerField(
+        required=False,
+        label='Priority',
+        help_text="The priority of the device in the virtual chassis"
+    )
 
 
     class Meta:
     class Meta:
         model = Device
         model = Device
         fields = [
         fields = [
             'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
             'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
             'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
             'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
-            'cluster_group', 'cluster', 'tenant_group', 'tenant', 'comments', 'tags', 'local_context_data'
+            'cluster_group', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority',
+            'comments', 'tags', 'local_context_data'
         ]
         ]
         help_texts = {
         help_texts = {
             'device_role': "The function this device serves",
             'device_role': "The function this device serves",

+ 3 - 3
netbox/dcim/migrations/0001_squashed.py

@@ -386,9 +386,9 @@ class Migration(migrations.Migration):
                 ('type', models.CharField(default='primary', max_length=50)),
                 ('type', models.CharField(default='primary', max_length=50)),
                 ('supply', models.CharField(default='ac', max_length=50)),
                 ('supply', models.CharField(default='ac', max_length=50)),
                 ('phase', models.CharField(default='single-phase', max_length=50)),
                 ('phase', models.CharField(default='single-phase', max_length=50)),
-                ('voltage', models.SmallIntegerField(default=120, validators=[utilities.validators.ExclusionValidator([0])])),
-                ('amperage', models.PositiveSmallIntegerField(default=20, validators=[django.core.validators.MinValueValidator(1)])),
-                ('max_utilization', models.PositiveSmallIntegerField(default=80, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
+                ('voltage', models.SmallIntegerField(validators=[utilities.validators.ExclusionValidator([0])])),
+                ('amperage', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1)])),
+                ('max_utilization', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])),
                 ('available_power', models.PositiveIntegerField(default=0, editable=False)),
                 ('available_power', models.PositiveIntegerField(default=0, editable=False)),
                 ('comments', models.TextField(blank=True)),
                 ('comments', models.TextField(blank=True)),
             ],
             ],

+ 1 - 2
netbox/dcim/models/device_components.py

@@ -95,8 +95,7 @@ class ModularComponentModel(ComponentModel):
     inventory_items = GenericRelation(
     inventory_items = GenericRelation(
         to='dcim.InventoryItem',
         to='dcim.InventoryItem',
         content_type_field='component_type',
         content_type_field='component_type',
-        object_id_field='component_id',
-        related_name='%(class)ss',
+        object_id_field='component_id'
     )
     )
 
 
     class Meta:
     class Meta:

+ 4 - 3
netbox/dcim/models/power.py

@@ -6,6 +6,7 @@ from django.urls import reverse
 
 
 from dcim.choices import *
 from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
+from netbox.config import ConfigItem
 from netbox.models import NetBoxModel
 from netbox.models import NetBoxModel
 from utilities.validators import ExclusionValidator
 from utilities.validators import ExclusionValidator
 from .device_components import LinkTermination, PathEndpoint
 from .device_components import LinkTermination, PathEndpoint
@@ -105,16 +106,16 @@ class PowerFeed(NetBoxModel, PathEndpoint, LinkTermination):
         default=PowerFeedPhaseChoices.PHASE_SINGLE
         default=PowerFeedPhaseChoices.PHASE_SINGLE
     )
     )
     voltage = models.SmallIntegerField(
     voltage = models.SmallIntegerField(
-        default=POWERFEED_VOLTAGE_DEFAULT,
+        default=ConfigItem('POWERFEED_DEFAULT_VOLTAGE'),
         validators=[ExclusionValidator([0])]
         validators=[ExclusionValidator([0])]
     )
     )
     amperage = models.PositiveSmallIntegerField(
     amperage = models.PositiveSmallIntegerField(
         validators=[MinValueValidator(1)],
         validators=[MinValueValidator(1)],
-        default=POWERFEED_AMPERAGE_DEFAULT
+        default=ConfigItem('POWERFEED_DEFAULT_AMPERAGE')
     )
     )
     max_utilization = models.PositiveSmallIntegerField(
     max_utilization = models.PositiveSmallIntegerField(
         validators=[MinValueValidator(1), MaxValueValidator(100)],
         validators=[MinValueValidator(1), MaxValueValidator(100)],
-        default=POWERFEED_MAX_UTILIZATION_DEFAULT,
+        default=ConfigItem('POWERFEED_DEFAULT_MAX_UTILIZATION'),
         help_text="Maximum permissible draw (percentage)"
         help_text="Maximum permissible draw (percentage)"
     )
     )
     available_power = models.PositiveIntegerField(
     available_power = models.PositiveIntegerField(

+ 3 - 4
netbox/dcim/tables/cables.py

@@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
 
 
 from dcim.models import Cable
 from dcim.models import Cable
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin
 from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
 from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
 
 
 __all__ = (
 __all__ = (
@@ -15,7 +15,7 @@ __all__ = (
 # Cables
 # Cables
 #
 #
 
 
-class CableTable(NetBoxTable):
+class CableTable(TenancyColumnsMixin, NetBoxTable):
     termination_a_parent = tables.TemplateColumn(
     termination_a_parent = tables.TemplateColumn(
         template_code=CABLE_TERMINATION_PARENT,
         template_code=CABLE_TERMINATION_PARENT,
         accessor=Accessor('termination_a'),
         accessor=Accessor('termination_a'),
@@ -53,7 +53,6 @@ class CableTable(NetBoxTable):
         verbose_name='Termination B'
         verbose_name='Termination B'
     )
     )
     status = columns.ChoiceFieldColumn()
     status = columns.ChoiceFieldColumn()
-    tenant = TenantColumn()
     length = columns.TemplateColumn(
     length = columns.TemplateColumn(
         template_code=CABLE_LENGTH,
         template_code=CABLE_LENGTH,
         order_by=('_abs_length', 'length_unit')
         order_by=('_abs_length', 'length_unit')
@@ -67,7 +66,7 @@ class CableTable(NetBoxTable):
         model = Cable
         model = Cable
         fields = (
         fields = (
             'pk', 'id', 'label', 'termination_a_parent', 'rack_a', 'termination_a', 'termination_b_parent', 'rack_b', 'termination_b',
             'pk', 'id', 'label', 'termination_a_parent', 'rack_a', 'termination_a', 'termination_b_parent', 'rack_b', 'termination_b',
-            'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated',
+            'status', 'type', 'tenant', 'tenant_group', 'color', 'length', 'tags', 'created', 'last_updated',
         )
         )
         default_columns = (
         default_columns = (
             'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
             'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',

+ 5 - 7
netbox/dcim/tables/devices.py

@@ -6,7 +6,7 @@ from dcim.models import (
     InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
     InventoryItemRole, ModuleBay, Platform, PowerOutlet, PowerPort, RearPort, VirtualChassis,
 )
 )
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin
 from .template_code import *
 from .template_code import *
 
 
 __all__ = (
 __all__ = (
@@ -137,13 +137,12 @@ class PlatformTable(NetBoxTable):
 # Devices
 # Devices
 #
 #
 
 
-class DeviceTable(NetBoxTable):
+class DeviceTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.TemplateColumn(
     name = tables.TemplateColumn(
         order_by=('_name',),
         order_by=('_name',),
         template_code=DEVICE_LINK
         template_code=DEVICE_LINK
     )
     )
     status = columns.ChoiceFieldColumn()
     status = columns.ChoiceFieldColumn()
-    tenant = TenantColumn()
     site = tables.Column(
     site = tables.Column(
         linkify=True
         linkify=True
     )
     )
@@ -200,7 +199,7 @@ class DeviceTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Device
         model = Device
         fields = (
         fields = (
-            'pk', 'id', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
+            'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial',
             'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
             'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'airflow', 'primary_ip4',
             'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
             'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
             'created', 'last_updated',
             'created', 'last_updated',
@@ -211,12 +210,11 @@ class DeviceTable(NetBoxTable):
         )
         )
 
 
 
 
-class DeviceImportTable(NetBoxTable):
+class DeviceImportTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.TemplateColumn(
     name = tables.TemplateColumn(
         template_code=DEVICE_LINK
         template_code=DEVICE_LINK
     )
     )
     status = columns.ChoiceFieldColumn()
     status = columns.ChoiceFieldColumn()
-    tenant = TenantColumn()
     site = tables.Column(
     site = tables.Column(
         linkify=True
         linkify=True
     )
     )
@@ -232,7 +230,7 @@ class DeviceImportTable(NetBoxTable):
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Device
         model = Device
-        fields = ('id', 'name', 'status', 'tenant', 'site', 'rack', 'position', 'device_role', 'device_type')
+        fields = ('id', 'name', 'status', 'tenant', 'tenant_group', 'site', 'rack', 'position', 'device_role', 'device_type')
         empty_text = False
         empty_text = False
 
 
 
 

+ 5 - 7
netbox/dcim/tables/racks.py

@@ -3,7 +3,7 @@ from django_tables2.utils import Accessor
 
 
 from dcim.models import Rack, RackReservation, RackRole
 from dcim.models import Rack, RackReservation, RackRole
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin
 
 
 __all__ = (
 __all__ = (
     'RackTable',
     'RackTable',
@@ -37,7 +37,7 @@ class RackRoleTable(NetBoxTable):
 # Racks
 # Racks
 #
 #
 
 
-class RackTable(NetBoxTable):
+class RackTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.Column(
     name = tables.Column(
         order_by=('_name',),
         order_by=('_name',),
         linkify=True
         linkify=True
@@ -48,7 +48,6 @@ class RackTable(NetBoxTable):
     site = tables.Column(
     site = tables.Column(
         linkify=True
         linkify=True
     )
     )
-    tenant = TenantColumn()
     status = columns.ChoiceFieldColumn()
     status = columns.ChoiceFieldColumn()
     role = columns.ColoredLabelColumn()
     role = columns.ColoredLabelColumn()
     u_height = tables.TemplateColumn(
     u_height = tables.TemplateColumn(
@@ -87,7 +86,7 @@ class RackTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Rack
         model = Rack
         fields = (
         fields = (
-            'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag',
+            'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial', 'asset_tag',
             'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
             'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
             'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
             'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
         )
         )
@@ -101,7 +100,7 @@ class RackTable(NetBoxTable):
 # Rack reservations
 # Rack reservations
 #
 #
 
 
-class RackReservationTable(NetBoxTable):
+class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
     reservation = tables.Column(
     reservation = tables.Column(
         accessor='pk',
         accessor='pk',
         linkify=True
         linkify=True
@@ -110,7 +109,6 @@ class RackReservationTable(NetBoxTable):
         accessor=Accessor('rack__site'),
         accessor=Accessor('rack__site'),
         linkify=True
         linkify=True
     )
     )
-    tenant = TenantColumn()
     rack = tables.Column(
     rack = tables.Column(
         linkify=True
         linkify=True
     )
     )
@@ -125,7 +123,7 @@ class RackReservationTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = RackReservation
         model = RackReservation
         fields = (
         fields = (
-            'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'description', 'tags',
+            'pk', 'id', 'reservation', 'site', 'rack', 'unit_list', 'user', 'created', 'tenant', 'tenant_group', 'description', 'tags',
             'actions', 'created', 'last_updated',
             'actions', 'created', 'last_updated',
         )
         )
         default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')
         default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'user', 'description')

+ 5 - 7
netbox/dcim/tables/sites.py

@@ -2,7 +2,7 @@ import django_tables2 as tables
 
 
 from dcim.models import Location, Region, Site, SiteGroup
 from dcim.models import Location, Region, Site, SiteGroup
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin
 from .template_code import LOCATION_BUTTONS
 from .template_code import LOCATION_BUTTONS
 
 
 __all__ = (
 __all__ = (
@@ -75,7 +75,7 @@ class SiteGroupTable(NetBoxTable):
 # Sites
 # Sites
 #
 #
 
 
-class SiteTable(NetBoxTable):
+class SiteTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.Column(
     name = tables.Column(
         linkify=True
         linkify=True
     )
     )
@@ -96,7 +96,6 @@ class SiteTable(NetBoxTable):
         url_params={'site_id': 'pk'},
         url_params={'site_id': 'pk'},
         verbose_name='ASN Count'
         verbose_name='ASN Count'
     )
     )
-    tenant = TenantColumn()
     comments = columns.MarkdownColumn()
     comments = columns.MarkdownColumn()
     contacts = columns.ManyToManyColumn(
     contacts = columns.ManyToManyColumn(
         linkify_item=True
         linkify_item=True
@@ -108,7 +107,7 @@ class SiteTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Site
         model = Site
         fields = (
         fields = (
-            'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asns', 'asn_count',
+            'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'tenant_group', 'asns', 'asn_count',
             'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments',
             'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments',
             'contacts', 'tags', 'created', 'last_updated', 'actions',
             'contacts', 'tags', 'created', 'last_updated', 'actions',
         )
         )
@@ -119,14 +118,13 @@ class SiteTable(NetBoxTable):
 # Locations
 # Locations
 #
 #
 
 
-class LocationTable(NetBoxTable):
+class LocationTable(TenancyColumnsMixin, NetBoxTable):
     name = columns.MPTTColumn(
     name = columns.MPTTColumn(
         linkify=True
         linkify=True
     )
     )
     site = tables.Column(
     site = tables.Column(
         linkify=True
         linkify=True
     )
     )
-    tenant = TenantColumn()
     rack_count = columns.LinkedCountColumn(
     rack_count = columns.LinkedCountColumn(
         viewname='dcim:rack_list',
         viewname='dcim:rack_list',
         url_params={'location_id': 'pk'},
         url_params={'location_id': 'pk'},
@@ -150,7 +148,7 @@ class LocationTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Location
         model = Location
         fields = (
         fields = (
-            'pk', 'id', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description', 'slug', 'contacts',
+            'pk', 'id', 'name', 'site', 'tenant', 'tenant_group', 'rack_count', 'device_count', 'description', 'slug', 'contacts',
             'tags', 'actions', 'created', 'last_updated',
             'tags', 'actions', 'created', 'last_updated',
         )
         )
         default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description')
         default_columns = ('pk', 'name', 'site', 'tenant', 'rack_count', 'device_count', 'description')

+ 5 - 0
netbox/dcim/tests/test_filtersets.py

@@ -1849,6 +1849,11 @@ class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'module_type': [module_types[0].model, module_types[1].model]}
         params = {'module_type': [module_types[0].model, module_types[1].model]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
 
 
+    def test_module_bay(self):
+        module_bays = ModuleBay.objects.all()[:2]
+        params = {'module_bay_id': [module_bays[0].pk, module_bays[1].pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
     def test_device(self):
     def test_device(self):
         device_types = Device.objects.all()[:2]
         device_types = Device.objects.all()[:2]
         params = {'device_id': [device_types[0].pk, device_types[1].pk]}
         params = {'device_id': [device_types[0].pk, device_types[1].pk]}

+ 1 - 1
netbox/dcim/views.py

@@ -561,7 +561,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
 
 
 class RackListView(generic.ObjectListView):
 class RackListView(generic.ObjectListView):
     queryset = Rack.objects.prefetch_related(
     queryset = Rack.objects.prefetch_related(
-        'site', 'location', 'tenant', 'role', 'devices__device_type'
+        'site', 'location', 'tenant', 'tenant_group', 'role', 'devices__device_type'
     ).annotate(
     ).annotate(
         device_count=count_related(Device, 'rack')
         device_count=count_related(Device, 'rack')
     )
     )

+ 3 - 0
netbox/extras/admin.py

@@ -15,6 +15,9 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
         ('Rack Elevations', {
         ('Rack Elevations', {
             'fields': ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH'),
             'fields': ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH'),
         }),
         }),
+        ('Power', {
+            'fields': ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')
+        }),
         ('IPAM', {
         ('IPAM', {
             'fields': ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4'),
             'fields': ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4'),
         }),
         }),

+ 1 - 1
netbox/extras/api/serializers.py

@@ -221,7 +221,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
         model = JournalEntry
         model = JournalEntry
         fields = [
         fields = [
             'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
             'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
-            'created_by', 'kind', 'comments', 'tags', 'custom_fields',
+            'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
         ]
         ]
 
 
     def validate(self, data):
     def validate(self, data):

+ 12 - 3
netbox/extras/filtersets.py

@@ -32,6 +32,9 @@ class WebhookFilterSet(BaseFilterSet):
         method='search',
         method='search',
         label='Search',
         label='Search',
     )
     )
+    content_type_id = MultiValueNumberFilter(
+        field_name='content_types__id'
+    )
     content_types = ContentTypeFilter()
     content_types = ContentTypeFilter()
     http_method = django_filters.MultipleChoiceFilter(
     http_method = django_filters.MultipleChoiceFilter(
         choices=WebhookHttpMethodChoices
         choices=WebhookHttpMethodChoices
@@ -40,8 +43,8 @@ class WebhookFilterSet(BaseFilterSet):
     class Meta:
     class Meta:
         model = Webhook
         model = Webhook
         fields = [
         fields = [
-            'id', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled',
-            'http_method', 'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
+            'id', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'enabled', 'http_method',
+            'http_content_type', 'secret', 'ssl_verification', 'ca_file_path',
         ]
         ]
 
 
     def search(self, queryset, name, value):
     def search(self, queryset, name, value):
@@ -58,11 +61,17 @@ class CustomFieldFilterSet(BaseFilterSet):
         method='search',
         method='search',
         label='Search',
         label='Search',
     )
     )
+    type = django_filters.MultipleChoiceFilter(
+        choices=CustomFieldTypeChoices
+    )
+    content_type_id = MultiValueNumberFilter(
+        field_name='content_types__id'
+    )
     content_types = ContentTypeFilter()
     content_types = ContentTypeFilter()
 
 
     class Meta:
     class Meta:
         model = CustomField
         model = CustomField
-        fields = ['id', 'content_types', 'name', 'required', 'filter_logic', 'weight', 'description']
+        fields = ['id', 'name', 'required', 'filter_logic', 'weight', 'description']
 
 
     def search(self, queryset, name, value):
     def search(self, queryset, name, value):
         if not value.strip():
         if not value.strip():

+ 8 - 6
netbox/extras/forms/filtersets.py

@@ -32,12 +32,13 @@ __all__ = (
 class CustomFieldFilterForm(FilterForm):
 class CustomFieldFilterForm(FilterForm):
     fieldsets = (
     fieldsets = (
         (None, ('q',)),
         (None, ('q',)),
-        ('Attributes', ('type', 'content_types', 'weight', 'required')),
+        ('Attributes', ('type', 'content_type_id', 'weight', 'required')),
     )
     )
-    content_types = ContentTypeMultipleChoiceField(
+    content_type_id = ContentTypeMultipleChoiceField(
         queryset=ContentType.objects.all(),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_fields'),
         limit_choices_to=FeatureQuery('custom_fields'),
-        required=False
+        required=False,
+        label='Object type'
     )
     )
     type = MultipleChoiceField(
     type = MultipleChoiceField(
         choices=CustomFieldTypeChoices,
         choices=CustomFieldTypeChoices,
@@ -110,13 +111,14 @@ class ExportTemplateFilterForm(FilterForm):
 class WebhookFilterForm(FilterForm):
 class WebhookFilterForm(FilterForm):
     fieldsets = (
     fieldsets = (
         (None, ('q',)),
         (None, ('q',)),
-        ('Attributes', ('content_types', 'http_method', 'enabled')),
+        ('Attributes', ('content_type_id', 'http_method', 'enabled')),
         ('Events', ('type_create', 'type_update', 'type_delete')),
         ('Events', ('type_create', 'type_update', 'type_delete')),
     )
     )
-    content_types = ContentTypeMultipleChoiceField(
+    content_type_id = ContentTypeMultipleChoiceField(
         queryset=ContentType.objects.all(),
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('webhooks'),
         limit_choices_to=FeatureQuery('webhooks'),
-        required=False
+        required=False,
+        label='Object type'
     )
     )
     http_method = MultipleChoiceField(
     http_method = MultipleChoiceField(
         choices=WebhookHttpMethodChoices,
         choices=WebhookHttpMethodChoices,

+ 2 - 7
netbox/extras/models/customfields.py

@@ -365,13 +365,8 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
 
 
         # Text
         # Text
         else:
         else:
-            if self.type == CustomFieldTypeChoices.TYPE_LONGTEXT:
-                max_length = None
-                widget = forms.Textarea
-            else:
-                max_length = 255
-                widget = None
-            field = forms.CharField(max_length=max_length, required=required, initial=initial, widget=widget)
+            widget = forms.Textarea if self.type == CustomFieldTypeChoices.TYPE_LONGTEXT else None
+            field = forms.CharField(required=required, initial=initial, widget=widget)
             if self.validation_regex:
             if self.validation_regex:
                 field.validators = [
                 field.validators = [
                     RegexValidator(
                     RegexValidator(

+ 64 - 1
netbox/extras/tests/test_filtersets.py

@@ -7,7 +7,9 @@ from django.test import TestCase
 
 
 from circuits.models import Provider
 from circuits.models import Provider
 from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
 from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
-from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
+from extras.choices import (
+    CustomFieldTypeChoices, CustomFieldFilterLogicChoices, JournalEntryKindChoices, ObjectChangeActionChoices,
+)
 from extras.filtersets import *
 from extras.filtersets import *
 from extras.models import *
 from extras.models import *
 from ipam.models import IPAddress
 from ipam.models import IPAddress
@@ -16,6 +18,65 @@ from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests, cr
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 
 
 
 
+class CustomFieldTestCase(TestCase, BaseFilterSetTests):
+    queryset = CustomField.objects.all()
+    filterset = CustomFieldFilterSet
+
+    @classmethod
+    def setUpTestData(cls):
+        content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
+
+        custom_fields = (
+            CustomField(
+                name='Custom Field 1',
+                type=CustomFieldTypeChoices.TYPE_TEXT,
+                required=True,
+                weight=100,
+                filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
+            ),
+            CustomField(
+                name='Custom Field 2',
+                type=CustomFieldTypeChoices.TYPE_INTEGER,
+                required=False,
+                weight=200,
+                filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
+            ),
+            CustomField(
+                name='Custom Field 3',
+                type=CustomFieldTypeChoices.TYPE_BOOLEAN,
+                required=False,
+                weight=300,
+                filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED
+            ),
+        )
+        CustomField.objects.bulk_create(custom_fields)
+        custom_fields[0].content_types.add(content_types[0])
+        custom_fields[1].content_types.add(content_types[1])
+        custom_fields[2].content_types.add(content_types[2])
+
+    def test_name(self):
+        params = {'name': ['Custom Field 1', 'Custom Field 2']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_content_types(self):
+        params = {'content_types': 'dcim.site'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+        params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+    def test_required(self):
+        params = {'required': True}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+    def test_weight(self):
+        params = {'weight': [100, 200]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_filter_logic(self):
+        params = {'filter_logic': CustomFieldFilterLogicChoices.FILTER_LOOSE}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+
 class WebhookTestCase(TestCase, BaseFilterSetTests):
 class WebhookTestCase(TestCase, BaseFilterSetTests):
     queryset = Webhook.objects.all()
     queryset = Webhook.objects.all()
     filterset = WebhookFilterSet
     filterset = WebhookFilterSet
@@ -62,6 +123,8 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
     def test_content_types(self):
     def test_content_types(self):
         params = {'content_types': 'dcim.site'}
         params = {'content_types': 'dcim.site'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+        params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_type_create(self):
     def test_type_create(self):
         params = {'type_create': True}
         params = {'type_create': True}

+ 13 - 1
netbox/ipam/forms/filtersets.py

@@ -1,7 +1,8 @@
 from django import forms
 from django import forms
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
-from dcim.models import Location, Rack, Region, Site, SiteGroup
+from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
+from virtualization.models import VirtualMachine
 from ipam.choices import *
 from ipam.choices import *
 from ipam.constants import *
 from ipam.constants import *
 from ipam.models import *
 from ipam.models import *
@@ -265,6 +266,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         ('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
         ('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
         ('VRF', ('vrf_id', 'present_in_vrf_id')),
         ('VRF', ('vrf_id', 'present_in_vrf_id')),
         ('Tenant', ('tenant_group_id', 'tenant_id')),
         ('Tenant', ('tenant_group_id', 'tenant_id')),
+        ('Device/VM', ('device_id', 'virtual_machine_id')),
     )
     )
     parent = forms.CharField(
     parent = forms.CharField(
         required=False,
         required=False,
@@ -298,6 +300,16 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         required=False,
         required=False,
         label=_('Present in VRF')
         label=_('Present in VRF')
     )
     )
+    device_id = DynamicModelMultipleChoiceField(
+        queryset=Device.objects.all(),
+        required=False,
+        label=_('Assigned Device'),
+    )
+    virtual_machine_id = DynamicModelMultipleChoiceField(
+        queryset=VirtualMachine.objects.all(),
+        required=False,
+        label=_('Assigned VM'),
+    )
     status = MultipleChoiceField(
     status = MultipleChoiceField(
         choices=IPAddressStatusChoices,
         choices=IPAddressStatusChoices,
         required=False
         required=False

+ 11 - 16
netbox/ipam/tables/ip.py

@@ -4,7 +4,7 @@ from django_tables2.utils import Accessor
 
 
 from ipam.models import *
 from ipam.models import *
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin, TenantColumn
 
 
 __all__ = (
 __all__ = (
     'AggregateTable',
     'AggregateTable',
@@ -99,7 +99,7 @@ class RIRTable(NetBoxTable):
 # ASNs
 # ASNs
 #
 #
 
 
-class ASNTable(NetBoxTable):
+class ASNTable(TenancyColumnsMixin, NetBoxTable):
     asn = tables.Column(
     asn = tables.Column(
         linkify=True
         linkify=True
     )
     )
@@ -122,7 +122,6 @@ class ASNTable(NetBoxTable):
         linkify_item=True,
         linkify_item=True,
         verbose_name='Sites'
         verbose_name='Sites'
     )
     )
-    tenant = TenantColumn()
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:asn_list'
         url_name='ipam:asn_list'
     )
     )
@@ -130,7 +129,7 @@ class ASNTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = ASN
         model = ASN
         fields = (
         fields = (
-            'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'description', 'sites', 'tags',
+            'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description', 'sites', 'tags',
             'created', 'last_updated', 'actions',
             'created', 'last_updated', 'actions',
         )
         )
         default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant')
         default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant')
@@ -140,12 +139,11 @@ class ASNTable(NetBoxTable):
 # Aggregates
 # Aggregates
 #
 #
 
 
-class AggregateTable(NetBoxTable):
+class AggregateTable(TenancyColumnsMixin, NetBoxTable):
     prefix = tables.Column(
     prefix = tables.Column(
         linkify=True,
         linkify=True,
         verbose_name='Aggregate'
         verbose_name='Aggregate'
     )
     )
-    tenant = TenantColumn()
     date_added = tables.DateColumn(
     date_added = tables.DateColumn(
         format="Y-m-d",
         format="Y-m-d",
         verbose_name='Added'
         verbose_name='Added'
@@ -164,7 +162,7 @@ class AggregateTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Aggregate
         model = Aggregate
         fields = (
         fields = (
-            'pk', 'id', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description', 'tags',
+            'pk', 'id', 'prefix', 'rir', 'tenant', 'tenant_group', 'child_count', 'utilization', 'date_added', 'description', 'tags',
             'created', 'last_updated',
             'created', 'last_updated',
         )
         )
         default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description')
         default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description')
@@ -225,7 +223,7 @@ class PrefixUtilizationColumn(columns.UtilizationColumn):
     """
     """
 
 
 
 
-class PrefixTable(NetBoxTable):
+class PrefixTable(TenancyColumnsMixin, NetBoxTable):
     prefix = columns.TemplateColumn(
     prefix = columns.TemplateColumn(
         template_code=PREFIX_LINK,
         template_code=PREFIX_LINK,
         export_raw=True,
         export_raw=True,
@@ -256,7 +254,6 @@ class PrefixTable(NetBoxTable):
         template_code=VRF_LINK,
         template_code=VRF_LINK,
         verbose_name='VRF'
         verbose_name='VRF'
     )
     )
-    tenant = TenantColumn()
     site = tables.Column(
     site = tables.Column(
         linkify=True
         linkify=True
     )
     )
@@ -289,7 +286,7 @@ class PrefixTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Prefix
         model = Prefix
         fields = (
         fields = (
-            'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site',
+            'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group', 'site',
             'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', 'created', 'last_updated',
             'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', 'created', 'last_updated',
         )
         )
         default_columns = (
         default_columns = (
@@ -303,7 +300,7 @@ class PrefixTable(NetBoxTable):
 #
 #
 # IP ranges
 # IP ranges
 #
 #
-class IPRangeTable(NetBoxTable):
+class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
     start_address = tables.Column(
     start_address = tables.Column(
         linkify=True
         linkify=True
     )
     )
@@ -317,7 +314,6 @@ class IPRangeTable(NetBoxTable):
     role = tables.Column(
     role = tables.Column(
         linkify=True
         linkify=True
     )
     )
-    tenant = TenantColumn()
     utilization = columns.UtilizationColumn(
     utilization = columns.UtilizationColumn(
         accessor='utilization',
         accessor='utilization',
         orderable=False
         orderable=False
@@ -329,7 +325,7 @@ class IPRangeTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = IPRange
         model = IPRange
         fields = (
         fields = (
-            'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
+            'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'description',
             'utilization', 'tags', 'created', 'last_updated',
             'utilization', 'tags', 'created', 'last_updated',
         )
         )
         default_columns = (
         default_columns = (
@@ -344,7 +340,7 @@ class IPRangeTable(NetBoxTable):
 # IPAddresses
 # IPAddresses
 #
 #
 
 
-class IPAddressTable(NetBoxTable):
+class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
     address = tables.TemplateColumn(
     address = tables.TemplateColumn(
         template_code=IPADDRESS_LINK,
         template_code=IPADDRESS_LINK,
         verbose_name='IP Address'
         verbose_name='IP Address'
@@ -357,7 +353,6 @@ class IPAddressTable(NetBoxTable):
         default=AVAILABLE_LABEL
         default=AVAILABLE_LABEL
     )
     )
     role = columns.ChoiceFieldColumn()
     role = columns.ChoiceFieldColumn()
-    tenant = TenantColumn()
     assigned_object = tables.Column(
     assigned_object = tables.Column(
         linkify=True,
         linkify=True,
         orderable=False,
         orderable=False,
@@ -386,7 +381,7 @@ class IPAddressTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = IPAddress
         model = IPAddress
         fields = (
         fields = (
-            'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name', 'description',
+            'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'assigned', 'dns_name', 'description',
             'tags', 'created', 'last_updated',
             'tags', 'created', 'last_updated',
         )
         )
         default_columns = (
         default_columns = (

+ 3 - 4
netbox/ipam/tables/vlans.py

@@ -5,7 +5,7 @@ from django_tables2.utils import Accessor
 from dcim.models import Interface
 from dcim.models import Interface
 from ipam.models import *
 from ipam.models import *
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin, TenantColumn
 from virtualization.models import VMInterface
 from virtualization.models import VMInterface
 
 
 __all__ = (
 __all__ = (
@@ -90,7 +90,7 @@ class VLANGroupTable(NetBoxTable):
 # VLANs
 # VLANs
 #
 #
 
 
-class VLANTable(NetBoxTable):
+class VLANTable(TenancyColumnsMixin, NetBoxTable):
     vid = tables.TemplateColumn(
     vid = tables.TemplateColumn(
         template_code=VLAN_LINK,
         template_code=VLAN_LINK,
         verbose_name='VID'
         verbose_name='VID'
@@ -104,7 +104,6 @@ class VLANTable(NetBoxTable):
     group = tables.Column(
     group = tables.Column(
         linkify=True
         linkify=True
     )
     )
-    tenant = TenantColumn()
     status = columns.ChoiceFieldColumn(
     status = columns.ChoiceFieldColumn(
         default=AVAILABLE_LABEL
         default=AVAILABLE_LABEL
     )
     )
@@ -123,7 +122,7 @@ class VLANTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = VLAN
         model = VLAN
         fields = (
         fields = (
-            'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags',
+            'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role', 'description', 'tags',
             'created', 'last_updated',
             'created', 'last_updated',
         )
         )
         default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')
         default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')

+ 5 - 7
netbox/ipam/tables/vrfs.py

@@ -2,7 +2,7 @@ import django_tables2 as tables
 
 
 from ipam.models import *
 from ipam.models import *
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin
 
 
 __all__ = (
 __all__ = (
     'RouteTargetTable',
     'RouteTargetTable',
@@ -20,14 +20,13 @@ VRF_TARGETS = """
 # VRFs
 # VRFs
 #
 #
 
 
-class VRFTable(NetBoxTable):
+class VRFTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.Column(
     name = tables.Column(
         linkify=True
         linkify=True
     )
     )
     rd = tables.Column(
     rd = tables.Column(
         verbose_name='RD'
         verbose_name='RD'
     )
     )
-    tenant = TenantColumn()
     enforce_unique = columns.BooleanColumn(
     enforce_unique = columns.BooleanColumn(
         verbose_name='Unique'
         verbose_name='Unique'
     )
     )
@@ -46,7 +45,7 @@ class VRFTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = VRF
         model = VRF
         fields = (
         fields = (
-            'pk', 'id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets',
+            'pk', 'id', 'name', 'rd', 'tenant', 'tenant_group', 'enforce_unique', 'description', 'import_targets', 'export_targets',
             'tags', 'created', 'last_updated',
             'tags', 'created', 'last_updated',
         )
         )
         default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
         default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
@@ -56,16 +55,15 @@ class VRFTable(NetBoxTable):
 # Route targets
 # Route targets
 #
 #
 
 
-class RouteTargetTable(NetBoxTable):
+class RouteTargetTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.Column(
     name = tables.Column(
         linkify=True
         linkify=True
     )
     )
-    tenant = TenantColumn()
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='ipam:vrf_list'
         url_name='ipam:vrf_list'
     )
     )
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = RouteTarget
         model = RouteTarget
-        fields = ('pk', 'id', 'name', 'tenant', 'description', 'tags', 'created', 'last_updated',)
+        fields = ('pk', 'id', 'name', 'tenant', 'tenant_group', 'description', 'tags', 'created', 'last_updated',)
         default_columns = ('pk', 'name', 'tenant', 'description')
         default_columns = ('pk', 'name', 'tenant', 'description')

+ 14 - 11
netbox/ipam/views.py

@@ -298,7 +298,7 @@ class AggregatePrefixesView(generic.ObjectChildrenView):
     def get_children(self, request, parent):
     def get_children(self, request, parent):
         return Prefix.objects.restrict(request.user, 'view').filter(
         return Prefix.objects.restrict(request.user, 'view').filter(
             prefix__net_contained_or_equal=str(parent.prefix)
             prefix__net_contained_or_equal=str(parent.prefix)
-        ).prefetch_related('site', 'role', 'tenant', 'vlan')
+        ).prefetch_related('site', 'role', 'tenant', 'tenant__group', 'vlan')
 
 
     def prep_table_data(self, request, queryset, parent):
     def prep_table_data(self, request, queryset, parent):
         # Determine whether to show assigned prefixes, available prefixes, or both
         # Determine whether to show assigned prefixes, available prefixes, or both
@@ -470,7 +470,7 @@ class PrefixPrefixesView(generic.ObjectChildrenView):
 
 
     def get_children(self, request, parent):
     def get_children(self, request, parent):
         return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
         return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
-            'site', 'vrf', 'vlan', 'role', 'tenant',
+            'site', 'vrf', 'vlan', 'role', 'tenant', 'tenant__group'
         )
         )
 
 
     def prep_table_data(self, request, queryset, parent):
     def prep_table_data(self, request, queryset, parent):
@@ -499,7 +499,7 @@ class PrefixIPRangesView(generic.ObjectChildrenView):
 
 
     def get_children(self, request, parent):
     def get_children(self, request, parent):
         return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related(
         return parent.get_child_ranges().restrict(request.user, 'view').prefetch_related(
-            'vrf', 'role', 'tenant',
+            'vrf', 'role', 'tenant', 'tenant__group',
         )
         )
 
 
     def get_extra_context(self, request, instance):
     def get_extra_context(self, request, instance):
@@ -587,7 +587,7 @@ class IPRangeIPAddressesView(generic.ObjectChildrenView):
 
 
     def get_children(self, request, parent):
     def get_children(self, request, parent):
         return parent.get_child_ips().restrict(request.user, 'view').prefetch_related(
         return parent.get_child_ips().restrict(request.user, 'view').prefetch_related(
-            'vrf', 'role', 'tenant',
+            'vrf', 'role', 'tenant', 'tenant__group',
         )
         )
 
 
     def get_extra_context(self, request, instance):
     def get_extra_context(self, request, instance):
@@ -680,13 +680,16 @@ class IPAddressView(generic.ObjectView):
         service_filter = Q(ipaddresses=instance)
         service_filter = Q(ipaddresses=instance)
 
 
         # Find services listening on all IPs on the assigned device/vm
         # Find services listening on all IPs on the assigned device/vm
-        if instance.assigned_object and instance.assigned_object.parent_object:
-            parent_object = instance.assigned_object.parent_object
-
-            if isinstance(parent_object, VirtualMachine):
-                service_filter |= (Q(virtual_machine=parent_object) & Q(ipaddresses=None))
-            elif isinstance(parent_object, Device):
-                service_filter |= (Q(device=parent_object) & Q(ipaddresses=None))
+        try:
+            if instance.assigned_object and instance.assigned_object.parent_object:
+                parent_object = instance.assigned_object.parent_object
+
+                if isinstance(parent_object, VirtualMachine):
+                    service_filter |= (Q(virtual_machine=parent_object) & Q(ipaddresses=None))
+                elif isinstance(parent_object, Device):
+                    service_filter |= (Q(device=parent_object) & Q(ipaddresses=None))
+        except AttributeError:
+            pass
 
 
         services = Service.objects.restrict(request.user, 'view').filter(service_filter)
         services = Service.objects.restrict(request.user, 'view').filter(service_filter)
 
 

+ 23 - 0
netbox/netbox/authentication.py

@@ -348,3 +348,26 @@ class LDAPBackend:
             ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
             ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
 
 
         return obj
         return obj
+
+
+# Custom Social Auth Pipeline Handlers
+def user_default_groups_handler(backend, user, response, *args, **kwargs):
+    """
+    Custom pipeline handler which adds remote auth users to the default group specified in the
+    configuration file.
+    """
+    logger = logging.getLogger('netbox.auth.user_default_groups_handler')
+    if settings.REMOTE_AUTH_DEFAULT_GROUPS:
+        # Assign default groups to the user
+        group_list = []
+        for name in settings.REMOTE_AUTH_DEFAULT_GROUPS:
+            try:
+                group_list.append(Group.objects.get(name=name))
+            except Group.DoesNotExist:
+                logging.error(
+                    f"Could not assign group {name} to remotely-authenticated user {user}: Group not found")
+        if group_list:
+            user.groups.add(*group_list)
+        else:
+            user.groups.clear()
+            logger.debug(f"Stripping user {user} from Groups")

+ 25 - 0
netbox/netbox/config/parameters.py

@@ -82,6 +82,31 @@ PARAMS = (
         field=forms.IntegerField
         field=forms.IntegerField
     ),
     ),
 
 
+    # Power
+    ConfigParam(
+        name='POWERFEED_DEFAULT_VOLTAGE',
+        label='Powerfeed voltage',
+        default=120,
+        description="Default voltage for powerfeeds",
+        field=forms.IntegerField
+    ),
+
+    ConfigParam(
+        name='POWERFEED_DEFAULT_AMPERAGE',
+        label='Powerfeed amperage',
+        default=15,
+        description="Default amperage for powerfeeds",
+        field=forms.IntegerField
+    ),
+
+    ConfigParam(
+        name='POWERFEED_DEFAULT_MAX_UTILIZATION',
+        label='Powerfeed max utilization',
+        default=80,
+        description="Default max utilization for powerfeeds",
+        field=forms.IntegerField
+    ),
+
     # Security
     # Security
     ConfigParam(
     ConfigParam(
         name='ALLOWED_URL_SCHEMES',
         name='ALLOWED_URL_SCHEMES',

+ 10 - 10
netbox/netbox/constants.py

@@ -34,7 +34,7 @@ CIRCUIT_TYPES = OrderedDict(
         }),
         }),
         ('circuit', {
         ('circuit', {
             'queryset': Circuit.objects.prefetch_related(
             'queryset': Circuit.objects.prefetch_related(
-                'type', 'provider', 'tenant', 'terminations__site'
+                'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
             ),
             ),
             'filterset': circuits.filtersets.CircuitFilterSet,
             'filterset': circuits.filtersets.CircuitFilterSet,
             'table': circuits.tables.CircuitTable,
             'table': circuits.tables.CircuitTable,
@@ -53,13 +53,13 @@ CIRCUIT_TYPES = OrderedDict(
 DCIM_TYPES = OrderedDict(
 DCIM_TYPES = OrderedDict(
     (
     (
         ('site', {
         ('site', {
-            'queryset': Site.objects.prefetch_related('region', 'tenant'),
+            'queryset': Site.objects.prefetch_related('region', 'tenant', 'tenant__group'),
             'filterset': dcim.filtersets.SiteFilterSet,
             'filterset': dcim.filtersets.SiteFilterSet,
             'table': dcim.tables.SiteTable,
             'table': dcim.tables.SiteTable,
             'url': 'dcim:site_list',
             'url': 'dcim:site_list',
         }),
         }),
         ('rack', {
         ('rack', {
-            'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'role').annotate(
+            'queryset': Rack.objects.prefetch_related('site', 'location', 'tenant', 'tenant__group', 'role').annotate(
                 device_count=count_related(Device, 'rack')
                 device_count=count_related(Device, 'rack')
             ),
             ),
             'filterset': dcim.filtersets.RackFilterSet,
             'filterset': dcim.filtersets.RackFilterSet,
@@ -100,7 +100,7 @@ DCIM_TYPES = OrderedDict(
         }),
         }),
         ('device', {
         ('device', {
             'queryset': Device.objects.prefetch_related(
             'queryset': Device.objects.prefetch_related(
-                'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack', 'primary_ip4', 'primary_ip6',
+                'device_type__manufacturer', 'device_role', 'tenant', 'tenant__group', 'site', 'rack', 'primary_ip4', 'primary_ip6',
             ),
             ),
             'filterset': dcim.filtersets.DeviceFilterSet,
             'filterset': dcim.filtersets.DeviceFilterSet,
             'table': dcim.tables.DeviceTable,
             'table': dcim.tables.DeviceTable,
@@ -148,7 +148,7 @@ DCIM_TYPES = OrderedDict(
 IPAM_TYPES = OrderedDict(
 IPAM_TYPES = OrderedDict(
     (
     (
         ('vrf', {
         ('vrf', {
-            'queryset': VRF.objects.prefetch_related('tenant'),
+            'queryset': VRF.objects.prefetch_related('tenant', 'tenant__group'),
             'filterset': ipam.filtersets.VRFFilterSet,
             'filterset': ipam.filtersets.VRFFilterSet,
             'table': ipam.tables.VRFTable,
             'table': ipam.tables.VRFTable,
             'url': 'ipam:vrf_list',
             'url': 'ipam:vrf_list',
@@ -160,25 +160,25 @@ IPAM_TYPES = OrderedDict(
             'url': 'ipam:aggregate_list',
             'url': 'ipam:aggregate_list',
         }),
         }),
         ('prefix', {
         ('prefix', {
-            'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
+            'queryset': Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'tenant__group', 'vlan', 'role'),
             'filterset': ipam.filtersets.PrefixFilterSet,
             'filterset': ipam.filtersets.PrefixFilterSet,
             'table': ipam.tables.PrefixTable,
             'table': ipam.tables.PrefixTable,
             'url': 'ipam:prefix_list',
             'url': 'ipam:prefix_list',
         }),
         }),
         ('ipaddress', {
         ('ipaddress', {
-            'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'),
+            'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group'),
             'filterset': ipam.filtersets.IPAddressFilterSet,
             'filterset': ipam.filtersets.IPAddressFilterSet,
             'table': ipam.tables.IPAddressTable,
             'table': ipam.tables.IPAddressTable,
             'url': 'ipam:ipaddress_list',
             'url': 'ipam:ipaddress_list',
         }),
         }),
         ('vlan', {
         ('vlan', {
-            'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role'),
+            'queryset': VLAN.objects.prefetch_related('site', 'group', 'tenant', 'tenant__group', 'role'),
             'filterset': ipam.filtersets.VLANFilterSet,
             'filterset': ipam.filtersets.VLANFilterSet,
             'table': ipam.tables.VLANTable,
             'table': ipam.tables.VLANTable,
             'url': 'ipam:vlan_list',
             'url': 'ipam:vlan_list',
         }),
         }),
         ('asn', {
         ('asn', {
-            'queryset': ASN.objects.prefetch_related('rir', 'tenant'),
+            'queryset': ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group'),
             'filterset': ipam.filtersets.ASNFilterSet,
             'filterset': ipam.filtersets.ASNFilterSet,
             'table': ipam.tables.ASNTable,
             'table': ipam.tables.ASNTable,
             'url': 'ipam:asn_list',
             'url': 'ipam:asn_list',
@@ -223,7 +223,7 @@ VIRTUALIZATION_TYPES = OrderedDict(
         }),
         }),
         ('virtualmachine', {
         ('virtualmachine', {
             'queryset': VirtualMachine.objects.prefetch_related(
             'queryset': VirtualMachine.objects.prefetch_related(
-                'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6',
+                'cluster', 'tenant', 'tenant__group', 'platform', 'primary_ip4', 'primary_ip6',
             ),
             ),
             'filterset': virtualization.filtersets.VirtualMachineFilterSet,
             'filterset': virtualization.filtersets.VirtualMachineFilterSet,
             'table': virtualization.tables.VirtualMachineTable,
             'table': virtualization.tables.VirtualMachineTable,

+ 14 - 1
netbox/netbox/settings.py

@@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
 # Environment setup
 # Environment setup
 #
 #
 
 
-VERSION = '3.2.5'
+VERSION = '3.2.6'
 
 
 # Hostname
 # Hostname
 HOSTNAME = platform.node()
 HOSTNAME = platform.node()
@@ -483,6 +483,19 @@ for param in dir(configuration):
 
 
 SOCIAL_AUTH_JSONFIELD_ENABLED = True
 SOCIAL_AUTH_JSONFIELD_ENABLED = True
 
 
+SOCIAL_AUTH_PIPELINE = (
+    'social_core.pipeline.social_auth.social_details',
+    'social_core.pipeline.social_auth.social_uid',
+    'social_core.pipeline.social_auth.social_user',
+    'social_core.pipeline.user.get_username',
+    'social_core.pipeline.social_auth.associate_by_email',
+    'social_core.pipeline.user.create_user',
+    'social_core.pipeline.social_auth.associate_user',
+    'netbox.authentication.user_default_groups_handler',
+    'social_core.pipeline.social_auth.load_extra_data',
+    'social_core.pipeline.user.user_details',
+)
+
 
 
 #
 #
 # Django Prometheus
 # Django Prometheus

+ 2 - 1
netbox/netbox/views/generic/bulk_views.py

@@ -8,6 +8,7 @@ from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import FieldDoesNotExist, ValidationError
 from django.core.exceptions import FieldDoesNotExist, ValidationError
 from django.db import transaction, IntegrityError
 from django.db import transaction, IntegrityError
 from django.db.models import ManyToManyField, ProtectedError
 from django.db.models import ManyToManyField, ProtectedError
+from django.db.models.fields.reverse_related import ManyToManyRel
 from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
 from django.forms import Form, ModelMultipleChoiceField, MultipleHiddenInput
 from django.http import HttpResponse
 from django.http import HttpResponse
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
@@ -484,7 +485,7 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
                         setattr(obj, name, None if model_field.null else '')
                         setattr(obj, name, None if model_field.null else '')
 
 
                 # ManyToManyFields
                 # ManyToManyFields
-                elif isinstance(model_field, ManyToManyField):
+                elif isinstance(model_field, (ManyToManyField, ManyToManyRel)):
                     if form.cleaned_data[name]:
                     if form.cleaned_data[name]:
                         getattr(obj, name).set(form.cleaned_data[name])
                         getattr(obj, name).set(form.cleaned_data[name])
                 # Normal fields
                 # Normal fields

File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox.js


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 1 - 0
netbox/project-static/src/select/api/apiSelect.ts

@@ -411,6 +411,7 @@ export class APISelect {
     } finally {
     } finally {
       this.setOptionStyles();
       this.setOptionStyles();
       this.enable();
       this.enable();
+      this.slim.slim.search.input.focus();
       this.base.dispatchEvent(this.loadEvent);
       this.base.dispatchEvent(this.loadEvent);
     }
     }
   }
   }

+ 4 - 12
netbox/templates/dcim/device.html

@@ -46,13 +46,7 @@
                         </tr>
                         </tr>
                         <tr>
                         <tr>
                             <th scope="row">Rack</th>
                             <th scope="row">Rack</th>
-                            <td>
-                                {% if object.rack %}
-                                    <a href="{% url 'dcim:rack' pk=object.rack.pk %}">{{ object.rack }}</a>
-                                {% else %}
-                                    {{ ''|placeholder }}
-                                {% endif %}
-                            </td>
+                            <td>{{ object.rack|linkify|placeholder }}</td>
                         </tr>
                         </tr>
                         <tr>
                         <tr>
                             <th scope="row">Position</th>
                             <th scope="row">Position</th>
@@ -161,9 +155,7 @@
                         </tr>
                         </tr>
                         <tr>
                         <tr>
                             <th scope="row">Role</th>
                             <th scope="row">Role</th>
-                            <td>
-                                <a href="{% url 'dcim:device_list' %}?role={{ object.device_role.slug }}">{{ object.device_role }}</a>
-                            </td>
+                            <td>{{ object.device_role|linkify }}</td>
                         </tr>
                         </tr>
                         <tr>
                         <tr>
                             <th scope="row">Platform</th>
                             <th scope="row">Platform</th>
@@ -173,7 +165,7 @@
                             <th scope="row">Primary IPv4</th>
                             <th scope="row">Primary IPv4</th>
                             <td>
                             <td>
                               {% if object.primary_ip4 %}
                               {% if object.primary_ip4 %}
-                                <a href="{% url 'ipam:ipaddress' pk=object.primary_ip4.pk %}">{{ object.primary_ip4.address.ip }}</a>
+                                <a href="{{ object.primary_ip4.get_absolute_url }}">{{ object.primary_ip4.address.ip }}</a>
                                 {% if object.primary_ip4.nat_inside %}
                                 {% if object.primary_ip4.nat_inside %}
                                   (NAT for {{ object.primary_ip4.nat_inside.address.ip|linkify }})
                                   (NAT for {{ object.primary_ip4.nat_inside.address.ip|linkify }})
                                 {% elif object.primary_ip4.nat_outside %}
                                 {% elif object.primary_ip4.nat_outside %}
@@ -188,7 +180,7 @@
                             <th scope="row">Primary IPv6</th>
                             <th scope="row">Primary IPv6</th>
                             <td>
                             <td>
                               {% if object.primary_ip6 %}
                               {% if object.primary_ip6 %}
-                                <a href="{% url 'ipam:ipaddress' pk=object.primary_ip6.pk %}">{{ object.primary_ip6.address.ip }}</a>
+                                <a href="{{ object.primary_ip6.get_absolute_url }}">{{ object.primary_ip6.address.ip }}</a>
                                 {% if object.primary_ip6.nat_inside %}
                                 {% if object.primary_ip6.nat_inside %}
                                   (NAT for {{ object.primary_ip6.nat_inside.address.ip|linkify }})
                                   (NAT for {{ object.primary_ip6.nat_inside.address.ip|linkify }})
                                 {% elif object.primary_ip6.nat_outside %}
                                 {% elif object.primary_ip6.nat_outside %}

+ 9 - 0
netbox/templates/dcim/device_edit.html

@@ -86,6 +86,15 @@
       {% render_field form.tenant %}
       {% render_field form.tenant %}
     </div>
     </div>
 
 
+    <div class="field-group my-5">
+      <div class="row mb-2">
+        <h5 class="offset-sm-3">Virtual Chassis</h5>
+      </div>
+      {% render_field form.virtual_chassis %}
+      {% render_field form.vc_position %}
+      {% render_field form.vc_priority %}
+    </div>
+
     {% if form.custom_fields %}
     {% if form.custom_fields %}
       <div class="field-group my-5">
       <div class="field-group my-5">
         <div class="row mb-2">
         <div class="row mb-2">

+ 19 - 15
netbox/templates/extras/object_journal.html

@@ -5,25 +5,29 @@
 {% render_errors form %}
 {% render_errors form %}
 
 
 {% block content %}
 {% block content %}
-  {% if perms.extras.add_journalentry %}
-    <form action="{% url 'extras:journalentry_add' %}" method="post" enctype="multipart/form-data">
-      <div class="container">
-        <div class="field-group">
-          <h4>New Journal Entry</h4>
-          {% csrf_token %}
-          {% render_form form %}
-        </div>
-        <div class="col col-md-12 text-end my-3">
-          <a href="{{ object.get_absolute_url }}" class="btn btn-outline-danger">Cancel</a>
-          <button type="submit" class="btn btn-primary">Save</button>
-        </div>
-      </div>
-    </form>
-  {% endif %}
   <div class="card">
   <div class="card">
     <div class="card-body table-responsive">
     <div class="card-body table-responsive">
       {% render_table table 'inc/table.html' %}
       {% render_table table 'inc/table.html' %}
       {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
       {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
     </div>
     </div>
   </div>
   </div>
+  {% if perms.extras.add_journalentry %}
+    <div class="card">
+      <div class="card-body table-responsive">
+        <h4 class="card-header">New Journal Entry</h4>
+        <form action="{% url 'extras:journalentry_add' %}" method="post" enctype="multipart/form-data">
+          <div class="container">
+            <div class="field-group">
+              {% csrf_token %}
+              {% render_form form %}
+            </div>
+            <div class="col col-md-12 text-end my-3">
+              <a href="{{ object.get_absolute_url }}" class="btn btn-outline-danger">Cancel</a>
+              <button type="submit" class="btn btn-primary">Save</button>
+            </div>
+          </div>
+        </form>
+      </div>
+    </div>
+  {% endif %}
 {% endblock %}
 {% endblock %}

+ 31 - 0
netbox/tenancy/tables/columns.py

@@ -2,6 +2,8 @@ import django_tables2 as tables
 
 
 __all__ = (
 __all__ = (
     'TenantColumn',
     'TenantColumn',
+    'TenantGroupColumn',
+    'TenancyColumnsMixin',
 )
 )
 
 
 
 
@@ -24,3 +26,32 @@ class TenantColumn(tables.TemplateColumn):
 
 
     def value(self, value):
     def value(self, value):
         return str(value) if value else None
         return str(value) if value else None
+
+
+class TenantGroupColumn(tables.TemplateColumn):
+    """
+    Include the tenant group description.
+    """
+    template_code = """
+    {% if record.tenant and record.tenant.group %}
+        <a href="{{ record.tenant.group.get_absolute_url }}" title="{{ record.tenant.group.description }}">{{ record.tenant.group }}</a>
+    {% elif record.vrf.tenant and record.vrf.tenant.group %}
+        <a href="{{ record.vrf.tenant.group.get_absolute_url }}" title="{{ record.vrf.tenant.group.description }}">{{ record.vrf.tenant.group }}</a>*
+    {% else %}
+        &mdash;
+    {% endif %}
+    """
+
+    def __init__(self, accessor=tables.A('tenant__group'), *args, **kwargs):
+        if 'verbose_name' not in kwargs:
+            kwargs['verbose_name'] = 'Tenant Group'
+
+        super().__init__(template_code=self.template_code, accessor=accessor, *args, **kwargs)
+
+    def value(self, value):
+        return str(value) if value else None
+
+
+class TenancyColumnsMixin(tables.Table):
+    tenant_group = TenantGroupColumn()
+    tenant = TenantColumn()

+ 7 - 0
netbox/utilities/management/commands/__init__.py

@@ -1,6 +1,8 @@
 from django.db import models
 from django.db import models
 from timezone_field import TimeZoneField
 from timezone_field import TimeZoneField
 
 
+from netbox.config import ConfigItem
+
 
 
 SKIP_FIELDS = (
 SKIP_FIELDS = (
     TimeZoneField,
     TimeZoneField,
@@ -26,4 +28,9 @@ def custom_deconstruct(field):
         for attr in EXEMPT_ATTRS:
         for attr in EXEMPT_ATTRS:
             kwargs.pop(attr, None)
             kwargs.pop(attr, None)
 
 
+    # Ignore any field defaults which reference a ConfigItem
+    kwargs = {
+        k: v for k, v in kwargs.items() if not isinstance(v, ConfigItem)
+    }
+
     return name, path, args, kwargs
     return name, path, args, kwargs

+ 3 - 5
netbox/virtualization/tables/clusters.py

@@ -1,6 +1,7 @@
 import django_tables2 as tables
 import django_tables2 as tables
 
 
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
+from tenancy.tables import TenancyColumnsMixin
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 
 
 __all__ = (
 __all__ = (
@@ -56,7 +57,7 @@ class ClusterGroupTable(NetBoxTable):
         default_columns = ('pk', 'name', 'cluster_count', 'description')
         default_columns = ('pk', 'name', 'cluster_count', 'description')
 
 
 
 
-class ClusterTable(NetBoxTable):
+class ClusterTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.Column(
     name = tables.Column(
         linkify=True
         linkify=True
     )
     )
@@ -66,9 +67,6 @@ class ClusterTable(NetBoxTable):
     group = tables.Column(
     group = tables.Column(
         linkify=True
         linkify=True
     )
     )
-    tenant = tables.Column(
-        linkify=True
-    )
     site = tables.Column(
     site = tables.Column(
         linkify=True
         linkify=True
     )
     )
@@ -93,7 +91,7 @@ class ClusterTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Cluster
         model = Cluster
         fields = (
         fields = (
-            'pk', 'id', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'device_count', 'vm_count', 'contacts',
+            'pk', 'id', 'name', 'type', 'group', 'tenant', 'tenant_group', 'site', 'comments', 'device_count', 'vm_count', 'contacts',
             'tags', 'created', 'last_updated',
             'tags', 'created', 'last_updated',
         )
         )
         default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count')
         default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count')

+ 3 - 4
netbox/virtualization/tables/virtualmachines.py

@@ -2,7 +2,7 @@ import django_tables2 as tables
 
 
 from dcim.tables.devices import BaseInterfaceTable
 from dcim.tables.devices import BaseInterfaceTable
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin
 from virtualization.models import VirtualMachine, VMInterface
 from virtualization.models import VirtualMachine, VMInterface
 
 
 __all__ = (
 __all__ = (
@@ -24,7 +24,7 @@ VMINTERFACE_BUTTONS = """
 # Virtual machines
 # Virtual machines
 #
 #
 
 
-class VirtualMachineTable(NetBoxTable):
+class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.Column(
     name = tables.Column(
         order_by=('_name',),
         order_by=('_name',),
         linkify=True
         linkify=True
@@ -34,7 +34,6 @@ class VirtualMachineTable(NetBoxTable):
         linkify=True
         linkify=True
     )
     )
     role = columns.ColoredLabelColumn()
     role = columns.ColoredLabelColumn()
-    tenant = TenantColumn()
     comments = columns.MarkdownColumn()
     comments = columns.MarkdownColumn()
     primary_ip4 = tables.Column(
     primary_ip4 = tables.Column(
         linkify=True,
         linkify=True,
@@ -56,7 +55,7 @@ class VirtualMachineTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = VirtualMachine
         model = VirtualMachine
         fields = (
         fields = (
-            'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
+            'pk', 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'tenant_group', 'platform', 'vcpus', 'memory', 'disk',
             'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
             'primary_ip4', 'primary_ip6', 'primary_ip', 'comments', 'tags', 'created', 'last_updated',
         )
         )
         default_columns = (
         default_columns = (

+ 6 - 6
requirements.txt

@@ -1,7 +1,7 @@
-bleach==5.0.0
-Django==4.0.5
+bleach==5.0.1
+Django==4.0.6
 django-cors-headers==3.13.0
 django-cors-headers==3.13.0
-django-debug-toolbar==3.4.0
+django-debug-toolbar==3.5.0
 django-filter==22.1
 django-filter==22.1
 django-graphiql-debug-toolbar==0.2.0
 django-graphiql-debug-toolbar==0.2.0
 django-mptt==0.13.4
 django-mptt==0.13.4
@@ -19,13 +19,13 @@ gunicorn==20.1.0
 Jinja2==3.1.2
 Jinja2==3.1.2
 Markdown==3.3.7
 Markdown==3.3.7
 markdown-include==0.6.0
 markdown-include==0.6.0
-mkdocs-material==8.3.6
+mkdocs-material==8.3.9
 mkdocstrings[python-legacy]==0.19.0
 mkdocstrings[python-legacy]==0.19.0
 netaddr==0.8.0
 netaddr==0.8.0
-Pillow==9.1.1
+Pillow==9.2.0
 psycopg2-binary==2.9.3
 psycopg2-binary==2.9.3
 PyYAML==6.0
 PyYAML==6.0
-sentry-sdk==1.5.12
+sentry-sdk==1.7.0
 social-auth-app-django==5.0.0
 social-auth-app-django==5.0.0
 social-auth-core==4.3.0
 social-auth-core==4.3.0
 svgwrite==1.4.2
 svgwrite==1.4.2

Some files were not shown because too many files changed in this diff