diff --git a/docs/additional-features/custom-scripts.md b/docs/additional-features/custom-scripts.md index 0904f8c82..1d84fea24 100644 --- a/docs/additional-features/custom-scripts.md +++ b/docs/additional-features/custom-scripts.md @@ -63,7 +63,7 @@ A human-friendly description of what your script does. ### `field_order` -A list of field names indicating the order in which the form fields should appear. This is optional, however on Python 3.5 and earlier the fields will appear in random order. (Declarative ordering is preserved on Python 3.6 and above.) For example: +A list of field names indicating the order in which the form fields should appear. This is optional, and should not be required on Python 3.6 and above. For example: ``` field_order = ['var1', 'var2', 'var3'] diff --git a/docs/administration/netbox-shell.md b/docs/administration/netbox-shell.md index bae4471b8..34cd5a30f 100644 --- a/docs/administration/netbox-shell.md +++ b/docs/administration/netbox-shell.md @@ -10,8 +10,8 @@ This will launch a customized version of [the built-in Django shell](https://doc ``` $ ./manage.py nbshell -### NetBox interactive shell (jstretch-laptop) -### Python 3.5.2 | Django 2.0.8 | NetBox 2.4.3 +### NetBox interactive shell (localhost) +### Python 3.6.9 | Django 2.2.11 | NetBox 2.7.10 ### lsmodels() will show available models. Use help() for more info. ``` diff --git a/docs/api/overview.md b/docs/api/overview.md index 1d8a91084..81e4caa25 100644 --- a/docs/api/overview.md +++ b/docs/api/overview.md @@ -187,37 +187,6 @@ GET /api/ipam/prefixes/13980/?brief=1 The brief format is supported for both lists and individual objects. -### Static Choice Fields - -Some model fields, such as the `status` field in the above example, utilize static integers corresponding to static choices. The available choices can be retrieved from the read-only `_choices` endpoint within each app. A specific `model:field` tuple may optionally be specified in the URL. - -Each choice includes a human-friendly label and its corresponding numeric value. For example, `GET /api/ipam/_choices/prefix:status/` will return: - -``` -[ - { - "value": 0, - "label": "Container" - }, - { - "value": 1, - "label": "Active" - }, - { - "value": 2, - "label": "Reserved" - }, - { - "value": 3, - "label": "Deprecated" - } -] -``` - -Thus, to set a prefix's status to "Reserved," it would be assigned the integer `2`. - -A request for `GET /api/ipam/_choices/` will return choices for _all_ fields belonging to models within the IPAM app. - ## Pagination API responses which contain a list of objects (for example, a request to `/api/dcim/devices/`) will be paginated to avoid unnecessary overhead. The root JSON object will contain the following attributes: @@ -280,27 +249,32 @@ A list of objects retrieved via the API can be filtered by passing one or more q GET /api/ipam/prefixes/?status=1 ``` -The choices available for fixed choice fields such as `status` are exposed in the API under a special `_choices` endpoint for each NetBox app. For example, the available choices for `Prefix.status` are listed at `/api/ipam/_choices/` under the key `prefix:status`: +The choices available for fixed choice fields such as `status` can be retrieved by sending an `OPTIONS` API request for the desired endpoint: + +```no-highlight +$ curl -s -X OPTIONS \ +-H "Content-Type: application/json" \ +-H "Accept: application/json; indent=4" \ +http://localhost:8000/api/ipam/prefixes/ | jq ".actions.POST.status.choices" +[ + { + "value": "container", + "display_name": "Container" + }, + { + "value": "active", + "display_name": "Active" + }, + { + "value": "reserved", + "display_name": "Reserved" + }, + { + "value": "deprecated", + "display_name": "Deprecated" + } +] -``` -"prefix:status": [ - { - "label": "Container", - "value": 0 - }, - { - "label": "Active", - "value": 1 - }, - { - "label": "Reserved", - "value": 2 - }, - { - "label": "Deprecated", - "value": 3 - } -], ``` For most fields, when a filter is passed multiple times, objects matching _any_ of the provided values will be returned. For example, `GET /api/dcim/sites/?name=Foo&name=Bar` will return all sites named "Foo" _or_ "Bar". The exception to this rule is ManyToManyFields which may have multiple values assigned. Tags are the most common example of a ManyToManyField. For example, `GET /api/dcim/sites/?tag=foo&tag=bar` will return only sites tagged with both "foo" _and_ "bar". diff --git a/docs/index.md b/docs/index.md index 4db2c55f5..3880c9d07 100644 --- a/docs/index.md +++ b/docs/index.md @@ -55,7 +55,7 @@ NetBox is built on the [Django](https://djangoproject.com/) Python framework and ## Supported Python Versions -NetBox supports Python 3.5, 3.6, and 3.7 environments currently. Python 3.5 is scheduled to be unsupported in NetBox v2.8. +NetBox supports Python 3.6 and 3.7 environments currently. (Support for Python 3.5 was removed in NetBox v2.8.) ## Getting Started diff --git a/docs/release-notes/version-2.7.md b/docs/release-notes/version-2.7.md index 69be137d7..f7460d92e 100644 --- a/docs/release-notes/version-2.7.md +++ b/docs/release-notes/version-2.7.md @@ -1,5 +1,20 @@ # NetBox v2.7 Release Notes +## v2.7.11 (FUTURE) + +### Enhancements + +* [#4309](https://github.com/netbox-community/netbox/issues/4309) - Add descriptive tooltip to custom fields on object views +* [#4369](https://github.com/netbox-community/netbox/issues/4369) - Add a dedicated view for rack reservations + +### Bug Fixes + +* [#4340](https://github.com/netbox-community/netbox/issues/4340) - Enforce unique constraints for device and virtual machine names in the API +* [#4343](https://github.com/netbox-community/netbox/issues/4343) - Fix Markdown support for tables +* [#4365](https://github.com/netbox-community/netbox/issues/4365) - Fix exception raised on IP address bulk add view + +--- + ## v2.7.10 (2020-03-10) **Note:** If your deployment requires any non-core Python packages (such as `napalm`, `django-storages`, or `django-auth-ldap`), list them in a file named `local_requirements.txt` in the NetBox root directory (alongside `requirements.txt`). This will ensure they are detected and re-installed by the upgrade script when the Python virtual environment is rebuilt. diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md index e3ed0291d..f42625ef2 100644 --- a/docs/release-notes/version-2.8.md +++ b/docs/release-notes/version-2.8.md @@ -20,12 +20,24 @@ If further customization of remote authentication is desired (for instance, if y * [#1754](https://github.com/netbox-community/netbox/issues/1754) - Added support for nested rack groups * [#3939](https://github.com/netbox-community/netbox/issues/3939) - Added support for nested tenant groups +* [#4078](https://github.com/netbox-community/netbox/issues/4078) - Standardized description fields across all models * [#4195](https://github.com/netbox-community/netbox/issues/4195) - Enabled application logging (see [logging configuration](../configuration/optional-settings.md#logging)) ### API Changes -* dcim.Rack: The `/api/dcim/racks//units/` endpoint has been replaced with `/api/dcim/racks//elevation/`. +* The `_choices` API endpoints have been removed. Instead, use an `OPTIONS` request to a model's endpoint to view the available values for all fields. ([#3416](https://github.com/netbox-community/netbox/issues/3416)) * The `id__in` filter has been removed. Use the format `?id=1&id=2` instead. ([#4313](https://github.com/netbox-community/netbox/issues/4313)) +* dcim.Manufacturer: Added a `description` field +* dcim.Platform: Added a `description` field +* dcim.Rack: The `/api/dcim/racks//units/` endpoint has been replaced with `/api/dcim/racks//elevation/`. +* dcim.RackGroup: Added a `description` field +* dcim.Region: Added a `description` field +* extras.Tag: Renamed `comments` to `description`; truncated length to 200 characters; removed Markdown rendering +* ipam.RIR: Added a `description` field +* ipam.VLANGroup: Added a `description` field +* tenancy.TenantGroup: Added a `description` field +* virtualization.ClusterGroup: Added a `description` field +* virtualization.ClusterType: Added a `description` field ### Other Changes diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index cd3015d0a..01fbfb62c 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -14,9 +14,6 @@ class CircuitsRootView(routers.APIRootView): router = routers.DefaultRouter() router.APIRootView = CircuitsRootView -# Field choices -router.register('_choices', views.CircuitsFieldChoicesViewSet, basename='field-choice') - # Providers router.register('providers', views.ProviderViewSet) diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 75f7e0e3e..363392a4d 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -8,21 +8,10 @@ from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from extras.api.serializers import RenderedGraphSerializer from extras.api.views import CustomFieldModelViewSet from extras.models import Graph -from utilities.api import FieldChoicesViewSet, ModelViewSet +from utilities.api import ModelViewSet from . import serializers -# -# Field choices -# - -class CircuitsFieldChoicesViewSet(FieldChoicesViewSet): - fields = ( - (serializers.CircuitSerializer, ['status']), - (serializers.CircuitTerminationSerializer, ['term_side']), - ) - - # # Providers # diff --git a/netbox/circuits/migrations/0008_standardize_description.py b/netbox/circuits/migrations/0008_standardize_description.py new file mode 100644 index 000000000..fecdee3ca --- /dev/null +++ b/netbox/circuits/migrations/0008_standardize_description.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.3 on 2020-03-13 20:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0007_circuit_add_description_squashed_0017_circuittype_description'), + ] + + operations = [ + migrations.AlterField( + model_name='circuit', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + migrations.AlterField( + model_name='circuittermination', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + migrations.AlterField( + model_name='circuittype', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 812eaa79e..e9e8f8aa1 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -7,6 +7,7 @@ from dcim.constants import CONNECTION_STATUS_CHOICES from dcim.fields import ASNField from dcim.models import CableTermination from extras.models import CustomFieldModel, ObjectChange, TaggedItem +from extras.utils import extras_features from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object from .choices import * @@ -21,6 +22,7 @@ __all__ = ( ) +@extras_features('custom_fields', 'custom_links', 'graphs', 'export_templates', 'webhooks') class Provider(ChangeLoggedModel, CustomFieldModel): """ Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model @@ -108,7 +110,7 @@ class CircuitType(ChangeLoggedModel): unique=True ) description = models.CharField( - max_length=100, + max_length=200, blank=True, ) @@ -131,6 +133,7 @@ class CircuitType(ChangeLoggedModel): ) +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class Circuit(ChangeLoggedModel, CustomFieldModel): """ A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple @@ -173,7 +176,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel): null=True, verbose_name='Commit rate (Kbps)') description = models.CharField( - max_length=100, + max_length=200, blank=True ) comments = models.TextField( @@ -292,7 +295,7 @@ class CircuitTermination(CableTermination): verbose_name='Patch panel/port(s)' ) description = models.CharField( - max_length=100, + max_length=200, blank=True ) diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index b1b6d9e14..b5f8758e7 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -6,7 +6,7 @@ from circuits.choices import * from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from dcim.models import Site from extras.models import Graph -from utilities.testing import APITestCase, choices_to_dict +from utilities.testing import APITestCase class AppTest(APITestCase): @@ -18,19 +18,6 @@ class AppTest(APITestCase): self.assertEqual(response.status_code, 200) - def test_choices(self): - - url = reverse('circuits-api:field-choice-list') - response = self.client.get(url, **self.header) - - self.assertEqual(response.status_code, 200) - - # Circuit - self.assertEqual(choices_to_dict(response.data.get('circuit:status')), CircuitStatusChoices.as_dict()) - - # CircuitTermination - self.assertEqual(choices_to_dict(response.data.get('circuit-termination:term_side')), CircuitTerminationSideChoices.as_dict()) - class ProviderTest(APITestCase): diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 85ff1895c..efc186f97 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -64,7 +64,7 @@ class RegionSerializer(serializers.ModelSerializer): class Meta: model = Region - fields = ['id', 'name', 'slug', 'parent', 'site_count'] + fields = ['id', 'name', 'slug', 'parent', 'description', 'site_count'] class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer): @@ -101,7 +101,7 @@ class RackGroupSerializer(ValidatedModelSerializer): class Meta: model = RackGroup - fields = ['id', 'name', 'slug', 'site', 'parent', 'rack_count'] + fields = ['id', 'name', 'slug', 'site', 'parent', 'description', 'rack_count'] class RackRoleSerializer(ValidatedModelSerializer): @@ -219,7 +219,9 @@ class ManufacturerSerializer(ValidatedModelSerializer): class Meta: model = Manufacturer - fields = ['id', 'name', 'slug', 'devicetype_count', 'inventoryitem_count', 'platform_count'] + fields = [ + 'id', 'name', 'slug', 'description', 'devicetype_count', 'inventoryitem_count', 'platform_count', + ] class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer): @@ -356,7 +358,7 @@ class PlatformSerializer(ValidatedModelSerializer): class Meta: model = Platform fields = [ - 'id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'device_count', + 'id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'device_count', 'virtualmachine_count', ] diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index 5a915becc..f989d817c 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -14,9 +14,6 @@ class DCIMRootView(routers.APIRootView): router = routers.DefaultRouter() router.APIRootView = DCIMRootView -# Field choices -router.register('_choices', views.DCIMFieldChoicesViewSet, basename='field-choice') - # Sites router.register('regions', views.RegionViewSet) router.register('sites', views.SiteViewSet) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index d044d6198..f61041b58 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -26,7 +26,7 @@ from extras.api.views import CustomFieldModelViewSet from extras.models import Graph from ipam.models import Prefix, VLAN from utilities.api import ( - get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable, + get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, ModelViewSet, ServiceUnavailable, ) from utilities.utils import get_subquery from virtualization.models import VirtualMachine @@ -34,35 +34,6 @@ from . import serializers from .exceptions import MissingFilterException -# -# Field choices -# - -class DCIMFieldChoicesViewSet(FieldChoicesViewSet): - fields = ( - (serializers.CableSerializer, ['length_unit', 'status', 'termination_a_type', 'termination_b_type', 'type']), - (serializers.ConsolePortSerializer, ['type', 'connection_status']), - (serializers.ConsolePortTemplateSerializer, ['type']), - (serializers.ConsoleServerPortSerializer, ['type']), - (serializers.ConsoleServerPortTemplateSerializer, ['type']), - (serializers.DeviceSerializer, ['face', 'status']), - (serializers.DeviceTypeSerializer, ['subdevice_role']), - (serializers.FrontPortSerializer, ['type']), - (serializers.FrontPortTemplateSerializer, ['type']), - (serializers.InterfaceSerializer, ['type', 'mode']), - (serializers.InterfaceTemplateSerializer, ['type']), - (serializers.PowerFeedSerializer, ['phase', 'status', 'supply', 'type']), - (serializers.PowerOutletSerializer, ['type', 'feed_leg']), - (serializers.PowerOutletTemplateSerializer, ['type', 'feed_leg']), - (serializers.PowerPortSerializer, ['type', 'connection_status']), - (serializers.PowerPortTemplateSerializer, ['type']), - (serializers.RackSerializer, ['outer_unit', 'status', 'type', 'width']), - (serializers.RearPortSerializer, ['type']), - (serializers.RearPortTemplateSerializer, ['type']), - (serializers.SiteSerializer, ['status']), - ) - - # Mixins class CableTraceMixin(object): diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index e4ddf792b..1fa7e7210 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -74,7 +74,7 @@ class RegionFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class Meta: model = Region - fields = ['id', 'name', 'slug'] + fields = ['id', 'name', 'slug', 'description'] class SiteFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): @@ -166,7 +166,7 @@ class RackGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class Meta: model = RackGroup - fields = ['id', 'name', 'slug'] + fields = ['id', 'name', 'slug', 'description'] class RackRoleFilterSet(BaseFilterSet, NameSlugSearchFilterSet): @@ -318,7 +318,7 @@ class ManufacturerFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class Meta: model = Manufacturer - fields = ['id', 'name', 'slug'] + fields = ['id', 'name', 'slug', 'description'] class DeviceTypeFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): @@ -493,7 +493,7 @@ class PlatformFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class Meta: model = Platform - fields = ['id', 'name', 'slug', 'napalm_driver'] + fields = ['id', 'name', 'slug', 'napalm_driver', 'description'] class DeviceFilterSet( diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 1a6d60b86..70bada0a2 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -192,7 +192,7 @@ class RegionForm(BootstrapMixin, forms.ModelForm): class Meta: model = Region fields = ( - 'parent', 'name', 'slug', + 'parent', 'name', 'slug', 'description', ) @@ -404,7 +404,7 @@ class RackGroupForm(BootstrapMixin, forms.ModelForm): class Meta: model = RackGroup fields = ( - 'site', 'parent', 'name', 'slug', + 'site', 'parent', 'name', 'slug', 'description', ) @@ -823,6 +823,13 @@ class RackElevationFilterForm(RackFilterForm): # class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm): + rack = forms.ModelChoiceField( + queryset=Rack.objects.all(), + required=False, + widget=forms.HiddenInput() + ) + # TODO: Change this to an API-backed form field. We can't do this currently because we want to retain + # the multi-line + + + + + + + +
+ {% if perms.dcim.change_rackreservation %} + {% edit_button rackreservation %} + {% endif %} + {% if perms.dcim.delete_rackreservation %} + {% delete_button rackreservation %} + {% endif %} +
+

{% block title %}{{ rackreservation }}{% endblock %}

+ {% include 'inc/created_updated.html' with obj=rackreservation %} + +{% endblock %} + +{% block content %} +
+
+
+
+ Rack +
+ + {% with rack=rackreservation.rack %} + + + + + + + + + + + + + {% endwith %} +
Site + {% if rack.site.region %} + {{ rack.site.region }} + + {% endif %} + {{ rack.site }} +
Group + {% if rack.group %} + {{ rack.group }} + {% else %} + None + {% endif %} +
Rack + {{ rack }} +
+
+
+
+ Reservation Details +
+ + + + + + + + + + + + + + + + + +
Units{{ rackreservation.unit_list }}
Tenant + {% if rackreservation.tenant %} + {% if rackreservation.tenant.group %} + {{ rackreservation.tenant.group }} + + {% endif %} + {{ rackreservation.tenant }} + {% else %} + None + {% endif %} +
User{{ rackreservation.user }}
Description{{ rackreservation.description }}
+
+
+
+ {% with rack=rackreservation.rack %} +
+
+
+

Front

+
+ {% include 'dcim/inc/rack_elevation.html' with face='front' %} +
+
+
+

Rear

+
+ {% include 'dcim/inc/rack_elevation.html' with face='rear' %} +
+
+ {% endwith %} +
+
+{% endblock %} + +{% block javascript %} + +{% endblock %} diff --git a/netbox/templates/dcim/rackreservation_edit.html b/netbox/templates/dcim/rackreservation_edit.html new file mode 100644 index 000000000..b2304974e --- /dev/null +++ b/netbox/templates/dcim/rackreservation_edit.html @@ -0,0 +1,21 @@ +{% extends 'utilities/obj_edit.html' %} +{% load form_helpers %} + +{% block form %} +
+
{{ obj_type|capfirst }}
+
+
+ +
+

{{ obj.rack }}

+
+
+ {% render_field form.units %} + {% render_field form.user %} + {% render_field form.tenant_group %} + {% render_field form.tenant %} + {% render_field form.description %} +
+
+{% endblock %} diff --git a/netbox/templates/extras/tag.html b/netbox/templates/extras/tag.html index 64e5bbebd..d87aec3f7 100644 --- a/netbox/templates/extras/tag.html +++ b/netbox/templates/extras/tag.html @@ -82,20 +82,13 @@   + + Description + + {{ tag.description }} + -
-
- Comments -
-
- {% if tag.comments %} - {{ tag.comments|render_markdown }} - {% else %} - None - {% endif %} -
-
{% include 'panel_table.html' with table=items_table heading='Tagged Objects' %} diff --git a/netbox/templates/extras/tag_edit.html b/netbox/templates/extras/tag_edit.html index 800db1d26..87b9a2e53 100644 --- a/netbox/templates/extras/tag_edit.html +++ b/netbox/templates/extras/tag_edit.html @@ -8,12 +8,7 @@ {% render_field form.name %} {% render_field form.slug %} {% render_field form.color %} -
- -
-
Comments
-
- {% render_field form.comments %} + {% render_field form.description %}
{% endblock %} diff --git a/netbox/templates/inc/custom_fields_panel.html b/netbox/templates/inc/custom_fields_panel.html index 84892f726..00e009611 100644 --- a/netbox/templates/inc/custom_fields_panel.html +++ b/netbox/templates/inc/custom_fields_panel.html @@ -7,7 +7,7 @@ {% for field, value in custom_fields.items %} - +
{{ field }}{{ field }} {% if field.type == 'boolean' and value == True %} diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index d2eb93ebd..e65d42623 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -462,6 +462,7 @@ {% if perms.secrets.add_secret %}
+
{% endif %} diff --git a/netbox/templates/secrets/secret_edit.html b/netbox/templates/secrets/secret_edit.html index 875e53c5c..699999a9d 100644 --- a/netbox/templates/secrets/secret_edit.html +++ b/netbox/templates/secrets/secret_edit.html @@ -21,12 +21,7 @@
Secret Attributes
-
- -
-

{{ secret.device }}

-
-
+ {% render_field form.device %} {% render_field form.role %} {% render_field form.name %} {% render_field form.userkeys %} diff --git a/netbox/templates/utilities/obj_edit.html b/netbox/templates/utilities/obj_edit.html index 0d7760f30..bbe162def 100644 --- a/netbox/templates/utilities/obj_edit.html +++ b/netbox/templates/utilities/obj_edit.html @@ -51,7 +51,7 @@
- {% if settings.DOCS_ROOT %} + {% if obj and settings.DOCS_ROOT %} {% include 'inc/modal.html' with name='docs' content=obj|get_docs %} {% endif %} {% endblock %} diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index ec5e60a34..9c7a099e4 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -17,7 +17,7 @@ class TenantGroupSerializer(ValidatedModelSerializer): class Meta: model = TenantGroup - fields = ['id', 'name', 'slug', 'parent', 'tenant_count'] + fields = ['id', 'name', 'slug', 'parent', 'description', 'tenant_count'] class TenantSerializer(TaggitSerializer, CustomFieldModelSerializer): diff --git a/netbox/tenancy/api/urls.py b/netbox/tenancy/api/urls.py index 5762f9a0d..645cc2edc 100644 --- a/netbox/tenancy/api/urls.py +++ b/netbox/tenancy/api/urls.py @@ -14,9 +14,6 @@ class TenancyRootView(routers.APIRootView): router = routers.DefaultRouter() router.APIRootView = TenancyRootView -# Field choices -router.register('_choices', views.TenancyFieldChoicesViewSet, basename='field-choice') - # Tenants router.register('tenant-groups', views.TenantGroupViewSet) router.register('tenants', views.TenantViewSet) diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index ab82c3cf5..148058a33 100644 --- a/netbox/tenancy/api/views.py +++ b/netbox/tenancy/api/views.py @@ -4,20 +4,12 @@ from extras.api.views import CustomFieldModelViewSet from ipam.models import IPAddress, Prefix, VLAN, VRF from tenancy import filters from tenancy.models import Tenant, TenantGroup -from utilities.api import FieldChoicesViewSet, ModelViewSet +from utilities.api import ModelViewSet from utilities.utils import get_subquery from virtualization.models import VirtualMachine from . import serializers -# -# Field choices -# - -class TenancyFieldChoicesViewSet(FieldChoicesViewSet): - fields = () - - # # Tenant Groups # diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index 12e852879..40e35270e 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -27,7 +27,7 @@ class TenantGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class Meta: model = TenantGroup - fields = ['id', 'name', 'slug'] + fields = ['id', 'name', 'slug', 'description'] class TenantFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 9b8fc59da..3af848f3d 100644 --- a/netbox/tenancy/forms.py +++ b/netbox/tenancy/forms.py @@ -28,7 +28,7 @@ class TenantGroupForm(BootstrapMixin, forms.ModelForm): class Meta: model = TenantGroup fields = [ - 'parent', 'name', 'slug', + 'parent', 'name', 'slug', 'description', ] diff --git a/netbox/tenancy/migrations/0009_standardize_description.py b/netbox/tenancy/migrations/0009_standardize_description.py new file mode 100644 index 000000000..0f65ced04 --- /dev/null +++ b/netbox/tenancy/migrations/0009_standardize_description.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.3 on 2020-03-13 20:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0008_nested_tenantgroups_rebuild'), + ] + + operations = [ + migrations.AddField( + model_name='tenantgroup', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + migrations.AlterField( + model_name='tenant', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + ] diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index 1a02184cd..077fb6ad1 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -5,6 +5,7 @@ from mptt.models import MPTTModel, TreeForeignKey from taggit.managers import TaggableManager from extras.models import CustomFieldModel, ObjectChange, TaggedItem +from extras.utils import extras_features from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object @@ -34,8 +35,12 @@ class TenantGroup(MPTTModel, ChangeLoggedModel): null=True, db_index=True ) + description = models.CharField( + max_length=200, + blank=True + ) - csv_headers = ['name', 'slug', 'parent'] + csv_headers = ['name', 'slug', 'parent', 'description'] class Meta: ordering = ['name'] @@ -54,6 +59,7 @@ class TenantGroup(MPTTModel, ChangeLoggedModel): self.name, self.slug, self.parent.name if self.parent else '', + self.description, ) def to_objectchange(self, action): @@ -66,6 +72,7 @@ class TenantGroup(MPTTModel, ChangeLoggedModel): ) +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class Tenant(ChangeLoggedModel, CustomFieldModel): """ A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal @@ -86,9 +93,8 @@ class Tenant(ChangeLoggedModel, CustomFieldModel): null=True ) description = models.CharField( - max_length=100, - blank=True, - help_text='Long-form name (optional)' + max_length=200, + blank=True ) comments = models.TextField( blank=True diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index adf73dc41..0eca7de71 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -53,7 +53,7 @@ class TenantGroupTable(BaseTable): class Meta(BaseTable.Meta): model = TenantGroup - fields = ('pk', 'name', 'tenant_count', 'slug', 'actions') + fields = ('pk', 'name', 'tenant_count', 'description', 'slug', 'actions') # diff --git a/netbox/tenancy/tests/test_api.py b/netbox/tenancy/tests/test_api.py index 1767c8f28..8da3d7594 100644 --- a/netbox/tenancy/tests/test_api.py +++ b/netbox/tenancy/tests/test_api.py @@ -14,13 +14,6 @@ class AppTest(APITestCase): self.assertEqual(response.status_code, 200) - def test_choices(self): - - url = reverse('tenancy-api:field-choice-list') - response = self.client.get(url, **self.header) - - self.assertEqual(response.status_code, 200) - class TenantGroupTest(APITestCase): diff --git a/netbox/tenancy/tests/test_filters.py b/netbox/tenancy/tests/test_filters.py index bb1ac889c..51deedde8 100644 --- a/netbox/tenancy/tests/test_filters.py +++ b/netbox/tenancy/tests/test_filters.py @@ -20,9 +20,9 @@ class TenantGroupTestCase(TestCase): tenantgroup.save() tenant_groups = ( - TenantGroup(name='Tenant Group 1', slug='tenant-group-1', parent=parent_tenant_groups[0]), - TenantGroup(name='Tenant Group 2', slug='tenant-group-2', parent=parent_tenant_groups[1]), - TenantGroup(name='Tenant Group 3', slug='tenant-group-3', parent=parent_tenant_groups[2]), + TenantGroup(name='Tenant Group 1', slug='tenant-group-1', parent=parent_tenant_groups[0], description='A'), + TenantGroup(name='Tenant Group 2', slug='tenant-group-2', parent=parent_tenant_groups[1], description='B'), + TenantGroup(name='Tenant Group 3', slug='tenant-group-3', parent=parent_tenant_groups[2], description='C'), ) for tenantgroup in tenant_groups: tenantgroup.save() @@ -40,6 +40,10 @@ class TenantGroupTestCase(TestCase): params = {'slug': ['tenant-group-1', 'tenant-group-2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_description(self): + params = {'description': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_parent(self): parent_groups = TenantGroup.objects.filter(name__startswith='Parent')[:2] params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]} diff --git a/netbox/tenancy/tests/test_views.py b/netbox/tenancy/tests/test_views.py index 5b47f8080..ca2c2633f 100644 --- a/netbox/tenancy/tests/test_views.py +++ b/netbox/tenancy/tests/test_views.py @@ -19,13 +19,14 @@ class TenantGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): cls.form_data = { 'name': 'Tenant Group X', 'slug': 'tenant-group-x', + 'description': 'A new tenant group', } cls.csv_data = ( - "name,slug", - "Tenant Group 4,tenant-group-4", - "Tenant Group 5,tenant-group-5", - "Tenant Group 6,tenant-group-6", + "name,slug,description", + "Tenant Group 4,tenant-group-4,Fourth tenant group", + "Tenant Group 5,tenant-group-5,Fifth tenant group", + "Tenant Group 6,tenant-group-6,Sixth tenant group", ) diff --git a/netbox/users/migrations/0002_standardize_description.py b/netbox/users/migrations/0002_standardize_description.py new file mode 100644 index 000000000..8916edcbd --- /dev/null +++ b/netbox/users/migrations/0002_standardize_description.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.3 on 2020-03-13 20:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_api_tokens_squashed_0003_token_permissions'), + ] + + operations = [ + migrations.AlterField( + model_name='token', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + ] diff --git a/netbox/users/models.py b/netbox/users/models.py index cf0d826b5..5be784777 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -39,7 +39,7 @@ class Token(models.Model): help_text='Permit create/update/delete operations using this key' ) description = models.CharField( - max_length=100, + max_length=200, blank=True ) diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 43062af69..205055669 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -235,6 +235,7 @@ class ValidatedModelSerializer(ModelSerializer): for k, v in attrs.items(): setattr(instance, k, v) instance.clean() + instance.validate_unique() return data @@ -371,49 +372,3 @@ class ModelViewSet(_ModelViewSet): logger = logging.getLogger('netbox.api.views.ModelViewSet') logger.info(f"Deleting {instance} (PK: {instance.pk})") return super().perform_destroy(instance) - - -class FieldChoicesViewSet(ViewSet): - """ - Expose the built-in numeric values which represent static choices for a model's field. - """ - permission_classes = [IsAuthenticatedOrLoginNotRequired] - fields = [] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Compile a dict of all fields in this view - self._fields = OrderedDict() - for serializer_class, field_list in self.fields: - for field_name in field_list: - - model_name = serializer_class.Meta.model._meta.verbose_name - key = ':'.join([model_name.lower().replace(' ', '-'), field_name]) - serializer = serializer_class() - choices = [] - - for k, v in serializer.get_fields()[field_name].choices.items(): - if type(v) in [list, tuple]: - for k2, v2 in v: - choices.append({ - 'value': k2, - 'label': v2, - }) - else: - choices.append({ - 'value': k, - 'label': v, - }) - self._fields[key] = choices - - def list(self, request): - return Response(self._fields) - - def retrieve(self, request, pk): - if pk not in self._fields: - raise Http404 - return Response(self._fields[pk]) - - def get_view_name(self): - return "Field Choices" diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 618641a07..7d05ce749 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -40,7 +40,7 @@ def render_markdown(value): value = strip_tags(value) # Render Markdown - html = markdown(value, extensions=['fenced_code']) + html = markdown(value, extensions=['fenced_code', 'tables']) return mark_safe(html) @@ -196,7 +196,7 @@ def get_docs(model): return "Unable to load documentation, error reading file: {}".format(path) # Render Markdown with the admonition extension - content = markdown(content, extensions=['admonition', 'fenced_code']) + content = markdown(content, extensions=['admonition', 'fenced_code', 'tables']) return mark_safe(content) diff --git a/netbox/utilities/testing/utils.py b/netbox/utilities/testing/utils.py index 38ec6e196..fd8c70f05 100644 --- a/netbox/utilities/testing/utils.py +++ b/netbox/utilities/testing/utils.py @@ -36,33 +36,6 @@ def create_test_user(username='testuser', permissions=None): return user -def choices_to_dict(choices_list): - """ - Convert a list of field choices to a dictionary suitable for direct comparison with a ChoiceSet. For example: - - [ - { - "value": "choice-1", - "label": "First Choice" - }, - { - "value": "choice-2", - "label": "Second Choice" - } - ] - - Becomes: - - { - "choice-1": "First Choice", - "choice-2": "Second Choice - } - """ - return { - choice['value']: choice['label'] for choice in choices_list - } - - @contextmanager def disable_warnings(logger_name): """ diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index a294cdb6f..3cca95b22 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -24,7 +24,7 @@ class ClusterTypeSerializer(ValidatedModelSerializer): class Meta: model = ClusterType - fields = ['id', 'name', 'slug', 'cluster_count'] + fields = ['id', 'name', 'slug', 'description', 'cluster_count'] class ClusterGroupSerializer(ValidatedModelSerializer): @@ -32,7 +32,7 @@ class ClusterGroupSerializer(ValidatedModelSerializer): class Meta: model = ClusterGroup - fields = ['id', 'name', 'slug', 'cluster_count'] + fields = ['id', 'name', 'slug', 'description', 'cluster_count'] class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer): diff --git a/netbox/virtualization/api/urls.py b/netbox/virtualization/api/urls.py index a94e043b2..c237f1e68 100644 --- a/netbox/virtualization/api/urls.py +++ b/netbox/virtualization/api/urls.py @@ -14,9 +14,6 @@ class VirtualizationRootView(routers.APIRootView): router = routers.DefaultRouter() router.APIRootView = VirtualizationRootView -# Field choices -router.register('_choices', views.VirtualizationFieldChoicesViewSet, basename='field-choice') - # Clusters router.register('cluster-types', views.ClusterTypeViewSet) router.register('cluster-groups', views.ClusterGroupViewSet) diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index 415fc6289..2a1d7c3a9 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -2,24 +2,13 @@ from django.db.models import Count from dcim.models import Device, Interface from extras.api.views import CustomFieldModelViewSet -from utilities.api import FieldChoicesViewSet, ModelViewSet +from utilities.api import ModelViewSet from utilities.utils import get_subquery from virtualization import filters from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine from . import serializers -# -# Field choices -# - -class VirtualizationFieldChoicesViewSet(FieldChoicesViewSet): - fields = ( - (serializers.VirtualMachineSerializer, ['status']), - (serializers.InterfaceSerializer, ['type']), - ) - - # # Clusters # diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 2c450e5a2..cf71b05e6 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -24,14 +24,14 @@ class ClusterTypeFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class Meta: model = ClusterType - fields = ['id', 'name', 'slug'] + fields = ['id', 'name', 'slug', 'description'] class ClusterGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet): class Meta: model = ClusterGroup - fields = ['id', 'name', 'slug'] + fields = ['id', 'name', 'slug', 'description'] class ClusterFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index d110545c7..9d595b35b 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -31,7 +31,7 @@ class ClusterTypeForm(BootstrapMixin, forms.ModelForm): class Meta: model = ClusterType fields = [ - 'name', 'slug', + 'name', 'slug', 'description', ] @@ -56,7 +56,7 @@ class ClusterGroupForm(BootstrapMixin, forms.ModelForm): class Meta: model = ClusterGroup fields = [ - 'name', 'slug', + 'name', 'slug', 'description', ] diff --git a/netbox/virtualization/migrations/0014_standardize_description.py b/netbox/virtualization/migrations/0014_standardize_description.py new file mode 100644 index 000000000..e02655bb7 --- /dev/null +++ b/netbox/virtualization/migrations/0014_standardize_description.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.3 on 2020-03-13 20:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('virtualization', '0013_deterministic_ordering'), + ] + + operations = [ + migrations.AddField( + model_name='clustergroup', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + migrations.AddField( + model_name='clustertype', + name='description', + field=models.CharField(blank=True, max_length=200), + ), + ] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 13b181137..3daeff013 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -7,6 +7,7 @@ from taggit.managers import TaggableManager from dcim.models import Device from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem +from extras.utils import extras_features from utilities.models import ChangeLoggedModel from .choices import * @@ -34,8 +35,12 @@ class ClusterType(ChangeLoggedModel): slug = models.SlugField( unique=True ) + description = models.CharField( + max_length=200, + blank=True + ) - csv_headers = ['name', 'slug'] + csv_headers = ['name', 'slug', 'description'] class Meta: ordering = ['name'] @@ -50,6 +55,7 @@ class ClusterType(ChangeLoggedModel): return ( self.name, self.slug, + self.description, ) @@ -68,8 +74,12 @@ class ClusterGroup(ChangeLoggedModel): slug = models.SlugField( unique=True ) + description = models.CharField( + max_length=200, + blank=True + ) - csv_headers = ['name', 'slug'] + csv_headers = ['name', 'slug', 'description'] class Meta: ordering = ['name'] @@ -84,6 +94,7 @@ class ClusterGroup(ChangeLoggedModel): return ( self.name, self.slug, + self.description, ) @@ -91,6 +102,7 @@ class ClusterGroup(ChangeLoggedModel): # Clusters # +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class Cluster(ChangeLoggedModel, CustomFieldModel): """ A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices. @@ -177,6 +189,7 @@ class Cluster(ChangeLoggedModel, CustomFieldModel): # Virtual machines # +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): """ A virtual machine which runs inside a Cluster. diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index fdb997dab..09c22ab8a 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -55,7 +55,7 @@ class ClusterTypeTable(BaseTable): class Meta(BaseTable.Meta): model = ClusterType - fields = ('pk', 'name', 'cluster_count', 'actions') + fields = ('pk', 'name', 'cluster_count', 'description', 'actions') # @@ -74,7 +74,7 @@ class ClusterGroupTable(BaseTable): class Meta(BaseTable.Meta): model = ClusterGroup - fields = ('pk', 'name', 'cluster_count', 'actions') + fields = ('pk', 'name', 'cluster_count', 'description', 'actions') # diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index 719954c10..8568e21e9 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -5,7 +5,7 @@ from rest_framework import status from dcim.choices import InterfaceModeChoices from dcim.models import Interface from ipam.models import IPAddress, VLAN -from utilities.testing import APITestCase, choices_to_dict, disable_warnings +from utilities.testing import APITestCase, disable_warnings from virtualization.choices import * from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -19,19 +19,6 @@ class AppTest(APITestCase): self.assertEqual(response.status_code, 200) - def test_choices(self): - - url = reverse('virtualization-api:field-choice-list') - response = self.client.get(url, **self.header) - - self.assertEqual(response.status_code, 200) - - # VirtualMachine - self.assertEqual(choices_to_dict(response.data.get('virtual-machine:status')), VirtualMachineStatusChoices.as_dict()) - - # Interface - self.assertEqual(choices_to_dict(response.data.get('interface:type')), VMInterfaceTypeChoices.as_dict()) - class ClusterTypeTest(APITestCase): @@ -501,6 +488,18 @@ class VirtualMachineTest(APITestCase): self.assertFalse('config_context' in response.data['results'][0]) + def test_unique_name_per_cluster_constraint(self): + + data = { + 'name': 'Test Virtual Machine 1', + 'cluster': self.cluster1.pk, + } + + url = reverse('virtualization-api:virtualmachine-list') + response = self.client.post(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + class InterfaceTest(APITestCase): diff --git a/netbox/virtualization/tests/test_filters.py b/netbox/virtualization/tests/test_filters.py index db5935be9..e69e358d4 100644 --- a/netbox/virtualization/tests/test_filters.py +++ b/netbox/virtualization/tests/test_filters.py @@ -15,9 +15,9 @@ class ClusterTypeTestCase(TestCase): def setUpTestData(cls): cluster_types = ( - ClusterType(name='Cluster Type 1', slug='cluster-type-1'), - ClusterType(name='Cluster Type 2', slug='cluster-type-2'), - ClusterType(name='Cluster Type 3', slug='cluster-type-3'), + ClusterType(name='Cluster Type 1', slug='cluster-type-1', description='A'), + ClusterType(name='Cluster Type 2', slug='cluster-type-2', description='B'), + ClusterType(name='Cluster Type 3', slug='cluster-type-3', description='C'), ) ClusterType.objects.bulk_create(cluster_types) @@ -34,6 +34,10 @@ class ClusterTypeTestCase(TestCase): params = {'slug': ['cluster-type-1', 'cluster-type-2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_description(self): + params = {'description': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class ClusterGroupTestCase(TestCase): queryset = ClusterGroup.objects.all() @@ -43,9 +47,9 @@ class ClusterGroupTestCase(TestCase): def setUpTestData(cls): cluster_groups = ( - ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'), - ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'), - ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'), + ClusterGroup(name='Cluster Group 1', slug='cluster-group-1', description='A'), + ClusterGroup(name='Cluster Group 2', slug='cluster-group-2', description='B'), + ClusterGroup(name='Cluster Group 3', slug='cluster-group-3', description='C'), ) ClusterGroup.objects.bulk_create(cluster_groups) @@ -62,6 +66,10 @@ class ClusterGroupTestCase(TestCase): params = {'slug': ['cluster-group-1', 'cluster-group-2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_description(self): + params = {'description': ['A', 'B']} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + class ClusterTestCase(TestCase): queryset = Cluster.objects.all() diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py index 639908977..e7bb19285 100644 --- a/netbox/virtualization/tests/test_views.py +++ b/netbox/virtualization/tests/test_views.py @@ -23,13 +23,14 @@ class ClusterGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): cls.form_data = { 'name': 'Cluster Group X', 'slug': 'cluster-group-x', + 'description': 'A new cluster group', } cls.csv_data = ( - "name,slug", - "Cluster Group 4,cluster-group-4", - "Cluster Group 5,cluster-group-5", - "Cluster Group 6,cluster-group-6", + "name,slug,description", + "Cluster Group 4,cluster-group-4,Fourth cluster group", + "Cluster Group 5,cluster-group-5,Fifth cluster group", + "Cluster Group 6,cluster-group-6,Sixth cluster group", ) @@ -48,13 +49,14 @@ class ClusterTypeTestCase(ViewTestCases.OrganizationalObjectViewTestCase): cls.form_data = { 'name': 'Cluster Type X', 'slug': 'cluster-type-x', + 'description': 'A new cluster type', } cls.csv_data = ( - "name,slug", - "Cluster Type 4,cluster-type-4", - "Cluster Type 5,cluster-type-5", - "Cluster Type 6,cluster-type-6", + "name,slug,description", + "Cluster Type 4,cluster-type-4,Fourth cluster type", + "Cluster Type 5,cluster-type-5,Fifth cluster type", + "Cluster Type 6,cluster-type-6,Sixth cluster type", )