Przeglądaj źródła

Merge pull request #9706 from netbox-community/develop

Release v3.2.6
Jeremy Stretch 3 lat temu
rodzic
commit
b72793a85a
51 zmienionych plików z 438 dodań i 185 usunięć
  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:
       label: NetBox version
       description: What version of NetBox are you currently running?
-      placeholder: v3.2.5
+      placeholder: v3.2.6
     validations:
       required: true
   - type: dropdown

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

@@ -14,7 +14,7 @@ body:
     attributes:
       label: NetBox version
       description: What version of NetBox are you currently running?
-      placeholder: v3.2.5
+      placeholder: v3.2.6
     validations:
       required: true
   - type: dropdown

+ 6 - 0
NOTICE

@@ -1 +1,7 @@
 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
 # https://github.com/django/django
 Django
@@ -126,7 +130,3 @@ tablib
 # Timezone data (required by django-timezone-field on Python 3.9+)
 # https://github.com/python/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
 
 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
 
 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
 
 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:
 
-* 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
 * Integer: A whole number (positive or negative)
 * Boolean: True or false

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

@@ -1,5 +1,28 @@
 # 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)
 
 ### Enhancements
@@ -25,7 +48,7 @@
 * [#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
 * [#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
 * [#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

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

@@ -2,7 +2,7 @@ import django_tables2 as tables
 
 from circuits.models import *
 from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin
 from .columns import CommitRateColumn
 
 __all__ = (
@@ -39,7 +39,7 @@ class CircuitTypeTable(NetBoxTable):
         default_columns = ('pk', 'name', 'circuit_count', 'description', 'slug')
 
 
-class CircuitTable(NetBoxTable):
+class CircuitTable(TenancyColumnsMixin, NetBoxTable):
     cid = tables.Column(
         linkify=True,
         verbose_name='Circuit ID'
@@ -48,7 +48,6 @@ class CircuitTable(NetBoxTable):
         linkify=True
     )
     status = columns.ChoiceFieldColumn()
-    tenant = TenantColumn()
     termination_a = tables.TemplateColumn(
         template_code=CIRCUITTERMINATION_LINK,
         verbose_name='Side A'
@@ -69,7 +68,7 @@ class CircuitTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = Circuit
         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',
         )
         default_columns = (

+ 3 - 3
netbox/circuits/views.py

@@ -30,7 +30,7 @@ class ProviderView(generic.ObjectView):
         circuits = Circuit.objects.restrict(request.user, 'view').filter(
             provider=instance
         ).prefetch_related(
-            'type', 'tenant', 'terminations__site'
+            'type', 'tenant', 'tenant__group', 'terminations__site'
         )
         circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('provider',))
         circuits_table.configure(request)
@@ -91,7 +91,7 @@ class ProviderNetworkView(generic.ObjectView):
             Q(termination_a__provider_network=instance.pk) |
             Q(termination_z__provider_network=instance.pk)
         ).prefetch_related(
-            'type', 'tenant', 'terminations__site'
+            'type', 'tenant', 'tenant__group', 'terminations__site'
         )
         circuits_table = tables.CircuitTable(circuits, user=request.user)
         circuits_table.configure(request)
@@ -192,7 +192,7 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
 
 class CircuitListView(generic.ObjectListView):
     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_form = forms.CircuitFilterForm

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

@@ -5,6 +5,7 @@ from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
 
 __all__ = [
     'ComponentNestedModuleSerializer',
+    'ModuleBayNestedModuleSerializer',
     'NestedCableSerializer',
     'NestedConsolePortSerializer',
     'NestedConsolePortTemplateSerializer',
@@ -281,6 +282,14 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
         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):
     """
     Used by device component serializers.

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

@@ -886,12 +886,12 @@ class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
 class ModuleBaySerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
     device = NestedDeviceSerializer()
-    # installed_module = NestedModuleSerializer(required=False, allow_null=True)
+    installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
 
     class Meta:
         model = ModuleBay
         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',
         ]
 

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

@@ -611,7 +611,7 @@ class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
 
 
 class ModuleBayViewSet(NetBoxModelViewSet):
-    queryset = ModuleBay.objects.prefetch_related('tags')
+    queryset = ModuleBay.objects.prefetch_related('tags', 'installed_module')
     serializer_class = serializers.ModuleBaySerializer
     filterset_class = filtersets.ModuleBayFilterSet
     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
 
 
-#
-# Power feeds
-#
-
-POWERFEED_VOLTAGE_DEFAULT = 120
-POWERFEED_AMPERAGE_DEFAULT = 20
-POWERFEED_MAX_UTILIZATION_DEFAULT = 80  # Percentage
-
-
 #
 # Device components
 #

+ 6 - 0
netbox/dcim/filtersets.py

@@ -992,6 +992,12 @@ class ModuleFilterSet(NetBoxModelFilterSet):
         to_field_name='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(
         queryset=Device.objects.all(),
         label='Device (ID)',

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

@@ -521,13 +521,28 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
         required=False,
         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:
         model = Device
         fields = [
             'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
             '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 = {
             '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)),
                 ('supply', models.CharField(default='ac', 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)),
                 ('comments', models.TextField(blank=True)),
             ],

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

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

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

@@ -6,6 +6,7 @@ from django.urls import reverse
 
 from dcim.choices import *
 from dcim.constants import *
+from netbox.config import ConfigItem
 from netbox.models import NetBoxModel
 from utilities.validators import ExclusionValidator
 from .device_components import LinkTermination, PathEndpoint
@@ -105,16 +106,16 @@ class PowerFeed(NetBoxModel, PathEndpoint, LinkTermination):
         default=PowerFeedPhaseChoices.PHASE_SINGLE
     )
     voltage = models.SmallIntegerField(
-        default=POWERFEED_VOLTAGE_DEFAULT,
+        default=ConfigItem('POWERFEED_DEFAULT_VOLTAGE'),
         validators=[ExclusionValidator([0])]
     )
     amperage = models.PositiveSmallIntegerField(
         validators=[MinValueValidator(1)],
-        default=POWERFEED_AMPERAGE_DEFAULT
+        default=ConfigItem('POWERFEED_DEFAULT_AMPERAGE')
     )
     max_utilization = models.PositiveSmallIntegerField(
         validators=[MinValueValidator(1), MaxValueValidator(100)],
-        default=POWERFEED_MAX_UTILIZATION_DEFAULT,
+        default=ConfigItem('POWERFEED_DEFAULT_MAX_UTILIZATION'),
         help_text="Maximum permissible draw (percentage)"
     )
     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 netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin
 from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
 
 __all__ = (
@@ -15,7 +15,7 @@ __all__ = (
 # Cables
 #
 
-class CableTable(NetBoxTable):
+class CableTable(TenancyColumnsMixin, NetBoxTable):
     termination_a_parent = tables.TemplateColumn(
         template_code=CABLE_TERMINATION_PARENT,
         accessor=Accessor('termination_a'),
@@ -53,7 +53,6 @@ class CableTable(NetBoxTable):
         verbose_name='Termination B'
     )
     status = columns.ChoiceFieldColumn()
-    tenant = TenantColumn()
     length = columns.TemplateColumn(
         template_code=CABLE_LENGTH,
         order_by=('_abs_length', 'length_unit')
@@ -67,7 +66,7 @@ class CableTable(NetBoxTable):
         model = Cable
         fields = (
             '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 = (
             '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,
 )
 from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin
 from .template_code import *
 
 __all__ = (
@@ -137,13 +137,12 @@ class PlatformTable(NetBoxTable):
 # Devices
 #
 
-class DeviceTable(NetBoxTable):
+class DeviceTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.TemplateColumn(
         order_by=('_name',),
         template_code=DEVICE_LINK
     )
     status = columns.ChoiceFieldColumn()
-    tenant = TenantColumn()
     site = tables.Column(
         linkify=True
     )
@@ -200,7 +199,7 @@ class DeviceTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = Device
         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',
             'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'contacts', 'tags',
             'created', 'last_updated',
@@ -211,12 +210,11 @@ class DeviceTable(NetBoxTable):
         )
 
 
-class DeviceImportTable(NetBoxTable):
+class DeviceImportTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.TemplateColumn(
         template_code=DEVICE_LINK
     )
     status = columns.ChoiceFieldColumn()
-    tenant = TenantColumn()
     site = tables.Column(
         linkify=True
     )
@@ -232,7 +230,7 @@ class DeviceImportTable(NetBoxTable):
 
     class Meta(NetBoxTable.Meta):
         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
 
 

+ 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 netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin
 
 __all__ = (
     'RackTable',
@@ -37,7 +37,7 @@ class RackRoleTable(NetBoxTable):
 # Racks
 #
 
-class RackTable(NetBoxTable):
+class RackTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.Column(
         order_by=('_name',),
         linkify=True
@@ -48,7 +48,6 @@ class RackTable(NetBoxTable):
     site = tables.Column(
         linkify=True
     )
-    tenant = TenantColumn()
     status = columns.ChoiceFieldColumn()
     role = columns.ColoredLabelColumn()
     u_height = tables.TemplateColumn(
@@ -87,7 +86,7 @@ class RackTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = Rack
         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',
             'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
         )
@@ -101,7 +100,7 @@ class RackTable(NetBoxTable):
 # Rack reservations
 #
 
-class RackReservationTable(NetBoxTable):
+class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
     reservation = tables.Column(
         accessor='pk',
         linkify=True
@@ -110,7 +109,6 @@ class RackReservationTable(NetBoxTable):
         accessor=Accessor('rack__site'),
         linkify=True
     )
-    tenant = TenantColumn()
     rack = tables.Column(
         linkify=True
     )
@@ -125,7 +123,7 @@ class RackReservationTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = RackReservation
         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',
         )
         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 netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin
 from .template_code import LOCATION_BUTTONS
 
 __all__ = (
@@ -75,7 +75,7 @@ class SiteGroupTable(NetBoxTable):
 # Sites
 #
 
-class SiteTable(NetBoxTable):
+class SiteTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.Column(
         linkify=True
     )
@@ -96,7 +96,6 @@ class SiteTable(NetBoxTable):
         url_params={'site_id': 'pk'},
         verbose_name='ASN Count'
     )
-    tenant = TenantColumn()
     comments = columns.MarkdownColumn()
     contacts = columns.ManyToManyColumn(
         linkify_item=True
@@ -108,7 +107,7 @@ class SiteTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = Site
         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',
             'contacts', 'tags', 'created', 'last_updated', 'actions',
         )
@@ -119,14 +118,13 @@ class SiteTable(NetBoxTable):
 # Locations
 #
 
-class LocationTable(NetBoxTable):
+class LocationTable(TenancyColumnsMixin, NetBoxTable):
     name = columns.MPTTColumn(
         linkify=True
     )
     site = tables.Column(
         linkify=True
     )
-    tenant = TenantColumn()
     rack_count = columns.LinkedCountColumn(
         viewname='dcim:rack_list',
         url_params={'location_id': 'pk'},
@@ -150,7 +148,7 @@ class LocationTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = Location
         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',
         )
         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]}
         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):
         device_types = Device.objects.all()[:2]
         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):
     queryset = Rack.objects.prefetch_related(
-        'site', 'location', 'tenant', 'role', 'devices__device_type'
+        'site', 'location', 'tenant', 'tenant_group', 'role', 'devices__device_type'
     ).annotate(
         device_count=count_related(Device, 'rack')
     )

+ 3 - 0
netbox/extras/admin.py

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

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

@@ -221,7 +221,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
         model = JournalEntry
         fields = [
             '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):

+ 12 - 3
netbox/extras/filtersets.py

@@ -32,6 +32,9 @@ class WebhookFilterSet(BaseFilterSet):
         method='search',
         label='Search',
     )
+    content_type_id = MultiValueNumberFilter(
+        field_name='content_types__id'
+    )
     content_types = ContentTypeFilter()
     http_method = django_filters.MultipleChoiceFilter(
         choices=WebhookHttpMethodChoices
@@ -40,8 +43,8 @@ class WebhookFilterSet(BaseFilterSet):
     class Meta:
         model = Webhook
         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):
@@ -58,11 +61,17 @@ class CustomFieldFilterSet(BaseFilterSet):
         method='search',
         label='Search',
     )
+    type = django_filters.MultipleChoiceFilter(
+        choices=CustomFieldTypeChoices
+    )
+    content_type_id = MultiValueNumberFilter(
+        field_name='content_types__id'
+    )
     content_types = ContentTypeFilter()
 
     class Meta:
         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):
         if not value.strip():

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

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

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

@@ -365,13 +365,8 @@ class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
 
         # Text
         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:
                 field.validators = [
                     RegexValidator(

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

@@ -7,7 +7,9 @@ from django.test import TestCase
 
 from circuits.models import Provider
 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.models import *
 from ipam.models import IPAddress
@@ -16,6 +18,65 @@ from utilities.testing import BaseFilterSetTests, ChangeLoggedFilterSetTests, cr
 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):
     queryset = Webhook.objects.all()
     filterset = WebhookFilterSet
@@ -62,6 +123,8 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
     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_type_create(self):
         params = {'type_create': True}

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

@@ -1,7 +1,8 @@
 from django import forms
 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.constants import *
 from ipam.models import *
@@ -265,6 +266,7 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         ('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
         ('VRF', ('vrf_id', 'present_in_vrf_id')),
         ('Tenant', ('tenant_group_id', 'tenant_id')),
+        ('Device/VM', ('device_id', 'virtual_machine_id')),
     )
     parent = forms.CharField(
         required=False,
@@ -298,6 +300,16 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         required=False,
         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(
         choices=IPAddressStatusChoices,
         required=False

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

@@ -4,7 +4,7 @@ from django_tables2.utils import Accessor
 
 from ipam.models import *
 from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin, TenantColumn
 
 __all__ = (
     'AggregateTable',
@@ -99,7 +99,7 @@ class RIRTable(NetBoxTable):
 # ASNs
 #
 
-class ASNTable(NetBoxTable):
+class ASNTable(TenancyColumnsMixin, NetBoxTable):
     asn = tables.Column(
         linkify=True
     )
@@ -122,7 +122,6 @@ class ASNTable(NetBoxTable):
         linkify_item=True,
         verbose_name='Sites'
     )
-    tenant = TenantColumn()
     tags = columns.TagColumn(
         url_name='ipam:asn_list'
     )
@@ -130,7 +129,7 @@ class ASNTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = ASN
         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',
         )
         default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant')
@@ -140,12 +139,11 @@ class ASNTable(NetBoxTable):
 # Aggregates
 #
 
-class AggregateTable(NetBoxTable):
+class AggregateTable(TenancyColumnsMixin, NetBoxTable):
     prefix = tables.Column(
         linkify=True,
         verbose_name='Aggregate'
     )
-    tenant = TenantColumn()
     date_added = tables.DateColumn(
         format="Y-m-d",
         verbose_name='Added'
@@ -164,7 +162,7 @@ class AggregateTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = Aggregate
         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',
         )
         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(
         template_code=PREFIX_LINK,
         export_raw=True,
@@ -256,7 +254,6 @@ class PrefixTable(NetBoxTable):
         template_code=VRF_LINK,
         verbose_name='VRF'
     )
-    tenant = TenantColumn()
     site = tables.Column(
         linkify=True
     )
@@ -289,7 +286,7 @@ class PrefixTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = Prefix
         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',
         )
         default_columns = (
@@ -303,7 +300,7 @@ class PrefixTable(NetBoxTable):
 #
 # IP ranges
 #
-class IPRangeTable(NetBoxTable):
+class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
     start_address = tables.Column(
         linkify=True
     )
@@ -317,7 +314,6 @@ class IPRangeTable(NetBoxTable):
     role = tables.Column(
         linkify=True
     )
-    tenant = TenantColumn()
     utilization = columns.UtilizationColumn(
         accessor='utilization',
         orderable=False
@@ -329,7 +325,7 @@ class IPRangeTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = IPRange
         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',
         )
         default_columns = (
@@ -344,7 +340,7 @@ class IPRangeTable(NetBoxTable):
 # IPAddresses
 #
 
-class IPAddressTable(NetBoxTable):
+class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
     address = tables.TemplateColumn(
         template_code=IPADDRESS_LINK,
         verbose_name='IP Address'
@@ -357,7 +353,6 @@ class IPAddressTable(NetBoxTable):
         default=AVAILABLE_LABEL
     )
     role = columns.ChoiceFieldColumn()
-    tenant = TenantColumn()
     assigned_object = tables.Column(
         linkify=True,
         orderable=False,
@@ -386,7 +381,7 @@ class IPAddressTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = IPAddress
         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',
         )
         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 ipam.models import *
 from netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin, TenantColumn
 from virtualization.models import VMInterface
 
 __all__ = (
@@ -90,7 +90,7 @@ class VLANGroupTable(NetBoxTable):
 # VLANs
 #
 
-class VLANTable(NetBoxTable):
+class VLANTable(TenancyColumnsMixin, NetBoxTable):
     vid = tables.TemplateColumn(
         template_code=VLAN_LINK,
         verbose_name='VID'
@@ -104,7 +104,6 @@ class VLANTable(NetBoxTable):
     group = tables.Column(
         linkify=True
     )
-    tenant = TenantColumn()
     status = columns.ChoiceFieldColumn(
         default=AVAILABLE_LABEL
     )
@@ -123,7 +122,7 @@ class VLANTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = VLAN
         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',
         )
         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 netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin
 
 __all__ = (
     'RouteTargetTable',
@@ -20,14 +20,13 @@ VRF_TARGETS = """
 # VRFs
 #
 
-class VRFTable(NetBoxTable):
+class VRFTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.Column(
         linkify=True
     )
     rd = tables.Column(
         verbose_name='RD'
     )
-    tenant = TenantColumn()
     enforce_unique = columns.BooleanColumn(
         verbose_name='Unique'
     )
@@ -46,7 +45,7 @@ class VRFTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = VRF
         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',
         )
         default_columns = ('pk', 'name', 'rd', 'tenant', 'description')
@@ -56,16 +55,15 @@ class VRFTable(NetBoxTable):
 # Route targets
 #
 
-class RouteTargetTable(NetBoxTable):
+class RouteTargetTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.Column(
         linkify=True
     )
-    tenant = TenantColumn()
     tags = columns.TagColumn(
         url_name='ipam:vrf_list'
     )
 
     class Meta(NetBoxTable.Meta):
         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')

+ 14 - 11
netbox/ipam/views.py

@@ -298,7 +298,7 @@ class AggregatePrefixesView(generic.ObjectChildrenView):
     def get_children(self, request, parent):
         return Prefix.objects.restrict(request.user, 'view').filter(
             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):
         # Determine whether to show assigned prefixes, available prefixes, or both
@@ -470,7 +470,7 @@ class PrefixPrefixesView(generic.ObjectChildrenView):
 
     def get_children(self, request, parent):
         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):
@@ -499,7 +499,7 @@ class PrefixIPRangesView(generic.ObjectChildrenView):
 
     def get_children(self, request, parent):
         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):
@@ -587,7 +587,7 @@ class IPRangeIPAddressesView(generic.ObjectChildrenView):
 
     def get_children(self, request, parent):
         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):
@@ -680,13 +680,16 @@ class IPAddressView(generic.ObjectView):
         service_filter = Q(ipaddresses=instance)
 
         # 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)
 

+ 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)
 
         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
     ),
 
+    # 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
     ConfigParam(
         name='ALLOWED_URL_SCHEMES',

+ 10 - 10
netbox/netbox/constants.py

@@ -34,7 +34,7 @@ CIRCUIT_TYPES = OrderedDict(
         }),
         ('circuit', {
             'queryset': Circuit.objects.prefetch_related(
-                'type', 'provider', 'tenant', 'terminations__site'
+                'type', 'provider', 'tenant', 'tenant__group', 'terminations__site'
             ),
             'filterset': circuits.filtersets.CircuitFilterSet,
             'table': circuits.tables.CircuitTable,
@@ -53,13 +53,13 @@ CIRCUIT_TYPES = OrderedDict(
 DCIM_TYPES = OrderedDict(
     (
         ('site', {
-            'queryset': Site.objects.prefetch_related('region', 'tenant'),
+            'queryset': Site.objects.prefetch_related('region', 'tenant', 'tenant__group'),
             'filterset': dcim.filtersets.SiteFilterSet,
             'table': dcim.tables.SiteTable,
             'url': 'dcim:site_list',
         }),
         ('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')
             ),
             'filterset': dcim.filtersets.RackFilterSet,
@@ -100,7 +100,7 @@ DCIM_TYPES = OrderedDict(
         }),
         ('device', {
             '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,
             'table': dcim.tables.DeviceTable,
@@ -148,7 +148,7 @@ DCIM_TYPES = OrderedDict(
 IPAM_TYPES = OrderedDict(
     (
         ('vrf', {
-            'queryset': VRF.objects.prefetch_related('tenant'),
+            'queryset': VRF.objects.prefetch_related('tenant', 'tenant__group'),
             'filterset': ipam.filtersets.VRFFilterSet,
             'table': ipam.tables.VRFTable,
             'url': 'ipam:vrf_list',
@@ -160,25 +160,25 @@ IPAM_TYPES = OrderedDict(
             'url': 'ipam:aggregate_list',
         }),
         ('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,
             'table': ipam.tables.PrefixTable,
             'url': 'ipam:prefix_list',
         }),
         ('ipaddress', {
-            'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'),
+            'queryset': IPAddress.objects.prefetch_related('vrf__tenant', 'tenant', 'tenant__group'),
             'filterset': ipam.filtersets.IPAddressFilterSet,
             'table': ipam.tables.IPAddressTable,
             'url': 'ipam:ipaddress_list',
         }),
         ('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,
             'table': ipam.tables.VLANTable,
             'url': 'ipam:vlan_list',
         }),
         ('asn', {
-            'queryset': ASN.objects.prefetch_related('rir', 'tenant'),
+            'queryset': ASN.objects.prefetch_related('rir', 'tenant', 'tenant__group'),
             'filterset': ipam.filtersets.ASNFilterSet,
             'table': ipam.tables.ASNTable,
             'url': 'ipam:asn_list',
@@ -223,7 +223,7 @@ VIRTUALIZATION_TYPES = OrderedDict(
         }),
         ('virtualmachine', {
             '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,
             'table': virtualization.tables.VirtualMachineTable,

+ 14 - 1
netbox/netbox/settings.py

@@ -29,7 +29,7 @@ django.utils.encoding.force_text = force_str
 # Environment setup
 #
 
-VERSION = '3.2.5'
+VERSION = '3.2.6'
 
 # Hostname
 HOSTNAME = platform.node()
@@ -483,6 +483,19 @@ for param in dir(configuration):
 
 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

+ 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.db import transaction, IntegrityError
 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.http import HttpResponse
 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 '')
 
                 # ManyToManyFields
-                elif isinstance(model_field, ManyToManyField):
+                elif isinstance(model_field, (ManyToManyField, ManyToManyRel)):
                     if form.cleaned_data[name]:
                         getattr(obj, name).set(form.cleaned_data[name])
                 # Normal fields

Plik diff jest za duży
+ 0 - 0
netbox/project-static/dist/netbox.js


Plik diff jest za duży
+ 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 {
       this.setOptionStyles();
       this.enable();
+      this.slim.slim.search.input.focus();
       this.base.dispatchEvent(this.loadEvent);
     }
   }

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

@@ -46,13 +46,7 @@
                         </tr>
                         <tr>
                             <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>
                             <th scope="row">Position</th>
@@ -161,9 +155,7 @@
                         </tr>
                         <tr>
                             <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>
                             <th scope="row">Platform</th>
@@ -173,7 +165,7 @@
                             <th scope="row">Primary IPv4</th>
                             <td>
                               {% 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 %}
                                   (NAT for {{ object.primary_ip4.nat_inside.address.ip|linkify }})
                                 {% elif object.primary_ip4.nat_outside %}
@@ -188,7 +180,7 @@
                             <th scope="row">Primary IPv6</th>
                             <td>
                               {% 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 %}
                                   (NAT for {{ object.primary_ip6.nat_inside.address.ip|linkify }})
                                 {% elif object.primary_ip6.nat_outside %}

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

@@ -86,6 +86,15 @@
       {% render_field form.tenant %}
     </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 %}
       <div class="field-group my-5">
         <div class="row mb-2">

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

@@ -5,25 +5,29 @@
 {% render_errors form %}
 
 {% 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-body table-responsive">
       {% render_table table 'inc/table.html' %}
       {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
     </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 %}

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

@@ -2,6 +2,8 @@ import django_tables2 as tables
 
 __all__ = (
     'TenantColumn',
+    'TenantGroupColumn',
+    'TenancyColumnsMixin',
 )
 
 
@@ -24,3 +26,32 @@ class TenantColumn(tables.TemplateColumn):
 
     def value(self, value):
         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 timezone_field import TimeZoneField
 
+from netbox.config import ConfigItem
+
 
 SKIP_FIELDS = (
     TimeZoneField,
@@ -26,4 +28,9 @@ def custom_deconstruct(field):
         for attr in EXEMPT_ATTRS:
             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

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

@@ -1,6 +1,7 @@
 import django_tables2 as tables
 
 from netbox.tables import NetBoxTable, columns
+from tenancy.tables import TenancyColumnsMixin
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 
 __all__ = (
@@ -56,7 +57,7 @@ class ClusterGroupTable(NetBoxTable):
         default_columns = ('pk', 'name', 'cluster_count', 'description')
 
 
-class ClusterTable(NetBoxTable):
+class ClusterTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.Column(
         linkify=True
     )
@@ -66,9 +67,6 @@ class ClusterTable(NetBoxTable):
     group = tables.Column(
         linkify=True
     )
-    tenant = tables.Column(
-        linkify=True
-    )
     site = tables.Column(
         linkify=True
     )
@@ -93,7 +91,7 @@ class ClusterTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = Cluster
         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',
         )
         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 netbox.tables import NetBoxTable, columns
-from tenancy.tables import TenantColumn
+from tenancy.tables import TenancyColumnsMixin
 from virtualization.models import VirtualMachine, VMInterface
 
 __all__ = (
@@ -24,7 +24,7 @@ VMINTERFACE_BUTTONS = """
 # Virtual machines
 #
 
-class VirtualMachineTable(NetBoxTable):
+class VirtualMachineTable(TenancyColumnsMixin, NetBoxTable):
     name = tables.Column(
         order_by=('_name',),
         linkify=True
@@ -34,7 +34,6 @@ class VirtualMachineTable(NetBoxTable):
         linkify=True
     )
     role = columns.ColoredLabelColumn()
-    tenant = TenantColumn()
     comments = columns.MarkdownColumn()
     primary_ip4 = tables.Column(
         linkify=True,
@@ -56,7 +55,7 @@ class VirtualMachineTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = VirtualMachine
         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',
         )
         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-debug-toolbar==3.4.0
+django-debug-toolbar==3.5.0
 django-filter==22.1
 django-graphiql-debug-toolbar==0.2.0
 django-mptt==0.13.4
@@ -19,13 +19,13 @@ gunicorn==20.1.0
 Jinja2==3.1.2
 Markdown==3.3.7
 markdown-include==0.6.0
-mkdocs-material==8.3.6
+mkdocs-material==8.3.9
 mkdocstrings[python-legacy]==0.19.0
 netaddr==0.8.0
-Pillow==9.1.1
+Pillow==9.2.0
 psycopg2-binary==2.9.3
 PyYAML==6.0
-sentry-sdk==1.5.12
+sentry-sdk==1.7.0
 social-auth-app-django==5.0.0
 social-auth-core==4.3.0
 svgwrite==1.4.2

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików