mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
@ -147,6 +147,33 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class RackReservationFilter(django_filters.FilterSet):
|
class RackReservationFilter(django_filters.FilterSet):
|
||||||
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='rack__site',
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
label='Site (ID)',
|
||||||
|
)
|
||||||
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
name='rack__site__slug',
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Site (slug)',
|
||||||
|
)
|
||||||
|
group_id = NullableModelMultipleChoiceFilter(
|
||||||
|
name='rack__group',
|
||||||
|
queryset=RackGroup.objects.all(),
|
||||||
|
label='Group (ID)',
|
||||||
|
)
|
||||||
|
group = NullableModelMultipleChoiceFilter(
|
||||||
|
name='rack__group',
|
||||||
|
queryset=RackGroup.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label='Group',
|
||||||
|
)
|
||||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
name='rack',
|
name='rack',
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
@ -157,6 +184,16 @@ class RackReservationFilter(django_filters.FilterSet):
|
|||||||
model = RackReservation
|
model = RackReservation
|
||||||
fields = ['rack', 'user']
|
fields = ['rack', 'user']
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(rack__name__icontains=value) |
|
||||||
|
Q(rack__facility_id__icontains=value) |
|
||||||
|
Q(user__username__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||||
|
@ -330,6 +330,19 @@ class RackReservationForm(BootstrapMixin, forms.ModelForm):
|
|||||||
return unit_choices
|
return unit_choices
|
||||||
|
|
||||||
|
|
||||||
|
class RackReservationFilterForm(BootstrapMixin, forms.Form):
|
||||||
|
q = forms.CharField(required=False, label='Search')
|
||||||
|
site = FilterChoiceField(
|
||||||
|
queryset=Site.objects.annotate(filter_count=Count('racks__reservations')),
|
||||||
|
to_field_name='slug'
|
||||||
|
)
|
||||||
|
group_id = FilterChoiceField(
|
||||||
|
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')),
|
||||||
|
label='Rack group',
|
||||||
|
null_option=(0, 'None')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from itertools import count, groupby
|
||||||
|
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
|
||||||
@ -571,6 +572,15 @@ class RackReservation(models.Model):
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_list(self):
|
||||||
|
"""
|
||||||
|
Express the assigned units as a string of summarized ranges. For example:
|
||||||
|
[0, 1, 2, 10, 14, 15, 16] => "0-2, 10, 14-16"
|
||||||
|
"""
|
||||||
|
group = (list(x) for _, x in groupby(sorted(self.units), lambda x, c=count(): next(c) - x))
|
||||||
|
return ', '.join('-'.join(map(str, (g[0], g[-1])[:len(g)])) for g in group)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device Types
|
# Device Types
|
||||||
@ -781,9 +791,9 @@ class InterfaceManager(models.Manager):
|
|||||||
IFACE_ORDERING_CHOICES (typically indicated by a parent Device's DeviceType).
|
IFACE_ORDERING_CHOICES (typically indicated by a parent Device's DeviceType).
|
||||||
|
|
||||||
To order interfaces naturally, the `name` field is split into five distinct components: leading text (name),
|
To order interfaces naturally, the `name` field is split into five distinct components: leading text (name),
|
||||||
slot, subslot, position, and channel:
|
slot, subslot, position, channel, and virtual circuit:
|
||||||
|
|
||||||
{name}{slot}/{subslot}/{position}:{channel}
|
{name}{slot}/{subslot}/{position}:{channel}.{vc}
|
||||||
|
|
||||||
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would
|
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would
|
||||||
be parsed as follows:
|
be parsed as follows:
|
||||||
@ -793,21 +803,23 @@ class InterfaceManager(models.Manager):
|
|||||||
subslot = 0
|
subslot = 0
|
||||||
position = 1
|
position = 1
|
||||||
channel = None
|
channel = None
|
||||||
|
vc = 0
|
||||||
|
|
||||||
The chosen sorting method will determine which fields are ordered first in the query.
|
The chosen sorting method will determine which fields are ordered first in the query.
|
||||||
"""
|
"""
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
sql_col = '{}.name'.format(queryset.model._meta.db_table)
|
sql_col = '{}.name'.format(queryset.model._meta.db_table)
|
||||||
ordering = {
|
ordering = {
|
||||||
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_name'),
|
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_name'),
|
||||||
IFACE_ORDERING_NAME: ('_name', '_slot', '_subslot', '_position', '_channel'),
|
IFACE_ORDERING_NAME: ('_name', '_slot', '_subslot', '_position', '_channel', '_vc'),
|
||||||
}[method]
|
}[method]
|
||||||
return queryset.extra(select={
|
return queryset.extra(select={
|
||||||
'_name': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
|
'_name': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
|
||||||
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
|
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||||
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
|
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||||
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?$') AS integer)".format(sql_col),
|
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||||
'_channel': "CAST(SUBSTRING({} FROM ':([0-9]+)$') AS integer)".format(sql_col),
|
'_channel': "COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)".format(sql_col),
|
||||||
|
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
|
||||||
}).order_by(*ordering)
|
}).order_by(*ordering)
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from utilities.tables import BaseTable, ToggleColumn
|
|||||||
from .models import (
|
from .models import (
|
||||||
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
||||||
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||||
RackGroup, Region, Site,
|
RackGroup, RackReservation, Region, Site,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -64,6 +64,12 @@ RACK_ROLE = """
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
RACKRESERVATION_ACTIONS = """
|
||||||
|
{% if perms.dcim.change_rackreservation %}
|
||||||
|
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
DEVICEROLE_ACTIONS = """
|
DEVICEROLE_ACTIONS = """
|
||||||
{% if perms.dcim.change_devicerole %}
|
{% if perms.dcim.change_devicerole %}
|
||||||
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||||
@ -226,6 +232,23 @@ class RackImportTable(BaseTable):
|
|||||||
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
|
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rack reservations
|
||||||
|
#
|
||||||
|
|
||||||
|
class RackReservationTable(BaseTable):
|
||||||
|
pk = ToggleColumn()
|
||||||
|
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||||
|
unit_list = tables.Column(orderable=False, verbose_name='Units')
|
||||||
|
actions = tables.TemplateColumn(
|
||||||
|
template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(BaseTable.Meta):
|
||||||
|
model = RackReservation
|
||||||
|
fields = ('pk', 'rack', 'unit_list', 'user', 'created', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
|
@ -36,6 +36,8 @@ urlpatterns = [
|
|||||||
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||||
|
|
||||||
# Rack reservations
|
# Rack reservations
|
||||||
|
url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'),
|
||||||
|
url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
|
||||||
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||||
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||||
|
|
||||||
|
@ -360,6 +360,14 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
# Rack reservations
|
# Rack reservations
|
||||||
#
|
#
|
||||||
|
|
||||||
|
class RackReservationListView(ObjectListView):
|
||||||
|
queryset = RackReservation.objects.all()
|
||||||
|
filter = filters.RackReservationFilter
|
||||||
|
filter_form = forms.RackReservationFilterForm
|
||||||
|
table = tables.RackReservationTable
|
||||||
|
template_name = 'dcim/rackreservation_list.html'
|
||||||
|
|
||||||
|
|
||||||
class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
|
class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'dcim.change_rackreservation'
|
permission_required = 'dcim.change_rackreservation'
|
||||||
model = RackReservation
|
model = RackReservation
|
||||||
@ -383,6 +391,12 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
return obj.rack.get_absolute_url()
|
return obj.rack.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
|
class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
|
permission_required = 'dcim.delete_rackreservation'
|
||||||
|
cls = RackReservation
|
||||||
|
default_return_url = 'dcim:rackreservation_list'
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
|
@ -586,27 +586,51 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
|||||||
|
|
||||||
|
|
||||||
class VLANFromCSVForm(forms.ModelForm):
|
class VLANFromCSVForm(forms.ModelForm):
|
||||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
|
site = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Site not found.'})
|
queryset=Site.objects.all(), required=False, to_field_name='name',
|
||||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
|
error_messages={'invalid_choice': 'Site not found.'}
|
||||||
error_messages={'invalid_choice': 'VLAN group not found.'})
|
)
|
||||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
group_name = forms.CharField(required=False)
|
||||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
tenant = forms.ModelChoiceField(
|
||||||
|
Tenant.objects.all(), to_field_name='name', required=False,
|
||||||
|
error_messages={'invalid_choice': 'Tenant not found.'}
|
||||||
|
)
|
||||||
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
|
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
|
||||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
|
role = forms.ModelChoiceField(
|
||||||
error_messages={'invalid_choice': 'Invalid role.'})
|
queryset=Role.objects.all(), required=False, to_field_name='name',
|
||||||
|
error_messages={'invalid_choice': 'Invalid role.'}
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLAN
|
model = VLAN
|
||||||
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
|
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
super(VLANFromCSVForm, self).clean()
|
||||||
|
|
||||||
|
# Validate VLANGroup
|
||||||
|
group_name = self.cleaned_data.get('group_name')
|
||||||
|
if group_name:
|
||||||
|
try:
|
||||||
|
vlan_group = VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name)
|
||||||
|
except VLANGroup.DoesNotExist:
|
||||||
|
self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
m = super(VLANFromCSVForm, self).save(commit=False)
|
|
||||||
|
vlan = super(VLANFromCSVForm, self).save(commit=False)
|
||||||
|
|
||||||
|
# Assign VLANGroup by site and name
|
||||||
|
if self.cleaned_data['group_name']:
|
||||||
|
vlan.group = VLANGroup.objects.get(site=self.cleaned_data['site'], name=self.cleaned_data['group_name'])
|
||||||
|
|
||||||
# Assign VLAN status by name
|
# Assign VLAN status by name
|
||||||
m.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
|
vlan.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
|
||||||
|
|
||||||
if kwargs.get('commit'):
|
if kwargs.get('commit'):
|
||||||
m.save()
|
vlan.save()
|
||||||
return m
|
return vlan
|
||||||
|
|
||||||
|
|
||||||
class VLANImportForm(BootstrapMixin, BulkImportForm):
|
class VLANImportForm(BootstrapMixin, BulkImportForm):
|
||||||
|
@ -12,7 +12,7 @@ except ImportError:
|
|||||||
"the documentation.")
|
"the documentation.")
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.9.4-r1'
|
VERSION = '1.9.5'
|
||||||
|
|
||||||
# Import local configuration
|
# Import local configuration
|
||||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from rest_framework_swagger.views import get_swagger_view
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
@ -7,6 +9,7 @@ from users.views import login, logout
|
|||||||
|
|
||||||
|
|
||||||
handler500 = handle_500
|
handler500 = handle_500
|
||||||
|
swagger_view = get_swagger_view(title='NetBox API')
|
||||||
|
|
||||||
_patterns = [
|
_patterns = [
|
||||||
|
|
||||||
@ -31,7 +34,7 @@ _patterns = [
|
|||||||
url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')),
|
url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')),
|
||||||
url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
|
url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
|
||||||
url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),
|
url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),
|
||||||
url(r'^api/docs/', include('rest_framework_swagger.urls')),
|
url(r'^api/docs/', swagger_view, name='api_docs'),
|
||||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||||
|
|
||||||
# Error testing
|
# Error testing
|
||||||
|
@ -72,6 +72,8 @@
|
|||||||
{% if perms.dcim.add_rackrole %}
|
{% if perms.dcim.add_rackrole %}
|
||||||
<li><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Role</a></li>
|
<li><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Role</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<li class="divider"></li>
|
||||||
|
<li><a href="{% url 'dcim:rackreservation_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Reservations</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/manufacturers/,/dcim/platforms/' %} active{% endif %}">
|
<li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/manufacturers/,/dcim/platforms/' %} active{% endif %}">
|
||||||
@ -294,7 +296,7 @@
|
|||||||
<div class="col-xs-4 text-right">
|
<div class="col-xs-4 text-right">
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/" target="_blank">Docs</a> ·
|
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/" target="_blank">Docs</a> ·
|
||||||
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'django.swagger.base.view' %}">API</a> ·
|
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'api_docs' %}">API</a> ·
|
||||||
<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> ·
|
||||||
<i class="fa fa-fw fa-support text-primary"></i> <a href="https://github.com/digitalocean/netbox/wiki">Help</a>
|
<i class="fa fa-fw fa-support text-primary"></i> <a href="https://github.com/digitalocean/netbox/wiki">Help</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -210,7 +210,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% for resv in reservations %}
|
{% for resv in reservations %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ resv.units|join:', ' }}</td>
|
<td>{{ resv.unit_list }}</td>
|
||||||
<td>
|
<td>
|
||||||
{{ resv.description }}<br />
|
{{ resv.description }}<br />
|
||||||
<small>{{ resv.user }} · {{ resv.created }}</small>
|
<small>{{ resv.user }} · {{ resv.created }}</small>
|
||||||
|
14
netbox/templates/dcim/rackreservation_list.html
Normal file
14
netbox/templates/dcim/rackreservation_list.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends '_base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% block title %}Rack Reservations{% endblock %}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackreservation_bulk_delete' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
{% include 'inc/search_panel.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -1,10 +1,10 @@
|
|||||||
cffi>=1.8
|
cffi>=1.8
|
||||||
cryptography>=1.4
|
cryptography>=1.4
|
||||||
Django>=1.10
|
Django>=1.10,<1.11
|
||||||
django-debug-toolbar>=1.6
|
django-debug-toolbar>=1.6
|
||||||
django-filter>=1.0.1
|
django-filter>=1.0.1
|
||||||
django-mptt==0.8.7
|
django-mptt==0.8.7
|
||||||
django-rest-swagger==0.3.10
|
django-rest-swagger>=2.1.0
|
||||||
django-tables2>=1.2.5
|
django-tables2>=1.2.5
|
||||||
djangorestframework>=3.5.0
|
djangorestframework>=3.5.0
|
||||||
graphviz>=0.4.10
|
graphviz>=0.4.10
|
||||||
|
Reference in New Issue
Block a user