diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 8cb548de2..42a716ae7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.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.5.6 + placeholder: v3.5.7 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index df931c77b..b04fda1b6 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.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.5.6 + placeholder: v3.5.7 validations: required: true - type: dropdown diff --git a/docs/features/ipam.md b/docs/features/ipam.md index d67645b17..3cbe4319d 100644 --- a/docs/features/ipam.md +++ b/docs/features/ipam.md @@ -38,7 +38,7 @@ An example hierarchy might look like this: * 100.64.16.1/24 (address) * 100.64.16.2/24 (address) * 100.64.16.3/24 (address) - * 100.64.16.9/24 (prefix) + * 100.64.19.0/24 (prefix) * 100.64.32.0/20 (prefix) * 100.64.32.1/24 (address) * 100.64.32.10-99/24 (range) diff --git a/docs/installation/1-postgresql.md b/docs/installation/1-postgresql.md index bc1bbf22c..184fc26d2 100644 --- a/docs/installation/1-postgresql.md +++ b/docs/installation/1-postgresql.md @@ -55,6 +55,9 @@ Within the shell, enter the following commands to create the database and user ( CREATE DATABASE netbox; CREATE USER netbox WITH PASSWORD 'J5brHrAXFLQSif0K'; ALTER DATABASE netbox OWNER TO netbox; +-- the next two commands are needed on PostgreSQL 15 and later +\connect netbox; +GRANT CREATE ON SCHEMA public TO netbox; ``` !!! danger "Use a strong password" diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index a81d8c954..a65794037 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -48,36 +48,40 @@ Download the [latest stable release](https://github.com/netbox-community/netbox/ Download and extract the latest version: ```no-highlight -wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz -sudo tar -xzf vX.Y.Z.tar.gz -C /opt -sudo ln -sfn /opt/netbox-X.Y.Z/ /opt/netbox +# Set $NEWVER to the NetBox version being installed +NEWVER=3.5.0 +wget https://github.com/netbox-community/netbox/archive/v$NEWVER.tar.gz +sudo tar -xzf v$NEWVER.tar.gz -C /opt +sudo ln -sfn /opt/netbox-$NEWVER/ /opt/netbox ``` Copy `local_requirements.txt`, `configuration.py`, and `ldap_config.py` (if present) from the current installation to the new version: ```no-highlight -sudo cp /opt/netbox-X.Y.Z/local_requirements.txt /opt/netbox/ -sudo cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/ -sudo cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ +# Set $OLDVER to the NetBox version currently installed +NEWVER=3.4.9 +sudo cp /opt/netbox-$OLDVER/local_requirements.txt /opt/netbox/ +sudo cp /opt/netbox-$OLDVER/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/ +sudo cp /opt/netbox-$OLDVER/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ ``` Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.) ```no-highlight -sudo cp -pr /opt/netbox-X.Y.Z/netbox/media/ /opt/netbox/netbox/ +sudo cp -pr /opt/netbox-$OLDVER/netbox/media/ /opt/netbox/netbox/ ``` Also make sure to copy or link any custom scripts and reports that you've made. Note that if these are stored outside the project root, you will not need to copy them. (Check the `SCRIPTS_ROOT` and `REPORTS_ROOT` parameters in the configuration file above if you're unsure.) ```no-highlight -sudo cp -r /opt/netbox-X.Y.Z/netbox/scripts /opt/netbox/netbox/ -sudo cp -r /opt/netbox-X.Y.Z/netbox/reports /opt/netbox/netbox/ +sudo cp -r /opt/netbox-$OLDVER/netbox/scripts /opt/netbox/netbox/ +sudo cp -r /opt/netbox-$OLDVER/netbox/reports /opt/netbox/netbox/ ``` If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well: ```no-highlight -sudo cp /opt/netbox-X.Y.Z/gunicorn.py /opt/netbox/ +sudo cp /opt/netbox-$OLDVER/gunicorn.py /opt/netbox/ ``` ### Option B: Clone the Git Repository diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index db301c55f..66c2cabce 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -1,6 +1,26 @@ # NetBox v3.5 -## v3.5.7 (FUTURE) +## v3.5.8 (FUTURE) + +--- + +## v3.5.7 (2023-07-28) + +### Enhancements + +* [#11803](https://github.com/netbox-community/netbox/issues/11803) - Move non-rack devices list to a separate tab under the rack view +* [#12625](https://github.com/netbox-community/netbox/issues/12625) - Mask sensitive parameters when viewing a configured data source +* [#13009](https://github.com/netbox-community/netbox/issues/13009) - Add IEC 10609-1 and NBR 14136 power port & outlet types +* [#13097](https://github.com/netbox-community/netbox/issues/13097) - Implement a faster initial poll for report & script results +* [#13234](https://github.com/netbox-community/netbox/issues/13234) - Add 100GBASE-X-DSFP and 100GBASE-X-SFPDD interface types + +### Bug Fixes + +* [#13051](https://github.com/netbox-community/netbox/issues/13051) - Fix Markdown support for table cell alignment +* [#13167](https://github.com/netbox-community/netbox/issues/13167) - Fix missing script results when fetched via REST API +* [#13233](https://github.com/netbox-community/netbox/issues/13233) - Remove extraneous VLAN group field from bulk edit form for interfaces +* [#13237](https://github.com/netbox-community/netbox/issues/13237) - Permit unauthenticated access to content types REST API endpoint when `LOGIN_REQUIRED` is false +* [#13285](https://github.com/netbox-community/netbox/issues/13285) - Fix exception when importing device type missing rack unit height value --- diff --git a/netbox/core/data_backends.py b/netbox/core/data_backends.py index 6cc534774..43e6f4e79 100644 --- a/netbox/core/data_backends.py +++ b/netbox/core/data_backends.py @@ -41,6 +41,7 @@ def register_backend(name): class DataBackend: parameters = {} + sensitive_parameters = [] def __init__(self, url, **kwargs): self.url = url @@ -86,6 +87,7 @@ class GitBackend(DataBackend): widget=forms.TextInput(attrs={'class': 'form-control'}) ) } + sensitive_parameters = ['password'] @contextmanager def fetch(self): @@ -135,6 +137,7 @@ class S3Backend(DataBackend): widget=forms.TextInput(attrs={'class': 'form-control'}) ), } + sensitive_parameters = ['aws_secret_access_key'] REGION_REGEX = r's3\.([a-z0-9-]+)\.amazonaws\.com' diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index f2f401718..e850a8c51 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -318,6 +318,10 @@ class PowerPortTypeChoices(ChoiceSet): TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h' TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h' TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h' + # IEC 60906-1 + TYPE_IEC_60906_1 = 'iec-60906-1' + TYPE_NBR_14136_10A = 'nbr-14136-10a' + TYPE_NBR_14136_20A = 'nbr-14136-20a' # NEMA non-locking TYPE_NEMA_115P = 'nema-1-15p' TYPE_NEMA_515P = 'nema-5-15p' @@ -429,6 +433,11 @@ class PowerPortTypeChoices(ChoiceSet): (TYPE_IEC_3PNE6H, '3P+N+E 6H'), (TYPE_IEC_3PNE9H, '3P+N+E 9H'), )), + ('IEC 60906-1', ( + (TYPE_IEC_60906_1, 'IEC 60906-1'), + (TYPE_NBR_14136_10A, '2P+T 10A (NBR 14136)'), + (TYPE_NBR_14136_20A, '2P+T 20A (NBR 14136)'), + )), ('NEMA (Non-locking)', ( (TYPE_NEMA_115P, 'NEMA 1-15P'), (TYPE_NEMA_515P, 'NEMA 5-15P'), @@ -553,6 +562,10 @@ class PowerOutletTypeChoices(ChoiceSet): TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h' TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h' TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h' + # IEC 60906-1 + TYPE_IEC_60906_1 = 'iec-60906-1' + TYPE_NBR_14136_10A = 'nbr-14136-10a' + TYPE_NBR_14136_20A = 'nbr-14136-20a' # NEMA non-locking TYPE_NEMA_115R = 'nema-1-15r' TYPE_NEMA_515R = 'nema-5-15r' @@ -657,6 +670,11 @@ class PowerOutletTypeChoices(ChoiceSet): (TYPE_IEC_3PNE6H, '3P+N+E 6H'), (TYPE_IEC_3PNE9H, '3P+N+E 9H'), )), + ('IEC 60906-1', ( + (TYPE_IEC_60906_1, 'IEC 60906-1'), + (TYPE_NBR_14136_10A, '2P+T 10A (NBR 14136)'), + (TYPE_NBR_14136_20A, '2P+T 20A (NBR 14136)'), + )), ('NEMA (Non-locking)', ( (TYPE_NEMA_115R, 'NEMA 1-15R'), (TYPE_NEMA_515R, 'NEMA 5-15R'), @@ -809,6 +827,8 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_100GE_CFP4 = '100gbase-x-cfp4' TYPE_100GE_CXP = '100gbase-x-cxp' TYPE_100GE_CPAK = '100gbase-x-cpak' + TYPE_100GE_DSFP = '100gbase-x-dsfp' + TYPE_100GE_SFP_DD = '100gbase-x-sfpdd' TYPE_100GE_QSFP28 = '100gbase-x-qsfp28' TYPE_100GE_QSFP_DD = '100gbase-x-qsfpdd' TYPE_200GE_CFP2 = '200gbase-x-cfp2' @@ -959,6 +979,8 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_100GE_CFP4, 'CFP4 (100GE)'), (TYPE_100GE_CXP, 'CXP (100GE)'), (TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'), + (TYPE_100GE_DSFP, 'DSFP (100GE)'), + (TYPE_100GE_SFP_DD, 'SFP-DD (100GE)'), (TYPE_100GE_QSFP28, 'QSFP28 (100GE)'), (TYPE_100GE_QSFP_DD, 'QSFP-DD (100GE)'), (TYPE_200GE_QSFP56, 'QSFP56 (200GE)'), diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 5a465bfc8..d721cb09b 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -1262,8 +1262,8 @@ class InterfaceBulkEditForm( ) nullable_fields = ( 'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'vdcs', 'mtu', 'description', - 'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', - 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vrf', 'wireless_lans' + 'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', + 'tagged_vlans', 'vrf', 'wireless_lans' ) def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 4aba73fde..66cb6a252 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -275,7 +275,7 @@ class DeviceType(PrimaryModel, WeightMixin): super().clean() # U height must be divisible by 0.5 - if self.u_height % decimal.Decimal(0.5): + if decimal.Decimal(self.u_height) % decimal.Decimal(0.5): raise ValidationError({ 'u_height': "U height must be in increments of 0.5 rack units." }) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 8611c136d..9ac5623e7 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -681,13 +681,6 @@ class RackView(generic.ObjectView): (PowerFeed.objects.restrict(request.user).filter(rack=instance), 'rack_id'), ) - # Get 0U devices located within the rack - nonracked_devices = Device.objects.filter( - rack=instance, - position__isnull=True, - parent_bay__isnull=True - ).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role') - peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site) if instance.location: @@ -704,7 +697,6 @@ class RackView(generic.ObjectView): return { 'related_models': related_models, - 'nonracked_devices': nonracked_devices, 'next_rack': next_rack, 'prev_rack': prev_rack, 'svg_extra': svg_extra, @@ -731,6 +723,26 @@ class RackRackReservationsView(generic.ObjectChildrenView): return parent.reservations.restrict(request.user, 'view') +@register_model_view(Rack, 'nonracked_devices', 'nonracked-devices') +class RackNonRackedView(generic.ObjectChildrenView): + queryset = Rack.objects.all() + child_model = Device + table = tables.DeviceTable + filterset = filtersets.DeviceFilterSet + template_name = 'dcim/rack/non_racked_devices.html' + tab = ViewTab( + label=_('Non-Racked Devices'), + badge=lambda obj: obj.devices.filter(rack=obj, position__isnull=True, parent_bay__isnull=True).count(), + weight=500, + permission='dcim.view_device', + ) + + def get_children(self, request, parent): + return parent.devices.restrict(request.user, 'view').filter( + rack=parent, position__isnull=True, parent_bay__isnull=True + ) + + @register_model_view(Rack, 'edit') class RackEditView(generic.ObjectEditView): queryset = Rack.objects.all() diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 5761d6767..21a82f0d5 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -5,7 +5,6 @@ from rest_framework import status from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied from rest_framework.generics import RetrieveUpdateDestroyAPIView -from rest_framework.permissions import IsAuthenticated from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.routers import APIRootView @@ -319,7 +318,7 @@ class ScriptViewSet(ViewSet): # Attach Job objects to each script (if any) for script in script_list: - script.result = results.get(script.name, None) + script.result = results.get(script.class_name, None) serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request}) @@ -330,7 +329,7 @@ class ScriptViewSet(ViewSet): object_type = ContentType.objects.get(app_label='extras', model='scriptmodule') script.result = Job.objects.filter( object_type=object_type, - name=script.name, + name=script.class_name, status__in=JobStatusChoices.TERMINAL_STATE_CHOICES ).first() serializer = serializers.ScriptDetailSerializer(script, context={'request': request}) @@ -397,7 +396,7 @@ class ContentTypeViewSet(ReadOnlyModelViewSet): """ Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects. """ - permission_classes = (IsAuthenticated,) + permission_classes = [IsAuthenticatedOrLoginNotRequired] queryset = ContentType.objects.order_by('app_label', 'model') serializer_class = serializers.ContentTypeSerializer filterset_class = filtersets.ContentTypeFilterSet diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 7e5d26186..6b883c838 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -47,7 +47,7 @@ ORGANIZATION_MENU = Menu( get_model_item('tenancy', 'contact', _('Contacts')), get_model_item('tenancy', 'contactgroup', _('Contact Groups')), get_model_item('tenancy', 'contactrole', _('Contact Roles')), - get_model_item('tenancy', 'contactassignment', _('Contact Assignments'), actions=[]), + get_model_item('tenancy', 'contactassignment', _('Contact Assignments'), actions=['import']), ), ), ), diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index da58b0dd6..090851c00 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.5.7-dev' +VERSION = '3.5.8-dev' # Hostname HOSTNAME = platform.node() diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index 11110069e..2aa24b72c 100644 Binary files a/netbox/project-static/dist/netbox-dark.css and b/netbox/project-static/dist/netbox-dark.css differ diff --git a/netbox/project-static/dist/netbox-light.css b/netbox/project-static/dist/netbox-light.css index 8a3c83af9..ffdd83285 100644 Binary files a/netbox/project-static/dist/netbox-light.css and b/netbox/project-static/dist/netbox-light.css differ diff --git a/netbox/project-static/dist/netbox-print.css b/netbox/project-static/dist/netbox-print.css index ef2682e0a..b492e4d1d 100644 Binary files a/netbox/project-static/dist/netbox-print.css and b/netbox/project-static/dist/netbox-print.css differ diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index b294d67bd..94fddc32c 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/netbox/project-static/styles/netbox.scss @@ -1002,6 +1002,18 @@ div.card-overlay { padding: 8px; } +th[align="left"] { + text-align: left; +} + +th[align="center"] { + text-align: center; +} + +th[align="right"] { + text-align: right; +} + /* Markdown widget */ .markdown-widget { .nav-link { diff --git a/netbox/templates/core/datasource.html b/netbox/templates/core/datasource.html index c69569358..992edb2d1 100644 --- a/netbox/templates/core/datasource.html +++ b/netbox/templates/core/datasource.html @@ -88,7 +88,11 @@ {% for name, field in object.get_backend.parameters.items %} {{ field.label }} - {{ object.parameters|get_key:name|placeholder }} + {% if name in object.get_backend.sensitive_parameters and not perms.core.change_datasource %} + ******** + {% else %} + {{ object.parameters|get_key:name|placeholder }} + {% endif %} {% empty %} diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 01aeacff1..a6d236e88 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -196,7 +196,6 @@ {% include 'inc/panels/related_objects.html' %} - {% include 'dcim/inc/nonracked_devices.html' %} {% plugin_right_page object %} diff --git a/netbox/templates/dcim/rack/non_racked_devices.html b/netbox/templates/dcim/rack/non_racked_devices.html new file mode 100644 index 000000000..700c66369 --- /dev/null +++ b/netbox/templates/dcim/rack/non_racked_devices.html @@ -0,0 +1,51 @@ +{% extends 'dcim/rack/base.html' %} +{% load helpers %} + +{% block extra_controls %} + {% if perms.dcim.add_device %} +
+ + Add non-racked device + +
+ {% endif %} +{% endblock %} + +{% block content %} + {% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %} + +
+ {% csrf_token %} + +
+
+ {% include 'htmx/table.html' %} +
+
+ +
+
+ {% if 'bulk_edit' in actions %} + + {% endif %} + {% if 'bulk_delete' in actions %} + + {% endif %} +
+
+
+{% endblock content %} + +{% block modals %} + {{ block.super }} + {% table_config_form table %} +{% endblock modals %} diff --git a/netbox/templates/extras/report_result.html b/netbox/templates/extras/report_result.html index 9358af364..f6a9a6398 100644 --- a/netbox/templates/extras/report_result.html +++ b/netbox/templates/extras/report_result.html @@ -4,7 +4,7 @@ {% block content-wrapper %}
-
+
{% include 'extras/htmx/report_result.html' %}
diff --git a/netbox/templates/extras/script_result.html b/netbox/templates/extras/script_result.html index 4dfd7482a..436ba7354 100644 --- a/netbox/templates/extras/script_result.html +++ b/netbox/templates/extras/script_result.html @@ -47,7 +47,7 @@
-
+
{% include 'extras/htmx/script_result.html' %}
diff --git a/netbox/tenancy/forms/bulk_import.py b/netbox/tenancy/forms/bulk_import.py index f9b8accd9..0aec0e28f 100644 --- a/netbox/tenancy/forms/bulk_import.py +++ b/netbox/tenancy/forms/bulk_import.py @@ -1,9 +1,11 @@ +from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ from netbox.forms import NetBoxModelImportForm from tenancy.models import * -from utilities.forms.fields import CSVModelChoiceField, SlugField +from utilities.forms.fields import CSVContentTypeField, CSVModelChoiceField, SlugField __all__ = ( + 'ContactAssignmentImportForm', 'ContactImportForm', 'ContactGroupImportForm', 'ContactRoleImportForm', @@ -81,3 +83,27 @@ class ContactImportForm(NetBoxModelImportForm): class Meta: model = Contact fields = ('name', 'title', 'phone', 'email', 'address', 'link', 'group', 'description', 'comments', 'tags') + + +class ContactAssignmentImportForm(NetBoxModelImportForm): + content_type = CSVContentTypeField( + queryset=ContentType.objects.all(), + help_text=_("One or more assigned object types") + ) + contact = CSVModelChoiceField( + queryset=Contact.objects.all(), + to_field_name='name', + help_text=_('Assigned contact') + ) + role = CSVModelChoiceField( + queryset=ContactRole.objects.all(), + to_field_name='name', + help_text=_('Assigned role') + ) + + # Remove the tags field added by NetBoxModelImportForm (unsupported by ContactAssignment) + tags = None + + class Meta: + model = ContactAssignment + fields = ('content_type', 'object_id', 'contact', 'priority', 'role') diff --git a/netbox/tenancy/urls.py b/netbox/tenancy/urls.py index 87491ea0e..ad9908c62 100644 --- a/netbox/tenancy/urls.py +++ b/netbox/tenancy/urls.py @@ -49,6 +49,7 @@ urlpatterns = [ # Contact assignments path('contact-assignments/', views.ContactAssignmentListView.as_view(), name='contactassignment_list'), path('contact-assignments/add/', views.ContactAssignmentEditView.as_view(), name='contactassignment_add'), + path('contact-assignments/import/', views.ContactAssignmentBulkImportView.as_view(), name='contactassignment_import'), path('contact-assignments/edit/', views.ContactAssignmentBulkEditView.as_view(), name='contactassignment_bulk_edit'), path('contact-assignments/delete/', views.ContactAssignmentBulkDeleteView.as_view(), name='contactassignment_bulk_delete'), path('contact-assignments//', include(get_model_urls('tenancy', 'contactassignment'))), diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index bbe901bde..23020e794 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -420,6 +420,11 @@ class ContactAssignmentBulkEditView(generic.BulkEditView): form = forms.ContactAssignmentBulkEditForm +class ContactAssignmentBulkImportView(generic.BulkImportView): + queryset = ContactAssignment.objects.all() + model_form = forms.ContactAssignmentImportForm + + class ContactAssignmentBulkDeleteView(generic.BulkDeleteView): queryset = ContactAssignment.objects.all() filterset = filtersets.ContactAssignmentFilterSet diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 114397dae..9524e242c 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -519,6 +519,8 @@ def clean_html(html, schemes): "h1": ["id"], "h2": ["id"], "h3": ["id"], "h4": ["id"], "h5": ["id"], "h6": ["id"], "a": ["href", "title"], "img": ["src", "title", "alt"], + "th": ["align"], + "td": ["align"], } return bleach.clean( diff --git a/requirements.txt b/requirements.txt index f707c60c5..eef9e1434 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ bleach==6.0.0 -boto3==1.28.1 -Django==4.2.2 +boto3==1.28.14 django-cors-headers==4.2.0 django-debug-toolbar==4.1.0 django-filter==23.2 @@ -15,21 +14,21 @@ django-tables2==2.6.0 django-taggit==4.0.0 django-timezone-field==5.1 djangorestframework==3.14.0 -drf-spectacular==0.26.3 +drf-spectacular==0.26.4 drf-spectacular-sidecar==2023.7.1 dulwich==0.21.5 feedparser==6.0.10 graphene-django==3.0.0 -gunicorn==20.1.0 +gunicorn==21.2.0 Jinja2==3.1.2 Markdown==3.3.7 -mkdocs-material==9.1.18 +mkdocs-material==9.1.21 mkdocstrings[python-legacy]==0.22.0 netaddr==0.8.0 Pillow==10.0.0 psycopg[binary,pool]==3.1.9 -PyYAML==6.0 -sentry-sdk==1.28.0 +PyYAML==6.0.1 +sentry-sdk==1.28.1 social-auth-app-django==5.2.0 social-auth-core[openidconnect]==4.4.2 svgwrite==1.4.3