mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
Merge branch 'develop' into 10201-cable-terminations
This commit is contained in:
commit
c8546256f1
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.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: v3.4.0
|
placeholder: v3.4.1
|
||||||
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: v3.4.0
|
placeholder: v3.4.1
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -12,6 +12,17 @@ BASE_PATH = 'netbox/'
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## DEFAULT_LANGUAGE
|
||||||
|
|
||||||
|
Default: `en-us` (US English)
|
||||||
|
|
||||||
|
Defines the default preferred language/locale for requests that do not specify one. This is used to alter e.g. the display of dates and numbers to fit the user's locale. See [this list](http://www.i18nguy.com/unicode/language-identifiers.html) of standard language codes. (This parameter maps to Django's [`LANGUAGE_CODE`](https://docs.djangoproject.com/en/stable/ref/settings/#language-code) internal setting.)
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Altering this parameter will *not* change the language used in NetBox. We hope to provide translation support in a future NetBox release.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## DOCS_ROOT
|
## DOCS_ROOT
|
||||||
|
|
||||||
Default: `$INSTALL_ROOT/docs/`
|
Default: `$INSTALL_ROOT/docs/`
|
||||||
|
@ -1,6 +1,35 @@
|
|||||||
# NetBox v3.4
|
# NetBox v3.4
|
||||||
|
|
||||||
## v3.4.1 (FUTURE)
|
## v3.4.2 (FUTURE)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#9285](https://github.com/netbox-community/netbox/issues/9285) - Enable specifying assigned component during bulk import of inventory items
|
||||||
|
* [#10700](https://github.com/netbox-community/netbox/issues/10700) - Match device name when using modules quick search
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#11290](https://github.com/netbox-community/netbox/issues/11290) - Correct reporting of scheduled job duration
|
||||||
|
* [#11232](https://github.com/netbox-community/netbox/issues/11232) - Enable partial & regular expression matching for non-string types in global search
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v3.4.1 (2022-12-16)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#9971](https://github.com/netbox-community/netbox/issues/9971) - Enable ordering of nested group models by name
|
||||||
|
* [#11214](https://github.com/netbox-community/netbox/issues/11214) - Introduce the `DEFAULT_LANGUAGE` configuration parameter
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#11175](https://github.com/netbox-community/netbox/issues/11175) - Fix cloning of fields containing special characters
|
||||||
|
* [#11178](https://github.com/netbox-community/netbox/issues/11178) - Pressing enter in quick search box should not trigger bulk operations
|
||||||
|
* [#11184](https://github.com/netbox-community/netbox/issues/11184) - Correct visualization of cable path which splits across multiple circuit terminations
|
||||||
|
* [#11185](https://github.com/netbox-community/netbox/issues/11185) - Fix TemplateSyntaxError when viewing custom script results
|
||||||
|
* [#11189](https://github.com/netbox-community/netbox/issues/11189) - Fix localization of dates & numbers
|
||||||
|
* [#11205](https://github.com/netbox-community/netbox/issues/11205) - Correct cloning behavior for recursively-nested models
|
||||||
|
* [#11206](https://github.com/netbox-community/netbox/issues/11206) - Avoid clearing assigned groups if `REMOTE_AUTH_DEFAULT_GROUPS` is invalid
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1098,6 +1098,7 @@ class ModuleFilterSet(NetBoxModelFilterSet):
|
|||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
|
Q(device__name__icontains=value.strip()) |
|
||||||
Q(serial__icontains=value.strip()) |
|
Q(serial__icontains=value.strip()) |
|
||||||
Q(asset_tag__icontains=value.strip()) |
|
Q(asset_tag__icontains=value.strip()) |
|
||||||
Q(comments__icontains=value)
|
Q(comments__icontains=value)
|
||||||
|
@ -885,12 +885,22 @@ class InventoryItemImportForm(NetBoxModelImportForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text=_('Parent inventory item')
|
help_text=_('Parent inventory item')
|
||||||
)
|
)
|
||||||
|
component_type = CSVContentTypeField(
|
||||||
|
queryset=ContentType.objects.all(),
|
||||||
|
limit_choices_to=MODULAR_COMPONENT_MODELS,
|
||||||
|
required=False,
|
||||||
|
help_text=_('Component Type')
|
||||||
|
)
|
||||||
|
component_name = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
help_text=_('Component Name')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
fields = (
|
fields = (
|
||||||
'device', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
'device', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
|
||||||
'description', 'tags'
|
'description', 'tags', 'component_type', 'component_name',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -908,6 +918,24 @@ class InventoryItemImportForm(NetBoxModelImportForm):
|
|||||||
else:
|
else:
|
||||||
self.fields['parent'].queryset = InventoryItem.objects.none()
|
self.fields['parent'].queryset = InventoryItem.objects.none()
|
||||||
|
|
||||||
|
def clean_component_name(self):
|
||||||
|
content_type = self.cleaned_data.get('component_type')
|
||||||
|
component_name = self.cleaned_data.get('component_name')
|
||||||
|
device = self.cleaned_data.get("device")
|
||||||
|
|
||||||
|
if not device and hasattr(self, 'instance'):
|
||||||
|
device = self.instance.device
|
||||||
|
|
||||||
|
if not all([device, content_type, component_name]):
|
||||||
|
return None
|
||||||
|
|
||||||
|
model = content_type.model_class()
|
||||||
|
try:
|
||||||
|
component = model.objects.get(device=device, name=component_name)
|
||||||
|
self.instance.component = component
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise forms.ValidationError(f"Component not found: {device} - {component_name}")
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device component roles
|
# Device component roles
|
||||||
|
@ -568,11 +568,12 @@ class CablePath(models.Model):
|
|||||||
|
|
||||||
elif isinstance(remote_terminations[0], CircuitTermination):
|
elif isinstance(remote_terminations[0], CircuitTermination):
|
||||||
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
|
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
|
||||||
term_side = remote_terminations[0].term_side
|
if len(remote_terminations) > 1:
|
||||||
assert all(ct.term_side == term_side for ct in remote_terminations[1:])
|
is_split = True
|
||||||
|
break
|
||||||
circuit_termination = CircuitTermination.objects.filter(
|
circuit_termination = CircuitTermination.objects.filter(
|
||||||
circuit=remote_terminations[0].circuit,
|
circuit=remote_terminations[0].circuit,
|
||||||
term_side='Z' if term_side == 'A' else 'A'
|
term_side='Z' if remote_terminations[0].term_side == 'A' else 'A'
|
||||||
).first()
|
).first()
|
||||||
if circuit_termination is None:
|
if circuit_termination is None:
|
||||||
break
|
break
|
||||||
@ -686,6 +687,7 @@ class CablePath(models.Model):
|
|||||||
"""
|
"""
|
||||||
Return all available next segments in a split cable path.
|
Return all available next segments in a split cable path.
|
||||||
"""
|
"""
|
||||||
|
from circuits.models import CircuitTermination
|
||||||
nodes = self.path_objects[-1]
|
nodes = self.path_objects[-1]
|
||||||
|
|
||||||
# RearPort splitting to multiple FrontPorts with no stack position
|
# RearPort splitting to multiple FrontPorts with no stack position
|
||||||
@ -695,3 +697,8 @@ class CablePath(models.Model):
|
|||||||
# RearPorts connected to different cables
|
# RearPorts connected to different cables
|
||||||
elif type(nodes[0]) is FrontPort:
|
elif type(nodes[0]) is FrontPort:
|
||||||
return RearPort.objects.filter(pk__in=[fp.rear_port_id for fp in nodes])
|
return RearPort.objects.filter(pk__in=[fp.rear_port_id for fp in nodes])
|
||||||
|
# Cable terminating to multiple CircuitTerminations
|
||||||
|
elif type(nodes[0]) is CircuitTermination:
|
||||||
|
return [
|
||||||
|
ct.get_peer_termination() for ct in nodes
|
||||||
|
]
|
||||||
|
@ -506,6 +506,9 @@ class BaseInterfaceTable(NetBoxTable):
|
|||||||
verbose_name='Tagged VLANs'
|
verbose_name='Tagged VLANs'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def value_ip_addresses(self, value):
|
||||||
|
return ",".join([str(obj.address) for obj in value.all()])
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
|
class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
|
||||||
device = tables.Column(
|
device = tables.Column(
|
||||||
|
@ -651,7 +651,12 @@ class JobResult(models.Model):
|
|||||||
if not self.completed:
|
if not self.completed:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
duration = self.completed - self.created
|
start_time = self.started or self.created
|
||||||
|
|
||||||
|
if not start_time:
|
||||||
|
return None
|
||||||
|
|
||||||
|
duration = self.completed - start_time
|
||||||
minutes, seconds = divmod(duration.total_seconds(), 60)
|
minutes, seconds = divmod(duration.total_seconds(), 60)
|
||||||
|
|
||||||
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
|
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
|
||||||
|
@ -33,6 +33,9 @@ class FHRPGroupTable(NetBoxTable):
|
|||||||
url_name='ipam:fhrpgroup_list'
|
url_name='ipam:fhrpgroup_list'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def value_ip_addresses(self, value):
|
||||||
|
return ",".join([str(obj.address) for obj in value.all()])
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = FHRPGroup
|
model = FHRPGroup
|
||||||
fields = (
|
fields = (
|
||||||
|
@ -382,5 +382,4 @@ def user_default_groups_handler(backend, user, response, *args, **kwargs):
|
|||||||
if group_list:
|
if group_list:
|
||||||
user.groups.add(*group_list)
|
user.groups.add(*group_list)
|
||||||
else:
|
else:
|
||||||
user.groups.clear()
|
logger.info(f"No valid group assignments for {user} - REMOTE_AUTH_DEFAULT_GROUPS may be incorrectly set?")
|
||||||
logger.debug(f"Stripping user {user} from Groups")
|
|
||||||
|
@ -106,6 +106,9 @@ CORS_ORIGIN_REGEX_WHITELIST = [
|
|||||||
# on a production system.
|
# on a production system.
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
|
# Set the default preferred language/locale
|
||||||
|
DEFAULT_LANGUAGE = 'en-us'
|
||||||
|
|
||||||
# Email settings
|
# Email settings
|
||||||
EMAIL = {
|
EMAIL = {
|
||||||
'SERVER': 'localhost',
|
'SERVER': 'localhost',
|
||||||
|
@ -75,7 +75,7 @@ class PrimaryModel(NetBoxModel):
|
|||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
|
class NestedGroupModel(CloningMixin, NetBoxFeatureSet, MPTTModel):
|
||||||
"""
|
"""
|
||||||
Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
|
Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
|
||||||
recursively using MPTT. Within each parent, each child instance must have a unique name.
|
recursively using MPTT. Within each parent, each child instance must have a unique name.
|
||||||
|
@ -99,8 +99,8 @@ class CachedValueSearchBackend(SearchBackend):
|
|||||||
params = {
|
params = {
|
||||||
f'value__{lookup}': value
|
f'value__{lookup}': value
|
||||||
}
|
}
|
||||||
if lookup != LookupTypes.EXACT:
|
if lookup in (LookupTypes.STARTSWITH, LookupTypes.ENDSWITH):
|
||||||
# Partial matches are valid only on string values
|
# Partial string matches are valid only on string values
|
||||||
params['type'] = FieldTypes.STRING
|
params['type'] = FieldTypes.STRING
|
||||||
if object_types:
|
if object_types:
|
||||||
params['object_type__in'] = object_types
|
params['object_type__in'] = object_types
|
||||||
|
@ -24,7 +24,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '3.4.1-dev'
|
VERSION = '3.4.2-dev'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
@ -94,6 +94,7 @@ FIELD_CHOICES = getattr(configuration, 'FIELD_CHOICES', {})
|
|||||||
HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None)
|
HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', None)
|
||||||
INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
|
INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
|
||||||
JINJA2_FILTERS = getattr(configuration, 'JINJA2_FILTERS', {})
|
JINJA2_FILTERS = getattr(configuration, 'JINJA2_FILTERS', {})
|
||||||
|
LANGUAGE_CODE = getattr(configuration, 'DEFAULT_LANGUAGE', 'en-us')
|
||||||
LOGGING = getattr(configuration, 'LOGGING', {})
|
LOGGING = getattr(configuration, 'LOGGING', {})
|
||||||
LOGIN_PERSISTENCE = getattr(configuration, 'LOGIN_PERSISTENCE', False)
|
LOGIN_PERSISTENCE = getattr(configuration, 'LOGIN_PERSISTENCE', False)
|
||||||
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
|
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
|
||||||
@ -339,6 +340,7 @@ MIDDLEWARE = [
|
|||||||
'django_prometheus.middleware.PrometheusBeforeMiddleware',
|
'django_prometheus.middleware.PrometheusBeforeMiddleware',
|
||||||
'corsheaders.middleware.CorsMiddleware',
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
@ -385,9 +387,6 @@ AUTHENTICATION_BACKENDS = [
|
|||||||
'netbox.authentication.ObjectPermissionBackend',
|
'netbox.authentication.ObjectPermissionBackend',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Internationalization
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
|
||||||
|
|
||||||
# Time zones
|
# Time zones
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
@ -537,14 +537,15 @@ class MPTTColumn(tables.TemplateColumn):
|
|||||||
"""
|
"""
|
||||||
template_code = """
|
template_code = """
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% for i in record.level|as_range %}<i class="mdi mdi-circle-small"></i>{% endfor %}
|
{% if not table.order_by %}
|
||||||
|
{% for i in record.level|as_range %}<i class="mdi mdi-circle-small"></i>{% endfor %}
|
||||||
|
{% endif %}
|
||||||
<a href="{{ record.get_absolute_url }}">{{ record.name }}</a>
|
<a href="{{ record.get_absolute_url }}">{{ record.name }}</a>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
template_code=self.template_code,
|
template_code=self.template_code,
|
||||||
orderable=False,
|
|
||||||
attrs={'td': {'class': 'text-nowrap'}},
|
attrs={'td': {'class': 'text-nowrap'}},
|
||||||
*args,
|
*args,
|
||||||
**kwargs
|
**kwargs
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
{% load humanize %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load log_levels %}
|
{% load log_levels %}
|
||||||
|
|
||||||
|
@ -70,6 +70,9 @@ Context:
|
|||||||
{% applied_filters model filter_form request.GET %}
|
{% applied_filters model filter_form request.GET %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{# Object table controls #}
|
||||||
|
{% include 'inc/table_controls_htmx.html' with table_modal="ObjectTable_config" %}
|
||||||
|
|
||||||
<form method="post" class="form form-horizontal">
|
<form method="post" class="form form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{# "Select all" form #}
|
{# "Select all" form #}
|
||||||
@ -96,9 +99,6 @@ Context:
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Object table controls #}
|
|
||||||
{% include 'inc/table_controls_htmx.html' with table_modal="ObjectTable_config" %}
|
|
||||||
|
|
||||||
<div class="form form-horizontal">
|
<div class="form form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
|
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
|
||||||
|
@ -55,6 +55,37 @@
|
|||||||
{% plugin_left_page object %}
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Allocated Resources</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover attr-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><i class="mdi mdi-gauge"></i> Virtual CPUs</th>
|
||||||
|
<td>{{ vcpus_sum|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><i class="mdi mdi-chip"></i> Memory</th>
|
||||||
|
<td>
|
||||||
|
{% if memory_sum %}
|
||||||
|
{{ memory_sum|humanize_megabytes }}
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><i class="mdi mdi-harddisk"></i> Disk Space</th>
|
||||||
|
<td>
|
||||||
|
{% if disk_sum %}
|
||||||
|
{{ disk_sum }} GB
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/tags.html' %}
|
{% include 'inc/panels/tags.html' %}
|
||||||
{% include 'inc/panels/contacts.html' %}
|
{% include 'inc/panels/contacts.html' %}
|
||||||
|
@ -19,6 +19,7 @@ from dcim.choices import CableLengthUnitChoices, WeightUnitChoices
|
|||||||
from extras.plugins import PluginConfig
|
from extras.plugins import PluginConfig
|
||||||
from extras.utils import is_taggable
|
from extras.utils import is_taggable
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
|
from urllib.parse import urlencode
|
||||||
from utilities.constants import HTTP_REQUEST_META_SAFE_COPY
|
from utilities.constants import HTTP_REQUEST_META_SAFE_COPY
|
||||||
|
|
||||||
|
|
||||||
@ -353,7 +354,7 @@ def prepare_cloned_fields(instance):
|
|||||||
params.append((key, ''))
|
params.append((key, ''))
|
||||||
|
|
||||||
# Return a QueryDict with the parameters
|
# Return a QueryDict with the parameters
|
||||||
return QueryDict('&'.join([f'{k}={v}' for k, v in params]), mutable=True)
|
return QueryDict(urlencode(params), mutable=True)
|
||||||
|
|
||||||
|
|
||||||
def shallow_compare_dict(source_dict, destination_dict, exclude=None):
|
def shallow_compare_dict(source_dict, destination_dict, exclude=None):
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch, Sum
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
@ -169,6 +169,9 @@ class ClusterListView(generic.ObjectListView):
|
|||||||
class ClusterView(generic.ObjectView):
|
class ClusterView(generic.ObjectView):
|
||||||
queryset = Cluster.objects.all()
|
queryset = Cluster.objects.all()
|
||||||
|
|
||||||
|
def get_extra_context(self, request, instance):
|
||||||
|
return instance.virtual_machines.aggregate(vcpus_sum=Sum('vcpus'), memory_sum=Sum('memory'), disk_sum=Sum('disk'))
|
||||||
|
|
||||||
|
|
||||||
@register_model_view(Cluster, 'virtualmachines', path='virtual-machines')
|
@register_model_view(Cluster, 'virtualmachines', path='virtual-machines')
|
||||||
class ClusterVirtualMachinesView(generic.ObjectChildrenView):
|
class ClusterVirtualMachinesView(generic.ObjectChildrenView):
|
||||||
|
Loading…
Reference in New Issue
Block a user