mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Merge v2.11.8 changes
This commit is contained in:
commit
88e382e7a1
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -17,7 +17,7 @@ body:
|
|||||||
What version of NetBox are you currently running? (If you don't have access to the most
|
What version of NetBox are you currently running? (If you don't have access to the most
|
||||||
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
|
||||||
before opening a bug report to see if your issue has already been addressed.)
|
before opening a bug report to see if your issue has already been addressed.)
|
||||||
placeholder: v2.11.7
|
placeholder: v2.11.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v2.11.7
|
placeholder: v2.11.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
# NetBox v2.11
|
# NetBox v2.11
|
||||||
|
|
||||||
## v2.11.8 (FUTURE)
|
## v2.11.8 (2021-07-06)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
* [#5503](https://github.com/netbox-community/netbox/issues/5503) - Annotate short date & time fields with their longer form
|
||||||
|
* [#6138](https://github.com/netbox-community/netbox/issues/6138) - Add an `empty` filter modifier for character fields
|
||||||
|
* [#6200](https://github.com/netbox-community/netbox/issues/6200) - Add rack reservations to global search
|
||||||
|
* [#6368](https://github.com/netbox-community/netbox/issues/6368) - Enable virtual chassis assignment during bulk import of devices
|
||||||
* [#6620](https://github.com/netbox-community/netbox/issues/6620) - Show assigned VMs count under device role view
|
* [#6620](https://github.com/netbox-community/netbox/issues/6620) - Show assigned VMs count under device role view
|
||||||
|
* [#6666](https://github.com/netbox-community/netbox/issues/6666) - Show management-only status under interface detail view
|
||||||
|
* [#6667](https://github.com/netbox-community/netbox/issues/6667) - Display VM memory as GB/TB as appropriate
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
@ -12,6 +18,9 @@
|
|||||||
* [#6637](https://github.com/netbox-community/netbox/issues/6637) - Fix group assignment in "available VLANs" link under VLAN group view
|
* [#6637](https://github.com/netbox-community/netbox/issues/6637) - Fix group assignment in "available VLANs" link under VLAN group view
|
||||||
* [#6640](https://github.com/netbox-community/netbox/issues/6640) - Disallow numeric values in custom text fields
|
* [#6640](https://github.com/netbox-community/netbox/issues/6640) - Disallow numeric values in custom text fields
|
||||||
* [#6652](https://github.com/netbox-community/netbox/issues/6652) - Fix exception when adding components in bulk to multiple devices
|
* [#6652](https://github.com/netbox-community/netbox/issues/6652) - Fix exception when adding components in bulk to multiple devices
|
||||||
|
* [#6676](https://github.com/netbox-community/netbox/issues/6676) - Fix device/VM counts per cluster under cluster type/group views
|
||||||
|
* [#6680](https://github.com/netbox-community/netbox/issues/6680) - Allow setting custom field values for VM interfaces on initial creation
|
||||||
|
* [#6695](https://github.com/netbox-community/netbox/issues/6695) - Fix exception when importing device type with invalid front port definition
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -61,25 +61,30 @@ These lookup expressions can be applied by adding a suffix to the desired field'
|
|||||||
|
|
||||||
Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions:
|
Numeric based fields (ASN, VLAN ID, etc) support these lookup expressions:
|
||||||
|
|
||||||
- `n` - not equal to (negation)
|
| Filter | Description |
|
||||||
- `lt` - less than
|
|--------|-------------|
|
||||||
- `lte` - less than or equal
|
| `n` | Not equal to |
|
||||||
- `gt` - greater than
|
| `lt` | Less than |
|
||||||
- `gte` - greater than or equal
|
| `lte` | Less than or equal to |
|
||||||
|
| `gt` | Greater than |
|
||||||
|
| `gte` | Greater than or equal to |
|
||||||
|
|
||||||
### String Fields
|
### String Fields
|
||||||
|
|
||||||
String based (char) fields (Name, Address, etc) support these lookup expressions:
|
String based (char) fields (Name, Address, etc) support these lookup expressions:
|
||||||
|
|
||||||
- `n` - not equal to (negation)
|
| Filter | Description |
|
||||||
- `ic` - case insensitive contains
|
|--------|-------------|
|
||||||
- `nic` - negated case insensitive contains
|
| `n` | Not equal to |
|
||||||
- `isw` - case insensitive starts with
|
| `ic` | Contains (case-insensitive) |
|
||||||
- `nisw` - negated case insensitive starts with
|
| `nic` | Does not contain (case-insensitive) |
|
||||||
- `iew` - case insensitive ends with
|
| `isw` | Starts with (case-insensitive) |
|
||||||
- `niew` - negated case insensitive ends with
|
| `nisw` | Does not start with (case-insensitive) |
|
||||||
- `ie` - case insensitive exact match
|
| `iew` | Ends with (case-insensitive) |
|
||||||
- `nie` - negated case insensitive exact match
|
| `niew` | Does not end with (case-insensitive) |
|
||||||
|
| `ie` | Exact match (case-insensitive) |
|
||||||
|
| `nie` | Inverse exact match (case-insensitive) |
|
||||||
|
| `empty` | Is empty (boolean) |
|
||||||
|
|
||||||
### Foreign Keys & Other Fields
|
### Foreign Keys & Other Fields
|
||||||
|
|
||||||
|
@ -1887,8 +1887,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
|
|||||||
)
|
)
|
||||||
rear_port = forms.ModelChoiceField(
|
rear_port = forms.ModelChoiceField(
|
||||||
queryset=RearPortTemplate.objects.all(),
|
queryset=RearPortTemplate.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name'
|
||||||
required=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -2243,6 +2242,12 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm):
|
|||||||
choices=DeviceStatusChoices,
|
choices=DeviceStatusChoices,
|
||||||
help_text='Operational status'
|
help_text='Operational status'
|
||||||
)
|
)
|
||||||
|
virtual_chassis = CSVModelChoiceField(
|
||||||
|
queryset=VirtualChassis.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
required=False,
|
||||||
|
help_text='Virtual chassis'
|
||||||
|
)
|
||||||
cluster = CSVModelChoiceField(
|
cluster = CSVModelChoiceField(
|
||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
@ -2253,6 +2258,10 @@ class BaseDeviceCSVForm(CustomFieldModelCSVForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
fields = []
|
fields = []
|
||||||
model = Device
|
model = Device
|
||||||
|
help_texts = {
|
||||||
|
'vc_position': 'Virtual chassis position',
|
||||||
|
'vc_priority': 'Virtual chassis priority',
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, data=None, *args, **kwargs):
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
super().__init__(data, *args, **kwargs)
|
super().__init__(data, *args, **kwargs)
|
||||||
@ -2291,7 +2300,8 @@ class DeviceCSVForm(BaseDeviceCSVForm):
|
|||||||
class Meta(BaseDeviceCSVForm.Meta):
|
class Meta(BaseDeviceCSVForm.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||||
'site', 'location', 'rack', 'position', 'face', 'cluster', 'comments',
|
'site', 'location', 'rack', 'position', 'face', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster',
|
||||||
|
'comments',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, data=None, *args, **kwargs):
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
@ -2326,7 +2336,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm):
|
|||||||
class Meta(BaseDeviceCSVForm.Meta):
|
class Meta(BaseDeviceCSVForm.Meta):
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
|
||||||
'parent', 'device_bay', 'cluster', 'comments',
|
'parent', 'device_bay', 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'comments',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, data=None, *args, **kwargs):
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
|
@ -293,6 +293,8 @@ class FrontPortTemplate(ComponentTemplateModel):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
# Validate rear port assignment
|
# Validate rear port assignment
|
||||||
if self.rear_port.device_type != self.device_type:
|
if self.rear_port.device_type != self.device_type:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
@ -307,6 +309,9 @@ class FrontPortTemplate(ComponentTemplateModel):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
except RearPortTemplate.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
def instantiate(self, device):
|
def instantiate(self, device):
|
||||||
if self.rear_port:
|
if self.rear_port:
|
||||||
rear_port = RearPort.objects.get(device=device, name=self.rear_port.name)
|
rear_port = RearPort.objects.get(device=device, name=self.rear_port.name)
|
||||||
|
@ -1028,6 +1028,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
|
|
||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
|
VirtualChassis.objects.create(name='Virtual Chassis 1')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'device_type': devicetypes[1].pk,
|
'device_type': devicetypes[1].pk,
|
||||||
'device_role': deviceroles[1].pk,
|
'device_role': deviceroles[1].pk,
|
||||||
@ -1053,10 +1055,10 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"device_role,manufacturer,device_type,status,name,site,location,rack,position,face",
|
"device_role,manufacturer,device_type,status,name,site,location,rack,position,face,virtual_chassis,vc_position,vc_priority",
|
||||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 4,Site 1,Location 1,Rack 1,10,front",
|
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 4,Site 1,Location 1,Rack 1,10,front,Virtual Chassis 1,1,10",
|
||||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 5,Site 1,Location 1,Rack 1,20,front",
|
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 5,Site 1,Location 1,Rack 1,20,front,Virtual Chassis 1,2,20",
|
||||||
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 6,Site 1,Location 1,Rack 1,30,front",
|
"Device Role 1,Manufacturer 1,Device Type 1,active,Device 6,Site 1,Location 1,Rack 1,30,front,Virtual Chassis 1,3,30",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
|
@ -5,4 +5,5 @@ class ExtrasConfig(AppConfig):
|
|||||||
name = "extras"
|
name = "extras"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
import extras.lookups
|
||||||
import extras.signals
|
import extras.signals
|
||||||
|
17
netbox/extras/lookups.py
Normal file
17
netbox/extras/lookups.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from django.db.models import CharField, Lookup
|
||||||
|
|
||||||
|
|
||||||
|
class Empty(Lookup):
|
||||||
|
"""
|
||||||
|
Filter on whether a string is empty.
|
||||||
|
"""
|
||||||
|
lookup_name = 'empty'
|
||||||
|
|
||||||
|
def as_sql(self, qn, connection):
|
||||||
|
lhs, lhs_params = self.process_lhs(qn, connection)
|
||||||
|
rhs, rhs_params = self.process_rhs(qn, connection)
|
||||||
|
params = lhs_params + rhs_params
|
||||||
|
return 'CAST(LENGTH(%s) AS BOOLEAN) != %s' % (lhs, rhs), params
|
||||||
|
|
||||||
|
|
||||||
|
CharField.register_lookup(Empty)
|
@ -442,9 +442,8 @@ class JournalEntry(ChangeLoggedModel):
|
|||||||
verbose_name_plural = 'journal entries'
|
verbose_name_plural = 'journal entries'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
created_date = timezone.localdate(self.created)
|
created = timezone.localtime(self.created)
|
||||||
created_time = timezone.localtime(self.created)
|
return f"{date_format(created, format='SHORT_DATETIME_FORMAT')} ({self.get_kind_display()})"
|
||||||
return f"{date_format(created_date)} - {time_format(created_time)} ({self.get_kind_display()})"
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('extras:journalentry', args=[self.pk])
|
return reverse('extras:journalentry', args=[self.pk])
|
||||||
|
@ -4,12 +4,12 @@ from circuits.filtersets import CircuitFilterSet, ProviderFilterSet, ProviderNet
|
|||||||
from circuits.models import Circuit, ProviderNetwork, Provider
|
from circuits.models import Circuit, ProviderNetwork, Provider
|
||||||
from circuits.tables import CircuitTable, ProviderNetworkTable, ProviderTable
|
from circuits.tables import CircuitTable, ProviderNetworkTable, ProviderTable
|
||||||
from dcim.filtersets import (
|
from dcim.filtersets import (
|
||||||
CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, PowerFeedFilterSet, RackFilterSet, LocationFilterSet,
|
CableFilterSet, DeviceFilterSet, DeviceTypeFilterSet, PowerFeedFilterSet, RackFilterSet, RackReservationFilterSet,
|
||||||
SiteFilterSet, VirtualChassisFilterSet,
|
LocationFilterSet, SiteFilterSet, VirtualChassisFilterSet,
|
||||||
)
|
)
|
||||||
from dcim.models import Cable, Device, DeviceType, PowerFeed, Rack, Location, Site, VirtualChassis
|
from dcim.models import Cable, Device, DeviceType, Location, PowerFeed, Rack, RackReservation, Site, VirtualChassis
|
||||||
from dcim.tables import (
|
from dcim.tables import (
|
||||||
CableTable, DeviceTable, DeviceTypeTable, PowerFeedTable, RackTable, LocationTable, SiteTable,
|
CableTable, DeviceTable, DeviceTypeTable, PowerFeedTable, RackTable, RackReservationTable, LocationTable, SiteTable,
|
||||||
VirtualChassisTable,
|
VirtualChassisTable,
|
||||||
)
|
)
|
||||||
from ipam.filtersets import AggregateFilterSet, IPAddressFilterSet, PrefixFilterSet, VLANFilterSet, VRFFilterSet
|
from ipam.filtersets import AggregateFilterSet, IPAddressFilterSet, PrefixFilterSet, VLANFilterSet, VRFFilterSet
|
||||||
@ -61,6 +61,12 @@ SEARCH_TYPES = OrderedDict((
|
|||||||
'table': RackTable,
|
'table': RackTable,
|
||||||
'url': 'dcim:rack_list',
|
'url': 'dcim:rack_list',
|
||||||
}),
|
}),
|
||||||
|
('rackreservation', {
|
||||||
|
'queryset': RackReservation.objects.prefetch_related('site', 'rack', 'user'),
|
||||||
|
'filterset': RackReservationFilterSet,
|
||||||
|
'table': RackReservationTable,
|
||||||
|
'url': 'dcim:rackreservation_list',
|
||||||
|
}),
|
||||||
('location', {
|
('location', {
|
||||||
'queryset': Location.objects.add_related_count(
|
'queryset': Location.objects.add_related_count(
|
||||||
Location.objects.all(),
|
Location.objects.all(),
|
||||||
|
@ -89,13 +89,13 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
filters.MultiValueNumberFilter,
|
filters.MultiValueNumberFilter,
|
||||||
filters.MultiValueTimeFilter
|
filters.MultiValueTimeFilter
|
||||||
)):
|
)):
|
||||||
lookup_map = FILTER_NUMERIC_BASED_LOOKUP_MAP
|
return FILTER_NUMERIC_BASED_LOOKUP_MAP
|
||||||
|
|
||||||
elif isinstance(existing_filter, (
|
elif isinstance(existing_filter, (
|
||||||
filters.TreeNodeMultipleChoiceFilter,
|
filters.TreeNodeMultipleChoiceFilter,
|
||||||
)):
|
)):
|
||||||
# TreeNodeMultipleChoiceFilter only support negation but must maintain the `in` lookup expression
|
# TreeNodeMultipleChoiceFilter only support negation but must maintain the `in` lookup expression
|
||||||
lookup_map = FILTER_TREENODE_NEGATION_LOOKUP_MAP
|
return FILTER_TREENODE_NEGATION_LOOKUP_MAP
|
||||||
|
|
||||||
elif isinstance(existing_filter, (
|
elif isinstance(existing_filter, (
|
||||||
django_filters.ModelChoiceFilter,
|
django_filters.ModelChoiceFilter,
|
||||||
@ -103,7 +103,7 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
TagFilter
|
TagFilter
|
||||||
)) or existing_filter.extra.get('choices'):
|
)) or existing_filter.extra.get('choices'):
|
||||||
# These filter types support only negation
|
# These filter types support only negation
|
||||||
lookup_map = FILTER_NEGATION_LOOKUP_MAP
|
return FILTER_NEGATION_LOOKUP_MAP
|
||||||
|
|
||||||
elif isinstance(existing_filter, (
|
elif isinstance(existing_filter, (
|
||||||
django_filters.filters.CharFilter,
|
django_filters.filters.CharFilter,
|
||||||
@ -111,12 +111,9 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
filters.MultiValueCharFilter,
|
filters.MultiValueCharFilter,
|
||||||
filters.MultiValueMACAddressFilter
|
filters.MultiValueMACAddressFilter
|
||||||
)):
|
)):
|
||||||
lookup_map = FILTER_CHAR_BASED_LOOKUP_MAP
|
return FILTER_CHAR_BASED_LOOKUP_MAP
|
||||||
|
|
||||||
else:
|
return None
|
||||||
lookup_map = None
|
|
||||||
|
|
||||||
return lookup_map
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_filters(cls):
|
def get_filters(cls):
|
||||||
|
@ -11,12 +11,13 @@ OBJ_TYPE_CHOICES = (
|
|||||||
('DCIM', (
|
('DCIM', (
|
||||||
('site', 'Sites'),
|
('site', 'Sites'),
|
||||||
('rack', 'Racks'),
|
('rack', 'Racks'),
|
||||||
|
('rackreservation', 'Rack reservations'),
|
||||||
('location', 'Locations'),
|
('location', 'Locations'),
|
||||||
('devicetype', 'Device Types'),
|
('devicetype', 'Device Types'),
|
||||||
('device', 'Devices'),
|
('device', 'Devices'),
|
||||||
('virtualchassis', 'Virtual Chassis'),
|
('virtualchassis', 'Virtual chassis'),
|
||||||
('cable', 'Cables'),
|
('cable', 'Cables'),
|
||||||
('powerfeed', 'Power Feeds'),
|
('powerfeed', 'Power feeds'),
|
||||||
)),
|
)),
|
||||||
('IPAM', (
|
('IPAM', (
|
||||||
('vrf', 'VRFs'),
|
('vrf', 'VRFs'),
|
||||||
|
@ -18,7 +18,7 @@ from django_tables2.export import TableExport
|
|||||||
|
|
||||||
from extras.models import ExportTemplate
|
from extras.models import ExportTemplate
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortTransaction
|
from utilities.exceptions import AbortTransaction, PermissionsViolation
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
BootstrapMixin, BulkRenameForm, ConfirmationForm, CSVDataField, ImportForm, TableConfigForm, restrict_form_fields,
|
BootstrapMixin, BulkRenameForm, ConfirmationForm, CSVDataField, ImportForm, TableConfigForm, restrict_form_fields,
|
||||||
)
|
)
|
||||||
@ -267,7 +267,8 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
obj = form.save()
|
obj = form.save()
|
||||||
|
|
||||||
# Check that the new object conforms with any assigned object-level permissions
|
# Check that the new object conforms with any assigned object-level permissions
|
||||||
self.queryset.get(pk=obj.pk)
|
if not self.queryset.filter(pk=obj.pk).first():
|
||||||
|
raise PermissionsViolation()
|
||||||
|
|
||||||
msg = '{} {}'.format(
|
msg = '{} {}'.format(
|
||||||
'Created' if object_created else 'Modified',
|
'Created' if object_created else 'Modified',
|
||||||
@ -295,7 +296,7 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
else:
|
else:
|
||||||
return redirect(self.get_return_url(request, obj))
|
return redirect(self.get_return_url(request, obj))
|
||||||
|
|
||||||
except ObjectDoesNotExist:
|
except PermissionsViolation:
|
||||||
msg = "Object save failed due to object-level permissions violation"
|
msg = "Object save failed due to object-level permissions violation"
|
||||||
logger.debug(msg)
|
logger.debug(msg)
|
||||||
form.add_error(None, msg)
|
form.add_error(None, msg)
|
||||||
@ -457,7 +458,7 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
|
|
||||||
# Enforce object-level permissions
|
# Enforce object-level permissions
|
||||||
if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
|
if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
|
||||||
raise ObjectDoesNotExist
|
raise PermissionsViolation
|
||||||
|
|
||||||
# If we make it to this point, validation has succeeded on all new objects.
|
# If we make it to this point, validation has succeeded on all new objects.
|
||||||
msg = "Added {} {}".format(len(new_objs), model._meta.verbose_name_plural)
|
msg = "Added {} {}".format(len(new_objs), model._meta.verbose_name_plural)
|
||||||
@ -471,7 +472,7 @@ class BulkCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
except ObjectDoesNotExist:
|
except PermissionsViolation:
|
||||||
msg = "Object creation failed due to object-level permissions violation"
|
msg = "Object creation failed due to object-level permissions violation"
|
||||||
logger.debug(msg)
|
logger.debug(msg)
|
||||||
form.add_error(None, msg)
|
form.add_error(None, msg)
|
||||||
@ -542,7 +543,8 @@ class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
obj = model_form.save()
|
obj = model_form.save()
|
||||||
|
|
||||||
# Enforce object-level permissions
|
# Enforce object-level permissions
|
||||||
self.queryset.get(pk=obj.pk)
|
if not self.queryset.filter(pk=obj.pk).first():
|
||||||
|
raise PermissionsViolation()
|
||||||
|
|
||||||
logger.debug(f"Created {obj} (PK: {obj.pk})")
|
logger.debug(f"Created {obj} (PK: {obj.pk})")
|
||||||
|
|
||||||
@ -578,7 +580,7 @@ class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
except AbortTransaction:
|
except AbortTransaction:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
except ObjectDoesNotExist:
|
except PermissionsViolation:
|
||||||
msg = "Object creation failed due to object-level permissions violation"
|
msg = "Object creation failed due to object-level permissions violation"
|
||||||
logger.debug(msg)
|
logger.debug(msg)
|
||||||
form.add_error(None, msg)
|
form.add_error(None, msg)
|
||||||
@ -689,7 +691,7 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
|
|
||||||
# Enforce object-level permissions
|
# Enforce object-level permissions
|
||||||
if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
|
if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
|
||||||
raise ObjectDoesNotExist
|
raise PermissionsViolation
|
||||||
|
|
||||||
# Compile a table containing the imported objects
|
# Compile a table containing the imported objects
|
||||||
obj_table = self.table(new_objs)
|
obj_table = self.table(new_objs)
|
||||||
@ -707,7 +709,7 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
except ValidationError:
|
except ValidationError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
except ObjectDoesNotExist:
|
except PermissionsViolation:
|
||||||
msg = "Object import failed due to object-level permissions violation"
|
msg = "Object import failed due to object-level permissions violation"
|
||||||
logger.debug(msg)
|
logger.debug(msg)
|
||||||
form.add_error(None, msg)
|
form.add_error(None, msg)
|
||||||
@ -822,7 +824,7 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
|
|
||||||
# Enforce object-level permissions
|
# Enforce object-level permissions
|
||||||
if self.queryset.filter(pk__in=[obj.pk for obj in updated_objects]).count() != len(updated_objects):
|
if self.queryset.filter(pk__in=[obj.pk for obj in updated_objects]).count() != len(updated_objects):
|
||||||
raise ObjectDoesNotExist
|
raise PermissionsViolation
|
||||||
|
|
||||||
if updated_objects:
|
if updated_objects:
|
||||||
msg = 'Updated {} {}'.format(len(updated_objects), model._meta.verbose_name_plural)
|
msg = 'Updated {} {}'.format(len(updated_objects), model._meta.verbose_name_plural)
|
||||||
@ -834,7 +836,7 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
messages.error(self.request, "{} failed validation: {}".format(obj, e))
|
messages.error(self.request, "{} failed validation: {}".format(obj, e))
|
||||||
|
|
||||||
except ObjectDoesNotExist:
|
except PermissionsViolation:
|
||||||
msg = "Object update failed due to object-level permissions violation"
|
msg = "Object update failed due to object-level permissions violation"
|
||||||
logger.debug(msg)
|
logger.debug(msg)
|
||||||
form.add_error(None, msg)
|
form.add_error(None, msg)
|
||||||
@ -929,7 +931,7 @@ class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
|
|
||||||
# Enforce constrained permissions
|
# Enforce constrained permissions
|
||||||
if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects):
|
if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects):
|
||||||
raise ObjectDoesNotExist
|
raise PermissionsViolation
|
||||||
|
|
||||||
messages.success(request, "Renamed {} {}".format(
|
messages.success(request, "Renamed {} {}".format(
|
||||||
len(selected_objects),
|
len(selected_objects),
|
||||||
@ -937,7 +939,7 @@ class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
|||||||
))
|
))
|
||||||
return redirect(self.get_return_url(request))
|
return redirect(self.get_return_url(request))
|
||||||
|
|
||||||
except ObjectDoesNotExist:
|
except PermissionsViolation:
|
||||||
msg = "Object update failed due to object-level permissions violation"
|
msg = "Object update failed due to object-level permissions violation"
|
||||||
logger.debug(msg)
|
logger.debug(msg)
|
||||||
form.add_error(None, msg)
|
form.add_error(None, msg)
|
||||||
@ -1142,7 +1144,7 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
|
|||||||
|
|
||||||
# Enforce object-level permissions
|
# Enforce object-level permissions
|
||||||
if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
|
if self.queryset.filter(pk__in=[obj.pk for obj in new_objs]).count() != len(new_objs):
|
||||||
raise ObjectDoesNotExist
|
raise PermissionsViolation
|
||||||
|
|
||||||
messages.success(request, "Added {} {}".format(
|
messages.success(request, "Added {} {}".format(
|
||||||
len(new_components), self.queryset.model._meta.verbose_name_plural
|
len(new_components), self.queryset.model._meta.verbose_name_plural
|
||||||
@ -1150,7 +1152,7 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
|
|||||||
# Return the newly created objects so overridden post methods can use the data as needed.
|
# Return the newly created objects so overridden post methods can use the data as needed.
|
||||||
return new_objs
|
return new_objs
|
||||||
|
|
||||||
except ObjectDoesNotExist:
|
except PermissionsViolation:
|
||||||
msg = "Component creation failed due to object-level permissions violation"
|
msg = "Component creation failed due to object-level permissions violation"
|
||||||
logger.debug(msg)
|
logger.debug(msg)
|
||||||
form.add_error(None, msg)
|
form.add_error(None, msg)
|
||||||
@ -1227,12 +1229,12 @@ class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin,
|
|||||||
|
|
||||||
# Enforce object-level permissions
|
# Enforce object-level permissions
|
||||||
if self.queryset.filter(pk__in=[obj.pk for obj in new_components]).count() != len(new_components):
|
if self.queryset.filter(pk__in=[obj.pk for obj in new_components]).count() != len(new_components):
|
||||||
raise ObjectDoesNotExist
|
raise PermissionsViolation
|
||||||
|
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
except ObjectDoesNotExist:
|
except PermissionsViolation:
|
||||||
msg = "Component creation failed due to object-level permissions violation"
|
msg = "Component creation failed due to object-level permissions violation"
|
||||||
logger.debug(msg)
|
logger.debug(msg)
|
||||||
form.add_error(None, msg)
|
form.add_error(None, msg)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{# Base layout for the core NetBox UI w/navbar and page content #}
|
{# Base layout for the core NetBox UI w/navbar and page content #}
|
||||||
{% extends 'base/base.html' %}
|
{% extends 'base/base.html' %}
|
||||||
|
{% load helpers %}
|
||||||
{% load nav %}
|
{% load nav %}
|
||||||
{% load search_options %}
|
{% load search_options %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
@ -115,7 +116,7 @@
|
|||||||
<div class="row align-items-center justify-content-end mx-0">
|
<div class="row align-items-center justify-content-end mx-0">
|
||||||
<div class="col-auto d-none d-md-block"></div>
|
<div class="col-auto d-none d-md-block"></div>
|
||||||
<div class="col text-center small text-muted">
|
<div class="col text-center small text-muted">
|
||||||
<span class="fw-light d-block d-md-inline">{% now 'Y-m-d H:i:s T' %}</span>
|
<span class="fw-light d-block d-md-inline">{% annotated_now %} {% now 'T' %}</span>
|
||||||
<span class="ms-md-3 d-block d-md-inline">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</span>
|
<span class="ms-md-3 d-block d-md-inline">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Install Date</th>
|
<th scope="row">Install Date</th>
|
||||||
<td>{{ object.install_date|placeholder }}</td>
|
<td>{{ object.install_date|annotated_date|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Commit Rate</th>
|
<th scope="row">Commit Rate</th>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{% extends 'base/layout.html' %}
|
{% extends 'base/layout.html' %}
|
||||||
|
{% load helpers %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Create {{ component_type }}{% endblock %}
|
{% block title %}Create {{ component_type }}{% endblock %}
|
||||||
@ -12,7 +13,37 @@
|
|||||||
<div class="col col-md-8">
|
<div class="col col-md-8">
|
||||||
<div class="field-group">
|
<div class="field-group">
|
||||||
<h4>{{ component_type|title }}</h4>
|
<h4>{{ component_type|title }}</h4>
|
||||||
{% render_form form %}
|
{% if form.non_field_errors %}
|
||||||
|
<div class="panel panel-danger">
|
||||||
|
<div class="panel-heading"><strong>Errors</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>{{ component_type|bettertitle }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% for field in form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
|
{% for field in form.visible_fields %}
|
||||||
|
{% if field.name not in form.custom_fields %}
|
||||||
|
{% render_field field %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if form.custom_fields %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_custom_fields form %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,6 +35,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
{% if object.mgmt_only %}
|
||||||
|
<span class="badge bg-success">Management Only</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">Not Management Only</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Device</th>
|
<th scope="row">Device</th>
|
||||||
<td>
|
<td>
|
||||||
|
@ -278,7 +278,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ resv.description }}<br />
|
{{ resv.description }}<br />
|
||||||
<small>{{ resv.user }} · {{ resv.created }}</small>
|
<small>{{ resv.user }} · {{ resv.created|annotated_date }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end noprint">
|
<td class="text-end noprint">
|
||||||
{% if perms.dcim.change_rackreservation %}
|
{% if perms.dcim.change_rackreservation %}
|
||||||
|
@ -68,7 +68,7 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if object.time_zone %}
|
{% if object.time_zone %}
|
||||||
{{ object.time_zone }} (UTC {{ object.time_zone|tzoffset }})<br />
|
{{ object.time_zone }} (UTC {{ object.time_zone|tzoffset }})<br />
|
||||||
<small class="text-muted">Site time: {% timezone object.time_zone %}{% now "SHORT_DATETIME_FORMAT" %}{% endtimezone %}</small>
|
<small class="text-muted">Site time: {% timezone object.time_zone %}{% annotated_now %}{% endtimezone %}</small>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">—</span>
|
<span class="text-muted">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Created</th>
|
<th scope="row">Created</th>
|
||||||
<td>
|
<td>
|
||||||
{{ object.created }}
|
{{ object.created|annotated_date }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Time</th>
|
<th scope="row">Time</th>
|
||||||
<td>
|
<td>
|
||||||
{{ object.time }}
|
{{ object.time|annotated_date }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
{% if report.result %}
|
{% if report.result %}
|
||||||
Last run: <a href="{% url 'extras:report_result' job_result_pk=report.result.pk %}">
|
Last run: <a href="{% url 'extras:report_result' job_result_pk=report.result.pk %}">
|
||||||
<strong>{{ report.result.created }}</strong>
|
<strong>{{ report.result.created|annotated_date }}</strong>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
<td>{{ report.description|render_markdown|placeholder }}</td>
|
<td>{{ report.description|render_markdown|placeholder }}</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
{% if report.result %}
|
{% if report.result %}
|
||||||
<a href="{% url 'extras:report_result' job_result_pk=report.result.pk %}">{{ report.result.created }}</a>
|
<a href="{% url 'extras:report_result' job_result_pk=report.result.pk %}">{{ report.result.created|annotated_date }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Never</span>
|
<span class="text-muted">Never</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<p>
|
<p>
|
||||||
Run: <strong>{{ result.created }}</strong>
|
Run: <strong>{{ result.created|annotated_date }}</strong>
|
||||||
{% if result.completed %}
|
{% if result.completed %}
|
||||||
Duration: <strong>{{ result.duration }}</strong>
|
Duration: <strong>{{ result.duration }}</strong>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
<td>{{ script.Meta.description|render_markdown }}</td>
|
<td>{{ script.Meta.description|render_markdown }}</td>
|
||||||
{% if script.result %}
|
{% if script.result %}
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<a href="{% url 'extras:script_result' job_result_pk=script.result.pk %}">{{ script.result.created }}</a>
|
<a href="{% url 'extras:script_result' job_result_pk=script.result.pk %}">{{ script.result.created|annotated_date }}</a>
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td class="text-end text-muted">Never</td>
|
<td class="text-end text-muted">Never</td>
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">Scripts</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}">Scripts</a></li>
|
||||||
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module.{{ script.module }}">{{ script.module|bettertitle }}</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'extras:script_list' %}#module.{{ script.module }}">{{ script.module|bettertitle }}</a></li>
|
||||||
<li class="breadcrumb-item"><a href="{% url 'extras:script' module=script.module name=class_name %}">{{ script }}</a></li>
|
<li class="breadcrumb-item"><a href="{% url 'extras:script' module=script.module name=class_name %}">{{ script }}</a></li>
|
||||||
<li class="breadcrumb-item">{{ result.created }}</li>
|
<li class="breadcrumb-item">{{ result.created|annotated_date }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@ -38,7 +38,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content my-3">
|
<div class="tab-content my-3">
|
||||||
<p>
|
<p>
|
||||||
Run: <strong>{{ result.created }}</strong>
|
Run: <strong>{{ result.created|annotated_date }}</strong>
|
||||||
{% if result.completed %}
|
{% if result.completed %}
|
||||||
Duration: <strong>{{ result.duration }}</strong>
|
Duration: <strong>{{ result.duration }}</strong>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -72,8 +72,8 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<p>
|
<p>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
Created {{ object.created }} ·
|
Created {{ object.created|annotated_date }} ·
|
||||||
Updated <span title="{{ object.last_updated }}">{{ object.last_updated|timesince }}</span> ago
|
Updated <span title="{{ object.last_updated|annotated_date }}">{{ object.last_updated|timesince }}</span> ago
|
||||||
</small>
|
</small>
|
||||||
<span class="badge bg-primary">{{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}</span>
|
<span class="badge bg-primary">{{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
|
||||||
{% if images %}
|
{% if images %}
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
@ -14,7 +15,7 @@
|
|||||||
<a class="image-preview" href="{{ attachment.image.url }}" target="_blank">{{ attachment }}</a>
|
<a class="image-preview" href="{{ attachment.image.url }}" target="_blank">{{ attachment }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ attachment.size|filesizeformat }}</td>
|
<td>{{ attachment.size|filesizeformat }}</td>
|
||||||
<td>{{ attachment.created }}</td>
|
<td>{{ attachment.created|annotated_date }}</td>
|
||||||
<td class="text-end noprint">
|
<td class="text-end noprint">
|
||||||
{% if perms.extras.change_imageattachment %}
|
{% if perms.extras.change_imageattachment %}
|
||||||
<a href="{% url 'extras:imageattachment_edit' pk=attachment.pk %}" class="btn btn-warning btn-sm lh-1" title="Edit Image">
|
<a href="{% url 'extras:imageattachment_edit' pk=attachment.pk %}" class="btn btn-warning btn-sm lh-1" title="Edit Image">
|
||||||
|
@ -53,7 +53,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Date Added</td>
|
<td>Date Added</td>
|
||||||
<td>{{ object.date_added|placeholder }}</td>
|
<td>{{ object.date_added|annotated_date|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Description</td>
|
<td>Description</td>
|
||||||
|
@ -24,12 +24,12 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-4">
|
<div class="col col-md-4">
|
||||||
<small class="text-muted">Created</small><br />
|
<small class="text-muted">Created</small><br />
|
||||||
<span title="{{ token.created }}">{{ token.created|date }}</span>
|
{{ token.created|annotated_date }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-4">
|
<div class="col col-md-4">
|
||||||
<small class="text-muted">Expires</small><br />
|
<small class="text-muted">Expires</small><br />
|
||||||
{% if token.expires %}
|
{% if token.expires %}
|
||||||
<span title="{{ token.expires }}">{{ token.expires|date }}</span>
|
{{ token.expires|annotated_date }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span>Never</span>
|
<span>Never</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<small class="text-muted">Email</small>
|
<small class="text-muted">Email</small>
|
||||||
<h5>{{ request.user.email }}</h5>
|
<h5>{{ request.user.email }}</h5>
|
||||||
<small class="text-muted">Registered</small>
|
<small class="text-muted">Registered</small>
|
||||||
<h5>{{ request.user.date_joined }}</h5>
|
<h5>{{ request.user.date_joined|annotated_date }}</h5>
|
||||||
<small class="text-muted">Groups</small>
|
<small class="text-muted">Groups</small>
|
||||||
<h5>
|
<h5>
|
||||||
{% if request.user.groups.all %}
|
{% if request.user.groups.all %}
|
||||||
|
@ -142,7 +142,7 @@
|
|||||||
<th scope="row"><i class="mdi mdi-chip"></i> Memory</th>
|
<th scope="row"><i class="mdi mdi-chip"></i> Memory</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.memory %}
|
{% if object.memory %}
|
||||||
{{ object.memory }} MB
|
{{ object.memory|humanize_megabytes }} MB
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">—</span>
|
<span class="text-muted">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
{% extends 'base/layout.html' %}
|
|
||||||
{% load helpers %}
|
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Create {{ component_type }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<form action="" method="post" class="form form-horizontal">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col col-md-6">
|
|
||||||
<div class="field-group">
|
|
||||||
<h4>{{ component_type|bettertitle }}</h4>
|
|
||||||
{% render_form form %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col col-md-6">
|
|
||||||
<div class="float-end">
|
|
||||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
|
||||||
<button type="submit" name="_addanother" class="btn btn-outline-primary">Create & Add More</button>
|
|
||||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
@ -11,7 +11,8 @@ FILTER_CHAR_BASED_LOOKUP_MAP = dict(
|
|||||||
isw='istartswith',
|
isw='istartswith',
|
||||||
nisw='istartswith',
|
nisw='istartswith',
|
||||||
ie='iexact',
|
ie='iexact',
|
||||||
nie='iexact'
|
nie='iexact',
|
||||||
|
empty='empty',
|
||||||
)
|
)
|
||||||
|
|
||||||
FILTER_NUMERIC_BASED_LOOKUP_MAP = dict(
|
FILTER_NUMERIC_BASED_LOOKUP_MAP = dict(
|
||||||
|
@ -9,6 +9,14 @@ class AbortTransaction(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionsViolation(Exception):
|
||||||
|
"""
|
||||||
|
Raised when an operation was prevented because it would violate the
|
||||||
|
allowed permissions.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RQWorkerNotRunningException(APIException):
|
class RQWorkerNotRunningException(APIException):
|
||||||
"""
|
"""
|
||||||
Indicates the temporary inability to enqueue a new task (e.g. custom script execution) because no RQ worker
|
Indicates the temporary inability to enqueue a new task (e.g. custom script execution) because no RQ worker
|
||||||
|
@ -6,7 +6,9 @@ from typing import Dict, Any
|
|||||||
import yaml
|
import yaml
|
||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.template.defaultfilters import date
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
@ -130,6 +132,20 @@ def humanize_speed(speed):
|
|||||||
return '{} Kbps'.format(speed)
|
return '{} Kbps'.format(speed)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter()
|
||||||
|
def humanize_megabytes(mb):
|
||||||
|
"""
|
||||||
|
Express a number of megabytes in the most suitable unit (e.g. gigabytes or terabytes).
|
||||||
|
"""
|
||||||
|
if not mb:
|
||||||
|
return ''
|
||||||
|
if mb >= 1048576:
|
||||||
|
return f'{int(mb / 1048576)} TB'
|
||||||
|
if mb >= 1024:
|
||||||
|
return f'{int(mb / 1024)} GB'
|
||||||
|
return f'{mb} MB'
|
||||||
|
|
||||||
|
|
||||||
@register.filter()
|
@register.filter()
|
||||||
def tzoffset(value):
|
def tzoffset(value):
|
||||||
"""
|
"""
|
||||||
@ -138,6 +154,36 @@ def tzoffset(value):
|
|||||||
return datetime.datetime.now(value).strftime('%z')
|
return datetime.datetime.now(value).strftime('%z')
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(expects_localtime=True)
|
||||||
|
def annotated_date(date_value):
|
||||||
|
"""
|
||||||
|
Returns date as HTML span with short date format as the content and the
|
||||||
|
(long) date format as the title.
|
||||||
|
"""
|
||||||
|
if not date_value:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
if type(date_value) == datetime.date:
|
||||||
|
long_ts = date(date_value, 'DATE_FORMAT')
|
||||||
|
short_ts = date(date_value, 'SHORT_DATE_FORMAT')
|
||||||
|
else:
|
||||||
|
long_ts = date(date_value, 'DATETIME_FORMAT')
|
||||||
|
short_ts = date(date_value, 'SHORT_DATETIME_FORMAT')
|
||||||
|
|
||||||
|
span = f'<span title="{long_ts}">{short_ts}</span>'
|
||||||
|
|
||||||
|
return mark_safe(span)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def annotated_now():
|
||||||
|
"""
|
||||||
|
Returns the current date piped through the annotated_date filter.
|
||||||
|
"""
|
||||||
|
tzinfo = timezone.get_current_timezone() if settings.USE_TZ else None
|
||||||
|
return annotated_date(datetime.datetime.now(tz=tzinfo))
|
||||||
|
|
||||||
|
|
||||||
@register.filter()
|
@register.filter()
|
||||||
def fgcolor(value):
|
def fgcolor(value):
|
||||||
"""
|
"""
|
||||||
|
@ -8,17 +8,18 @@ from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
|
|||||||
from dcim.forms import InterfaceCommonForm, INTERFACE_MODE_HELP_TEXT
|
from dcim.forms import InterfaceCommonForm, INTERFACE_MODE_HELP_TEXT
|
||||||
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
|
||||||
from extras.forms import (
|
from extras.forms import (
|
||||||
AddRemoveTagsForm, CustomFieldModelBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldModelFilterForm,
|
AddRemoveTagsForm, CustomFieldModelBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm,
|
||||||
|
CustomFieldModelFilterForm, CustomFieldsMixin,
|
||||||
)
|
)
|
||||||
from extras.models import Tag
|
from extras.models import Tag
|
||||||
from ipam.models import IPAddress, VLAN
|
from ipam.models import IPAddress, VLAN
|
||||||
from tenancy.forms import TenancyFilterForm, TenancyForm
|
from tenancy.forms import TenancyFilterForm, TenancyForm
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, BulkRenameForm, CommentField,
|
add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, BulkRenameForm, CommentField, ConfirmationForm,
|
||||||
ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField,
|
CSVChoiceField, CSVModelChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField,
|
||||||
DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, SlugField, SmallTextarea,
|
form_from_model, JSONField, SlugField, SmallTextarea, StaticSelect2, StaticSelect2Multiple, TagFilterField,
|
||||||
StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
@ -667,7 +668,8 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm)
|
|||||||
self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)
|
self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)
|
||||||
|
|
||||||
|
|
||||||
class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm):
|
class VMInterfaceCreateForm(BootstrapMixin, CustomFieldsMixin, InterfaceCommonForm):
|
||||||
|
model = VMInterface
|
||||||
virtual_machine = DynamicModelChoiceField(
|
virtual_machine = DynamicModelChoiceField(
|
||||||
queryset=VirtualMachine.objects.all()
|
queryset=VirtualMachine.objects.all()
|
||||||
)
|
)
|
||||||
@ -730,7 +732,7 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm):
|
|||||||
self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)
|
self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)
|
||||||
|
|
||||||
|
|
||||||
class VMInterfaceCSVForm(CSVModelForm):
|
class VMInterfaceCSVForm(CustomFieldModelCSVForm):
|
||||||
virtual_machine = CSVModelChoiceField(
|
virtual_machine = CSVModelChoiceField(
|
||||||
queryset=VirtualMachine.objects.all(),
|
queryset=VirtualMachine.objects.all(),
|
||||||
to_field_name='name'
|
to_field_name='name'
|
||||||
@ -755,7 +757,7 @@ class VMInterfaceCSVForm(CSVModelForm):
|
|||||||
return self.cleaned_data['enabled']
|
return self.cleaned_data['enabled']
|
||||||
|
|
||||||
|
|
||||||
class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=VMInterface.objects.all(),
|
queryset=VMInterface.objects.all(),
|
||||||
widget=forms.MultipleHiddenInput()
|
widget=forms.MultipleHiddenInput()
|
||||||
|
@ -33,6 +33,9 @@ class ClusterTypeView(generic.ObjectView):
|
|||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
clusters = Cluster.objects.restrict(request.user, 'view').filter(
|
clusters = Cluster.objects.restrict(request.user, 'view').filter(
|
||||||
type=instance
|
type=instance
|
||||||
|
).annotate(
|
||||||
|
device_count=count_related(Device, 'cluster'),
|
||||||
|
vm_count=count_related(VirtualMachine, 'cluster')
|
||||||
)
|
)
|
||||||
|
|
||||||
clusters_table = tables.ClusterTable(clusters)
|
clusters_table = tables.ClusterTable(clusters)
|
||||||
@ -92,6 +95,9 @@ class ClusterGroupView(generic.ObjectView):
|
|||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
clusters = Cluster.objects.restrict(request.user, 'view').filter(
|
clusters = Cluster.objects.restrict(request.user, 'view').filter(
|
||||||
group=instance
|
group=instance
|
||||||
|
).annotate(
|
||||||
|
device_count=count_related(Device, 'cluster'),
|
||||||
|
vm_count=count_related(VirtualMachine, 'cluster')
|
||||||
)
|
)
|
||||||
|
|
||||||
clusters_table = tables.ClusterTable(clusters)
|
clusters_table = tables.ClusterTable(clusters)
|
||||||
@ -450,7 +456,7 @@ class VMInterfaceCreateView(generic.ComponentCreateView):
|
|||||||
queryset = VMInterface.objects.all()
|
queryset = VMInterface.objects.all()
|
||||||
form = forms.VMInterfaceCreateForm
|
form = forms.VMInterfaceCreateForm
|
||||||
model_form = forms.VMInterfaceForm
|
model_form = forms.VMInterfaceForm
|
||||||
template_name = 'virtualization/virtualmachine_component_add.html'
|
template_name = 'dcim/device_component_add.html'
|
||||||
|
|
||||||
|
|
||||||
class VMInterfaceEditView(generic.ObjectEditView):
|
class VMInterfaceEditView(generic.ObjectEditView):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Django==3.2.4
|
Django==3.2.5
|
||||||
django-cacheops==6.0
|
django-cacheops==6.0
|
||||||
django-cors-headers==3.7.0
|
django-cors-headers==3.7.0
|
||||||
django-debug-toolbar==3.2.1
|
django-debug-toolbar==3.2.1
|
||||||
@ -9,7 +9,7 @@ django-pglocks==1.0.4
|
|||||||
django-prometheus==2.1.0
|
django-prometheus==2.1.0
|
||||||
django-rq==2.4.1
|
django-rq==2.4.1
|
||||||
django-tables2==2.4.0
|
django-tables2==2.4.0
|
||||||
django-taggit==1.4.0
|
django-taggit==1.5.1
|
||||||
django-timezone-field==4.1.2
|
django-timezone-field==4.1.2
|
||||||
djangorestframework==3.12.4
|
djangorestframework==3.12.4
|
||||||
drf-yasg[validation]==1.20.0
|
drf-yasg[validation]==1.20.0
|
||||||
@ -18,8 +18,8 @@ gunicorn==20.1.0
|
|||||||
Jinja2==3.0.1
|
Jinja2==3.0.1
|
||||||
Markdown==3.3.4
|
Markdown==3.3.4
|
||||||
netaddr==0.8.0
|
netaddr==0.8.0
|
||||||
Pillow==8.2.0
|
Pillow==8.3.0
|
||||||
psycopg2-binary==2.9
|
psycopg2-binary==2.9.1
|
||||||
pycryptodome==3.10.1
|
pycryptodome==3.10.1
|
||||||
PyYAML==5.4.1
|
PyYAML==5.4.1
|
||||||
svgwrite==1.4.1
|
svgwrite==1.4.1
|
||||||
|
Loading…
Reference in New Issue
Block a user