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/netbox/circuits/models.py b/netbox/circuits/models.py index c650e27b7..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 @@ -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 diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index acecc3598..70bada0a2 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -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/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/models.py b/netbox/tenancy/models.py index 5eeb687d7..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 @@ -71,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 diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 25501a182..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 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/virtualization/models.py b/netbox/virtualization/models.py index 8eb94eea3..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 * @@ -101,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. @@ -187,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/tests/test_api.py b/netbox/virtualization/tests/test_api.py index 7aa4e929f..8568e21e9 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -488,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):