Skip to content

Commit

Permalink
Merge pull request #1852 from digitalocean/develop
Browse files Browse the repository at this point in the history
Release v2.2.9
  • Loading branch information
jeremystretch authored Jan 31, 2018
2 parents ec0cb7a + b324370 commit 6436d70
Show file tree
Hide file tree
Showing 41 changed files with 368 additions and 122 deletions.
7 changes: 4 additions & 3 deletions docs/api/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,15 @@ $ curl -H "Accept: application/json; indent=4" http://localhost/api/dcim/sites/6

### Creating a new site

Send a `POST` request to the site list endpoint with token authentication and JSON-formatted data. Only mandatory fields are required.
Send a `POST` request to the site list endpoint with token authentication and JSON-formatted data. Only mandatory fields are required. This example includes one non required field, "region."

```
$ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/ --data '{"name": "My New Site", "slug": "my-new-site"}'
$ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/dcim/sites/ --data '{"name": "My New Site", "slug": "my-new-site", "region": 5}'
{
"id": 16,
"name": "My New Site",
"slug": "my-new-site",
"region": null,
"region": 5,
"tenant": null,
"facility": "",
"asn": null,
Expand All @@ -102,6 +102,7 @@ $ curl -X POST -H "Authorization: Token d2f763479f703d80de0ec15254237bc651f9cdc0
"comments": ""
}
```
Note that in this example we are creating a site bound to a region with the ID of 5. For write API actions (`POST`, `PUT`, and `PATCH`) the integer ID value is used for `ForeignKey` (related model) relationships, instead of the nested representation that is used in the `GET` (list) action.

### Modify an existing site

Expand Down
2 changes: 1 addition & 1 deletion docs/api/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ The base serializer is used to represent the default view of a model. This inclu
}
```

Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name.
Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. When performing write api actions (`POST`, `PUT`, and `PATCH`), any `ForeignKey` relationships do not use the nested serializer, instead you will pass just the integer ID of the related model.

When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object.

Expand Down
2 changes: 1 addition & 1 deletion docs/installation/ldap.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ AUTH_LDAP_USER_ATTR_MAP = {

# User Groups for Permissions
!!! info
When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`.
When using Microsoft Active Directory, support for nested groups can be activated by using `NestedGroupOfNamesType()` instead of `GroupOfNamesType()` for `AUTH_LDAP_GROUP_TYPE`.

```python
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
Expand Down
7 changes: 7 additions & 0 deletions docs/installation/netbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ Resolving deltas: 100% (1495/1495), done.
Checking connectivity... done.
```

!!! warning
Ensure that the media directory (`/opt/netbox/netbox/media/` in this example) and all its subdirectories are writable by the user account as which NetBox runs. If the NetBox process does not have permission to write to this directory, attempts to upload files (e.g. image attachments) will fail. (The appropriate user account will vary by platform.)

```
# chown -R netbox:netbox /opt/netbox/netbox/media/
```

## Install Python Packages

Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.)
Expand Down
2 changes: 1 addition & 1 deletion netbox/circuits/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
to_field_name='slug',
null_option=(0, 'None')
null_label='-- None --'
)
site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
Expand Down
3 changes: 2 additions & 1 deletion netbox/circuits/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.utils.safestring import mark_safe
from django_tables2.utils import Accessor

from tenancy.tables import COL_TENANT
from utilities.tables import BaseTable, ToggleColumn
from .models import Circuit, CircuitType, Provider

Expand Down Expand Up @@ -75,7 +76,7 @@ class CircuitTable(BaseTable):
pk = ToggleColumn()
cid = tables.LinkColumn(verbose_name='ID')
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
tenant = tables.TemplateColumn(template_code=COL_TENANT)
termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side')
termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side')

Expand Down
2 changes: 2 additions & 0 deletions netbox/dcim/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,8 @@ class Meta:


class WritableInventoryItemSerializer(ValidatedModelSerializer):
# Provide a default value to satisfy UniqueTogetherValidator
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)

class Meta:
model = InventoryItem
Expand Down
31 changes: 30 additions & 1 deletion netbox/dcim/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@


class RegionFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Region.objects.all(),
label='Parent region (ID)',
Expand All @@ -37,6 +41,15 @@ class Meta:
model = Region
fields = ['name', 'slug']

def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(slug__icontains=value)
)
return queryset.filter(qs_filter)


class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
Expand Down Expand Up @@ -600,6 +613,10 @@ class Meta:


class InventoryItemFilter(DeviceComponentFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=InventoryItem.objects.all(),
label='Parent inventory item (ID)',
Expand All @@ -618,7 +635,19 @@ class InventoryItemFilter(DeviceComponentFilterSet):

class Meta:
model = InventoryItem
fields = ['name', 'part_id', 'serial', 'discovered']
fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']

def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value) |
Q(part_id__icontains=value) |
Q(serial__iexact=value) |
Q(asset_tag__iexact=value) |
Q(description__icontains=value)
)
return queryset.filter(qs_filter)


class ConsoleConnectionFilter(django_filters.FilterSet):
Expand Down
65 changes: 57 additions & 8 deletions netbox/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ class Meta:
}


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


#
# Sites
#
Expand Down Expand Up @@ -163,7 +168,7 @@ class SiteFilterForm(BootstrapMixin, CustomFieldFilterForm):
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('sites')),
to_field_name='slug',
null_option=(0, 'None')
null_label='-- None --'
)


Expand Down Expand Up @@ -359,17 +364,17 @@ class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
group_id = FilterChoiceField(
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks')),
label='Rack group',
null_option=(0, 'None')
null_label='-- None --'
)
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('racks')),
to_field_name='slug',
null_option=(0, 'None')
null_label='-- None --'
)
role = FilterChoiceField(
queryset=RackRole.objects.annotate(filter_count=Count('racks')),
to_field_name='slug',
null_option=(0, 'None')
null_label='-- None --'
)


Expand Down Expand Up @@ -411,7 +416,7 @@ class RackReservationFilterForm(BootstrapMixin, forms.Form):
group_id = FilterChoiceField(
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')),
label='Rack group',
null_option=(0, 'None')
null_label='-- None --'
)


Expand Down Expand Up @@ -1031,7 +1036,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
rack_id = FilterChoiceField(
queryset=Rack.objects.annotate(filter_count=Count('devices')),
label='Rack',
null_option=(0, 'None'),
null_label='-- None --',
)
role = FilterChoiceField(
queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),
Expand All @@ -1040,7 +1045,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
tenant = FilterChoiceField(
queryset=Tenant.objects.annotate(filter_count=Count('devices')),
to_field_name='slug',
null_option=(0, 'None'),
null_label='-- None --',
)
manufacturer_id = FilterChoiceField(queryset=Manufacturer.objects.all(), label='Manufacturer')
device_type_id = FilterChoiceField(
Expand All @@ -1052,7 +1057,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
platform = FilterChoiceField(
queryset=Platform.objects.annotate(filter_count=Count('devices')),
to_field_name='slug',
null_option=(0, 'None'),
null_label='-- None --',
)
status = forms.MultipleChoiceField(choices=device_status_choices, required=False)
mac_address = forms.CharField(required=False, label='MAC address')
Expand Down Expand Up @@ -1923,3 +1928,47 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = InventoryItem
fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']


class InventoryItemCSVForm(forms.ModelForm):
device = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
help_text='Device name or ID',
error_messages={
'invalid_choice': 'Device not found.',
}
)
manufacturer = forms.ModelChoiceField(
queryset=Manufacturer.objects.all(),
to_field_name='name',
required=False,
help_text='Manufacturer name',
error_messages={
'invalid_choice': 'Invalid manufacturer.',
}
)

class Meta:
model = InventoryItem
fields = ['device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']


class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=InventoryItem.objects.all(), widget=forms.MultipleHiddenInput)
manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), required=False)
part_id = forms.CharField(max_length=50, required=False, label='Part ID')
description = forms.CharField(max_length=100, required=False)

class Meta:
nullable_fields = ['manufacturer', 'part_id', 'description']


class InventoryItemFilterForm(BootstrapMixin, forms.Form):
model = InventoryItem
q = forms.CharField(required=False, label='Search')
manufacturer = FilterChoiceField(
queryset=Manufacturer.objects.annotate(filter_count=Count('inventory_items')),
to_field_name='slug',
null_label='-- None --'
)
15 changes: 15 additions & 0 deletions netbox/dcim/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1452,9 +1452,24 @@ class InventoryItem(models.Model):
discovered = models.BooleanField(default=False, verbose_name='Discovered')
description = models.CharField(max_length=100, blank=True)

csv_headers = [
'device', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
]

class Meta:
ordering = ['device__id', 'parent__id', 'name']
unique_together = ['device', 'parent', 'name']

def __str__(self):
return self.name

def to_csv(self):
return csv_format([
self.device.name or '{' + self.device.pk + '}',
self.name,
self.manufacturer.name if self.manufacturer else None,
self.part_id,
self.serial,
self.asset_tag,
self.description
])
Loading

0 comments on commit 6436d70

Please sign in to comment.