Compare commits

..

17 Commits

Author SHA1 Message Date
Brian Tiemann
0c95ac6b1a Make url a property on MenuItem/PluginMenuItem etc, overridable via a setter
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-07-13 20:19:46 -04:00
Brian Tiemann
7338898ccb Back out support for callables but keep alternate prerendered url param 2025-07-08 15:38:12 -04:00
Brian Tiemann
aa4533e331 Merge branch 'main' into nav-menu-callables 2025-07-08 15:32:38 -04:00
Jason Novinger
ee94fb0b94 Closes #19550: Enhancement: Refactor rack elevations template for lazy loading /dcim/rack-elevations/ (#19823)
* Refactor rack elevation template to use htmx for dynamic loading and improved user experience

* rework to prevent dup loading

* Update netbox/templates/dcim/inc/rack_elevation.html

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* Update netbox/templates/dcim/inc/rack_elevation.html

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* Move inline styles to styles/custom/racks.css

---------

Co-authored-by: tony.nealon@wholesailnetworks.com <tony.nealon@wholesailnetworks.com>
Co-authored-by: tbotnz <tonynealon1989@gmail.com>
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-07-08 11:20:04 -04:00
Harry
8fb8f4c75b Closes #19571: Create expansion_card.json (#19689)
* Create expansion_card.json

* Update 0206_load_module_type_profiles.py

* Update expansion_card.json

Fixed
2025-07-08 08:27:48 -05:00
Brian Tiemann
e400a7cb29 Merge remote-tracking branch 'origin/nav-menu-callables' into nav-menu-callables 2025-07-07 19:30:03 -04:00
github-actions
e33793dc82 Update source translation strings 2025-07-03 05:04:46 +00:00
Jeremy Stretch
3b8841ee3b Fixes #19806: Introduce JobFailed exception to allow marking background jobs as failed (#19807) 2025-07-02 14:02:49 -05:00
dieck
ea4c205a37 Upgrade documentation: have git fetch new tags
fixes #19778
2025-07-02 13:59:56 -04:00
btiemann
600f85ca83 Clarify docstring to differentiate link and url 2025-06-30 14:26:43 -04:00
github-actions
2a5d3abafb Update source translation strings 2025-06-27 05:03:03 +00:00
Brian Tiemann
9d6abcf57b Merge branch 'main' into nav-menu-callables 2025-06-12 16:42:50 -04:00
Brian Tiemann
fbf639fad1 Merge branch 'main' into nav-menu-callables 2025-06-10 08:50:44 -04:00
Brian Tiemann
9a46c8e30d Merge branch 'main' into nav-menu-callables 2025-05-21 20:25:46 -04:00
Brian Tiemann
4ca48843af Fix quote on add button 2025-05-09 18:17:56 -04:00
Brian Tiemann
874d020d57 Merge branch 'main' into nav-menu-callables 2025-05-01 14:52:32 -04:00
Brian Tiemann
d0129811e2 Support menu items that are callables 2025-04-29 19:11:55 -04:00
17 changed files with 185 additions and 63 deletions

View File

@@ -2,9 +2,9 @@
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
* 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).

View File

@@ -135,7 +135,7 @@ Check out the desired release by specifying its tag. For example:
```
cd /opt/netbox && \
sudo git fetch && \
sudo git fetch --tags && \
sudo git checkout v4.2.7
```

View File

@@ -15,7 +15,6 @@ A background job implements a basic [Job](../../models/core/job.md) executor for
```python title="jobs.py"
from netbox.jobs import JobRunner
class MyTestJob(JobRunner):
class Meta:
name = "My Test Job"
@@ -25,6 +24,8 @@ class MyTestJob(JobRunner):
# 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.
!!! tip

View File

@@ -1,9 +1,19 @@
from django.core.exceptions import ImproperlyConfigured
class SyncError(Exception):
pass
__all__ = (
'IncompatiblePluginError',
'JobFailed',
'SyncError',
)
class IncompatiblePluginError(ImproperlyConfigured):
pass
class JobFailed(Exception):
pass
class SyncError(Exception):
pass

View File

@@ -187,15 +187,14 @@ class Job(models.Model):
"""
Mark the job as completed, optionally specifying a particular termination status.
"""
valid_statuses = JobStatusChoices.TERMINAL_STATE_CHOICES
if status not in valid_statuses:
if status not in JobStatusChoices.TERMINAL_STATE_CHOICES:
raise ValueError(
_("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
if error:
self.error = error

View File

@@ -19,7 +19,8 @@ def load_initial_data(apps, schema_editor):
'gpu',
'hard_disk',
'memory',
'power_supply'
'power_supply',
'expansion_card'
)
for name in initial_profiles:

View File

@@ -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"
}
}
}
}

View File

@@ -8,6 +8,7 @@ from django_pglocks import advisory_lock
from rq.timeouts import JobTimeoutException
from core.choices import JobStatusChoices
from core.exceptions import JobFailed
from core.models import Job, ObjectType
from netbox.constants import ADVISORY_LOCK_KEYS
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
job's metadata and handle errors. For periodic jobs, a new job is automatically scheduled using its `interval`.
"""
logger = logging.getLogger('netbox.jobs')
try:
job.start()
cls(job).run(*args, **kwargs)
job.terminate()
except JobFailed:
logger.warning(f"Job {job} failed")
job.terminate(status=JobStatusChoices.STATUS_FAILED)
except Exception as e:
job.terminate(status=JobStatusChoices.STATUS_ERRORED, error=repr(e))
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.
finally:

View File

@@ -1,6 +1,8 @@
from dataclasses import dataclass
from typing import Sequence, Optional
from django.urls import reverse
__all__ = (
'get_model_item',
@@ -22,20 +24,46 @@ class MenuItemButton:
link: str
title: str
icon_class: str
_url: Optional[str] = None
permissions: Optional[Sequence[str]] = ()
color: Optional[str] = None
def __post_init__(self):
if self.link:
self._url = reverse(self.link)
@property
def url(self):
return self._url
@url.setter
def url(self, value):
self._url = value
@dataclass
class MenuItem:
link: str
link_text: str
_url: Optional[str] = None
permissions: Optional[Sequence[str]] = ()
auth_required: Optional[bool] = False
staff_only: Optional[bool] = False
buttons: Optional[Sequence[MenuItemButton]] = ()
def __post_init__(self):
if self.link:
self._url = reverse(self.link)
@property
def url(self):
return self._url
@url.setter
def url(self, value):
self._url = value
@dataclass
class MenuGroup:

View File

@@ -1,3 +1,4 @@
from django.urls import reverse
from django.utils.text import slugify
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
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.
"""
permissions = []
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_text = link_text
self.auth_required = auth_required
self.staff_only = staff_only
if link:
self._url = reverse(link)
if permissions is not None:
if type(permissions) not in (list, tuple):
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."))
self.buttons = buttons
@property
def url(self):
return self._url
@url.setter
def url(self, value):
self._url = value
class PluginMenuButton:
"""
@@ -60,11 +75,14 @@ class PluginMenuButton:
"""
color = ButtonColorChoices.DEFAULT
permissions = []
_url = None
def __init__(self, link, title, icon_class, color=None, permissions=None):
self.link = link
self.title = title
self.icon_class = icon_class
if link:
self._url = reverse(link)
if permissions is not None:
if type(permissions) not in (list, tuple):
raise TypeError(_("Permissions must be passed as a tuple or list."))
@@ -73,3 +91,11 @@ class PluginMenuButton:
if color not in ButtonColorChoices.values():
raise ValueError(_("Button color must be a choice within ButtonColorChoices."))
self.color = color
@property
def url(self):
return self._url
@url.setter
def url(self, value):
self._url = value

View File

@@ -7,11 +7,15 @@ from django_rq import get_queue
from ..jobs import *
from core.models import DataSource, Job
from core.choices import JobStatusChoices
from core.exceptions import JobFailed
from utilities.testing import disable_warnings
class TestJobRunner(JobRunner):
def run(self, *args, **kwargs):
pass
if kwargs.get('make_fail', False):
raise JobFailed()
class JobRunnerTestCase(TestCase):
@@ -49,6 +53,12 @@ class JobRunnerTest(JobRunnerTestCase):
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):
class ErroredJobRunner(TestJobRunner):
EXP = Exception('Test error')

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
.rack-loading-container {
min-height: 200px;
margin-left: 30px;
}

View File

@@ -27,3 +27,4 @@
@import 'custom/markdown';
@import 'custom/misc';
@import 'custom/notifications';
@import 'custom/racks';

View File

@@ -1,6 +1,17 @@
{% load i18n %}
<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 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">

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-26 05:02+0000\n"
"POT-Creation-Date: 2025-07-03 05:04+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\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/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"
msgstr ""
@@ -57,7 +57,7 @@ msgstr ""
#: 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/users/forms/model_forms.py:124
#: netbox/users/forms/model_forms.py:125
msgid "Allowed IPs"
msgstr ""
@@ -705,7 +705,7 @@ msgstr ""
#: 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/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/virtualcircuittermination.html:64
#: 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/wireless/wirelesslan.html:22
#: 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:100
#: 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:505 netbox/ipam/forms/filtersets.py:530
#: 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:109
#: netbox/virtualization/forms/filtersets.py:204
@@ -1369,7 +1369,7 @@ msgstr ""
#: netbox/templates/extras/configcontext.html:60
#: netbox/templates/ipam/ipaddress.html:59
#: 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"
msgstr ""
@@ -2179,7 +2179,7 @@ msgstr ""
#: netbox/core/data_backends.py:56 netbox/templates/account/base.html:23
#: netbox/templates/account/password.html:12
#: netbox/users/forms/model_forms.py:170
#: netbox/users/forms/model_forms.py:171
msgid "Password"
msgstr ""
@@ -2231,7 +2231,7 @@ msgstr ""
#: 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: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/extras/customlink.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/users/filtersets.py:107 netbox/users/filtersets.py:174
#: 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
msgid "User"
msgstr ""
@@ -2449,7 +2449,7 @@ msgstr ""
#: netbox/core/forms/model_forms.py:170 netbox/dcim/forms/filtersets.py:752
#: netbox/templates/core/inc/config_data.html:127
#: netbox/users/forms/model_forms.py:64
#: netbox/users/forms/model_forms.py:65
msgid "Miscellaneous"
msgstr ""
@@ -2738,12 +2738,12 @@ msgstr ""
msgid "Jobs cannot be assigned to this object type ({type})."
msgstr ""
#: netbox/core/models/jobs.py:193
#: netbox/core/models/jobs.py:192
#, python-brace-format
msgid "Invalid status for job termination. Choices are: {choices}"
msgstr ""
#: netbox/core/models/jobs.py:235
#: netbox/core/models/jobs.py:234
msgid ""
"enqueue() cannot be called with values for both schedule_at and immediate."
msgstr ""
@@ -2763,7 +2763,7 @@ msgstr ""
#: 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: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/extras/eventrule.html:78
#: netbox/templates/extras/journalentry.html:18
@@ -2801,7 +2801,7 @@ msgstr ""
#: 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/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/utilities/forms/forms.py:73 netbox/wireless/tables/wirelesslink.py:16
msgid "ID"
@@ -3381,8 +3381,9 @@ msgid "Three-phase"
msgstr ""
#: netbox/dcim/choices.py:1657 netbox/extras/choices.py:53
#: netbox/netbox/preferences.py:21 netbox/templates/extras/customfield.html:78
#: netbox/vpn/choices.py:20 netbox/wireless/choices.py:27
#: netbox/netbox/preferences.py:21 netbox/netbox/preferences.py:60
#: netbox/templates/extras/customfield.html:78 netbox/vpn/choices.py:20
#: netbox/wireless/choices.py:27
msgid "Disabled"
msgstr ""
@@ -8201,7 +8202,7 @@ msgstr ""
#: netbox/extras/forms/model_forms.py:254
#: netbox/extras/forms/model_forms.py:297
#: 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"
msgstr ""
@@ -8297,8 +8298,8 @@ msgstr ""
#: netbox/extras/forms/bulk_import.py:275
#: netbox/extras/forms/model_forms.py:398 netbox/netbox/navigation/menu.py:413
#: netbox/templates/extras/notificationgroup.html:41
#: netbox/templates/users/group.html:29 netbox/users/forms/model_forms.py:236
#: netbox/users/forms/model_forms.py:248 netbox/users/forms/model_forms.py:300
#: netbox/templates/users/group.html:29 netbox/users/forms/model_forms.py:237
#: netbox/users/forms/model_forms.py:249 netbox/users/forms/model_forms.py:301
#: netbox/users/tables.py:102
msgid "Users"
msgstr ""
@@ -8314,8 +8315,8 @@ msgstr ""
#: netbox/templates/tenancy/contact.html:21
#: 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/users/forms/model_forms.py:181 netbox/users/forms/model_forms.py:193
#: netbox/users/forms/model_forms.py:305 netbox/users/tables.py:35
#: netbox/users/forms/model_forms.py:182 netbox/users/forms/model_forms.py:194
#: netbox/users/forms/model_forms.py:306 netbox/users/tables.py:35
#: netbox/users/tables.py:106
msgid "Groups"
msgstr ""
@@ -10212,7 +10213,7 @@ msgid ""
"One of parent or parent_object_id must be included with parent_object_type"
msgstr ""
#: netbox/ipam/forms/bulk_import.py:638
#: netbox/ipam/forms/bulk_import.py:641
#, python-brace-format
msgid "{ip} is not assigned to this parent."
msgstr ""
@@ -11834,9 +11835,9 @@ msgstr ""
msgid "API Tokens"
msgstr ""
#: netbox/netbox/navigation/menu.py:460 netbox/users/forms/model_forms.py:187
#: netbox/users/forms/model_forms.py:195 netbox/users/forms/model_forms.py:242
#: netbox/users/forms/model_forms.py:249
#: netbox/netbox/navigation/menu.py:460 netbox/users/forms/model_forms.py:188
#: netbox/users/forms/model_forms.py:196 netbox/users/forms/model_forms.py:243
#: netbox/users/forms/model_forms.py:250
msgid "Permissions"
msgstr ""
@@ -11959,11 +11960,19 @@ msgstr ""
msgid "Where the paginator controls will be displayed relative to a table"
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"
msgstr ""
#: netbox/netbox/preferences.py:65
#: netbox/netbox/preferences.py:73
msgid "The preferred syntax for displaying generic data within the UI"
msgstr ""
@@ -12062,12 +12071,12 @@ msgstr ""
msgid "No {model_name} found"
msgstr ""
#: netbox/netbox/tables/tables.py:281
#: netbox/netbox/tables/tables.py:283
#: netbox/templates/generic/bulk_import.html:117
msgid "Field"
msgstr ""
#: netbox/netbox/tables/tables.py:284
#: netbox/netbox/tables/tables.py:286
msgid "Value"
msgstr ""
@@ -13716,7 +13725,7 @@ msgstr ""
#: netbox/templates/dcim/virtualchassis_add_member.html:27
#: netbox/templates/generic/object_edit.html:78
#: 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"
msgstr ""
@@ -14874,7 +14883,7 @@ msgid "View"
msgstr ""
#: netbox/templates/users/objectpermission.html:52
#: netbox/users/forms/model_forms.py:315
#: netbox/users/forms/model_forms.py:316
msgid "Constraints"
msgstr ""
@@ -15358,60 +15367,60 @@ msgstr ""
msgid "Can Delete"
msgstr ""
#: netbox/users/forms/model_forms.py:62
#: netbox/users/forms/model_forms.py:63
msgid "User Interface"
msgstr ""
#: netbox/users/forms/model_forms.py:114
#: netbox/users/forms/model_forms.py:115
msgid ""
"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 "
"accessible once the token has been created."
msgstr ""
#: netbox/users/forms/model_forms.py:126
#: netbox/users/forms/model_forms.py:127
msgid ""
"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:"
"db8:1::/64</code>"
msgstr ""
#: netbox/users/forms/model_forms.py:175
#: netbox/users/forms/model_forms.py:176
msgid "Confirm password"
msgstr ""
#: netbox/users/forms/model_forms.py:178
#: netbox/users/forms/model_forms.py:179
msgid "Enter the same password as before, for verification."
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."
msgstr ""
#: netbox/users/forms/model_forms.py:294
#: netbox/users/forms/model_forms.py:295
msgid "Additional actions"
msgstr ""
#: netbox/users/forms/model_forms.py:297
#: netbox/users/forms/model_forms.py:298
msgid "Actions granted in addition to those listed above"
msgstr ""
#: netbox/users/forms/model_forms.py:313
#: netbox/users/forms/model_forms.py:314
msgid "Objects"
msgstr ""
#: netbox/users/forms/model_forms.py:325
#: netbox/users/forms/model_forms.py:326
msgid ""
"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 will result in a logical OR operation."
msgstr ""
#: netbox/users/forms/model_forms.py:364
#: netbox/users/forms/model_forms.py:365
msgid "At least one action must be selected."
msgstr ""
#: netbox/users/forms/model_forms.py:382
#: netbox/users/forms/model_forms.py:383
#, python-brace-format
msgid "Invalid filter for {model}: {error}"
msgstr ""

View File

@@ -41,11 +41,11 @@
</div>
{% for item, buttons in items %}
<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 %}
<div class="btn-group ms-1">
{% 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>
</a>
{% endfor %}