Compare commits

..

31 Commits

Author SHA1 Message Date
Jeremy Stretch
be393a9d10 Merge pull request #989 from digitalocean/develop
Release v1.9.3
2017-03-23 16:27:06 -04:00
Jeremy Stretch
ef59f38ec4 Release v1.9.3 2017-03-23 16:24:35 -04:00
Jeremy Stretch
47120fae01 Rack assignment is optional for devices 2017-03-23 15:36:24 -04:00
Jeremy Stretch
c0417c1989 Closes #972: Add ability to filter connections list by device name 2017-03-23 10:07:02 -04:00
Jeremy Stretch
fb6cfa45fd Merge pull request #974 from marc-us/develop
Filter on mac address on interface
2017-03-23 09:35:01 -04:00
Mark
b875cea10d Filter on mac address on interface via API 2017-03-23 12:57:35 +01:00
Jeremy Stretch
32bf17c076 Closes #978: Allow filtering device types by function and subdevice role 2017-03-22 17:29:47 -04:00
Jeremy Stretch
66a6a8f33c Closes #983: Include peer device names when listing circuits in device view 2017-03-22 16:58:56 -04:00
Jeremy Stretch
05b71564d8 Closes #981: Allow filtering primary objects by a given set of IDs 2017-03-22 09:39:30 -04:00
Jeremy Stretch
1682de59df Added a footer link to the GitHub wiki 2017-03-20 14:05:26 -04:00
Mark
f26253ec49 Filter on mac address on interface 2017-03-18 21:26:33 +01:00
Mark
f2dc287f14 Filter on mac address on interface 2017-03-18 21:21:49 +01:00
Mark
3fe3151af7 Filter on mac address on interface
Extension to be able filter on mac address via API
2017-03-18 21:10:36 +01:00
Jeremy Stretch
1c1fd8f210 Limit tests to one per major Python version 2017-03-17 21:43:46 -04:00
Jeremy Stretch
3ce2f0d100 Fix error when assigning a new interface to a LAG 2017-03-16 22:27:01 -04:00
Jeremy Stretch
92d726bbd4 Added examples to the graphs documentation 2017-03-15 12:16:46 -04:00
Jeremy Stretch
e2ef0bc3a6 Added survey announcement to README 2017-03-15 12:00:53 -04:00
Jeremy Stretch
13c29cb7a9 Post-release version bump 2017-03-14 17:18:05 -04:00
Jeremy Stretch
27eefd8705 Merge pull request #966 from digitalocean/develop
Release v1.9.2
2017-03-14 17:14:19 -04:00
Jeremy Stretch
b22c6a0078 Release v1.9.2 2017-03-14 17:07:04 -04:00
Jeremy Stretch
f4784412de Fixes #964: Fix bug when bulk editing/deleting filtered set of objects 2017-03-14 15:22:08 -04:00
Jeremy Stretch
33c5ea1f4e Fixes #963: Fix bug in IPv6 address range expansion 2017-03-14 15:06:34 -04:00
Jeremy Stretch
d9f1bcbf15 Renamed user URL namespace 2017-03-14 12:36:44 -04:00
Jeremy Stretch
4b7af8d3b4 Merge pull request #954 from psr/develop
Force Unix line endings on shell scripts
2017-03-13 11:35:05 -04:00
Jeremy Stretch
f3fd82a24a Allow assigning child devices to rackless parents 2017-03-13 11:31:28 -04:00
Jeremy Stretch
cd97b2fb96 Fix parent device position display 2017-03-13 11:25:06 -04:00
Jeremy Stretch
f661c233be Fixes #950: Fix site_id error on child device import 2017-03-13 11:18:33 -04:00
Jeremy Stretch
6a2a2d5d11 Fixes #957: Correct device site filter count to include unracked devices 2017-03-13 10:13:04 -04:00
Jeremy Stretch
87ff433ef8 Fixes #956: Correct bug affecting unnamed rackless devices 2017-03-13 10:06:32 -04:00
Peter Russell
d68b34cefe Force Unix line endings on shell scripts
When following the quickstart docker instructions on Windows (using
Docker for Windows), an error is encountered when running the netbox
container. This is caused by git converting the line endings of the
docker-entrypoint.sh script to Windows format, which are then copied
into the container image.

By setting .gitattributes, we force LF rather than CRLF line endings on
shell scripts on Windows. Other files are left as is.
2017-03-09 16:20:32 +00:00
Jeremy Stretch
70a05b4280 Post-release version bump 2017-03-08 14:45:23 -05:00
29 changed files with 216 additions and 74 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.sh text eol=lf

View File

@@ -9,9 +9,7 @@ env:
language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
install:
- pip install -r requirements.txt
- pip install pep8

View File

@@ -1,3 +1,7 @@
**The [2017 NetBox User Survey](https://goo.gl/forms/75HnNS2iE0Y1hVFH3) is open!** Please consider taking a moment to respond. Your feedback helps shape the pace and focus of NetBox development. The survey will remain open until 2017-03-31. Results will be published on the mailing list.
---
![NetBox](docs/netbox_logo.png "NetBox logo")
NetBox is an IP address management (IPAM) and data center infrastructure management (DCIM) tool. Initially conceived by the network engineering team at [DigitalOcean](https://www.digitalocean.com/), NetBox was developed specifically to address the needs of network and infrastructure engineers.

View File

@@ -90,6 +90,22 @@ NetBox does not have the ability to generate graphs natively, but this feature a
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
* **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`.
## Examples
You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this:
```
https://my.nms.local/graphs/?node={{ obj.device.name }}&interface={{ obj.name }}&duration=5m
```
You can define several graphs to provide multiple contexts when viewing an object. For example:
```
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=24h
https://my.nms.local/graphs/?type=errors&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
```
# Topology Maps
NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.

View File

@@ -5,12 +5,13 @@ 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 NullableModelMultipleChoiceFilter
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import Provider, Circuit, CircuitType
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -42,6 +43,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',

View File

@@ -5,7 +5,7 @@ from django.db.models import Q
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import (
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection,
Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
@@ -14,6 +14,7 @@ from .models import (
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -81,6 +82,7 @@ class RackGroupFilter(django_filters.FilterSet):
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -157,6 +159,7 @@ class RackReservationFilter(django_filters.FilterSet):
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -191,6 +194,7 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -405,6 +409,10 @@ class InterfaceFilter(django_filters.FilterSet):
method='filter_type',
label='Interface type',
)
mac_address = django_filters.CharFilter(
method='_mac_address',
label='MAC address',
)
class Meta:
model = Interface
@@ -420,48 +428,73 @@ class InterfaceFilter(django_filters.FilterSet):
return queryset.filter(form_factor=IFACE_FF_LAG)
return queryset
def _mac_address(self, queryset, name, value):
value = value.strip()
if not value:
return queryset
try:
return queryset.filter(mac_address=value)
except AddrFormatError:
return queryset.none()
class ConsoleConnectionFilter(django_filters.FilterSet):
site = django_filters.CharFilter(
method='filter_site',
label='Site (slug)',
)
class Meta:
model = ConsoleServerPort
fields = []
device = django_filters.CharFilter(
method='filter_device',
label='Device',
)
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(cs_port__device__site__slug=value)
def filter_device(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(device__name__icontains=value) |
Q(cs_port__device__name__icontains=value)
)
class PowerConnectionFilter(django_filters.FilterSet):
site = django_filters.CharFilter(
method='filter_site',
label='Site (slug)',
)
class Meta:
model = PowerOutlet
fields = []
device = django_filters.CharFilter(
method='filter_device',
label='Device',
)
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(power_outlet__device__site__slug=value)
def filter_device(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(device__name__icontains=value) |
Q(power_outlet__device__name__icontains=value)
)
class InterfaceConnectionFilter(django_filters.FilterSet):
site = django_filters.CharFilter(
method='filter_site',
label='Site (slug)',
)
class Meta:
model = InterfaceConnection
fields = []
device = django_filters.CharFilter(
method='filter_device',
label='Device',
)
def filter_site(self, queryset, name, value):
if not value.strip():
@@ -470,3 +503,11 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
Q(interface_a__device__site__slug=value) |
Q(interface_b__device__site__slug=value)
)
def filter_device(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(interface_a__device__name__icontains=value) |
Q(interface_b__device__name__icontains=value)
)

View File

@@ -23,7 +23,7 @@ from .models import (
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
VIRTUAL_IFACE_TYPES
SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES
)
@@ -375,6 +375,21 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
to_field_name='slug'
)
is_console_server = forms.BooleanField(
required=False, label='Is a console server', widget=forms.CheckboxInput(attrs={'value': 'True'}))
is_pdu = forms.BooleanField(
required=False, label='Is a PDU', widget=forms.CheckboxInput(attrs={'value': 'True'})
)
is_network_device = forms.BooleanField(
required=False, label='Is a network device', widget=forms.CheckboxInput(attrs={'value': 'True'})
)
subdevice_role = forms.NullBooleanField(
required=False, label='Subdevice role', widget=forms.Select(choices=(
('', '---------'),
(SUBDEVICE_ROLE_PARENT, 'Parent'),
(SUBDEVICE_ROLE_CHILD, 'Child'),
))
)
#
@@ -680,13 +695,21 @@ class DeviceFromCSVForm(BaseDeviceFromCSVForm):
class ChildDeviceFromCSVForm(BaseDeviceFromCSVForm):
parent = FlexibleModelChoiceField(queryset=Device.objects.all(), to_field_name='name', required=False,
error_messages={'invalid_choice': 'Parent device not found.'})
parent = FlexibleModelChoiceField(
queryset=Device.objects.all(),
to_field_name='name',
required=False,
error_messages={
'invalid_choice': 'Parent device not found.'
}
)
device_bay_name = forms.CharField(required=False)
class Meta(BaseDeviceFromCSVForm.Meta):
fields = ['name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag',
'parent', 'device_bay_name']
fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'parent',
'device_bay_name',
]
def clean(self):
@@ -733,7 +756,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Device
q = forms.CharField(required=False, label='Search')
site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('racks__devices')),
queryset=Site.objects.annotate(filter_count=Count('devices')),
to_field_name='slug',
)
rack_group_id = FilterChoiceField(
@@ -1610,20 +1633,23 @@ class DeviceBayCreateForm(DeviceComponentForm):
class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
installed_device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Child Device',
help_text="Child devices must first be created within the rack occupied "
"by the parent device. Then they can be assigned to a bay.")
installed_device = forms.ModelChoiceField(
queryset=Device.objects.all(),
label='Child Device',
help_text="Child devices must first be created and assigned to the site/rack of the parent device."
)
def __init__(self, device_bay, *args, **kwargs):
super(PopulateDeviceBayForm, self).__init__(*args, **kwargs)
children_queryset = Device.objects.filter(rack=device_bay.device.rack,
parent_bay__isnull=True,
device_type__u_height=0,
device_type__subdevice_role=SUBDEVICE_ROLE_CHILD)\
.exclude(pk=device_bay.device.pk)
self.fields['installed_device'].queryset = children_queryset
self.fields['installed_device'].queryset = Device.objects.filter(
site=device_bay.device.site,
rack=device_bay.device.rack,
parent_bay__isnull=True,
device_type__u_height=0,
device_type__subdevice_role=SUBDEVICE_ROLE_CHILD
).exclude(pk=device_bay.device.pk)
#
@@ -1632,14 +1658,17 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
device = forms.CharField(required=False, label='Device name')
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
device = forms.CharField(required=False, label='Device name')
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
device = forms.CharField(required=False, label='Device name')
#

View File

@@ -975,25 +975,26 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
# Child devices cannot be assigned to a rack face/unit
if self.device_type.is_child_device and self.face is not None:
raise ValidationError({
'face': "Child device types cannot be assigned to a rack face. This is an attribute of the parent "
"device."
'face': "Child device types cannot be assigned to a rack face. This is an attribute of the "
"parent device."
})
if self.device_type.is_child_device and self.position:
raise ValidationError({
'position': "Child device types cannot be assigned to a rack position. This is an attribute of the "
"parent device."
'position': "Child device types cannot be assigned to a rack position. This is an attribute of "
"the parent device."
})
# Validate rack space
rack_face = self.face if not self.device_type.is_full_depth else None
exclude_list = [self.pk] if self.pk else []
try:
available_units = self.rack.get_available_units(u_height=self.device_type.u_height, rack_face=rack_face,
exclude=exclude_list)
available_units = self.rack.get_available_units(
u_height=self.device_type.u_height, rack_face=rack_face, exclude=exclude_list
)
if self.position and self.position not in available_units:
raise ValidationError({
'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) {} "
"({}U).".format(self.position, self.device_type, self.device_type.u_height)
'position': "U{} is already occupied or does not have sufficient space to accommodate a(n) "
"{} ({}U).".format(self.position, self.device_type, self.device_type.u_height)
})
except Rack.DoesNotExist:
pass
@@ -1034,8 +1035,8 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
self.device_type.device_bay_templates.all()]
)
# Update Rack assignment for any child Devices
Device.objects.filter(parent_bay__device=self).update(rack=self.rack)
# Update Site and Rack assignment for any child Devices
Device.objects.filter(parent_bay__device=self).update(site=self.site, rack=self.rack)
def to_csv(self):
return csv_format([
@@ -1059,8 +1060,10 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
return self.name
elif self.position:
return u"{} ({} U{})".format(self.device_type, self.rack.name, self.position)
else:
elif self.rack:
return u"{} ({})".format(self.device_type, self.rack.name)
else:
return u"{} ({})".format(self.device_type, self.site.name)
@property
def identifier(self):

View File

@@ -100,6 +100,10 @@ DEVICE_PRIMARY_IP = """
{{ record.primary_ip4.address.ip|default:"" }}
"""
SUBDEVICE_ROLE_TEMPLATE = """
{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}—{% endif %}
"""
UTILIZATION_GRAPH = """
{% load helpers %}
{% utilization_graph value %}
@@ -249,11 +253,18 @@ class DeviceTypeTable(BaseTable):
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
part_number = tables.Column(verbose_name='Part Number')
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
is_console_server = tables.BooleanColumn(verbose_name='CS')
is_pdu = tables.BooleanColumn(verbose_name='PDU')
is_network_device = tables.BooleanColumn(verbose_name='Net')
subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role')
instance_count = tables.Column(verbose_name='Instances')
class Meta(BaseTable.Meta):
model = DeviceType
fields = ('pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count')
fields = (
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device', 'subdevice_role', 'instance_count'
)
#

View File

@@ -90,7 +90,12 @@ class ComponentCreateView(View):
self.parent_field: parent.pk,
'name': name,
}
component_data.update(data)
# Replace objects with their primary key to keep component_form.clean() happy
for k, v in data.items():
if hasattr(v, 'pk'):
component_data[k] = v.pk
else:
component_data[k] = v
component_form = self.model_form(component_data)
if component_form.is_valid():
new_components.append(component_form.save(commit=False))
@@ -763,9 +768,12 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
default_return_url = 'dcim:device_list'
def save_obj(self, obj):
# Inherent rack from parent device
# Inherit site and rack from parent device
obj.site = obj.parent_bay.device.site
obj.rack = obj.parent_bay.device.rack
obj.save()
# Save the reverse relation
device_bay = obj.parent_bay
device_bay.installed_device = obj

View File

@@ -7,12 +7,13 @@ from django.db.models import Q
from dcim.models import Site, Device, Interface
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -44,6 +45,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
class RIRFilter(django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
class Meta:
model = RIR
@@ -51,6 +53,7 @@ class RIRFilter(django_filters.FilterSet):
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -84,6 +87,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -182,6 +186,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',
@@ -283,6 +288,7 @@ class VLANGroupFilter(django_filters.FilterSet):
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',

View File

@@ -12,7 +12,7 @@ except ImportError:
"the documentation.")
VERSION = '1.9.1'
VERSION = '1.9.3'
# Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:

View File

@@ -23,7 +23,7 @@ _patterns = [
url(r'^ipam/', include('ipam.urls', namespace='ipam')),
url(r'^secrets/', include('secrets.urls', namespace='secrets')),
url(r'^tenancy/', include('tenancy.urls', namespace='tenancy')),
url(r'^profile/', include('users.urls', namespace='users')),
url(r'^user/', include('users.urls', namespace='user')),
# API
url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-api')),

View File

@@ -15,10 +15,10 @@ def userkey_required():
uk = UserKey.objects.get(user=request.user)
except UserKey.DoesNotExist:
messages.warning(request, u"This operation requires an active user key, but you don't have one.")
return redirect('users:userkey')
return redirect('user:userkey')
if not uk.is_active():
messages.warning(request, u"This operation is not available. Your user key has not been activated.")
return redirect('users:userkey')
return redirect('user:userkey')
return view(request, *args, **kwargs)
return wrapped_view
return _decorator

View File

@@ -4,9 +4,11 @@ from django.db.models import Q
from .models import Secret, SecretRole
from dcim.models import Device
from utilities.filters import NumericInFilter
class SecretFilter(django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',

View File

@@ -245,7 +245,7 @@
{% if request.user.is_staff %}
<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs" aria-hidden="true"></i> Admin</a></li>
{% endif %}
<li><a href="{% url 'users:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> {{ request.user }}</a></li>
<li><a href="{% url 'user:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> {{ request.user }}</a></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out" aria-hidden="true"></i> Log out</a></li>
{% else %}
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li>
@@ -295,7 +295,8 @@
<p class="text-muted">
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/" target="_blank">Docs</a> &middot;
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'django.swagger.base.view' %}">API</a> &middot;
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a>
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a> &middot;
<i class="fa fa-fw fa-support text-primary"></i> <a href="https://github.com/digitalocean/netbox/wiki">Help</a>
</p>
</div>
</div>

View File

@@ -43,8 +43,10 @@
<td>
{% if device.parent_bay %}
{% with device.parent_bay.device as parent %}
<span>U{{ parent.position }} / {{ parent.get_face_display }}
(<a href="{{ parent.get_absolute_url }}">{{ parent }}</a> - {{ device.parent_bay.name }})</span>
<a href="{{ parent.get_absolute_url }}">{{ parent }}</a> <i class="fa fa-angle-right"></i> {{ device.parent_bay.name }}
{% if parent.position %}
(U{{ parent.position }} / {{ parent.get_face_display }})
{% endif %}
{% endwith %}
{% elif device.rack and device.position %}
<span>U{{ device.position }} / {{ device.get_face_display }}</span>

View File

@@ -73,7 +73,7 @@
</tr>
<tr>
<td>Rack</td>
<td>Rack name</td>
<td>Rack name (optional)</td>
<td>R101</td>
</tr>
<tr>

View File

@@ -35,7 +35,13 @@
<td colspan="2">
<i class="fa fa-fw fa-globe" title="Circuit"></i>
{% if peer_termination %}
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a> via
{% if peer_termination.interface %}
<a href="{% url 'dcim:device' pk=peer_termination.interface.device.pk %}">{{ peer_termination.interface.device }}</a>
(<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a>)
{% else %}
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a>
{% endif %}
via
{% endif %}
<a href="{% url 'circuits:circuit' pk=iface.circuit_termination.circuit_id %}">{{ iface.circuit_termination.circuit }}</a>
</td>

View File

@@ -9,10 +9,10 @@
<div class="row">
<div class="col-sm-3 col-md-2 col-md-offset-2">
<ul class="nav nav-pills nav-stacked">
<li{% ifequal active_tab "profile" %} class="active"{% endifequal %}><a href="{% url 'users:profile' %}">Profile</a></li>
<li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}><a href="{% url 'users:change_password' %}">Change Password</a></li>
<li{% ifequal active_tab "userkey" %} class="active"{% endifequal %}><a href="{% url 'users:userkey' %}">User Key</a></li>
<li{% ifequal active_tab "recent_activity" %} class="active"{% endifequal %}><a href="{% url 'users:recent_activity' %}">Recent Activity</a></li>
<li{% ifequal active_tab "profile" %} class="active"{% endifequal %}><a href="{% url 'user:profile' %}">Profile</a></li>
<li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}><a href="{% url 'user:change_password' %}">Change Password</a></li>
<li{% ifequal active_tab "userkey" %} class="active"{% endifequal %}><a href="{% url 'user:userkey' %}">User Key</a></li>
<li{% ifequal active_tab "recent_activity" %} class="active"{% endifequal %}><a href="{% url 'user:recent_activity' %}">Recent Activity</a></li>
</ul>
</div>
<div class="col-sm-9 col-md-6">

View File

@@ -24,7 +24,7 @@
</div>
<div class="text-right">
<button type="submit" name="_update" class="btn btn-primary">Update</button>
<a href="{% url 'users:profile' %}" class="btn btn-default">Cancel</a>
<a href="{% url 'user:profile' %}" class="btn btn-default">Cancel</a>
</div>
</form>
{% endblock %}

View File

@@ -15,7 +15,7 @@
<p>Your public key is below.</p>
<pre>{{ userkey.public_key }}</pre>
<div class="pull-right">
<a href="{% url 'users:userkey_edit' %}" class="btn btn-warning">
<a href="{% url 'user:userkey_edit' %}" class="btn btn-warning">
<span class="fa fa-pencil" aria-hidden="true"></span>
Edit user key
</a>
@@ -24,7 +24,7 @@
{% else %}
<p>You don't have a user key on file.</p>
<p>
<a href="{% url 'users:userkey_edit' %}" class="btn btn-primary">
<a href="{% url 'user:userkey_edit' %}" class="btn btn-primary">
<span class="fa fa-plus" aria-hidden="true"></span>
Create a User Key
</a>

View File

@@ -23,7 +23,7 @@
</div>
<div class="col-sm-6 col-md-6 text-right">
<button type="submit" name="_update" class="btn btn-primary">Save</button>
<a href="{% url 'users:userkey' %}" class="btn btn-default">Cancel</a>
<a href="{% url 'user:userkey' %}" class="btn btn-default">Cancel</a>
</div>
</div>
</div>

View File

@@ -3,11 +3,12 @@ import django_filters
from django.db.models import Q
from extras.filters import CustomFieldFilterSet
from utilities.filters import NullableModelMultipleChoiceFilter
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import Tenant, TenantGroup
class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter(
method='search',
label='Search',

View File

@@ -7,9 +7,9 @@ urlpatterns = [
# User profiles
url(r'^profile/$', views.profile, name='profile'),
url(r'^profile/password/$', views.change_password, name='change_password'),
url(r'^profile/user-key/$', views.userkey, name='userkey'),
url(r'^profile/user-key/edit/$', views.userkey_edit, name='userkey_edit'),
url(r'^profile/recent-activity/$', views.recent_activity, name='recent_activity'),
url(r'^password/$', views.change_password, name='change_password'),
url(r'^user-key/$', views.userkey, name='userkey'),
url(r'^user-key/edit/$', views.userkey_edit, name='userkey_edit'),
url(r'^recent-activity/$', views.recent_activity, name='recent_activity'),
]

View File

@@ -69,7 +69,7 @@ def change_password(request):
form.save()
update_session_auth_hash(request, form.user)
messages.success(request, u"Your password has been changed successfully.")
return redirect('users:profile')
return redirect('user:profile')
else:
form = PasswordChangeForm(user=request.user)
@@ -109,7 +109,7 @@ def userkey_edit(request):
uk.user = request.user
uk.save()
messages.success(request, u"Your user key has been saved.")
return redirect('users:userkey')
return redirect('user:userkey')
else:
form = UserKeyForm(instance=userkey)

View File

@@ -6,6 +6,17 @@ from django.db.models import Q
from django.utils.encoding import force_text
#
# Filters
#
class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
"""
Filters for a set of numeric values. Example: id__in=100,200,300
"""
pass
class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField):
"""
This field operates like a normal ModelMultipleChoiceField except that it allows for one additional choice which is

View File

@@ -57,7 +57,7 @@ def parse_numeric_range(string, base=10):
begin, end = dash_range.split('-')
except ValueError:
begin, end = dash_range, dash_range
begin, end = int(begin.strip()), int(end.strip(), base=base) + 1
begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1
values.extend(range(begin, end))
return list(set(values))

View File

@@ -434,7 +434,7 @@ class BulkEditView(View):
# Are we editing *all* objects in the queryset or just a selected subset?
if request.POST.get('_all') and self.filter is not None:
pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk'))]
pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk')).qs]
else:
pk_list = [int(pk) for pk in request.POST.getlist('pk')]
@@ -572,7 +572,7 @@ class BulkDeleteView(View):
# Are we deleting *all* objects in the queryset or just a selected subset?
if request.POST.get('_all') and self.filter is not None:
pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk'))]
pk_list = [obj.pk for obj in self.filter(request.GET, self.cls.objects.only('pk')).qs]
else:
pk_list = [int(pk) for pk in request.POST.getlist('pk')]