From 8bad25b860db1c6815d5db73f3020d9380171c85 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 6 Nov 2018 10:57:38 -0500 Subject: [PATCH 01/16] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index ee54ef346..1b09989c7 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ if sys.version_info[0] < 3: DeprecationWarning ) -VERSION = '2.4.7' +VERSION = '2.4.8-dev' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From c716ca1e874162499f400d2be0ad9ea887c27241 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 7 Nov 2018 10:42:04 -0500 Subject: [PATCH 02/16] Changelog query optimization --- netbox/netbox/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index c6814c068..75d3dd182 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -197,7 +197,7 @@ class HomeView(View): 'stats': stats, 'topology_maps': TopologyMap.objects.filter(site__isnull=True), 'report_results': ReportResult.objects.order_by('-created')[:10], - 'changelog': ObjectChange.objects.select_related('user')[:50] + 'changelog': ObjectChange.objects.select_related('user', 'changed_object_type')[:50] }) From c1838104aef40cbbcdf80abbc6b41e8396b22386 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 7 Nov 2018 12:20:14 -0500 Subject: [PATCH 03/16] Add lag description to lag column --- netbox/templates/dcim/inc/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 229f6f2eb..57390e582 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -18,7 +18,7 @@ {# LAG #} {% if iface.lag %} - {{ iface.lag }} + {{ iface.lag }} {% endif %} From 69d829ce8d46e3a66752818c0894730d7182781d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 7 Nov 2018 13:44:16 -0500 Subject: [PATCH 04/16] Fixes #2473: Fix encoding of long (>127 character) secrets --- CHANGELOG.md | 8 ++++++++ netbox/secrets/models.py | 16 ++++++++++------ netbox/secrets/tests/test_models.py | 3 ++- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42bece984..88ef26a6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +v2.4.8 (FUTURE) + +## Bug Fixes + +* [#2473](https://github.com/digitalocean/netbox/issues/2473) - Fix encoding of long (>127 character) secrets + +--- + v2.4.7 (2018-11-06) ## Enhancements diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index 8bbf3d14d..6beb86c9e 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import os +import sys from Crypto.Cipher import AES, PKCS1_OAEP from Crypto.PublicKey import RSA @@ -392,6 +393,7 @@ class Secret(ChangeLoggedModel, CustomFieldModel): s = s.encode('utf8') if len(s) > 65535: raise ValueError("Maximum plaintext size is 65535 bytes.") + # Minimum ciphertext size is 64 bytes to conceal the length of short secrets. if len(s) <= 62: pad_length = 62 - len(s) @@ -399,12 +401,14 @@ class Secret(ChangeLoggedModel, CustomFieldModel): pad_length = 16 - ((len(s) + 2) % 16) else: pad_length = 0 - return ( - chr(len(s) >> 8).encode() + - chr(len(s) % 256).encode() + - s + - os.urandom(pad_length) - ) + + # Python 2 compatibility + if sys.version_info[0] < 3: + header = chr(len(s) >> 8) + chr(len(s) % 256) + else: + header = bytes([len(s) >> 8]) + bytes([len(s) % 256]) + + return header + s + os.urandom(pad_length) def _unpad(self, s): """ diff --git a/netbox/secrets/tests/test_models.py b/netbox/secrets/tests/test_models.py index 887c048bf..2fb7c3781 100644 --- a/netbox/secrets/tests/test_models.py +++ b/netbox/secrets/tests/test_models.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +import string from Crypto.PublicKey import RSA from django.conf import settings @@ -88,7 +89,7 @@ class SecretTestCase(TestCase): """ Test basic encryption and decryption functionality using a random master key. """ - plaintext = "FooBar123" + plaintext = string.printable * 2 secret_key = generate_random_key() s = Secret(plaintext=plaintext) s.encrypt(secret_key) From 61ca7ee7c2d4deb87a9f4b3ce5538ff3fd8d06fd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Nov 2018 13:52:34 -0500 Subject: [PATCH 05/16] Closes #2559: Add a pre-commit git hook to enforce PEP8 validation --- scripts/git-hooks/pre-commit | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100755 scripts/git-hooks/pre-commit diff --git a/scripts/git-hooks/pre-commit b/scripts/git-hooks/pre-commit new file mode 100755 index 000000000..5974f91d8 --- /dev/null +++ b/scripts/git-hooks/pre-commit @@ -0,0 +1,14 @@ +#!/bin/sh +# Create a link to this file at .git/hooks/pre-commit to +# force PEP8 validation prior to committing +# +# Ignored violations: +# +# W504: Line break after binary operator +# E501: Line too long + +exec 1>&2 + +echo "Validating PEP8 compliance..." +pycodestyle --ignore=W504,E501 netbox/ + From 845d467fd97ab499e5547b16b16af5e385d9d483 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 9 Nov 2018 09:46:30 -0500 Subject: [PATCH 06/16] Fixes #2575: Correct model specified for rack roles table --- CHANGELOG.md | 1 + netbox/dcim/tables.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88ef26a6a..fd9ded8a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ v2.4.8 (FUTURE) ## Bug Fixes * [#2473](https://github.com/digitalocean/netbox/issues/2473) - Fix encoding of long (>127 character) secrets +* [#2575](https://github.com/digitalocean/netbox/issues/2575) - Correct model specified for rack roles table --- diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 2630a9ba2..edd30d89f 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -9,7 +9,7 @@ from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, - RackReservation, Region, Site, VirtualChassis, + RackReservation, RackRole, Region, Site, VirtualChassis, ) REGION_LINK = """ @@ -250,7 +250,7 @@ class RackRoleTable(BaseTable): verbose_name='') class Meta(BaseTable.Meta): - model = RackGroup + model = RackRole fields = ('pk', 'name', 'rack_count', 'color', 'slug', 'actions') From 5785fb6ba258d0bb597e9a02fefc986ce81941a5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 12 Nov 2018 13:59:58 -0500 Subject: [PATCH 07/16] Added development docs for extending a model --- docs/development/extending-models.md | 70 ++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 71 insertions(+) create mode 100644 docs/development/extending-models.md diff --git a/docs/development/extending-models.md b/docs/development/extending-models.md new file mode 100644 index 000000000..190fad5e0 --- /dev/null +++ b/docs/development/extending-models.md @@ -0,0 +1,70 @@ +# Extending Models + +Below is a list of items to consider when adding a new field to a model: + +### 1. Generate and run database migration + +Django migrations are used to express changes to the database schema. In most cases, Django can generate these automatically, however very complex changes may require manual intervention. Always remember to specify a short but descriptive name when generating a new migration. + +``` +./manage.py makemigrations -n +./manage.py migrate +``` + +Where possible, try to merge related changes into a single migration. For example, if three new fields are being added to different models within an app, these can be expressed in the same migration. You can merge a new migration with an existing one by combining their `operations` lists. + +!!! note + Migrations can only be merged within a release. Once a new release has been published, its migrations cannot be altered. + +### 2. Add validation logic to `clean()` + +If the new field introduces additional validation requirements (beyond what's included with the field itself), implement them in the model's `clean()` method. Remember to call the model's original method using `super()` before or agter your custom validation as appropriate: + +``` +class Foo(models.Model): + + def clean(self): + + super(DeviceCSVForm, self).clean() + + # Custom validation goes here + if self.bar is None: + raise ValidationError() +``` + +### 3. Add CSV helpers + +Add the name of the new field to `csv_headers` and included a CSV-friendly representation of its data in the model's `to_csv()` method. These will be used when exporting objects in CSV format. + +### 4. Update relevant querysets + +If you're adding a relational field (e.g. `ForeignKey`) and intend to include the data when retreiving a list of objects, be sure to include the field using `select_related()` or `prefetch_related()` as appropriate. This will optimize the view and avoid excessive database lookups. + +### 5. Update API serializer + +Extend the model's API serializer in `.api.serializers` to include the new field. In most cases, it will not be necessary to also extend the nested serializer, which produces a minimal represenation of the model. + +### 6. Add field to forms + +Extend any forms to include the new field as appropriate. Common forms include: + +* **Credit/edit** - Manipulating a single object +* **Bulk edit** - Performing a change on mnay objects at once +* **CSV import** - The form used when bulk importing objects in CSV format +* **Filter** - Displays the options available for filtering a list of objects (both UI and API) + +### 7. Extend object filter set + +If the new field should be filterable, add it to the `FilterSet` for the model. If the field should be searchable, remember to reference it in the FilterSet's `search()` method. + +### 8. Add column to object table + +If the new field will be included in the object list view, add a column to the model's table. For simple fields, adding the field name to `Meta.fields` will be sufficient. More complex fields may require explicitly declaring a new column. + +### 9. Update the UI templates + +Edit the object's view template to display the new field. There may also be a custom add/edit form template that needs to be updated. + +### 10. Adjust API and model tests + +Extend the model and/or API tests to verify that the new field and any accompanying validation logic perform as expected. This is especially important for relational fields. diff --git a/mkdocs.yml b/mkdocs.yml index 87b7da254..d2cb68159 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,6 +46,7 @@ pages: - Development: - Introduction: 'development/index.md' - Utility Views: 'development/utility-views.md' + - Extending Models: 'development/extending-models.md' - Release Checklist: 'development/release-checklist.md' markdown_extensions: From b6a256dc5db57360773530477ec9aee46c3177dc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 12 Nov 2018 14:36:09 -0500 Subject: [PATCH 08/16] Expanded the development style guide --- docs/development/index.md | 7 ------ docs/development/style-guide.md | 41 +++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 docs/development/style-guide.md diff --git a/docs/development/index.md b/docs/development/index.md index 91086c61e..5830da765 100644 --- a/docs/development/index.md +++ b/docs/development/index.md @@ -28,10 +28,3 @@ NetBox components are arranged into functional subsections called _apps_ (a carr * `tenancy`: Tenants (such as customers) to which NetBox objects may be assigned * `utilities`: Resources which are not user-facing (extendable classes, etc.) * `virtualization`: Virtual machines and clusters - -## Style Guide - -NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). The following exceptions are noted: - -* [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations. See `scripts/cibuild.sh`. -* Constants may be imported via wildcard (for example, `from .constants import *`). diff --git a/docs/development/style-guide.md b/docs/development/style-guide.md new file mode 100644 index 000000000..18dadd2d2 --- /dev/null +++ b/docs/development/style-guide.md @@ -0,0 +1,41 @@ +# Style Guide + +NetBox generally follows the [Django style guide](https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/), which is itself based on [PEP 8](https://www.python.org/dev/peps/pep-0008/). [Pycodestyle](https://github.com/pycqa/pycodestyle) is used to validate code formatting, ignoring certain violations. See `scripts/cibuild.sh`. + +## PEP 8 Exceptions + +* Wildcard imports (for example, `from .constants import *`) are acceptable under any of the following conditions: + * The library being import contains only constant declarations (`constants.py`) + * The library being imported explicitly defines `__all__` (e.g. `.api.nested_serializers`) + +* Maximum line length is 120 characters (E501) + * This does not apply to HTML templates or to automatically generated code (e.g. database migrations). + +* Line breaks are permitted following binary operators (W504) + +## Enforcing Code Style + +The `pycodestyle` utility (previously `pep8`) is used by the CI process to enforce code style. It is strongly recommended to include as part of your commit process. A git commit hook is provided in the source at `scripts/git-hooks/pre-commit`. Linking to this script from `.git/hooks/` will invoke `pycodestyle` prior to every commit attempt and abort if the validation fails. + +``` +$ cd .git/hooks/ +$ ln -s ../../scripts/git-hooks/pre-commit +``` + +To invoke `pycodestyle` manually, run: + +``` +pycodestyle --ignore=W504,E501 netbox/ +``` + +## General Guidance + +* When in doubt, remain consistent: It is better to be consistently incorrect than inconsistently correct. If you notice in the course of unrelated work a pattern that should be corrected, continue to follow the pattern for now and open a bug so that the entire code base can be evaluated at a later point. + +* No easter eggs. While they can be fun, NetBox must be considered as a business-critical tool. The potential, however minor, for introducing a bug caused by unnecessary logic is best avoided entirely. + +* Constants (variables which generally do not change) should be declared in `constants.py` within each app. Wildcard imports from the file are acceptable. + +* Every model should have a docstring. Every custom method should include an expalantion of its function. + +* Nested API serializers generate minimal representations of an object. These are stored separately from the primary serializers to avoid circular dependencies. Always import nested serializers from other apps directly. For example, from within the DCIM app you would write `from ipam.api.nested_serializers import NestedIPAddressSerializer`. diff --git a/mkdocs.yml b/mkdocs.yml index d2cb68159..cd718cc76 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -45,6 +45,7 @@ pages: - Examples: 'api/examples.md' - Development: - Introduction: 'development/index.md' + - Style Guide: 'development/style-guide.md' - Utility Views: 'development/utility-views.md' - Extending Models: 'development/extending-models.md' - Release Checklist: 'development/release-checklist.md' From 0c33af2140d18e3179880d44c71d1e5c7bc14ef7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 12 Nov 2018 15:48:58 -0500 Subject: [PATCH 09/16] Fixes #2558: Filter on all tags when multiple are passed --- CHANGELOG.md | 1 + netbox/circuits/filters.py | 10 +++------- netbox/dcim/filters.py | 30 ++++++++---------------------- netbox/ipam/filters.py | 26 +++++++------------------- netbox/secrets/filters.py | 6 ++---- netbox/tenancy/filters.py | 6 ++---- netbox/utilities/filters.py | 16 ++++++++++++++++ netbox/virtualization/filters.py | 10 +++------- 8 files changed, 42 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd9ded8a5..4e02f1446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ v2.4.8 (FUTURE) ## Bug Fixes * [#2473](https://github.com/digitalocean/netbox/issues/2473) - Fix encoding of long (>127 character) secrets +* [#2558](https://github.com/digitalocean/netbox/issues/2558) - Filter on all tags when multiple are passed * [#2575](https://github.com/digitalocean/netbox/issues/2575) - Correct model specified for rack roles table --- diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index 79efdc950..a159fad42 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -6,7 +6,7 @@ from django.db.models import Q from dcim.models import Site from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant -from utilities.filters import NumericInFilter +from utilities.filters import NumericInFilter, TagFilter from .constants import CIRCUIT_STATUS_CHOICES from .models import Provider, Circuit, CircuitTermination, CircuitType @@ -28,9 +28,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Site (slug)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = Provider @@ -106,9 +104,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Site (slug)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = Circuit diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 689e88a5d..8b40ca7b7 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -9,7 +9,7 @@ from netaddr.core import AddrFormatError from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant -from utilities.filters import NullableCharFieldFilter, NumericInFilter +from utilities.filters import NullableCharFieldFilter, NumericInFilter, TagFilter from virtualization.models import Cluster from .constants import ( DEVICE_STATUS_CHOICES, IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, SITE_STATUS_CHOICES, VIRTUAL_IFACE_TYPES, @@ -83,9 +83,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Tenant (slug)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = Site @@ -196,9 +194,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Role (slug)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = Rack @@ -306,9 +302,7 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Manufacturer (slug)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = DeviceType @@ -530,9 +524,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): queryset=VirtualChassis.objects.all(), label='Virtual chassis (ID)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = Device @@ -592,9 +584,7 @@ class DeviceComponentFilterSet(django_filters.FilterSet): to_field_name='name', label='Device (name)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class ConsolePortFilter(DeviceComponentFilterSet): @@ -653,9 +643,7 @@ class InterfaceFilter(django_filters.FilterSet): method='_mac_address', label='MAC address', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() vlan_id = django_filters.CharFilter( method='filter_vlan_id', label='Assigned VLAN' @@ -797,9 +785,7 @@ class VirtualChassisFilter(django_filters.FilterSet): to_field_name='slug', label='Tenant (slug)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = VirtualChassis diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 0a8606e52..700a25ae9 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -9,7 +9,7 @@ from netaddr.core import AddrFormatError from dcim.models import Site, Device, Interface from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant -from utilities.filters import NumericInFilter +from utilities.filters import NumericInFilter, TagFilter from virtualization.models import VirtualMachine from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF @@ -31,9 +31,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Tenant (slug)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() def search(self, queryset, name, value): if not value.strip(): @@ -73,9 +71,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='RIR (slug)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = Aggregate @@ -174,9 +170,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): choices=PREFIX_STATUS_CHOICES, null_value=None ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = Prefix @@ -303,9 +297,7 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet): role = django_filters.MultipleChoiceFilter( choices=IPADDRESS_ROLE_CHOICES ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = IPAddress @@ -422,9 +414,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): choices=VLAN_STATUS_CHOICES, null_value=None ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = VLAN @@ -466,9 +456,7 @@ class ServiceFilter(django_filters.FilterSet): to_field_name='name', label='Virtual machine (name)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = Service diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py index f43a82b22..aa7e02e43 100644 --- a/netbox/secrets/filters.py +++ b/netbox/secrets/filters.py @@ -5,7 +5,7 @@ from django.db.models import Q from dcim.models import Device from extras.filters import CustomFieldFilterSet -from utilities.filters import NumericInFilter +from utilities.filters import NumericInFilter, TagFilter from .models import Secret, SecretRole @@ -42,9 +42,7 @@ class SecretFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='name', label='Device (name)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = Secret diff --git a/netbox/tenancy/filters.py b/netbox/tenancy/filters.py index 7eccff5d3..4ff620d39 100644 --- a/netbox/tenancy/filters.py +++ b/netbox/tenancy/filters.py @@ -4,7 +4,7 @@ import django_filters from django.db.models import Q from extras.filters import CustomFieldFilterSet -from utilities.filters import NumericInFilter +from utilities.filters import NumericInFilter, TagFilter from .models import Tenant, TenantGroup @@ -31,9 +31,7 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet): to_field_name='slug', label='Group (slug)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = Tenant diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py index 90cdcd9fc..a7f23d2f6 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -5,6 +5,7 @@ import itertools import django_filters from django import forms from django.utils.encoding import force_text +from taggit.models import Tag # @@ -68,3 +69,18 @@ class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField): stripped_value = value super(NullableModelMultipleChoiceField, self).clean(stripped_value) return value + + +class TagFilter(django_filters.ModelMultipleChoiceFilter): + """ + Match on one or more assigned tags. If multiple tags are specified (e.g. ?tag=foo&tag=bar), the queryset is filtered + to objects matching all tags. + """ + def __init__(self, *args, **kwargs): + + kwargs.setdefault('name', 'tags__slug') + kwargs.setdefault('to_field_name', 'slug') + kwargs.setdefault('conjoined', True) + kwargs.setdefault('queryset', Tag.objects.all()) + + super(TagFilter, self).__init__(*args, **kwargs) diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 99df19aee..5f0f834cc 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -9,7 +9,7 @@ from netaddr.core import AddrFormatError from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant -from utilities.filters import NumericInFilter +from utilities.filters import NumericInFilter, TagFilter from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -64,9 +64,7 @@ class ClusterFilter(CustomFieldFilterSet): to_field_name='slug', label='Site (slug)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = Cluster @@ -168,9 +166,7 @@ class VirtualMachineFilter(CustomFieldFilterSet): to_field_name='slug', label='Platform (slug)', ) - tag = django_filters.CharFilter( - name='tags__slug', - ) + tag = TagFilter() class Meta: model = VirtualMachine From 2fce7ebd8fee511a0fef8b3aada11037e8ef8e9f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 13 Nov 2018 11:02:48 -0500 Subject: [PATCH 10/16] Fixes #2565: Improved rendering of Markdown tables --- CHANGELOG.md | 1 + netbox/project-static/css/base.css | 13 +++++++++++++ netbox/templates/circuits/circuit.html | 2 +- netbox/templates/circuits/provider.html | 2 +- netbox/templates/dcim/device.html | 2 +- netbox/templates/dcim/devicetype.html | 2 +- netbox/templates/dcim/rack.html | 2 +- netbox/templates/dcim/site.html | 2 +- netbox/templates/tenancy/tenant.html | 2 +- netbox/templates/virtualization/cluster.html | 2 +- netbox/templates/virtualization/virtualmachine.html | 2 +- 11 files changed, 23 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e02f1446..21d286875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ v2.4.8 (FUTURE) * [#2473](https://github.com/digitalocean/netbox/issues/2473) - Fix encoding of long (>127 character) secrets * [#2558](https://github.com/digitalocean/netbox/issues/2558) - Filter on all tags when multiple are passed +* [#2565](https://github.com/digitalocean/netbox/issues/2565) - Improved rendering of Markdown tables * [#2575](https://github.com/digitalocean/netbox/issues/2575) - Correct model specified for rack roles table --- diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index 6222a477d..56104515c 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -390,6 +390,19 @@ table.report th a { top: -51px; } +/* Rendered Markdown */ +.rendered-markdown table { + width: 100%; +} +.rendered-markdown th { + border-bottom: 2px solid #dddddd; + padding: 8px; +} +.rendered-markdown td { + border-top: 1px solid #dddddd; + padding: 8px; +} + /* AJAX loader */ .loading { position: fixed; diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index 5c86cb24e..5b15782c9 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -131,7 +131,7 @@
Comments
-
+
{% if circuit.comments %} {{ circuit.comments|gfm }} {% else %} diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 4ec9adee1..157c66918 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -129,7 +129,7 @@
Comments
-
+
{% if provider.comments %} {{ provider.comments|gfm }} {% else %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 7b56269b1..61fb649c9 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -290,7 +290,7 @@
Comments
-
+
{% if device.comments %} {{ device.comments|gfm }} {% else %} diff --git a/netbox/templates/dcim/devicetype.html b/netbox/templates/dcim/devicetype.html index 652c291e6..35931b49f 100644 --- a/netbox/templates/dcim/devicetype.html +++ b/netbox/templates/dcim/devicetype.html @@ -164,7 +164,7 @@
Comments
-
+
{% if devicetype.comments %} {{ devicetype.comments|gfm }} {% else %} diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index aaebe02da..ebe9a8870 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -164,7 +164,7 @@
Comments
-
+
{% if rack.comments %} {{ rack.comments|gfm }} {% else %} diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index f4623b57b..f592434c4 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -230,7 +230,7 @@
Comments
-
+
{% if site.comments %} {{ site.comments|gfm }} {% else %} diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index 6f2131a51..6068a7102 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -87,7 +87,7 @@
Comments
-
+
{% if tenant.comments %} {{ tenant.comments|gfm }} {% else %} diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html index 69ed4e212..d3606e624 100644 --- a/netbox/templates/virtualization/cluster.html +++ b/netbox/templates/virtualization/cluster.html @@ -99,7 +99,7 @@
Comments
-
+
{% if cluster.comments %} {{ cluster.comments|gfm }} {% else %} diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 552aaa997..9f8ec8308 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -143,7 +143,7 @@
Comments
-
+
{% if virtualmachine.comments %} {{ virtualmachine.comments|gfm }} {% else %} From 7bed48f5fee3180dce82de2623cc4ddc30ff3091 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 13 Nov 2018 14:18:00 -0500 Subject: [PATCH 11/16] Expanded device interfaces display to include MTU, MAC address, and tags --- netbox/dcim/views.py | 6 ++-- netbox/templates/dcim/device.html | 1 + netbox/templates/dcim/inc/interface.html | 38 +++++++++++++++++++----- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index eb7f71a25..91b2a25a4 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -858,8 +858,10 @@ class DeviceView(View): device.device_type.interface_ordering ).select_related( 'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', - 'circuit_termination__circuit' - ).prefetch_related('ip_addresses') + 'circuit_termination__circuit__provider' + ).prefetch_related( + 'tags', 'ip_addresses' + ) # Device bays device_bays = natsorted( diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 61fb649c9..faa946f2e 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -525,6 +525,7 @@ Name LAG Description + MTU Mode Connection diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 57390e582..8a36e74ed 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -1,4 +1,5 @@ - +{% load helpers %} + {# Checkbox #} {% if perms.dcim.change_interface or perms.dcim.delete_interface %} @@ -7,32 +8,53 @@ {% endif %} - {# Icon and name #} + {# Icon/name/MAC #} {{ iface }} + {% if iface.mac_address %} +
{{ iface.mac_address }} + {% endif %} {# LAG #} {% if iface.lag %} - {{ iface.lag }} + {{ iface.lag }} {% endif %} - {# Description #} - {{ iface.description|default:"—" }} + {# Description/tags #} + + {% if iface.description %} + {{ iface.description }}
+ {% endif %} + {% for tag in iface.tags.all %} + {% tag tag %} + {% empty %} + {% if not iface.description %}—{% endif %} + {% endfor %} + + + {# MTU #} + {{ iface.mtu|default:"—" }} {# 802.1Q mode #} - {{ iface.get_mode_display }} + {{ iface.get_mode_display|default:"—" }} {# Connection or type #} {% if iface.is_lag %} LAG interface
- {{ iface.member_interfaces.all|join:", "|default:"No members" }} + + {% for member in iface.member_interfaces.all %} + {{ member }}{% if not forloop.last %}, {% endif %} + {% empty %} + No members + {% endfor %} + {% elif iface.is_virtual %} Virtual interface @@ -138,7 +160,7 @@ {% endif %} {# IP addresses table #} - + From 83be0b5db4314e4eb2313bbd371fc4d5f21ed2a8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 13 Nov 2018 15:08:55 -0500 Subject: [PATCH 12/16] Closes #2490: Added bulk editing for config contexts --- CHANGELOG.md | 4 +++ netbox/extras/forms.py | 26 +++++++++++++++++-- netbox/extras/urls.py | 1 + netbox/extras/views.py | 16 ++++++++++-- .../templates/extras/configcontext_list.html | 2 +- 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21d286875..3f9cdf168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ v2.4.8 (FUTURE) +## Enhancements + +* [#2490](https://github.com/digitalocean/netbox/issues/2490) - Added bulk editing for config contexts + ## Bug Fixes * [#2473](https://github.com/digitalocean/netbox/issues/2473) - Fix encoding of long (>127 character) secrets diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 7dfceb390..238dd831a 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -13,8 +13,8 @@ from taggit.models import Tag from dcim.models import DeviceRole, Platform, Region, Site from tenancy.models import Tenant, TenantGroup from utilities.forms import ( - add_blank_choice, BootstrapMixin, BulkEditForm, FilterChoiceField, FilterTreeNodeMultipleChoiceField, LaxURLField, - JSONField, SlugField, + add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, FilterChoiceField, + FilterTreeNodeMultipleChoiceField, LaxURLField, JSONField, SlugField, ) from .constants import ( CF_FILTER_DISABLED, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, @@ -227,6 +227,28 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm): ] +class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm): + pk = forms.ModelMultipleChoiceField( + queryset=ConfigContext.objects.all(), + widget=forms.MultipleHiddenInput + ) + weight = forms.IntegerField( + required=False, + min_value=0 + ) + is_active = forms.NullBooleanField( + required=False, + widget=BulkEditNullBooleanSelect() + ) + description = forms.CharField( + required=False, + max_length=100 + ) + + class Meta: + nullable_fields = ['description'] + + class ConfigContextFilterForm(BootstrapMixin, forms.Form): q = forms.CharField( required=False, diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index e56652280..8af4e3910 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -16,6 +16,7 @@ urlpatterns = [ # Config contexts url(r'^config-contexts/$', views.ConfigContextListView.as_view(), name='configcontext_list'), url(r'^config-contexts/add/$', views.ConfigContextCreateView.as_view(), name='configcontext_add'), + url(r'^config-contexts/edit/$', views.ConfigContextBulkEditView.as_view(), name='configcontext_bulk_edit'), url(r'^config-contexts/(?P\d+)/$', views.ConfigContextView.as_view(), name='configcontext'), url(r'^config-contexts/(?P\d+)/edit/$', views.ConfigContextEditView.as_view(), name='configcontext_edit'), url(r'^config-contexts/(?P\d+)/delete/$', views.ConfigContextDeleteView.as_view(), name='configcontext_delete'), diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 7626d4012..2b656a28b 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -12,9 +12,12 @@ from django.views.generic import View from taggit.models import Tag from utilities.forms import ConfirmationForm -from utilities.views import BulkDeleteView, ObjectDeleteView, ObjectEditView, ObjectListView +from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView from . import filters -from .forms import ConfigContextForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm, TagForm +from .forms import ( + ConfigContextForm, ConfigContextBulkEditForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm, + TagForm, +) from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult from .reports import get_report, get_reports from .tables import ConfigContextTable, ObjectChangeTable, TagTable @@ -85,6 +88,15 @@ class ConfigContextEditView(ConfigContextCreateView): permission_required = 'extras.change_configcontext' +class ConfigContextBulkEditView(PermissionRequiredMixin, BulkEditView): + permission_required = 'extras.change_configcontext' + queryset = ConfigContext.objects.all() + filter = filters.ConfigContextFilter + table = ConfigContextTable + form = ConfigContextBulkEditForm + default_return_url = 'extras:configcontext_list' + + class ConfigContextDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'extras.delete_configcontext' model = ConfigContext diff --git a/netbox/templates/extras/configcontext_list.html b/netbox/templates/extras/configcontext_list.html index c35ba76ff..16a1dc220 100644 --- a/netbox/templates/extras/configcontext_list.html +++ b/netbox/templates/extras/configcontext_list.html @@ -10,7 +10,7 @@

{% block title %}Config Contexts{% endblock %}

- {% include 'utilities/obj_table.html' with bulk_delete_url='extras:configcontext_bulk_delete' %} + {% include 'utilities/obj_table.html' with bulk_edit_url='extras:configcontext_bulk_edit' bulk_delete_url='extras:configcontext_bulk_delete' %}
{% include 'inc/search_panel.html' %} From 408f6326361026fa0ac13afffe89a2277bf4f17c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 14 Nov 2018 10:12:35 -0500 Subject: [PATCH 13/16] Fixes #2588: Catch all exceptions from failed NAPALM API Calls --- CHANGELOG.md | 1 + netbox/dcim/api/views.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f9cdf168..ca2f3efd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ v2.4.8 (FUTURE) * [#2558](https://github.com/digitalocean/netbox/issues/2558) - Filter on all tags when multiple are passed * [#2565](https://github.com/digitalocean/netbox/issues/2565) - Improved rendering of Markdown tables * [#2575](https://github.com/digitalocean/netbox/issues/2575) - Correct model specified for rack roles table +* [#2588](https://github.com/digitalocean/netbox/issues/2588) - Catch all exceptions from failed NAPALM API Calls --- diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 2159661ef..fd4d37096 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -263,9 +263,9 @@ class DeviceViewSet(CustomFieldModelViewSet): # Check that NAPALM is installed try: import napalm + from napalm.base.exceptions import ModuleImportError except ImportError: raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.") - from napalm.base.exceptions import ModuleImportError # Validate the configured driver try: @@ -309,7 +309,9 @@ class DeviceViewSet(CustomFieldModelViewSet): try: response[method] = getattr(d, method)() except NotImplementedError: - response[method] = {'error': 'Method not implemented for NAPALM driver {}'.format(driver)} + response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)} + except Exception as e: + response[method] = {'error': 'Method {} failed: {}'.format(method, e)} d.close() return Response(response) From 23cde65add0ce5fd38a65b12eca7f3c84dbcccb3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 14 Nov 2018 10:38:53 -0500 Subject: [PATCH 14/16] Fixes #2589: Virtual machine API serializer should require cluster assignment --- CHANGELOG.md | 1 + netbox/virtualization/api/serializers.py | 2 +- netbox/virtualization/tests/test_api.py | 12 ++++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca2f3efd9..f7f928d78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ v2.4.8 (FUTURE) * [#2565](https://github.com/digitalocean/netbox/issues/2565) - Improved rendering of Markdown tables * [#2575](https://github.com/digitalocean/netbox/issues/2575) - Correct model specified for rack roles table * [#2588](https://github.com/digitalocean/netbox/issues/2588) - Catch all exceptions from failed NAPALM API Calls +* [#2589](https://github.com/digitalocean/netbox/issues/2589) - Virtual machine API serializer should require cluster assignment --- diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index b749f1e5e..80fa73002 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -93,7 +93,7 @@ class VirtualMachineIPAddressSerializer(WritableNestedSerializer): class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer): status = ChoiceField(choices=VM_STATUS_CHOICES, required=False) site = NestedSiteSerializer(read_only=True) - cluster = NestedClusterSerializer(required=False, allow_null=True) + cluster = NestedClusterSerializer() role = NestedDeviceRoleSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True) platform = NestedPlatformSerializer(required=False, allow_null=True) diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index 32f56b99b..99e57b201 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -380,6 +380,18 @@ class VirtualMachineTest(APITestCase): self.assertEqual(virtualmachine4.name, data['name']) self.assertEqual(virtualmachine4.cluster.pk, data['cluster']) + def test_create_virtualmachine_without_cluster(self): + + data = { + 'name': 'Test Virtual Machine 4', + } + + url = reverse('virtualization-api:virtualmachine-list') + response = self.client.post(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + self.assertEqual(VirtualMachine.objects.count(), 3) + def test_create_virtualmachine_bulk(self): data = [ From 3366a6ae3d420365a5b0b9dd93df6d56125e1d05 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 15 Nov 2018 16:47:41 -0500 Subject: [PATCH 15/16] Closes #2557: Added object view for tags --- CHANGELOG.md | 1 + netbox/extras/forms.py | 5 ++ netbox/extras/tables.py | 30 +++++++++++- netbox/extras/urls.py | 1 + netbox/extras/views.py | 51 ++++++++++++++++++-- netbox/templates/extras/tag.html | 69 +++++++++++++++++++++++++++ netbox/templates/extras/tag_list.html | 5 +- 7 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 netbox/templates/extras/tag.html diff --git a/CHANGELOG.md b/CHANGELOG.md index f7f928d78..bf5f1d6ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ v2.4.8 (FUTURE) ## Enhancements * [#2490](https://github.com/digitalocean/netbox/issues/2490) - Added bulk editing for config contexts +* [#2557](https://github.com/digitalocean/netbox/issues/2557) - Added object view for tags ## Bug Fixes diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 238dd831a..6fc4b8859 100644 --- a/netbox/extras/forms.py +++ b/netbox/extras/forms.py @@ -208,6 +208,11 @@ class AddRemoveTagsForm(forms.Form): self.fields['remove_tags'] = TagField(required=False) +class TagFilterForm(BootstrapMixin, forms.Form): + model = Tag + q = forms.CharField(required=False, label='Search') + + # # Config contexts # diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index 22bf26cce..cf2b6f888 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -1,7 +1,8 @@ from __future__ import unicode_literals import django_tables2 as tables -from taggit.models import Tag +from django_tables2.utils import Accessor +from taggit.models import Tag, TaggedItem from utilities.tables import BaseTable, BooleanColumn, ToggleColumn from .models import ConfigContext, ObjectChange @@ -15,6 +16,14 @@ TAG_ACTIONS = """ {% endif %} """ +TAGGED_ITEM = """ +{% if value.get_absolute_url %} + {{ value }} +{% else %} + {{ value }} +{% endif %} +""" + CONFIGCONTEXT_ACTIONS = """ {% if perms.extras.change_configcontext %} @@ -55,6 +64,10 @@ OBJECTCHANGE_REQUEST_ID = """ class TagTable(BaseTable): pk = ToggleColumn() + name = tables.LinkColumn( + viewname='extras:tag', + args=[Accessor('slug')] + ) actions = tables.TemplateColumn( template_code=TAG_ACTIONS, attrs={'td': {'class': 'text-right'}}, @@ -66,6 +79,21 @@ class TagTable(BaseTable): fields = ('pk', 'name', 'items', 'slug', 'actions') +class TaggedItemTable(BaseTable): + content_object = tables.TemplateColumn( + template_code=TAGGED_ITEM, + orderable=False, + verbose_name='Object' + ) + content_type = tables.Column( + verbose_name='Type' + ) + + class Meta(BaseTable.Meta): + model = TaggedItem + fields = ('content_object', 'content_type') + + class ConfigContextTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 8af4e3910..a97019a04 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -9,6 +9,7 @@ urlpatterns = [ # Tags url(r'^tags/$', views.TagListView.as_view(), name='tag_list'), + url(r'^tags/(?P[\w-]+)/$', views.TagView.as_view(), name='tag'), url(r'^tags/(?P[\w-]+)/edit/$', views.TagEditView.as_view(), name='tag_edit'), url(r'^tags/(?P[\w-]+)/delete/$', views.TagDeleteView.as_view(), name='tag_delete'), url(r'^tags/delete/$', views.TagBulkDeleteView.as_view(), name='tag_bulk_delete'), diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 2b656a28b..3e9186490 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from django import template +from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.contenttypes.models import ContentType @@ -9,18 +10,20 @@ from django.http import Http404 from django.shortcuts import get_object_or_404, redirect, render from django.utils.safestring import mark_safe from django.views.generic import View -from taggit.models import Tag +from django_tables2 import RequestConfig +from taggit.models import Tag, TaggedItem from utilities.forms import ConfirmationForm +from utilities.paginator import EnhancedPaginator from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView from . import filters from .forms import ( ConfigContextForm, ConfigContextBulkEditForm, ConfigContextFilterForm, ImageAttachmentForm, ObjectChangeFilterForm, - TagForm, + TagFilterForm, TagForm, ) from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult from .reports import get_report, get_reports -from .tables import ConfigContextTable, ObjectChangeTable, TagTable +from .tables import ConfigContextTable, ObjectChangeTable, TagTable, TaggedItemTable # @@ -28,11 +31,45 @@ from .tables import ConfigContextTable, ObjectChangeTable, TagTable # class TagListView(ObjectListView): - queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name') + queryset = Tag.objects.annotate( + items=Count('taggit_taggeditem_items') + ).order_by( + 'name' + ) + filter = filters.TagFilter + filter_form = TagFilterForm table = TagTable template_name = 'extras/tag_list.html' +class TagView(View): + + def get(self, request, slug): + + tag = get_object_or_404(Tag, slug=slug) + tagged_items = TaggedItem.objects.filter( + tag=tag + ).select_related( + 'content_type' + ).prefetch_related( + 'content_object' + ) + + # Generate a table of all items tagged with this Tag + items_table = TaggedItemTable(tagged_items) + paginate = { + 'klass': EnhancedPaginator, + 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) + } + RequestConfig(request, paginate).configure(items_table) + + return render(request, 'extras/tag.html', { + 'tag': tag, + 'items_count': tagged_items.count(), + 'items_table': items_table, + }) + + class TagEditView(PermissionRequiredMixin, ObjectEditView): permission_required = 'taggit.change_tag' model = Tag @@ -48,7 +85,11 @@ class TagDeleteView(PermissionRequiredMixin, ObjectDeleteView): class TagBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'circuits.delete_circuittype' - queryset = Tag.objects.annotate(items=Count('taggit_taggeditem_items')).order_by('name') + queryset = Tag.objects.annotate( + items=Count('taggit_taggeditem_items') + ).order_by( + 'name' + ) table = TagTable default_return_url = 'extras:tag_list' diff --git a/netbox/templates/extras/tag.html b/netbox/templates/extras/tag.html new file mode 100644 index 000000000..aceb0ca94 --- /dev/null +++ b/netbox/templates/extras/tag.html @@ -0,0 +1,69 @@ +{% extends '_base.html' %} +{% load helpers %} + +{% block header %} +
+
+ +
+
+
+
+ + + + +
+ +
+
+
+ {% if perms.taggit.change_tag %} + + + Edit this tag + + {% endif %} +
+

{% block title %}Tag: {{ tag }}{% endblock %}

+{% endblock %} + +{% block content %} +
+
+
+
+ Tag +
+
+ + + + + + + + + + + + +
Name + {{ tag.name }} +
Slug + {{ tag.slug }} +
Tagged Items + {{ items_count }} +
+
+
+
+ {% include 'panel_table.html' with table=items_table heading='Tagged Objects' %} +
+
+{% endblock %} diff --git a/netbox/templates/extras/tag_list.html b/netbox/templates/extras/tag_list.html index 3136991a0..8178e5538 100644 --- a/netbox/templates/extras/tag_list.html +++ b/netbox/templates/extras/tag_list.html @@ -4,8 +4,11 @@ {% block content %}

{% block title %}Tags{% endblock %}

-
+
{% include 'utilities/obj_table.html' with bulk_delete_url='extras:tag_bulk_delete' %}
+
+ {% include 'inc/search_panel.html' %} +
{% endblock %} From 55c153c5a9585a032edca1d85921ef09393aa53f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 20 Nov 2018 11:56:14 -0500 Subject: [PATCH 16/16] Release v2.4.8 --- CHANGELOG.md | 2 +- netbox/netbox/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf5f1d6ee..6560f0e68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -v2.4.8 (FUTURE) +v2.4.8 (2018-11-20) ## Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 1b09989c7..42451c9a2 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ if sys.version_info[0] < 3: DeprecationWarning ) -VERSION = '2.4.8-dev' +VERSION = '2.4.8' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))