Skip to content

Commit

Permalink
Merge pull request #2600 from digitalocean/develop
Browse files Browse the repository at this point in the history
Release v2.4.8
  • Loading branch information
jeremystretch authored Nov 20, 2018
2 parents cb83eb2 + 55c153c commit 8d43291
Show file tree
Hide file tree
Showing 40 changed files with 469 additions and 114 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
v2.4.8 (2018-11-20)

## 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

* [#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
* [#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

---

v2.4.7 (2018-11-06)

## Enhancements
Expand Down
70 changes: 70 additions & 0 deletions docs/development/extending-models.md
Original file line number Diff line number Diff line change
@@ -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 <app> -n <name>
./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 `<app>.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.
7 changes: 0 additions & 7 deletions docs/development/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 *`).
41 changes: 41 additions & 0 deletions docs/development/style-guide.md
Original file line number Diff line number Diff line change
@@ -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. `<app>.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`.
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ 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'

markdown_extensions:
Expand Down
10 changes: 3 additions & 7 deletions netbox/circuits/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,9 @@ def napalm(self, request, pk):
# 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:
Expand Down Expand Up @@ -309,7 +309,9 @@ def napalm(self, request, pk):
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)
Expand Down
30 changes: 8 additions & 22 deletions netbox/dcim/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions netbox/dcim/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
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 = """
Expand Down Expand Up @@ -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')


Expand Down
6 changes: 4 additions & 2 deletions netbox/dcim/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -858,8 +858,10 @@ def get(self, request, pk):
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(
Expand Down
31 changes: 29 additions & 2 deletions netbox/extras/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
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,
Expand Down Expand Up @@ -208,6 +208,11 @@ def __init__(self, *args, **kwargs):
self.fields['remove_tags'] = TagField(required=False)


class TagFilterForm(BootstrapMixin, forms.Form):
model = Tag
q = forms.CharField(required=False, label='Search')


#
# Config contexts
#
Expand All @@ -227,6 +232,28 @@ class Meta:
]


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,
Expand Down
Loading

0 comments on commit 8d43291

Please sign in to comment.