diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 6795726a6..05dc0ea6f 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -112,9 +112,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): class Meta: model = Site fields = [ - 'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'description', - 'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'time_zone', - 'comments', + 'name', 'slug', 'status', 'region', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone', 'description', + 'physical_address', 'shipping_address', 'contact_name', 'contact_phone', 'contact_email', 'comments', ] widgets = { 'physical_address': SmallTextarea(attrs={'rows': 3}), @@ -124,6 +123,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): 'name': "Full name of the site", 'facility': "Data center provider and facility (e.g. Equinix NY7)", 'asn': "BGP autonomous system number", + 'time_zone': "Local time zone", + 'description': "Short description (will appear in sites list)", 'physical_address': "Physical location of the building (e.g. for GPS)", 'shipping_address': "If different from the physical address" } @@ -131,7 +132,7 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldForm): class SiteCSVForm(forms.ModelForm): status = CSVChoiceField( - choices=DEVICE_STATUS_CHOICES, + choices=SITE_STATUS_CHOICES, required=False, help_text='Operational status' ) @@ -705,7 +706,7 @@ class PlatformCSVForm(forms.ModelForm): slug = SlugField() manufacturer = forms.ModelChoiceField( queryset=Manufacturer.objects.all(), - required=True, + required=False, to_field_name='name', help_text='Manufacturer name', error_messages={ diff --git a/netbox/dcim/querysets.py b/netbox/dcim/querysets.py index 3e977ddc6..32275ce01 100644 --- a/netbox/dcim/querysets.py +++ b/netbox/dcim/querysets.py @@ -43,13 +43,13 @@ class InterfaceQuerySet(QuerySet): }[method] TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')" - ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)" - SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?([0-9]+)\/') AS integer)" - SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/)([0-9]+)') AS integer), 0)" - POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)" - SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)" - CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)" - VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)" + ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(\d{{1,9}})$') AS integer)" + SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(\d{{1,9}})\/') AS integer)" + SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/)(\d{{1,9}})') AS integer), 0)" + POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{2}}(\d{{1,9}})') AS integer), 0)" + SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:\d{{1,9}}\/){{3}}(\d{{1,9}})') AS integer), 0)" + CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':(\d{{1,9}})(\.\d{{1,9}})?$') AS integer), 0)" + VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.(\d{{1,9}})$') AS integer), 0)" fields = { '_type': RawSQL(TYPE_RE.format(sql_col), []), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 84760348b..924fe67fb 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -41,19 +41,21 @@ class BulkRenameView(View): """ An extendable view for renaming device components in bulk. """ - model = None + queryset = None form = None template_name = 'dcim/bulk_rename.html' def post(self, request): + model = self.queryset.model + return_url = request.GET.get('return_url') if not return_url or not is_safe_url(url=return_url, host=request.get_host()): return_url = 'home' if '_preview' in request.POST or '_apply' in request.POST: form = self.form(request.POST, initial={'pk': request.POST.getlist('pk')}) - selected_objects = self.model.objects.filter(pk__in=form.initial['pk']) + selected_objects = self.queryset.filter(pk__in=form.initial['pk']) if form.is_valid(): for obj in selected_objects: @@ -65,17 +67,17 @@ class BulkRenameView(View): obj.save() messages.success(request, "Renamed {} {}".format( len(selected_objects), - self.model._meta.verbose_name_plural + model._meta.verbose_name_plural )) return redirect(return_url) else: form = self.form(initial={'pk': request.POST.getlist('pk')}) - selected_objects = self.model.objects.filter(pk__in=form.initial['pk']) + selected_objects = self.queryset.filter(pk__in=form.initial['pk']) return render(request, self.template_name, { 'form': form, - 'obj_type_plural': self.model._meta.verbose_name_plural, + 'obj_type_plural': model._meta.verbose_name_plural, 'selected_objects': selected_objects, 'return_url': return_url, }) @@ -1316,7 +1318,7 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ObjectDeleteView): class ConsoleServerPortBulkRenameView(PermissionRequiredMixin, BulkRenameView): permission_required = 'dcim.change_consoleserverport' - model = ConsoleServerPort + queryset = ConsoleServerPort.objects.all() form = forms.ConsoleServerPortBulkRenameForm @@ -1600,7 +1602,7 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ObjectDeleteView): class PowerOutletBulkRenameView(PermissionRequiredMixin, BulkRenameView): permission_required = 'dcim.change_poweroutlet' - model = PowerOutlet + queryset = PowerOutlet.objects.all() form = forms.PowerOutletBulkRenameForm @@ -1676,7 +1678,7 @@ class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): class InterfaceBulkRenameView(PermissionRequiredMixin, BulkRenameView): permission_required = 'dcim.change_interface' - model = Interface + queryset = Interface.objects.order_naturally() form = forms.InterfaceBulkRenameForm @@ -1783,7 +1785,7 @@ class DeviceBayDepopulateView(PermissionRequiredMixin, View): class DeviceBayBulkRenameView(PermissionRequiredMixin, BulkRenameView): permission_required = 'dcim.change_devicebay' - model = DeviceBay + queryset = DeviceBay.objects.all() form = forms.DeviceBayBulkRenameForm diff --git a/netbox/extras/filters.py b/netbox/extras/filters.py index f21c228db..4a991471b 100644 --- a/netbox/extras/filters.py +++ b/netbox/extras/filters.py @@ -43,11 +43,18 @@ class CustomFieldFilter(django_filters.Filter): return queryset.none() # Apply the assigned filter logic (exact or loose) - queryset = queryset.filter(custom_field_values__field__name=self.name) if self.cf_type == CF_TYPE_BOOLEAN or self.filter_logic == CF_FILTER_EXACT: - return queryset.filter(custom_field_values__serialized_value=value) + queryset = queryset.filter( + custom_field_values__field__name=self.name, + custom_field_values__serialized_value=value + ) else: - return queryset.filter(custom_field_values__serialized_value__icontains=value) + queryset = queryset.filter( + custom_field_values__field__name=self.name, + custom_field_values__serialized_value__icontains=value + ) + + return queryset class CustomFieldFilterSet(django_filters.FilterSet): diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 5b2c6e672..3353d981f 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -508,7 +508,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) ipaddress = super(IPAddressForm, self).save(*args, **kwargs) - # Assign this IPAddress as the primary for the associated Device. + # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine. if self.cleaned_data['primary_for_parent']: parent = self.cleaned_data['interface'].parent if ipaddress.address.version == 4: @@ -516,14 +516,12 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) else: parent.primary_ip6 = ipaddress parent.save() - - # Clear assignment as primary for device if set. elif self.cleaned_data['interface']: parent = self.cleaned_data['interface'].parent - if ipaddress.address.version == 4 and parent.primary_ip4 == self: + if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress: parent.primary_ip4 = None parent.save() - elif ipaddress.address.version == 6 and parent.primary_ip6 == self: + elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress: parent.primary_ip6 = None parent.save() diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index f0b05b3db..08035d549 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -329,7 +329,7 @@ class IPAddressAssignTable(BaseTable): class Meta(BaseTable.Meta): model = IPAddress - fields = ('address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface') + fields = ('address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface', 'description') orderable = False diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 5c8ce68b6..1d4575e34 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -729,8 +729,8 @@ class IPAddressAssignView(PermissionRequiredMixin, View): 'vrf', 'tenant', 'interface__device', 'interface__virtual_machine' ).filter( vrf=form.cleaned_data['vrf'], - address__net_host=form.cleaned_data['address'], - ) + address__istartswith=form.cleaned_data['address'], + )[:100] # Limit to 100 results table = tables.IPAddressAssignTable(queryset) return render(request, 'ipam/ipaddress_assign.html', { diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 52cd80169..7a8dd1a98 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ if sys.version_info[0] < 3: DeprecationWarning ) -VERSION = '2.3.2' +VERSION = '2.3.3' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff --git a/netbox/templates/dcim/site_edit.html b/netbox/templates/dcim/site_edit.html index 582f93996..399551434 100644 --- a/netbox/templates/dcim/site_edit.html +++ b/netbox/templates/dcim/site_edit.html @@ -12,6 +12,7 @@ {% render_field form.facility %} {% render_field form.asn %} {% render_field form.time_zone %} + {% render_field form.description %}