mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-15 08:12:18 -06:00
Compare commits
15 Commits
v4.3.3
...
2a27e475e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a27e475e4 | ||
|
|
44efa037cc | ||
|
|
6c17629159 | ||
|
|
f13d028c98 | ||
|
|
f5d32b1bf1 | ||
|
|
f05897d61a | ||
|
|
b5421f1cd6 | ||
|
|
9c2cd66162 | ||
|
|
f61a2964c8 | ||
|
|
ee94fb0b94 | ||
|
|
8fb8f4c75b | ||
|
|
e33793dc82 | ||
|
|
3b8841ee3b | ||
|
|
ea4c205a37 | ||
|
|
2a5d3abafb |
@@ -6,7 +6,7 @@
|
|||||||
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
|
<a href="https://github.com/netbox-community/netbox/graphs/contributors"><img src="https://img.shields.io/github/contributors/netbox-community/netbox?color=blue" alt="Contributors" /></a>
|
||||||
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
|
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
|
||||||
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
|
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
|
||||||
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=main" alt="CI status" /></a>
|
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/actions/workflows/ci.yml/badge.svg" alt="CI status" /></a>
|
||||||
<p>
|
<p>
|
||||||
<strong><a href="https://netboxlabs.com/community/">NetBox Community</a></strong> |
|
<strong><a href="https://netboxlabs.com/community/">NetBox Community</a></strong> |
|
||||||
<strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> |
|
<strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> |
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ For UI development you will need to review the [Web UI Development Guide](web-ui
|
|||||||
|
|
||||||
## Populating Demo Data
|
## Populating Demo Data
|
||||||
|
|
||||||
Once you have your development environment up and running, it might be helpful to populate some "dummy" data to make interacting with the UI and APIs more convenient. Check out the [netbox-demo-data](https://github.com/netbox-community/netbox-demo-data) repo on GitHub, which houses a collection of sample data that can be easily imported to any new NetBox deployment. (This sample data is used to populate the public demo instance at <https://demo.netbox.dev>.)
|
Once you have your development environment up and running, it might be helpful to populate some "dummy" data to make interacting with the UI and APIs more convenient. Check out the [netbox-demo-data](https://github.com/netbox-community/netbox-demo-data) repo on GitHub, which houses a collection of sample data that can be easily imported to any new NetBox deployment. This sample data is used to populate the [public demo instance](https://demo.netbox.dev).
|
||||||
|
|
||||||
The demo data is provided in JSON format and loaded into an empty database using Django's `loaddata` management command. Consult the demo data repo's `README` file for complete instructions on populating the data.
|
The demo data is provided in JSON format and loaded into an empty database using Django's `loaddata` management command. Consult the demo data repo's `README` file for complete instructions on populating the data.
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
NetBox includes the ability to execute certain functions as background tasks. These include:
|
NetBox includes the ability to execute certain functions as background tasks. These include:
|
||||||
|
|
||||||
* [Report](../customization/reports.md) execution
|
|
||||||
* [Custom script](../customization/custom-scripts.md) execution
|
* [Custom script](../customization/custom-scripts.md) execution
|
||||||
* Synchronization of [remote data sources](../integrations/synchronized-data.md)
|
* Synchronization of [remote data sources](../integrations/synchronized-data.md)
|
||||||
|
* Housekeeping tasks
|
||||||
|
|
||||||
Additionally, NetBox plugins can enqueue their own background tasks. This is accomplished using the [Job model](../models/core/job.md). Background tasks are executed by the `rqworker` process(es).
|
Additionally, NetBox plugins can enqueue their own background tasks. This is accomplished using the [Job model](../models/core/job.md). Background tasks are executed by the `rqworker` process(es).
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ Check out the desired release by specifying its tag. For example:
|
|||||||
|
|
||||||
```
|
```
|
||||||
cd /opt/netbox && \
|
cd /opt/netbox && \
|
||||||
sudo git fetch && \
|
sudo git fetch --tags && \
|
||||||
sudo git checkout v4.2.7
|
sudo git checkout v4.2.7
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 24 KiB |
@@ -15,7 +15,6 @@ A background job implements a basic [Job](../../models/core/job.md) executor for
|
|||||||
```python title="jobs.py"
|
```python title="jobs.py"
|
||||||
from netbox.jobs import JobRunner
|
from netbox.jobs import JobRunner
|
||||||
|
|
||||||
|
|
||||||
class MyTestJob(JobRunner):
|
class MyTestJob(JobRunner):
|
||||||
class Meta:
|
class Meta:
|
||||||
name = "My Test Job"
|
name = "My Test Job"
|
||||||
@@ -25,6 +24,8 @@ class MyTestJob(JobRunner):
|
|||||||
# your logic goes here
|
# your logic goes here
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Completed jobs will have their status updated to "completed" by default, or "errored" if an unhandled exception was raised by the `run()` method. To intentionally mark a job as failed, raise the `core.exceptions.JobFailed` exception. (Note that "failed" differs from "errored" in that a failure may be expected under certain conditions, whereas an error is not.)
|
||||||
|
|
||||||
You can schedule the background job from within your code (e.g. from a model's `save()` method or a view) by calling `MyTestJob.enqueue()`. This method passes through all arguments to `Job.enqueue()`. However, no `name` argument must be passed, as the background job name will be used instead.
|
You can schedule the background job from within your code (e.g. from a model's `save()` method or a view) by calling `MyTestJob.enqueue()`. This method passes through all arguments to `Job.enqueue()`. However, no `name` argument must be passed, as the background job name will be used instead.
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
class SyncError(Exception):
|
'IncompatiblePluginError',
|
||||||
pass
|
'JobFailed',
|
||||||
|
'SyncError',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class IncompatiblePluginError(ImproperlyConfigured):
|
class IncompatiblePluginError(ImproperlyConfigured):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class JobFailed(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SyncError(Exception):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -187,15 +187,14 @@ class Job(models.Model):
|
|||||||
"""
|
"""
|
||||||
Mark the job as completed, optionally specifying a particular termination status.
|
Mark the job as completed, optionally specifying a particular termination status.
|
||||||
"""
|
"""
|
||||||
valid_statuses = JobStatusChoices.TERMINAL_STATE_CHOICES
|
if status not in JobStatusChoices.TERMINAL_STATE_CHOICES:
|
||||||
if status not in valid_statuses:
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
_("Invalid status for job termination. Choices are: {choices}").format(
|
_("Invalid status for job termination. Choices are: {choices}").format(
|
||||||
choices=', '.join(valid_statuses)
|
choices=', '.join(JobStatusChoices.TERMINAL_STATE_CHOICES)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mark the job as completed
|
# Set the job's status and completion time
|
||||||
self.status = status
|
self.status = status
|
||||||
if error:
|
if error:
|
||||||
self.error = error
|
self.error = error
|
||||||
|
|||||||
@@ -470,8 +470,8 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = [
|
fields = [
|
||||||
'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'comments',
|
'manufacturer', 'model', 'part_number', 'description', 'airflow', 'weight', 'weight_unit', 'profile',
|
||||||
'tags',
|
'comments', 'tags'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ if TYPE_CHECKING:
|
|||||||
from tenancy.graphql.types import TenantType
|
from tenancy.graphql.types import TenantType
|
||||||
from users.graphql.types import UserType
|
from users.graphql.types import UserType
|
||||||
from virtualization.graphql.types import ClusterType, VMInterfaceType, VirtualMachineType
|
from virtualization.graphql.types import ClusterType, VMInterfaceType, VirtualMachineType
|
||||||
|
from vpn.graphql.types import L2VPNTerminationType
|
||||||
from wireless.graphql.types import WirelessLANType, WirelessLinkType
|
from wireless.graphql.types import WirelessLANType, WirelessLinkType
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@@ -440,6 +441,7 @@ class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, P
|
|||||||
primary_mac_address: Annotated["MACAddressType", strawberry.lazy('dcim.graphql.types')] | None
|
primary_mac_address: Annotated["MACAddressType", strawberry.lazy('dcim.graphql.types')] | None
|
||||||
qinq_svlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
qinq_svlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||||
vlan_translation_policy: Annotated["VLANTranslationPolicyType", strawberry.lazy('ipam.graphql.types')] | None
|
vlan_translation_policy: Annotated["VLANTranslationPolicyType", strawberry.lazy('ipam.graphql.types')] | None
|
||||||
|
l2vpn_termination: Annotated["L2VPNTerminationType", strawberry.lazy('vpn.graphql.types')] | None
|
||||||
|
|
||||||
vdcs: List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]]
|
vdcs: List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
tagged_vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
|
tagged_vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ def load_initial_data(apps, schema_editor):
|
|||||||
'gpu',
|
'gpu',
|
||||||
'hard_disk',
|
'hard_disk',
|
||||||
'memory',
|
'memory',
|
||||||
'power_supply'
|
'power_supply',
|
||||||
|
'expansion_card'
|
||||||
)
|
)
|
||||||
|
|
||||||
for name in initial_profiles:
|
for name in initial_profiles:
|
||||||
|
|||||||
44
netbox/dcim/migrations/0208_devicerole_uniqueness.py
Normal file
44
netbox/dcim/migrations/0208_devicerole_uniqueness.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0207_remove_redundant_indexes'),
|
||||||
|
('extras', '0129_fix_script_paths'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='devicerole',
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=('parent', 'name'),
|
||||||
|
name='dcim_devicerole_parent_name'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='devicerole',
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(('parent__isnull', True)),
|
||||||
|
fields=('name',),
|
||||||
|
name='dcim_devicerole_name',
|
||||||
|
violation_error_message='A top-level device role with this name already exists.'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='devicerole',
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=('parent', 'slug'),
|
||||||
|
name='dcim_devicerole_parent_slug'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='devicerole',
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(('parent__isnull', True)),
|
||||||
|
fields=('slug',),
|
||||||
|
name='dcim_devicerole_slug',
|
||||||
|
violation_error_message='A top-level device role with this slug already exists.'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "Expansion card",
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"connector_type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Connector type e.g. PCIe x4"
|
||||||
|
},
|
||||||
|
"bandwidth": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Total Bandwidth for this module"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -398,6 +398,28 @@ class DeviceRole(NestedGroupModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
|
constraints = (
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('parent', 'name'),
|
||||||
|
name='%(app_label)s_%(class)s_parent_name'
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('name',),
|
||||||
|
name='%(app_label)s_%(class)s_name',
|
||||||
|
condition=Q(parent__isnull=True),
|
||||||
|
violation_error_message=_("A top-level device role with this name already exists.")
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('parent', 'slug'),
|
||||||
|
name='%(app_label)s_%(class)s_parent_slug'
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('slug',),
|
||||||
|
name='%(app_label)s_%(class)s_slug',
|
||||||
|
condition=Q(parent__isnull=True),
|
||||||
|
violation_error_message=_("A top-level device role with this slug already exists.")
|
||||||
|
),
|
||||||
|
)
|
||||||
verbose_name = _('device role')
|
verbose_name = _('device role')
|
||||||
verbose_name_plural = _('device roles')
|
verbose_name_plural = _('device roles')
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from decimal import Decimal
|
|||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from django.test import override_settings
|
from django.test import override_settings, tag
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from netaddr import EUI
|
from netaddr import EUI
|
||||||
|
|
||||||
@@ -1000,18 +1000,7 @@ inventory-items:
|
|||||||
self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
|
self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
|
||||||
|
|
||||||
|
|
||||||
# TODO: Change base class to PrimaryObjectViewTestCase
|
class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
# Blocked by absence of bulk import view for ModuleTypes
|
|
||||||
class ModuleTypeTestCase(
|
|
||||||
ViewTestCases.GetObjectViewTestCase,
|
|
||||||
ViewTestCases.GetObjectChangelogViewTestCase,
|
|
||||||
ViewTestCases.CreateObjectViewTestCase,
|
|
||||||
ViewTestCases.EditObjectViewTestCase,
|
|
||||||
ViewTestCases.DeleteObjectViewTestCase,
|
|
||||||
ViewTestCases.ListObjectsViewTestCase,
|
|
||||||
ViewTestCases.BulkEditObjectsViewTestCase,
|
|
||||||
ViewTestCases.BulkDeleteObjectsViewTestCase
|
|
||||||
):
|
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1023,7 +1012,7 @@ class ModuleTypeTestCase(
|
|||||||
)
|
)
|
||||||
Manufacturer.objects.bulk_create(manufacturers)
|
Manufacturer.objects.bulk_create(manufacturers)
|
||||||
|
|
||||||
ModuleType.objects.bulk_create([
|
module_types = ModuleType.objects.bulk_create([
|
||||||
ModuleType(model='Module Type 1', manufacturer=manufacturers[0]),
|
ModuleType(model='Module Type 1', manufacturer=manufacturers[0]),
|
||||||
ModuleType(model='Module Type 2', manufacturer=manufacturers[0]),
|
ModuleType(model='Module Type 2', manufacturer=manufacturers[0]),
|
||||||
ModuleType(model='Module Type 3', manufacturer=manufacturers[0]),
|
ModuleType(model='Module Type 3', manufacturer=manufacturers[0]),
|
||||||
@@ -1031,6 +1020,8 @@ class ModuleTypeTestCase(
|
|||||||
|
|
||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
|
fan_module_type_profile = ModuleTypeProfile.objects.get(name='Fan')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'manufacturer': manufacturers[1].pk,
|
'manufacturer': manufacturers[1].pk,
|
||||||
'model': 'Device Type X',
|
'model': 'Device Type X',
|
||||||
@@ -1044,6 +1035,70 @@ class ModuleTypeTestCase(
|
|||||||
'part_number': '456DEF',
|
'part_number': '456DEF',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"manufacturer,model,part_number,comments,profile",
|
||||||
|
f"Manufacturer 1,fan0,generic-fan,,{fan_module_type_profile.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.csv_update_data = (
|
||||||
|
"id,model",
|
||||||
|
f"{module_types[0].id},test model",
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
def test_bulk_update_objects_with_permission(self):
|
||||||
|
self.add_permissions(
|
||||||
|
'dcim.add_consoleporttemplate',
|
||||||
|
'dcim.add_consoleserverporttemplate',
|
||||||
|
'dcim.add_powerporttemplate',
|
||||||
|
'dcim.add_poweroutlettemplate',
|
||||||
|
'dcim.add_interfacetemplate',
|
||||||
|
'dcim.add_frontporttemplate',
|
||||||
|
'dcim.add_rearporttemplate',
|
||||||
|
'dcim.add_modulebaytemplate',
|
||||||
|
)
|
||||||
|
|
||||||
|
# run base test
|
||||||
|
super().test_bulk_update_objects_with_permission()
|
||||||
|
|
||||||
|
@tag('regression')
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
|
||||||
|
def test_bulk_import_objects_with_permission(self):
|
||||||
|
self.add_permissions(
|
||||||
|
'dcim.add_consoleporttemplate',
|
||||||
|
'dcim.add_consoleserverporttemplate',
|
||||||
|
'dcim.add_powerporttemplate',
|
||||||
|
'dcim.add_poweroutlettemplate',
|
||||||
|
'dcim.add_interfacetemplate',
|
||||||
|
'dcim.add_frontporttemplate',
|
||||||
|
'dcim.add_rearporttemplate',
|
||||||
|
'dcim.add_modulebaytemplate',
|
||||||
|
)
|
||||||
|
|
||||||
|
# run base test
|
||||||
|
super().test_bulk_import_objects_with_permission()
|
||||||
|
|
||||||
|
# TODO: remove extra regression asserts once parent test supports testing all import fields
|
||||||
|
fan_module_type = ModuleType.objects.get(part_number='generic-fan')
|
||||||
|
fan_module_type_profile = ModuleTypeProfile.objects.get(name='Fan')
|
||||||
|
|
||||||
|
assert fan_module_type.profile == fan_module_type_profile
|
||||||
|
|
||||||
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
|
||||||
|
def test_bulk_import_objects_with_constrained_permission(self):
|
||||||
|
self.add_permissions(
|
||||||
|
'dcim.add_consoleporttemplate',
|
||||||
|
'dcim.add_consoleserverporttemplate',
|
||||||
|
'dcim.add_powerporttemplate',
|
||||||
|
'dcim.add_poweroutlettemplate',
|
||||||
|
'dcim.add_interfacetemplate',
|
||||||
|
'dcim.add_frontporttemplate',
|
||||||
|
'dcim.add_rearporttemplate',
|
||||||
|
'dcim.add_modulebaytemplate',
|
||||||
|
)
|
||||||
|
|
||||||
|
super().test_bulk_import_objects_with_constrained_permission()
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
def test_moduletype_consoleports(self):
|
def test_moduletype_consoleports(self):
|
||||||
moduletype = ModuleType.objects.first()
|
moduletype = ModuleType.objects.first()
|
||||||
@@ -1804,9 +1859,9 @@ class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"name,slug,color",
|
"name,slug,color",
|
||||||
"Device Role 4,device-role-4,ff0000",
|
"Device Role 6,device-role-6,ff0000",
|
||||||
"Device Role 5,device-role-5,00ff00",
|
"Device Role 7,device-role-7,00ff00",
|
||||||
"Device Role 6,device-role-6,0000ff",
|
"Device Role 8,device-role-8,0000ff",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.csv_update_data = (
|
cls.csv_update_data = (
|
||||||
|
|||||||
@@ -18,9 +18,22 @@ class Empty(Lookup):
|
|||||||
return f"CAST(LENGTH({sql}) AS BOOLEAN) IS TRUE", params
|
return f"CAST(LENGTH({sql}) AS BOOLEAN) IS TRUE", params
|
||||||
|
|
||||||
|
|
||||||
|
class NetHost(Lookup):
|
||||||
|
"""
|
||||||
|
Similar to ipam.lookups.NetHost, but casts the field to INET.
|
||||||
|
"""
|
||||||
|
lookup_name = 'net_host'
|
||||||
|
|
||||||
|
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 'HOST(CAST(%s AS INET)) = HOST(%s)' % (lhs, rhs), params
|
||||||
|
|
||||||
|
|
||||||
class NetContainsOrEquals(Lookup):
|
class NetContainsOrEquals(Lookup):
|
||||||
"""
|
"""
|
||||||
This lookup has the same functionality as the one from the ipam app except lhs is cast to inet
|
Similar to ipam.lookups.NetContainsOrEquals, but casts the field to INET.
|
||||||
"""
|
"""
|
||||||
lookup_name = 'net_contains_or_equals'
|
lookup_name = 'net_contains_or_equals'
|
||||||
|
|
||||||
@@ -32,4 +45,5 @@ class NetContainsOrEquals(Lookup):
|
|||||||
|
|
||||||
|
|
||||||
CharField.register_lookup(Empty)
|
CharField.register_lookup(Empty)
|
||||||
|
CachedValueField.register_lookup(NetHost)
|
||||||
CachedValueField.register_lookup(NetContainsOrEquals)
|
CachedValueField.register_lookup(NetContainsOrEquals)
|
||||||
|
|||||||
@@ -162,6 +162,11 @@ class Aggregate(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
|||||||
return self.prefix.version
|
return self.prefix.version
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ipv6_full(self):
|
||||||
|
if self.prefix and self.prefix.version == 6:
|
||||||
|
return netaddr.IPAddress(self.prefix).format(netaddr.ipv6_full)
|
||||||
|
|
||||||
def get_child_prefixes(self):
|
def get_child_prefixes(self):
|
||||||
"""
|
"""
|
||||||
Return all Prefixes within this Aggregate
|
Return all Prefixes within this Aggregate
|
||||||
@@ -330,6 +335,11 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
|||||||
def mask_length(self):
|
def mask_length(self):
|
||||||
return self.prefix.prefixlen if self.prefix else None
|
return self.prefix.prefixlen if self.prefix else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ipv6_full(self):
|
||||||
|
if self.prefix and self.prefix.version == 6:
|
||||||
|
return netaddr.IPAddress(self.prefix).format(netaddr.ipv6_full)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def depth(self):
|
def depth(self):
|
||||||
return self._depth
|
return self._depth
|
||||||
@@ -808,6 +818,11 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
|||||||
self._original_assigned_object_id = self.__dict__.get('assigned_object_id')
|
self._original_assigned_object_id = self.__dict__.get('assigned_object_id')
|
||||||
self._original_assigned_object_type_id = self.__dict__.get('assigned_object_type_id')
|
self._original_assigned_object_type_id = self.__dict__.get('assigned_object_type_id')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ipv6_full(self):
|
||||||
|
if self.address and self.address.version == 6:
|
||||||
|
return netaddr.IPAddress(self.address).format(netaddr.ipv6_full)
|
||||||
|
|
||||||
def get_duplicates(self):
|
def get_duplicates(self):
|
||||||
return IPAddress.objects.filter(
|
return IPAddress.objects.filter(
|
||||||
vrf=self.vrf,
|
vrf=self.vrf,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from django_pglocks import advisory_lock
|
|||||||
from rq.timeouts import JobTimeoutException
|
from rq.timeouts import JobTimeoutException
|
||||||
|
|
||||||
from core.choices import JobStatusChoices
|
from core.choices import JobStatusChoices
|
||||||
|
from core.exceptions import JobFailed
|
||||||
from core.models import Job, ObjectType
|
from core.models import Job, ObjectType
|
||||||
from netbox.constants import ADVISORY_LOCK_KEYS
|
from netbox.constants import ADVISORY_LOCK_KEYS
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
@@ -73,15 +74,21 @@ class JobRunner(ABC):
|
|||||||
This method is called by the Job Scheduler to handle the execution of all job commands. It will maintain the
|
This method is called by the Job Scheduler to handle the execution of all job commands. It will maintain the
|
||||||
job's metadata and handle errors. For periodic jobs, a new job is automatically scheduled using its `interval`.
|
job's metadata and handle errors. For periodic jobs, a new job is automatically scheduled using its `interval`.
|
||||||
"""
|
"""
|
||||||
|
logger = logging.getLogger('netbox.jobs')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
job.start()
|
job.start()
|
||||||
cls(job).run(*args, **kwargs)
|
cls(job).run(*args, **kwargs)
|
||||||
job.terminate()
|
job.terminate()
|
||||||
|
|
||||||
|
except JobFailed:
|
||||||
|
logger.warning(f"Job {job} failed")
|
||||||
|
job.terminate(status=JobStatusChoices.STATUS_FAILED)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
|
job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
|
||||||
if type(e) is JobTimeoutException:
|
if type(e) is JobTimeoutException:
|
||||||
logging.error(e)
|
logger.error(e)
|
||||||
|
|
||||||
# If the executed job is a periodic job, schedule its next execution at the specified interval.
|
# If the executed job is a periodic job, schedule its next execution at the specified interval.
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Sequence, Optional
|
from typing import Sequence, Optional
|
||||||
|
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'get_model_item',
|
'get_model_item',
|
||||||
@@ -22,20 +24,46 @@ class MenuItemButton:
|
|||||||
link: str
|
link: str
|
||||||
title: str
|
title: str
|
||||||
icon_class: str
|
icon_class: str
|
||||||
|
_url: Optional[str] = None
|
||||||
permissions: Optional[Sequence[str]] = ()
|
permissions: Optional[Sequence[str]] = ()
|
||||||
color: Optional[str] = None
|
color: Optional[str] = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.link:
|
||||||
|
self._url = reverse_lazy(self.link)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return self._url
|
||||||
|
|
||||||
|
@url.setter
|
||||||
|
def url(self, value):
|
||||||
|
self._url = value
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MenuItem:
|
class MenuItem:
|
||||||
|
|
||||||
link: str
|
link: str
|
||||||
link_text: str
|
link_text: str
|
||||||
|
_url: Optional[str] = None
|
||||||
permissions: Optional[Sequence[str]] = ()
|
permissions: Optional[Sequence[str]] = ()
|
||||||
auth_required: Optional[bool] = False
|
auth_required: Optional[bool] = False
|
||||||
staff_only: Optional[bool] = False
|
staff_only: Optional[bool] = False
|
||||||
buttons: Optional[Sequence[MenuItemButton]] = ()
|
buttons: Optional[Sequence[MenuItemButton]] = ()
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.link:
|
||||||
|
self._url = reverse_lazy(self.link)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return self._url
|
||||||
|
|
||||||
|
@url.setter
|
||||||
|
def url(self, value):
|
||||||
|
self._url = value
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MenuGroup:
|
class MenuGroup:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from django.urls import reverse_lazy
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
@@ -32,17 +33,23 @@ class PluginMenuItem:
|
|||||||
This class represents a navigation menu item. This constitutes primary link and its text, but also allows for
|
This class represents a navigation menu item. This constitutes primary link and its text, but also allows for
|
||||||
specifying additional link buttons that appear to the right of the item in the van menu.
|
specifying additional link buttons that appear to the right of the item in the van menu.
|
||||||
|
|
||||||
Links are specified as Django reverse URL strings.
|
Links are specified as Django reverse URL strings suitable for rendering via {% url item.link %}.
|
||||||
|
Alternatively, a pre-generated url can be set on the object which will be rendered literally.
|
||||||
Buttons are each specified as a list of PluginMenuButton instances.
|
Buttons are each specified as a list of PluginMenuButton instances.
|
||||||
"""
|
"""
|
||||||
permissions = []
|
permissions = []
|
||||||
buttons = []
|
buttons = []
|
||||||
|
_url = None
|
||||||
|
|
||||||
def __init__(self, link, link_text, auth_required=False, staff_only=False, permissions=None, buttons=None):
|
def __init__(
|
||||||
|
self, link, link_text, auth_required=False, staff_only=False, permissions=None, buttons=None
|
||||||
|
):
|
||||||
self.link = link
|
self.link = link
|
||||||
self.link_text = link_text
|
self.link_text = link_text
|
||||||
self.auth_required = auth_required
|
self.auth_required = auth_required
|
||||||
self.staff_only = staff_only
|
self.staff_only = staff_only
|
||||||
|
if link:
|
||||||
|
self._url = reverse_lazy(link)
|
||||||
if permissions is not None:
|
if permissions is not None:
|
||||||
if type(permissions) not in (list, tuple):
|
if type(permissions) not in (list, tuple):
|
||||||
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
||||||
@@ -52,6 +59,14 @@ class PluginMenuItem:
|
|||||||
raise TypeError(_("Buttons must be passed as a tuple or list."))
|
raise TypeError(_("Buttons must be passed as a tuple or list."))
|
||||||
self.buttons = buttons
|
self.buttons = buttons
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return self._url
|
||||||
|
|
||||||
|
@url.setter
|
||||||
|
def url(self, value):
|
||||||
|
self._url = value
|
||||||
|
|
||||||
|
|
||||||
class PluginMenuButton:
|
class PluginMenuButton:
|
||||||
"""
|
"""
|
||||||
@@ -60,11 +75,14 @@ class PluginMenuButton:
|
|||||||
"""
|
"""
|
||||||
color = ButtonColorChoices.DEFAULT
|
color = ButtonColorChoices.DEFAULT
|
||||||
permissions = []
|
permissions = []
|
||||||
|
_url = None
|
||||||
|
|
||||||
def __init__(self, link, title, icon_class, color=None, permissions=None):
|
def __init__(self, link, title, icon_class, color=None, permissions=None):
|
||||||
self.link = link
|
self.link = link
|
||||||
self.title = title
|
self.title = title
|
||||||
self.icon_class = icon_class
|
self.icon_class = icon_class
|
||||||
|
if link:
|
||||||
|
self._url = reverse_lazy(link)
|
||||||
if permissions is not None:
|
if permissions is not None:
|
||||||
if type(permissions) not in (list, tuple):
|
if type(permissions) not in (list, tuple):
|
||||||
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
raise TypeError(_("Permissions must be passed as a tuple or list."))
|
||||||
@@ -73,3 +91,11 @@ class PluginMenuButton:
|
|||||||
if color not in ButtonColorChoices.values():
|
if color not in ButtonColorChoices.values():
|
||||||
raise ValueError(_("Button color must be a choice within ButtonColorChoices."))
|
raise ValueError(_("Button color must be a choice within ButtonColorChoices."))
|
||||||
self.color = color
|
self.color = color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return self._url
|
||||||
|
|
||||||
|
@url.setter
|
||||||
|
def url(self, value):
|
||||||
|
self._url = value
|
||||||
|
|||||||
@@ -115,11 +115,13 @@ class CachedValueSearchBackend(SearchBackend):
|
|||||||
if lookup in (LookupTypes.STARTSWITH, LookupTypes.ENDSWITH):
|
if lookup in (LookupTypes.STARTSWITH, LookupTypes.ENDSWITH):
|
||||||
# "Starts/ends with" matches are valid only on string values
|
# "Starts/ends with" matches are valid only on string values
|
||||||
query_filter &= Q(type=FieldTypes.STRING)
|
query_filter &= Q(type=FieldTypes.STRING)
|
||||||
elif lookup == LookupTypes.PARTIAL:
|
elif lookup in (LookupTypes.PARTIAL, LookupTypes.EXACT):
|
||||||
try:
|
try:
|
||||||
# If the value looks like an IP address, add an extra match for CIDR values
|
# If the value looks like an IP address, add extra filters for CIDR/INET values
|
||||||
address = str(netaddr.IPNetwork(value.strip()).cidr)
|
address = str(netaddr.IPNetwork(value.strip()).cidr)
|
||||||
query_filter |= Q(type=FieldTypes.CIDR) & Q(value__net_contains_or_equals=address)
|
query_filter |= Q(type=FieldTypes.INET) & Q(value__net_host=address)
|
||||||
|
if lookup == LookupTypes.PARTIAL:
|
||||||
|
query_filter |= Q(type=FieldTypes.CIDR) & Q(value__net_contains_or_equals=address)
|
||||||
except (AddrFormatError, ValueError):
|
except (AddrFormatError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ from django_rq import get_queue
|
|||||||
from ..jobs import *
|
from ..jobs import *
|
||||||
from core.models import DataSource, Job
|
from core.models import DataSource, Job
|
||||||
from core.choices import JobStatusChoices
|
from core.choices import JobStatusChoices
|
||||||
|
from core.exceptions import JobFailed
|
||||||
|
from utilities.testing import disable_warnings
|
||||||
|
|
||||||
|
|
||||||
class TestJobRunner(JobRunner):
|
class TestJobRunner(JobRunner):
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
pass
|
if kwargs.get('make_fail', False):
|
||||||
|
raise JobFailed()
|
||||||
|
|
||||||
|
|
||||||
class JobRunnerTestCase(TestCase):
|
class JobRunnerTestCase(TestCase):
|
||||||
@@ -49,6 +53,12 @@ class JobRunnerTest(JobRunnerTestCase):
|
|||||||
|
|
||||||
self.assertEqual(job.status, JobStatusChoices.STATUS_COMPLETED)
|
self.assertEqual(job.status, JobStatusChoices.STATUS_COMPLETED)
|
||||||
|
|
||||||
|
def test_handle_failed(self):
|
||||||
|
with disable_warnings('netbox.jobs'):
|
||||||
|
job = TestJobRunner.enqueue(immediate=True, make_fail=True)
|
||||||
|
|
||||||
|
self.assertEqual(job.status, JobStatusChoices.STATUS_FAILED)
|
||||||
|
|
||||||
def test_handle_errored(self):
|
def test_handle_errored(self):
|
||||||
class ErroredJobRunner(TestJobRunner):
|
class ErroredJobRunner(TestJobRunner):
|
||||||
EXP = Exception('Test error')
|
EXP = Exception('Test error')
|
||||||
|
|||||||
2
netbox/project-static/dist/netbox.css
vendored
2
netbox/project-static/dist/netbox.css
vendored
File diff suppressed because one or more lines are too long
4
netbox/project-static/styles/custom/racks.scss
Normal file
4
netbox/project-static/styles/custom/racks.scss
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.rack-loading-container {
|
||||||
|
min-height: 200px;
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
@@ -27,3 +27,4 @@
|
|||||||
@import 'custom/markdown';
|
@import 'custom/markdown';
|
||||||
@import 'custom/misc';
|
@import 'custom/misc';
|
||||||
@import 'custom/notifications';
|
@import 'custom/notifications';
|
||||||
|
@import 'custom/racks';
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div style="margin-left: -30px">
|
<div style="margin-left: -30px">
|
||||||
<object data="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}" class="rack_elevation" aria-label="{% trans "Rack elevation" %}"></object>
|
<div
|
||||||
|
hx-get="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{ face }}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}"
|
||||||
|
hx-trigger="intersect"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
aria-label="{% trans "Rack elevation" %}"
|
||||||
|
>
|
||||||
|
<div class="d-flex justify-content-center align-items-center rack-loading-container">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">{% trans "Loading..." %}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center mt-3">
|
<div class="text-center mt-3">
|
||||||
<a class="btn btn-outline-primary" href="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}" hx-boost="false">
|
<a class="btn btn-outline-primary" href="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}" hx-boost="false">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-06-26 05:02+0000\n"
|
"POT-Creation-Date: 2025-07-09 05:04+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -20,7 +20,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: netbox/account/tables.py:27 netbox/templates/account/token.html:22
|
#: netbox/account/tables.py:27 netbox/templates/account/token.html:22
|
||||||
#: netbox/templates/users/token.html:17 netbox/users/forms/bulk_import.py:39
|
#: netbox/templates/users/token.html:17 netbox/users/forms/bulk_import.py:39
|
||||||
#: netbox/users/forms/model_forms.py:112
|
#: netbox/users/forms/model_forms.py:113
|
||||||
msgid "Key"
|
msgid "Key"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: netbox/account/tables.py:45 netbox/templates/account/token.html:55
|
#: netbox/account/tables.py:45 netbox/templates/account/token.html:55
|
||||||
#: netbox/templates/users/token.html:47 netbox/users/forms/bulk_edit.py:122
|
#: netbox/templates/users/token.html:47 netbox/users/forms/bulk_edit.py:122
|
||||||
#: netbox/users/forms/model_forms.py:124
|
#: netbox/users/forms/model_forms.py:125
|
||||||
msgid "Allowed IPs"
|
msgid "Allowed IPs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -705,7 +705,7 @@ msgstr ""
|
|||||||
#: netbox/dcim/tables/devices.py:852 netbox/dcim/tables/power.py:77
|
#: netbox/dcim/tables/devices.py:852 netbox/dcim/tables/power.py:77
|
||||||
#: netbox/dcim/tables/racks.py:141 netbox/extras/forms/bulk_import.py:42
|
#: netbox/dcim/tables/racks.py:141 netbox/extras/forms/bulk_import.py:42
|
||||||
#: netbox/extras/tables/tables.py:449 netbox/extras/tables/tables.py:509
|
#: netbox/extras/tables/tables.py:449 netbox/extras/tables/tables.py:509
|
||||||
#: netbox/netbox/tables/tables.py:272 netbox/templates/circuits/circuit.html:30
|
#: netbox/netbox/tables/tables.py:274 netbox/templates/circuits/circuit.html:30
|
||||||
#: netbox/templates/circuits/virtualcircuit.html:39
|
#: netbox/templates/circuits/virtualcircuit.html:39
|
||||||
#: netbox/templates/circuits/virtualcircuittermination.html:64
|
#: netbox/templates/circuits/virtualcircuittermination.html:64
|
||||||
#: netbox/templates/core/datasource.html:38 netbox/templates/dcim/cable.html:15
|
#: netbox/templates/core/datasource.html:38 netbox/templates/dcim/cable.html:15
|
||||||
@@ -804,7 +804,7 @@ msgstr ""
|
|||||||
#: netbox/templates/vpn/l2vpn.html:26 netbox/templates/vpn/tunnel.html:25
|
#: netbox/templates/vpn/l2vpn.html:26 netbox/templates/vpn/tunnel.html:25
|
||||||
#: netbox/templates/wireless/wirelesslan.html:22
|
#: netbox/templates/wireless/wirelesslan.html:22
|
||||||
#: netbox/templates/wireless/wirelesslink.html:17
|
#: netbox/templates/wireless/wirelesslink.html:17
|
||||||
#: netbox/users/forms/filtersets.py:32 netbox/users/forms/model_forms.py:194
|
#: netbox/users/forms/filtersets.py:32 netbox/users/forms/model_forms.py:195
|
||||||
#: netbox/virtualization/forms/bulk_edit.py:71
|
#: netbox/virtualization/forms/bulk_edit.py:71
|
||||||
#: netbox/virtualization/forms/bulk_edit.py:100
|
#: netbox/virtualization/forms/bulk_edit.py:100
|
||||||
#: netbox/virtualization/forms/bulk_import.py:55
|
#: netbox/virtualization/forms/bulk_import.py:55
|
||||||
@@ -972,7 +972,7 @@ msgstr ""
|
|||||||
#: netbox/ipam/forms/filtersets.py:406 netbox/ipam/forms/filtersets.py:492
|
#: netbox/ipam/forms/filtersets.py:406 netbox/ipam/forms/filtersets.py:492
|
||||||
#: netbox/ipam/forms/filtersets.py:505 netbox/ipam/forms/filtersets.py:530
|
#: netbox/ipam/forms/filtersets.py:505 netbox/ipam/forms/filtersets.py:530
|
||||||
#: netbox/ipam/forms/filtersets.py:601 netbox/ipam/forms/filtersets.py:619
|
#: netbox/ipam/forms/filtersets.py:601 netbox/ipam/forms/filtersets.py:619
|
||||||
#: netbox/netbox/tables/tables.py:288 netbox/templates/dcim/moduletype.html:68
|
#: netbox/netbox/tables/tables.py:290 netbox/templates/dcim/moduletype.html:68
|
||||||
#: netbox/virtualization/forms/filtersets.py:46
|
#: netbox/virtualization/forms/filtersets.py:46
|
||||||
#: netbox/virtualization/forms/filtersets.py:109
|
#: netbox/virtualization/forms/filtersets.py:109
|
||||||
#: netbox/virtualization/forms/filtersets.py:204
|
#: netbox/virtualization/forms/filtersets.py:204
|
||||||
@@ -1369,7 +1369,7 @@ msgstr ""
|
|||||||
#: netbox/templates/extras/configcontext.html:60
|
#: netbox/templates/extras/configcontext.html:60
|
||||||
#: netbox/templates/ipam/ipaddress.html:59
|
#: netbox/templates/ipam/ipaddress.html:59
|
||||||
#: netbox/templates/ipam/vlan_edit.html:42
|
#: netbox/templates/ipam/vlan_edit.html:42
|
||||||
#: netbox/tenancy/forms/filtersets.py:87 netbox/users/forms/model_forms.py:314
|
#: netbox/tenancy/forms/filtersets.py:87 netbox/users/forms/model_forms.py:315
|
||||||
msgid "Assignment"
|
msgid "Assignment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2179,7 +2179,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: netbox/core/data_backends.py:56 netbox/templates/account/base.html:23
|
#: netbox/core/data_backends.py:56 netbox/templates/account/base.html:23
|
||||||
#: netbox/templates/account/password.html:12
|
#: netbox/templates/account/password.html:12
|
||||||
#: netbox/users/forms/model_forms.py:170
|
#: netbox/users/forms/model_forms.py:171
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2231,7 +2231,7 @@ msgstr ""
|
|||||||
#: netbox/extras/forms/filtersets.py:335 netbox/extras/tables/tables.py:166
|
#: netbox/extras/forms/filtersets.py:335 netbox/extras/tables/tables.py:166
|
||||||
#: netbox/extras/tables/tables.py:267 netbox/extras/tables/tables.py:300
|
#: netbox/extras/tables/tables.py:267 netbox/extras/tables/tables.py:300
|
||||||
#: netbox/extras/tables/tables.py:459 netbox/netbox/preferences.py:22
|
#: netbox/extras/tables/tables.py:459 netbox/netbox/preferences.py:22
|
||||||
#: netbox/templates/core/datasource.html:42
|
#: netbox/netbox/preferences.py:61 netbox/templates/core/datasource.html:42
|
||||||
#: netbox/templates/dcim/interface.html:61
|
#: netbox/templates/dcim/interface.html:61
|
||||||
#: netbox/templates/extras/customlink.html:17
|
#: netbox/templates/extras/customlink.html:17
|
||||||
#: netbox/templates/extras/eventrule.html:17
|
#: netbox/templates/extras/eventrule.html:17
|
||||||
@@ -2346,7 +2346,7 @@ msgstr ""
|
|||||||
#: netbox/templates/users/user.html:4 netbox/templates/users/user.html:12
|
#: netbox/templates/users/user.html:4 netbox/templates/users/user.html:12
|
||||||
#: netbox/users/filtersets.py:107 netbox/users/filtersets.py:174
|
#: netbox/users/filtersets.py:107 netbox/users/filtersets.py:174
|
||||||
#: netbox/users/forms/filtersets.py:84 netbox/users/forms/filtersets.py:125
|
#: netbox/users/forms/filtersets.py:84 netbox/users/forms/filtersets.py:125
|
||||||
#: netbox/users/forms/model_forms.py:155 netbox/users/forms/model_forms.py:192
|
#: netbox/users/forms/model_forms.py:156 netbox/users/forms/model_forms.py:193
|
||||||
#: netbox/users/tables.py:19
|
#: netbox/users/tables.py:19
|
||||||
msgid "User"
|
msgid "User"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2449,7 +2449,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: netbox/core/forms/model_forms.py:170 netbox/dcim/forms/filtersets.py:752
|
#: netbox/core/forms/model_forms.py:170 netbox/dcim/forms/filtersets.py:752
|
||||||
#: netbox/templates/core/inc/config_data.html:127
|
#: netbox/templates/core/inc/config_data.html:127
|
||||||
#: netbox/users/forms/model_forms.py:64
|
#: netbox/users/forms/model_forms.py:65
|
||||||
msgid "Miscellaneous"
|
msgid "Miscellaneous"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2738,12 +2738,12 @@ msgstr ""
|
|||||||
msgid "Jobs cannot be assigned to this object type ({type})."
|
msgid "Jobs cannot be assigned to this object type ({type})."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/jobs.py:193
|
#: netbox/core/models/jobs.py:192
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Invalid status for job termination. Choices are: {choices}"
|
msgid "Invalid status for job termination. Choices are: {choices}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/core/models/jobs.py:235
|
#: netbox/core/models/jobs.py:234
|
||||||
msgid ""
|
msgid ""
|
||||||
"enqueue() cannot be called with values for both schedule_at and immediate."
|
"enqueue() cannot be called with values for both schedule_at and immediate."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2763,7 +2763,7 @@ msgstr ""
|
|||||||
#: netbox/extras/tables/tables.py:341 netbox/extras/tables/tables.py:373
|
#: netbox/extras/tables/tables.py:341 netbox/extras/tables/tables.py:373
|
||||||
#: netbox/extras/tables/tables.py:453 netbox/extras/tables/tables.py:514
|
#: netbox/extras/tables/tables.py:453 netbox/extras/tables/tables.py:514
|
||||||
#: netbox/extras/tables/tables.py:637 netbox/extras/tables/tables.py:677
|
#: netbox/extras/tables/tables.py:637 netbox/extras/tables/tables.py:677
|
||||||
#: netbox/extras/tables/tables.py:731 netbox/netbox/tables/tables.py:276
|
#: netbox/extras/tables/tables.py:731 netbox/netbox/tables/tables.py:278
|
||||||
#: netbox/templates/core/objectchange.html:58
|
#: netbox/templates/core/objectchange.html:58
|
||||||
#: netbox/templates/extras/eventrule.html:78
|
#: netbox/templates/extras/eventrule.html:78
|
||||||
#: netbox/templates/extras/journalentry.html:18
|
#: netbox/templates/extras/journalentry.html:18
|
||||||
@@ -2801,7 +2801,7 @@ msgstr ""
|
|||||||
#: netbox/core/tables/jobs.py:10 netbox/core/tables/tasks.py:76
|
#: netbox/core/tables/jobs.py:10 netbox/core/tables/tasks.py:76
|
||||||
#: netbox/dcim/tables/devicetypes.py:169 netbox/extras/tables/tables.py:230
|
#: netbox/dcim/tables/devicetypes.py:169 netbox/extras/tables/tables.py:230
|
||||||
#: netbox/extras/tables/tables.py:504 netbox/extras/tables/tables.py:702
|
#: netbox/extras/tables/tables.py:504 netbox/extras/tables/tables.py:702
|
||||||
#: netbox/netbox/tables/tables.py:221
|
#: netbox/netbox/tables/tables.py:223
|
||||||
#: netbox/templates/dcim/virtualchassis_edit.html:56
|
#: netbox/templates/dcim/virtualchassis_edit.html:56
|
||||||
#: netbox/utilities/forms/forms.py:73 netbox/wireless/tables/wirelesslink.py:16
|
#: netbox/utilities/forms/forms.py:73 netbox/wireless/tables/wirelesslink.py:16
|
||||||
msgid "ID"
|
msgid "ID"
|
||||||
@@ -3381,8 +3381,9 @@ msgid "Three-phase"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/choices.py:1657 netbox/extras/choices.py:53
|
#: netbox/dcim/choices.py:1657 netbox/extras/choices.py:53
|
||||||
#: netbox/netbox/preferences.py:21 netbox/templates/extras/customfield.html:78
|
#: netbox/netbox/preferences.py:21 netbox/netbox/preferences.py:60
|
||||||
#: netbox/vpn/choices.py:20 netbox/wireless/choices.py:27
|
#: netbox/templates/extras/customfield.html:78 netbox/vpn/choices.py:20
|
||||||
|
#: netbox/wireless/choices.py:27
|
||||||
msgid "Disabled"
|
msgid "Disabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -8201,7 +8202,7 @@ msgstr ""
|
|||||||
#: netbox/extras/forms/model_forms.py:254
|
#: netbox/extras/forms/model_forms.py:254
|
||||||
#: netbox/extras/forms/model_forms.py:297
|
#: netbox/extras/forms/model_forms.py:297
|
||||||
#: netbox/extras/forms/model_forms.py:450
|
#: netbox/extras/forms/model_forms.py:450
|
||||||
#: netbox/extras/forms/model_forms.py:567 netbox/users/forms/model_forms.py:276
|
#: netbox/extras/forms/model_forms.py:567 netbox/users/forms/model_forms.py:277
|
||||||
msgid "Object types"
|
msgid "Object types"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -8297,8 +8298,8 @@ msgstr ""
|
|||||||
#: netbox/extras/forms/bulk_import.py:275
|
#: netbox/extras/forms/bulk_import.py:275
|
||||||
#: netbox/extras/forms/model_forms.py:398 netbox/netbox/navigation/menu.py:413
|
#: netbox/extras/forms/model_forms.py:398 netbox/netbox/navigation/menu.py:413
|
||||||
#: netbox/templates/extras/notificationgroup.html:41
|
#: netbox/templates/extras/notificationgroup.html:41
|
||||||
#: netbox/templates/users/group.html:29 netbox/users/forms/model_forms.py:236
|
#: netbox/templates/users/group.html:29 netbox/users/forms/model_forms.py:237
|
||||||
#: netbox/users/forms/model_forms.py:248 netbox/users/forms/model_forms.py:300
|
#: netbox/users/forms/model_forms.py:249 netbox/users/forms/model_forms.py:301
|
||||||
#: netbox/users/tables.py:102
|
#: netbox/users/tables.py:102
|
||||||
msgid "Users"
|
msgid "Users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -8314,8 +8315,8 @@ msgstr ""
|
|||||||
#: netbox/templates/tenancy/contact.html:21
|
#: netbox/templates/tenancy/contact.html:21
|
||||||
#: netbox/tenancy/forms/bulk_edit.py:139 netbox/tenancy/forms/filtersets.py:78
|
#: netbox/tenancy/forms/bulk_edit.py:139 netbox/tenancy/forms/filtersets.py:78
|
||||||
#: netbox/tenancy/forms/model_forms.py:99 netbox/tenancy/tables/contacts.py:64
|
#: netbox/tenancy/forms/model_forms.py:99 netbox/tenancy/tables/contacts.py:64
|
||||||
#: netbox/users/forms/model_forms.py:181 netbox/users/forms/model_forms.py:193
|
#: netbox/users/forms/model_forms.py:182 netbox/users/forms/model_forms.py:194
|
||||||
#: netbox/users/forms/model_forms.py:305 netbox/users/tables.py:35
|
#: netbox/users/forms/model_forms.py:306 netbox/users/tables.py:35
|
||||||
#: netbox/users/tables.py:106
|
#: netbox/users/tables.py:106
|
||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -10212,7 +10213,7 @@ msgid ""
|
|||||||
"One of parent or parent_object_id must be included with parent_object_type"
|
"One of parent or parent_object_id must be included with parent_object_type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/ipam/forms/bulk_import.py:638
|
#: netbox/ipam/forms/bulk_import.py:641
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{ip} is not assigned to this parent."
|
msgid "{ip} is not assigned to this parent."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -11834,9 +11835,9 @@ msgstr ""
|
|||||||
msgid "API Tokens"
|
msgid "API Tokens"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/navigation/menu.py:460 netbox/users/forms/model_forms.py:187
|
#: netbox/netbox/navigation/menu.py:460 netbox/users/forms/model_forms.py:188
|
||||||
#: netbox/users/forms/model_forms.py:195 netbox/users/forms/model_forms.py:242
|
#: netbox/users/forms/model_forms.py:196 netbox/users/forms/model_forms.py:243
|
||||||
#: netbox/users/forms/model_forms.py:249
|
#: netbox/users/forms/model_forms.py:250
|
||||||
msgid "Permissions"
|
msgid "Permissions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -11959,11 +11960,19 @@ msgstr ""
|
|||||||
msgid "Where the paginator controls will be displayed relative to a table"
|
msgid "Where the paginator controls will be displayed relative to a table"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/preferences.py:60
|
#: netbox/netbox/preferences.py:58
|
||||||
|
msgid "Striped table rows"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: netbox/netbox/preferences.py:63
|
||||||
|
msgid "Render table rows with alternating colors to increase readability"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: netbox/netbox/preferences.py:68
|
||||||
msgid "Data format"
|
msgid "Data format"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/preferences.py:65
|
#: netbox/netbox/preferences.py:73
|
||||||
msgid "The preferred syntax for displaying generic data within the UI"
|
msgid "The preferred syntax for displaying generic data within the UI"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -12062,12 +12071,12 @@ msgstr ""
|
|||||||
msgid "No {model_name} found"
|
msgid "No {model_name} found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/tables/tables.py:281
|
#: netbox/netbox/tables/tables.py:283
|
||||||
#: netbox/templates/generic/bulk_import.html:117
|
#: netbox/templates/generic/bulk_import.html:117
|
||||||
msgid "Field"
|
msgid "Field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/tables/tables.py:284
|
#: netbox/netbox/tables/tables.py:286
|
||||||
msgid "Value"
|
msgid "Value"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -13012,7 +13021,7 @@ msgid "Cable Trace for %(object_type)s %(object)s"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/dcim/cable_trace.html:24
|
#: netbox/templates/dcim/cable_trace.html:24
|
||||||
#: netbox/templates/dcim/inc/rack_elevation.html:7
|
#: netbox/templates/dcim/inc/rack_elevation.html:18
|
||||||
msgid "Download SVG"
|
msgid "Download SVG"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -13415,10 +13424,14 @@ msgstr ""
|
|||||||
msgid "Descending Units"
|
msgid "Descending Units"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/dcim/inc/rack_elevation.html:3
|
#: netbox/templates/dcim/inc/rack_elevation.html:7
|
||||||
msgid "Rack elevation"
|
msgid "Rack elevation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: netbox/templates/dcim/inc/rack_elevation.html:11
|
||||||
|
msgid "Loading..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/dcim/interface.html:17
|
#: netbox/templates/dcim/interface.html:17
|
||||||
msgid "Add Child Interface"
|
msgid "Add Child Interface"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -13716,7 +13729,7 @@ msgstr ""
|
|||||||
#: netbox/templates/dcim/virtualchassis_add_member.html:27
|
#: netbox/templates/dcim/virtualchassis_add_member.html:27
|
||||||
#: netbox/templates/generic/object_edit.html:78
|
#: netbox/templates/generic/object_edit.html:78
|
||||||
#: netbox/templates/users/objectpermission.html:31
|
#: netbox/templates/users/objectpermission.html:31
|
||||||
#: netbox/users/forms/filtersets.py:67 netbox/users/forms/model_forms.py:312
|
#: netbox/users/forms/filtersets.py:67 netbox/users/forms/model_forms.py:313
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -14874,7 +14887,7 @@ msgid "View"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/templates/users/objectpermission.html:52
|
#: netbox/templates/users/objectpermission.html:52
|
||||||
#: netbox/users/forms/model_forms.py:315
|
#: netbox/users/forms/model_forms.py:316
|
||||||
msgid "Constraints"
|
msgid "Constraints"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -15358,60 +15371,60 @@ msgstr ""
|
|||||||
msgid "Can Delete"
|
msgid "Can Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:62
|
#: netbox/users/forms/model_forms.py:63
|
||||||
msgid "User Interface"
|
msgid "User Interface"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:114
|
#: netbox/users/forms/model_forms.py:115
|
||||||
msgid ""
|
msgid ""
|
||||||
"Keys must be at least 40 characters in length. <strong>Be sure to record "
|
"Keys must be at least 40 characters in length. <strong>Be sure to record "
|
||||||
"your key</strong> prior to submitting this form, as it may no longer be "
|
"your key</strong> prior to submitting this form, as it may no longer be "
|
||||||
"accessible once the token has been created."
|
"accessible once the token has been created."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:126
|
#: netbox/users/forms/model_forms.py:127
|
||||||
msgid ""
|
msgid ""
|
||||||
"Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for "
|
"Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for "
|
||||||
"no restrictions. Example: <code>10.1.1.0/24,192.168.10.16/32,2001:"
|
"no restrictions. Example: <code>10.1.1.0/24,192.168.10.16/32,2001:"
|
||||||
"db8:1::/64</code>"
|
"db8:1::/64</code>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:175
|
#: netbox/users/forms/model_forms.py:176
|
||||||
msgid "Confirm password"
|
msgid "Confirm password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:178
|
#: netbox/users/forms/model_forms.py:179
|
||||||
msgid "Enter the same password as before, for verification."
|
msgid "Enter the same password as before, for verification."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:227
|
#: netbox/users/forms/model_forms.py:228
|
||||||
msgid "Passwords do not match! Please check your input and try again."
|
msgid "Passwords do not match! Please check your input and try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:294
|
#: netbox/users/forms/model_forms.py:295
|
||||||
msgid "Additional actions"
|
msgid "Additional actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:297
|
#: netbox/users/forms/model_forms.py:298
|
||||||
msgid "Actions granted in addition to those listed above"
|
msgid "Actions granted in addition to those listed above"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:313
|
#: netbox/users/forms/model_forms.py:314
|
||||||
msgid "Objects"
|
msgid "Objects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:325
|
#: netbox/users/forms/model_forms.py:326
|
||||||
msgid ""
|
msgid ""
|
||||||
"JSON expression of a queryset filter that will return only permitted "
|
"JSON expression of a queryset filter that will return only permitted "
|
||||||
"objects. Leave null to match all objects of this type. A list of multiple "
|
"objects. Leave null to match all objects of this type. A list of multiple "
|
||||||
"objects will result in a logical OR operation."
|
"objects will result in a logical OR operation."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:364
|
#: netbox/users/forms/model_forms.py:365
|
||||||
msgid "At least one action must be selected."
|
msgid "At least one action must be selected."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/forms/model_forms.py:382
|
#: netbox/users/forms/model_forms.py:383
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Invalid filter for {model}: {error}"
|
msgid "Invalid filter for {model}: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -41,11 +41,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{% for item, buttons in items %}
|
{% for item, buttons in items %}
|
||||||
<div class="dropdown-item d-flex justify-content-between ps-3 py-0">
|
<div class="dropdown-item d-flex justify-content-between ps-3 py-0">
|
||||||
<a href="{% url item.link %}" class="d-inline-flex flex-fill py-1">{{ item.link_text }}</a>
|
<a href="{{ item.url }}" class="d-inline-flex flex-fill py-1">{{ item.link_text }}</a>
|
||||||
{% if buttons %}
|
{% if buttons %}
|
||||||
<div class="btn-group ms-1">
|
<div class="btn-group ms-1">
|
||||||
{% for button in buttons %}
|
{% for button in buttons %}
|
||||||
<a href="{% url button.link %}" class="btn btn-sm btn-{{ button.color|default:"outline" }} lh-2 px-2" title="{{ button.title }}">
|
<a href="{{ button.url }}" class="btn btn-sm btn-{{ button.color|default:"outline" }} lh-2 px-2" title="{{ button.title }}">
|
||||||
<i class="{{ button.icon_class }}"></i>
|
<i class="{{ button.icon_class }}"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
Reference in New Issue
Block a user