Merge branch 'feature' into 13149-form-gettext

This commit is contained in:
Jeremy Stretch 2023-07-31 11:22:56 -04:00
commit 406cee91cc
325 changed files with 124714 additions and 2434 deletions

View File

@ -68,8 +68,13 @@ When defining a permission constraint, administrators may use the special token
The `$user` token can be used only as a constraint value, or as an item within a list of values. It cannot be modified or extended to reference specific user attributes. The `$user` token can be used only as a constraint value, or as an item within a list of values. It cannot be modified or extended to reference specific user attributes.
### Default Permissions
#### Example Constraint Definitions !!! info "This feature was introduced in NetBox v3.6."
While permissions are typically assigned to specific groups and/or users, it is also possible to define a set of default permissions that are applied to _all_ authenticated users. This is done using the [`DEFAULT_PERMISSIONS`](../configuration/security.md#default_permissions) configuration parameter. Note that statically configuring permissions for specific users or groups is **not** supported.
### Example Constraint Definitions
| Constraints | Description | | Constraints | Description |
| ----------- | ----------- | | ----------- | ----------- |

View File

@ -90,6 +90,38 @@ CSRF_TRUSTED_ORIGINS = (
--- ---
## DEFAULT_PERMISSIONS
!!! info "This parameter was introduced in NetBox v3.6."
Default:
```python
{
'users.view_token': ({'user': '$user'},),
'users.add_token': ({'user': '$user'},),
'users.change_token': ({'user': '$user'},),
'users.delete_token': ({'user': '$user'},),
}
```
This parameter defines object permissions that are applied automatically to _any_ authenticated user, regardless of what permissions have been defined in the database. By default, this parameter is defined to allow all users to manage their own API tokens, however it can be overriden for any purpose.
For example, to allow all users to create a device role beginning with the word "temp," you could configure the following:
```python
DEFAULT_PERMISSIONS = {
'dcim.add_devicerole': (
{'name__startswith': 'temp'},
)
}
```
!!! warning
Setting a custom value for this parameter will overwrite the default permission mapping shown above. If you want to retain the default mapping, be sure to reproduce it in your custom configuration.
---
## EXEMPT_VIEW_PERMISSIONS ## EXEMPT_VIEW_PERMISSIONS
Default: Empty list Default: Empty list

View File

@ -1,6 +1,8 @@
# Custom Field Choice Sets # Custom Field Choice Sets
Single- and multi-selection [custom fields documentation](../../customization/custom-fields.md) must define a set of valid choices from which the user may choose when defining the field value. These choices are defined as sets that may be reused among multiple custom fields. Single- and multi-selection [custom fields](../../customization/custom-fields.md) must define a set of valid choices from which the user may choose when defining the field value. These choices are defined as sets that may be reused among multiple custom fields.
A choice set must define a base choice set and/or a set of arbitrary extra choices.
## Fields ## Fields
@ -8,9 +10,17 @@ Single- and multi-selection [custom fields documentation](../../customization/cu
The human-friendly name of the choice set. The human-friendly name of the choice set.
### Base Choices
The set of pre-defined choices to include. Available sets are listed below. This is an optional setting.
* IATA airport codes
* ISO 3166 - Two-letter country codes
* UN/LOCODE - Five-character location identifiers
### Extra Choices ### Extra Choices
The list of valid choices, entered as a comma-separated list. A set of custom choices that will be appended to the base choice set (if any).
### Order Alphabetically ### Order Alphabetically

View File

@ -21,13 +21,27 @@ Management views for the following object types, previously available only under
The admin UI is scheduled for removal in NetBox v4.0. The admin UI is scheduled for removal in NetBox v4.0.
#### Configurable Default Permissions ([#13038](https://github.com/netbox-community/netbox/issues/13038))
Administrators now have the option of configuring a set of default permissions to be applied to _all_ users, regardless of explicit permission or group assignment. This is accomplished by defining the `DEFAULT_PERMISSIONS` configuration parameter.
#### User Bookmarks ([#8248](https://github.com/netbox-community/netbox/issues/8248)) #### User Bookmarks ([#8248](https://github.com/netbox-community/netbox/issues/8248))
Users can now bookmark their most commonly-visited objects in NetBox. Bookmarks will display both on the dashboard (if configured) and on a user-specific bookmarks view. Users can now bookmark their most commonly-visited objects in NetBox. Bookmarks will display both on the dashboard (if configured) and on a user-specific bookmarks view.
#### Custom Field Choice Sets ([#12988](https://github.com/netbox-community/netbox/issues/12988)) #### Custom Field Choice Sets ([#12988](https://github.com/netbox-community/netbox/issues/12988))
Select and multi-select custom fields now employ discrete, reusable choice sets containing the valid options for each field. A choice set may be shared by multiple custom fields. Select and multi-select custom fields now employ discrete, reusable choice sets containing the valid options for each field. A choice set may be shared by multiple custom fields. Additionally, each choice within a set can now specify both a value and a human-friendly label (see [#13241](https://github.com/netbox-community/netbox/issues/13241)).
#### Pre-Defined Location Choices for Custom Fields ([#12194](https://github.com/netbox-community/netbox/issues/12194))
Users now have the option to employ one of several pre-defined choice sets when creating a custom field. These include:
* IATA airport codes
* ISO 3166 country codes
* UN/LOCODE identifiers
Extra choices may also be appended to the pre-defined sets as needed.
#### Restrict Tag Usage by Object Type ([#11541](https://github.com/netbox-community/netbox/issues/11541)) #### Restrict Tag Usage by Object Type ([#11541](https://github.com/netbox-community/netbox/issues/11541))
@ -40,10 +54,13 @@ Tags may now be restricted to use with designated object types. Tags that have n
* [#10197](https://github.com/netbox-community/netbox/issues/10197) - Cache the number of member devices on each virtual chassis * [#10197](https://github.com/netbox-community/netbox/issues/10197) - Cache the number of member devices on each virtual chassis
* [#11305](https://github.com/netbox-community/netbox/issues/11305) - Add GPS coordinate fields to the device model * [#11305](https://github.com/netbox-community/netbox/issues/11305) - Add GPS coordinate fields to the device model
* [#12175](https://github.com/netbox-community/netbox/issues/12175) - Permit racks to start numbering at values greater than one * [#12175](https://github.com/netbox-community/netbox/issues/12175) - Permit racks to start numbering at values greater than one
* [#12210](https://github.com/netbox-community/netbox/issues/12210) - Add tenancy assignment for power feeds
* [#13170](https://github.com/netbox-community/netbox/issues/13170) - Add `rf_role` to InterfaceTemplate
* [#13269](https://github.com/netbox-community/netbox/issues/13269) - Cache the number of assigned component templates for device types * [#13269](https://github.com/netbox-community/netbox/issues/13269) - Cache the number of assigned component templates for device types
### Other Changes ### Other Changes
* Work has begun on introducing translation and localization support in NetBox. This work is being performed in preparation for release 4.0.
* [#9077](https://github.com/netbox-community/netbox/issues/9077) - Prevent the errant execution of dangerous instance methods in Django templates * [#9077](https://github.com/netbox-community/netbox/issues/9077) - Prevent the errant execution of dangerous instance methods in Django templates
* [#11766](https://github.com/netbox-community/netbox/issues/11766) - Remove obsolete custom `ChoiceField` and `MultipleChoiceField` classes * [#11766](https://github.com/netbox-community/netbox/issues/11766) - Remove obsolete custom `ChoiceField` and `MultipleChoiceField` classes
* [#12180](https://github.com/netbox-community/netbox/issues/12180) - All API endpoints for available objects (e.g. IP addresses) now inherit from a common parent view * [#12180](https://github.com/netbox-community/netbox/issues/12180) - All API endpoints for available objects (e.g. IP addresses) now inherit from a common parent view

View File

View File

@ -1,10 +1,12 @@
# Generated by Django 4.1.10 on 2023-07-25 15:19 # Generated by Django 4.1.10 on 2023-07-30 17:49
from django.db import migrations from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True
dependencies = [ dependencies = [
('users', '0004_netboxgroup_netboxuser'), ('users', '0004_netboxgroup_netboxuser'),
] ]
@ -15,10 +17,10 @@ class Migration(migrations.Migration):
fields=[ fields=[
], ],
options={ options={
'verbose_name': 'token',
'proxy': True, 'proxy': True,
'indexes': [], 'indexes': [],
'constraints': [], 'constraints': [],
'verbose_name': 'token',
}, },
bases=('users.token',), bases=('users.token',),
), ),

View File

15
netbox/account/models.py Normal file
View File

@ -0,0 +1,15 @@
from django.urls import reverse
from users.models import Token
class UserToken(Token):
"""
Proxy model for users to manage their own API tokens.
"""
class Meta:
proxy = True
verbose_name = 'token'
def get_absolute_url(self):
return reverse('account:usertoken', args=[self.pk])

55
netbox/account/tables.py Normal file
View File

@ -0,0 +1,55 @@
from django.utils.translation import gettext as _
from account.models import UserToken
from netbox.tables import NetBoxTable, columns
__all__ = (
'UserTokenTable',
)
TOKEN = """<samp><span id="token_{{ record.pk }}">{{ record }}</span></samp>"""
ALLOWED_IPS = """{{ value|join:", " }}"""
COPY_BUTTON = """
{% if settings.ALLOW_TOKEN_RETRIEVAL %}
{% copy_content record.pk prefix="token_" color="success" %}
{% endif %}
"""
class UserTokenTable(NetBoxTable):
"""
Table for users to manager their own API tokens under account views.
"""
key = columns.TemplateColumn(
verbose_name=_('Key'),
template_code=TOKEN,
)
write_enabled = columns.BooleanColumn(
verbose_name=_('Write Enabled')
)
created = columns.DateColumn(
verbose_name=_('Created'),
)
expires = columns.DateColumn(
verbose_name=_('Expires'),
)
last_used = columns.DateTimeColumn(
verbose_name=_('Last Used'),
)
allowed_ips = columns.TemplateColumn(
verbose_name=_('Allowed IPs'),
template_code=ALLOWED_IPS
)
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
extra_buttons=COPY_BUTTON
)
class Meta(NetBoxTable.Meta):
model = UserToken
fields = (
'pk', 'id', 'key', 'description', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips',
)

View File

@ -13,6 +13,6 @@ urlpatterns = [
path('password/', views.ChangePasswordView.as_view(), name='change_password'), path('password/', views.ChangePasswordView.as_view(), name='change_password'),
path('api-tokens/', views.UserTokenListView.as_view(), name='usertoken_list'), path('api-tokens/', views.UserTokenListView.as_view(), name='usertoken_list'),
path('api-tokens/add/', views.UserTokenEditView.as_view(), name='usertoken_add'), path('api-tokens/add/', views.UserTokenEditView.as_view(), name='usertoken_add'),
path('api-tokens/<int:pk>/', include(get_model_urls('users', 'usertoken'))), path('api-tokens/<int:pk>/', include(get_model_urls('account', 'usertoken'))),
] ]

298
netbox/account/views.py Normal file
View File

@ -0,0 +1,298 @@
import logging
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login as auth_login, logout as auth_logout
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import update_last_login
from django.contrib.auth.signals import user_logged_in
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import render, resolve_url
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import View
from social_core.backends.utils import load_backends
from account.models import UserToken
from extras.models import Bookmark, ObjectChange
from extras.tables import BookmarkTable, ObjectChangeTable
from netbox.authentication import get_auth_backend_display, get_saml_idps
from netbox.config import get_config
from netbox.views import generic
from users import forms, tables
from users.models import UserConfig
from utilities.views import register_model_view
#
# Login/logout
#
class LoginView(View):
"""
Perform user authentication via the web UI.
"""
template_name = 'login.html'
@method_decorator(sensitive_post_parameters('password'))
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def gen_auth_data(self, name, url, params):
display_name, icon_name = get_auth_backend_display(name)
return {
'display_name': display_name,
'icon_name': icon_name,
'url': f'{url}?{urlencode(params)}',
}
def get_auth_backends(self, request):
auth_backends = []
saml_idps = get_saml_idps()
for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys():
url = reverse('social:begin', args=[name])
params = {}
if next := request.GET.get('next'):
params['next'] = next
if name.lower() == 'saml' and saml_idps:
for idp in saml_idps:
params['idp'] = idp
data = self.gen_auth_data(name, url, params)
data['display_name'] = f'{data["display_name"]} ({idp})'
auth_backends.append(data)
else:
auth_backends.append(self.gen_auth_data(name, url, params))
return auth_backends
def get(self, request):
form = forms.LoginForm(request)
if request.user.is_authenticated:
logger = logging.getLogger('netbox.auth.login')
return self.redirect_to_next(request, logger)
return render(request, self.template_name, {
'form': form,
'auth_backends': self.get_auth_backends(request),
})
def post(self, request):
logger = logging.getLogger('netbox.auth.login')
form = forms.LoginForm(request, data=request.POST)
if form.is_valid():
logger.debug("Login form validation was successful")
# If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
# last_login time upon authentication.
if get_config().MAINTENANCE_MODE:
logger.warning("Maintenance mode enabled: disabling update of most recent login time")
user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
# Authenticate user
auth_login(request, form.get_user())
logger.info(f"User {request.user} successfully authenticated")
messages.success(request, f"Logged in as {request.user}.")
# Ensure the user has a UserConfig defined. (This should normally be handled by
# create_userconfig() on user creation.)
if not hasattr(request.user, 'config'):
config = get_config()
UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save()
return self.redirect_to_next(request, logger)
else:
logger.debug(f"Login form validation failed for username: {form['username'].value()}")
return render(request, self.template_name, {
'form': form,
'auth_backends': self.get_auth_backends(request),
})
def redirect_to_next(self, request, logger):
data = request.POST if request.method == "POST" else request.GET
redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None):
logger.debug(f"Redirecting user to {redirect_url}")
else:
if redirect_url:
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_url}")
redirect_url = reverse('home')
return HttpResponseRedirect(redirect_url)
class LogoutView(View):
"""
Deauthenticate a web user.
"""
def get(self, request):
logger = logging.getLogger('netbox.auth.logout')
# Log out the user
username = request.user
auth_logout(request)
logger.info(f"User {username} has logged out")
messages.info(request, "You have logged out.")
# Delete session key cookie (if set) upon logout
response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
response.delete_cookie('session_key')
return response
#
# User profiles
#
class ProfileView(LoginRequiredMixin, View):
template_name = 'account/profile.html'
def get(self, request):
# Compile changelog table
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
user=request.user
).prefetch_related(
'changed_object_type'
)[:20]
changelog_table = ObjectChangeTable(changelog)
return render(request, self.template_name, {
'changelog_table': changelog_table,
'active_tab': 'profile',
})
class UserConfigView(LoginRequiredMixin, View):
template_name = 'account/preferences.html'
def get(self, request):
userconfig = request.user.config
form = forms.UserConfigForm(instance=userconfig)
return render(request, self.template_name, {
'form': form,
'active_tab': 'preferences',
})
def post(self, request):
userconfig = request.user.config
form = forms.UserConfigForm(request.POST, instance=userconfig)
if form.is_valid():
form.save()
messages.success(request, "Your preferences have been updated.")
return redirect('account:preferences')
return render(request, self.template_name, {
'form': form,
'active_tab': 'preferences',
})
class ChangePasswordView(LoginRequiredMixin, View):
template_name = 'account/password.html'
def get(self, request):
# LDAP users cannot change their password here
if getattr(request.user, 'ldap_username', None):
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
return redirect('account:profile')
form = forms.PasswordChangeForm(user=request.user)
return render(request, self.template_name, {
'form': form,
'active_tab': 'password',
})
def post(self, request):
form = forms.PasswordChangeForm(user=request.user, data=request.POST)
if form.is_valid():
form.save()
update_session_auth_hash(request, form.user)
messages.success(request, "Your password has been changed successfully.")
return redirect('account:profile')
return render(request, self.template_name, {
'form': form,
'active_tab': 'change_password',
})
#
# Bookmarks
#
class BookmarkListView(LoginRequiredMixin, generic.ObjectListView):
table = BookmarkTable
template_name = 'account/bookmarks.html'
def get_queryset(self, request):
return Bookmark.objects.filter(user=request.user)
def get_extra_context(self, request):
return {
'active_tab': 'bookmarks',
}
#
# User views for token management
#
class UserTokenListView(LoginRequiredMixin, View):
def get(self, request):
tokens = UserToken.objects.filter(user=request.user)
table = tables.UserTokenTable(tokens)
table.configure(request)
return render(request, 'account/token_list.html', {
'tokens': tokens,
'active_tab': 'api-tokens',
'table': table,
})
@register_model_view(UserToken)
class UserTokenView(LoginRequiredMixin, View):
def get(self, request, pk):
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
key = token.key if settings.ALLOW_TOKEN_RETRIEVAL else None
return render(request, 'account/token.html', {
'object': token,
'key': key,
})
@register_model_view(UserToken, 'edit')
class UserTokenEditView(generic.ObjectEditView):
queryset = UserToken.objects.all()
form = forms.UserTokenForm
default_return_url = 'account:usertoken_list'
def alter_object(self, obj, request, url_args, url_kwargs):
if not obj.pk:
obj.user = request.user
return obj
@register_model_view(UserToken, 'delete')
class UserTokenDeleteView(generic.ObjectDeleteView):
queryset = UserToken.objects.all()
default_return_url = 'account:usertoken_list'

View File

@ -1236,6 +1236,10 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
choices=PowerFeedPhaseChoices, choices=PowerFeedPhaseChoices,
default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE, default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE,
) )
tenant = NestedTenantSerializer(
required=False,
allow_null=True
)
class Meta: class Meta:
model = PowerFeed model = PowerFeed
@ -1243,5 +1247,5 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', 'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
] ]

View File

@ -6,7 +6,6 @@ from netaddr import AddrFormatError, EUI, eui64_unix_expanded, mac_unix_expanded
from .lookups import PathContains from .lookups import PathContains
__all__ = ( __all__ = (
'ASNField',
'MACAddressField', 'MACAddressField',
'PathField', 'PathField',
'WWNField', 'WWNField',

View File

@ -1880,7 +1880,7 @@ class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet): class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet, TenancyFilterSet):
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='power_panel__site__region', field_name='power_panel__site__region',

View File

@ -854,6 +854,10 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
required=False, required=False,
widget=BulkEditNullBooleanSelect widget=BulkEditNullBooleanSelect
) )
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
required=False
)
description = forms.CharField( description = forms.CharField(
label=_('Description'), label=_('Description'),
max_length=200, max_length=200,
@ -865,10 +869,10 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
model = PowerFeed model = PowerFeed
fieldsets = ( fieldsets = (
(None, ('power_panel', 'rack', 'status', 'type', 'mark_connected', 'description')), (None, ('power_panel', 'rack', 'status', 'type', 'mark_connected', 'description', 'tenant')),
(_('Power'), ('supply', 'phase', 'voltage', 'amperage', 'max_utilization')) (_('Power'), ('supply', 'phase', 'voltage', 'amperage', 'max_utilization'))
) )
nullable_fields = ('location', 'description', 'comments') nullable_fields = ('location', 'tenant', 'description', 'comments')
# #

View File

@ -1291,6 +1291,12 @@ class PowerFeedImportForm(NetBoxModelImportForm):
required=False, required=False,
help_text=_('Rack') help_text=_('Rack')
) )
tenant = CSVModelChoiceField(
queryset=Tenant.objects.all(),
to_field_name='name',
required=False,
help_text=_('Assigned tenant')
)
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'), label=_('Status'),
choices=PowerFeedStatusChoices, choices=PowerFeedStatusChoices,
@ -1316,7 +1322,7 @@ class PowerFeedImportForm(NetBoxModelImportForm):
model = PowerFeed model = PowerFeed
fields = ( fields = (
'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
'voltage', 'amperage', 'max_utilization', 'description', 'comments', 'tags', 'voltage', 'amperage', 'max_utilization', 'tenant', 'description', 'comments', 'tags',
) )
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):

View File

@ -1020,11 +1020,12 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class PowerFeedFilterForm(NetBoxModelFilterSetForm): class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = PowerFeed model = PowerFeed
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
(_('Attributes'), ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')), (_('Attributes'), ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(

View File

@ -675,7 +675,7 @@ class PowerPanelForm(NetBoxModelForm):
] ]
class PowerFeedForm(NetBoxModelForm): class PowerFeedForm(TenancyForm, NetBoxModelForm):
power_panel = DynamicModelChoiceField( power_panel = DynamicModelChoiceField(
label=_('Power panel'), label=_('Power panel'),
queryset=PowerPanel.objects.all(), queryset=PowerPanel.objects.all(),
@ -694,13 +694,14 @@ class PowerFeedForm(NetBoxModelForm):
fieldsets = ( fieldsets = (
(_('Power Feed'), ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')), (_('Power Feed'), ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
(_('Characteristics'), ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')), (_('Characteristics'), ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
(_('Tenancy'), ('tenant_group', 'tenant')),
) )
class Meta: class Meta:
model = PowerFeed model = PowerFeed
fields = [ fields = [
'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
'max_utilization', 'description', 'comments', 'tags', 'max_utilization', 'tenant_group', 'tenant', 'description', 'comments', 'tags'
] ]

View File

@ -53,23 +53,23 @@ class LinkPeerType(graphene.Union):
@classmethod @classmethod
def resolve_type(cls, instance, info): def resolve_type(cls, instance, info):
if type(instance) == CircuitTermination: if type(instance) is CircuitTermination:
return CircuitTerminationType return CircuitTerminationType
if type(instance) == ConsolePortType: if type(instance) is ConsolePortType:
return ConsolePortType return ConsolePortType
if type(instance) == ConsoleServerPort: if type(instance) is ConsoleServerPort:
return ConsoleServerPortType return ConsoleServerPortType
if type(instance) == FrontPort: if type(instance) is FrontPort:
return FrontPortType return FrontPortType
if type(instance) == Interface: if type(instance) is Interface:
return InterfaceType return InterfaceType
if type(instance) == PowerFeed: if type(instance) is PowerFeed:
return PowerFeedType return PowerFeedType
if type(instance) == PowerOutlet: if type(instance) is PowerOutlet:
return PowerOutletType return PowerOutletType
if type(instance) == PowerPort: if type(instance) is PowerPort:
return PowerPortType return PowerPortType
if type(instance) == RearPort: if type(instance) is RearPort:
return RearPortType return RearPortType
@ -89,23 +89,23 @@ class CableTerminationTerminationType(graphene.Union):
@classmethod @classmethod
def resolve_type(cls, instance, info): def resolve_type(cls, instance, info):
if type(instance) == CircuitTermination: if type(instance) is CircuitTermination:
return CircuitTerminationType return CircuitTerminationType
if type(instance) == ConsolePortType: if type(instance) is ConsolePortType:
return ConsolePortType return ConsolePortType
if type(instance) == ConsoleServerPort: if type(instance) is ConsoleServerPort:
return ConsoleServerPortType return ConsoleServerPortType
if type(instance) == FrontPort: if type(instance) is FrontPort:
return FrontPortType return FrontPortType
if type(instance) == Interface: if type(instance) is Interface:
return InterfaceType return InterfaceType
if type(instance) == PowerFeed: if type(instance) is PowerFeed:
return PowerFeedType return PowerFeedType
if type(instance) == PowerOutlet: if type(instance) is PowerOutlet:
return PowerOutletType return PowerOutletType
if type(instance) == PowerPort: if type(instance) is PowerPort:
return PowerPortType return PowerPortType
if type(instance) == RearPort: if type(instance) is RearPort:
return RearPortType return RearPortType
@ -123,19 +123,19 @@ class InventoryItemTemplateComponentType(graphene.Union):
@classmethod @classmethod
def resolve_type(cls, instance, info): def resolve_type(cls, instance, info):
if type(instance) == ConsolePortTemplate: if type(instance) is ConsolePortTemplate:
return ConsolePortTemplateType return ConsolePortTemplateType
if type(instance) == ConsoleServerPortTemplate: if type(instance) is ConsoleServerPortTemplate:
return ConsoleServerPortTemplateType return ConsoleServerPortTemplateType
if type(instance) == FrontPortTemplate: if type(instance) is FrontPortTemplate:
return FrontPortTemplateType return FrontPortTemplateType
if type(instance) == InterfaceTemplate: if type(instance) is InterfaceTemplate:
return InterfaceTemplateType return InterfaceTemplateType
if type(instance) == PowerOutletTemplate: if type(instance) is PowerOutletTemplate:
return PowerOutletTemplateType return PowerOutletTemplateType
if type(instance) == PowerPortTemplate: if type(instance) is PowerPortTemplate:
return PowerPortTemplateType return PowerPortTemplateType
if type(instance) == RearPortTemplate: if type(instance) is RearPortTemplate:
return RearPortTemplateType return RearPortTemplateType
@ -153,17 +153,17 @@ class InventoryItemComponentType(graphene.Union):
@classmethod @classmethod
def resolve_type(cls, instance, info): def resolve_type(cls, instance, info):
if type(instance) == ConsolePort: if type(instance) is ConsolePort:
return ConsolePortType return ConsolePortType
if type(instance) == ConsoleServerPort: if type(instance) is ConsoleServerPort:
return ConsoleServerPortType return ConsoleServerPortType
if type(instance) == FrontPort: if type(instance) is FrontPort:
return FrontPortType return FrontPortType
if type(instance) == Interface: if type(instance) is Interface:
return InterfaceType return InterfaceType
if type(instance) == PowerOutlet: if type(instance) is PowerOutlet:
return PowerOutletType return PowerOutletType
if type(instance) == PowerPort: if type(instance) is PowerPort:
return PowerPortType return PowerPortType
if type(instance) == RearPort: if type(instance) is RearPort:
return RearPortType return RearPortType

View File

@ -0,0 +1,20 @@
# Generated by Django 4.1.8 on 2023-07-29 11:29
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0010_tenant_relax_uniqueness'),
('dcim', '0179_interfacetemplate_rf_role'),
]
operations = [
migrations.AddField(
model_name='powerfeed',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='power_feeds', to='tenancy.tenant'),
),
]

View File

@ -131,10 +131,17 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
default=0, default=0,
editable=False editable=False
) )
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='power_feeds',
blank=True,
null=True
)
clone_fields = ( clone_fields = (
'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
'max_utilization', 'max_utilization', 'tenant',
) )
prerequisite_models = ( prerequisite_models = (
'dcim.PowerPanel', 'dcim.PowerPanel',

View File

@ -1,6 +1,6 @@
import django_tables2 as tables import django_tables2 as tables
from dcim.models import PowerFeed, PowerPanel from dcim.models import PowerFeed, PowerPanel
from tenancy.tables import ContactsColumnMixin from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
@ -51,7 +51,7 @@ class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
# We're not using PathEndpointTable for PowerFeed because power connections # We're not using PathEndpointTable for PowerFeed because power connections
# cannot traverse pass-through ports. # cannot traverse pass-through ports.
class PowerFeedTable(CableTerminationTable): class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
@ -69,6 +69,9 @@ class PowerFeedTable(CableTerminationTable):
available_power = tables.Column( available_power = tables.Column(
verbose_name='Available power (VA)' verbose_name='Available power (VA)'
) )
tenant = tables.Column(
linkify=True
)
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:powerfeed_list' url_name='dcim:powerfeed_list'
@ -78,8 +81,8 @@ class PowerFeedTable(CableTerminationTable):
model = PowerFeed model = PowerFeed
fields = ( fields = (
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power', 'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power', 'tenant',
'description', 'comments', 'tags', 'created', 'last_updated', 'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable', 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',

View File

@ -4419,6 +4419,21 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
) )
Rack.objects.bulk_create(racks) Rack.objects.bulk_create(racks)
tenant_groups = (
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
)
for tenantgroup in tenant_groups:
tenantgroup.save()
tenants = (
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
)
Tenant.objects.bulk_create(tenants)
power_panels = ( power_panels = (
PowerPanel(name='Power Panel 1', site=sites[0]), PowerPanel(name='Power Panel 1', site=sites[0]),
PowerPanel(name='Power Panel 2', site=sites[1]), PowerPanel(name='Power Panel 2', site=sites[1]),
@ -4427,9 +4442,44 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
PowerPanel.objects.bulk_create(power_panels) PowerPanel.objects.bulk_create(power_panels)
power_feeds = ( power_feeds = (
PowerFeed(power_panel=power_panels[0], rack=racks[0], name='Power Feed 1', status=PowerFeedStatusChoices.STATUS_ACTIVE, type=PowerFeedTypeChoices.TYPE_PRIMARY, supply=PowerFeedSupplyChoices.SUPPLY_AC, phase=PowerFeedPhaseChoices.PHASE_3PHASE, voltage=100, amperage=100, max_utilization=10), PowerFeed(
PowerFeed(power_panel=power_panels[1], rack=racks[1], name='Power Feed 2', status=PowerFeedStatusChoices.STATUS_FAILED, type=PowerFeedTypeChoices.TYPE_PRIMARY, supply=PowerFeedSupplyChoices.SUPPLY_AC, phase=PowerFeedPhaseChoices.PHASE_3PHASE, voltage=200, amperage=200, max_utilization=20), power_panel=power_panels[0],
PowerFeed(power_panel=power_panels[2], rack=racks[2], name='Power Feed 3', status=PowerFeedStatusChoices.STATUS_OFFLINE, type=PowerFeedTypeChoices.TYPE_REDUNDANT, supply=PowerFeedSupplyChoices.SUPPLY_DC, phase=PowerFeedPhaseChoices.PHASE_SINGLE, voltage=300, amperage=300, max_utilization=30), rack=racks[0],
name='Power Feed 1',
tenant=tenants[0],
status=PowerFeedStatusChoices.STATUS_ACTIVE,
type=PowerFeedTypeChoices.TYPE_PRIMARY,
supply=PowerFeedSupplyChoices.SUPPLY_AC,
phase=PowerFeedPhaseChoices.PHASE_3PHASE,
voltage=100,
amperage=100,
max_utilization=10
),
PowerFeed(
power_panel=power_panels[1],
rack=racks[1],
name='Power Feed 2',
tenant=tenants[1],
status=PowerFeedStatusChoices.STATUS_FAILED,
type=PowerFeedTypeChoices.TYPE_PRIMARY,
supply=PowerFeedSupplyChoices.SUPPLY_AC,
phase=PowerFeedPhaseChoices.PHASE_3PHASE,
voltage=200,
amperage=200,
max_utilization=20),
PowerFeed(
power_panel=power_panels[2],
rack=racks[2],
name='Power Feed 3',
tenant=tenants[2],
status=PowerFeedStatusChoices.STATUS_OFFLINE,
type=PowerFeedTypeChoices.TYPE_REDUNDANT,
supply=PowerFeedSupplyChoices.SUPPLY_DC,
phase=PowerFeedPhaseChoices.PHASE_SINGLE,
voltage=300,
amperage=300,
max_utilization=30
),
) )
PowerFeed.objects.bulk_create(power_feeds) PowerFeed.objects.bulk_create(power_feeds)
@ -4520,6 +4570,20 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'connected': False} params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_tenant(self):
tenants = Tenant.objects.all()[:2]
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant_group(self):
tenant_groups = TenantGroup.objects.all()[:2]
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests): class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualDeviceContext.objects.all() queryset = VirtualDeviceContext.objects.all()

View File

@ -131,12 +131,16 @@ class CustomFieldSerializer(ValidatedModelSerializer):
class CustomFieldChoiceSetSerializer(ValidatedModelSerializer): class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail') url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
base_choices = ChoiceField(
choices=CustomFieldChoiceSetBaseChoices,
required=False
)
class Meta: class Meta:
model = CustomFieldChoiceSet model = CustomFieldChoiceSet
fields = [ fields = [
'id', 'url', 'display', 'name', 'description', 'extra_choices', 'order_alphabetically', 'choices_count', 'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
'created', 'last_updated', 'choices_count', 'created', 'last_updated',
] ]

View File

@ -1,5 +1,6 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404
from django_rq.queues import get_connection from django_rq.queues import get_connection
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
@ -63,6 +64,26 @@ class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
serializer_class = serializers.CustomFieldChoiceSetSerializer serializer_class = serializers.CustomFieldChoiceSetSerializer
filterset_class = filtersets.CustomFieldChoiceSetFilterSet filterset_class = filtersets.CustomFieldChoiceSetFilterSet
@action(detail=True)
def choices(self, request, pk):
"""
Provides an endpoint to iterate through each choice in a set.
"""
choiceset = get_object_or_404(self.queryset, pk=pk)
choices = choiceset.choices
# Enable filtering
if q := request.GET.get('q'):
q = q.lower()
choices = [c for c in choices if q in c[0].lower() or q in c[1].lower()]
# Paginate data
if page := self.paginate_queryset(choices):
data = [
{'value': c[0], 'label': c[1]} for c in page
]
return self.get_paginated_response(data)
# #
# Custom links # Custom links

View File

@ -66,6 +66,19 @@ class CustomFieldVisibilityChoices(ChoiceSet):
) )
class CustomFieldChoiceSetBaseChoices(ChoiceSet):
IATA = 'IATA'
ISO_3166 = 'ISO_3166'
UN_LOCODE = 'UN_LOCODE'
CHOICES = (
(IATA, 'IATA (Airport codes)'),
(ISO_3166, 'ISO 3166 (Country codes)'),
(UN_LOCODE, 'UN/LOCODE (Location codes)'),
)
# #
# CustomLinks # CustomLinks
# #

View File

@ -0,0 +1,9 @@
from .iata import IATA
from .iso_3166 import ISO_3166
from .un_locode import UN_LOCODE
CHOICE_SETS = {
'IATA': IATA,
'ISO_3166': ISO_3166,
'UN_LOCODE': UN_LOCODE,
}

9768
netbox/extras/data/iata.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,253 @@
# Two-letter country codes defined by ISO 3166
# Source: https://datahub.io/core/country-list
ISO_3166 = [
('AD', 'AD (Andorra)'),
('AE', 'AE (United Arab Emirates)'),
('AF', 'AF (Afghanistan)'),
('AG', 'AG (Antigua and Barbuda)'),
('AI', 'AI (Anguilla)'),
('AL', 'AL (Albania)'),
('AM', 'AM (Armenia)'),
('AO', 'AO (Angola)'),
('AQ', 'AQ (Antarctica)'),
('AR', 'AR (Argentina)'),
('AS', 'AS (American Samoa)'),
('AT', 'AT (Austria)'),
('AU', 'AU (Australia)'),
('AW', 'AW (Aruba)'),
('AX', 'AX (Åland Islands)'),
('AZ', 'AZ (Azerbaijan)'),
('BA', 'BA (Bosnia and Herzegovina)'),
('BB', 'BB (Barbados)'),
('BD', 'BD (Bangladesh)'),
('BE', 'BE (Belgium)'),
('BF', 'BF (Burkina Faso)'),
('BG', 'BG (Bulgaria)'),
('BH', 'BH (Bahrain)'),
('BI', 'BI (Burundi)'),
('BJ', 'BJ (Benin)'),
('BL', 'BL (Saint Barthélemy)'),
('BM', 'BM (Bermuda)'),
('BN', 'BN (Brunei Darussalam)'),
('BO', 'BO (Bolivia, Plurinational State of)'),
('BQ', 'BQ (Bonaire, Sint Eustatius and Saba)'),
('BR', 'BR (Brazil)'),
('BS', 'BS (Bahamas)'),
('BT', 'BT (Bhutan)'),
('BV', 'BV (Bouvet Island)'),
('BW', 'BW (Botswana)'),
('BY', 'BY (Belarus)'),
('BZ', 'BZ (Belize)'),
('CA', 'CA (Canada)'),
('CC', 'CC (Cocos (Keeling) Islands)'),
('CD', 'CD (Congo, the Democratic Republic of the)'),
('CF', 'CF (Central African Republic)'),
('CG', 'CG (Congo)'),
('CH', 'CH (Switzerland)'),
('CI', "CI (Côte d'Ivoire)"),
('CK', 'CK (Cook Islands)'),
('CL', 'CL (Chile)'),
('CM', 'CM (Cameroon)'),
('CN', 'CN (China)'),
('CO', 'CO (Colombia)'),
('CR', 'CR (Costa Rica)'),
('CU', 'CU (Cuba)'),
('CV', 'CV (Cape Verde)'),
('CW', 'CW (Curaçao)'),
('CX', 'CX (Christmas Island)'),
('CY', 'CY (Cyprus)'),
('CZ', 'CZ (Czech Republic)'),
('DE', 'DE (Germany)'),
('DJ', 'DJ (Djibouti)'),
('DK', 'DK (Denmark)'),
('DM', 'DM (Dominica)'),
('DO', 'DO (Dominican Republic)'),
('DZ', 'DZ (Algeria)'),
('EC', 'EC (Ecuador)'),
('EE', 'EE (Estonia)'),
('EG', 'EG (Egypt)'),
('EH', 'EH (Western Sahara)'),
('ER', 'ER (Eritrea)'),
('ES', 'ES (Spain)'),
('ET', 'ET (Ethiopia)'),
('FI', 'FI (Finland)'),
('FJ', 'FJ (Fiji)'),
('FK', 'FK (Falkland Islands (Malvinas))'),
('FM', 'FM (Micronesia, Federated States of)'),
('FO', 'FO (Faroe Islands)'),
('FR', 'FR (France)'),
('GA', 'GA (Gabon)'),
('GB', 'GB (United Kingdom)'),
('GD', 'GD (Grenada)'),
('GE', 'GE (Georgia)'),
('GF', 'GF (French Guiana)'),
('GG', 'GG (Guernsey)'),
('GH', 'GH (Ghana)'),
('GI', 'GI (Gibraltar)'),
('GL', 'GL (Greenland)'),
('GM', 'GM (Gambia)'),
('GN', 'GN (Guinea)'),
('GP', 'GP (Guadeloupe)'),
('GQ', 'GQ (Equatorial Guinea)'),
('GR', 'GR (Greece)'),
('GS', 'GS (South Georgia and the South Sandwich Islands)'),
('GT', 'GT (Guatemala)'),
('GU', 'GU (Guam)'),
('GW', 'GW (Guinea-Bissau)'),
('GY', 'GY (Guyana)'),
('HK', 'HK (Hong Kong)'),
('HM', 'HM (Heard Island and McDonald Islands)'),
('HN', 'HN (Honduras)'),
('HR', 'HR (Croatia)'),
('HT', 'HT (Haiti)'),
('HU', 'HU (Hungary)'),
('ID', 'ID (Indonesia)'),
('IE', 'IE (Ireland)'),
('IL', 'IL (Israel)'),
('IM', 'IM (Isle of Man)'),
('IN', 'IN (India)'),
('IO', 'IO (British Indian Ocean Territory)'),
('IQ', 'IQ (Iraq)'),
('IR', 'IR (Iran, Islamic Republic of)'),
('IS', 'IS (Iceland)'),
('IT', 'IT (Italy)'),
('JE', 'JE (Jersey)'),
('JM', 'JM (Jamaica)'),
('JO', 'JO (Jordan)'),
('JP', 'JP (Japan)'),
('KE', 'KE (Kenya)'),
('KG', 'KG (Kyrgyzstan)'),
('KH', 'KH (Cambodia)'),
('KI', 'KI (Kiribati)'),
('KM', 'KM (Comoros)'),
('KN', 'KN (Saint Kitts and Nevis)'),
('KP', "KP (Korea, Democratic People's Republic of)"),
('KR', 'KR (Korea, Republic of)'),
('KW', 'KW (Kuwait)'),
('KY', 'KY (Cayman Islands)'),
('KZ', 'KZ (Kazakhstan)'),
('LA', "LA (Lao People's Democratic Republic)"),
('LB', 'LB (Lebanon)'),
('LC', 'LC (Saint Lucia)'),
('LI', 'LI (Liechtenstein)'),
('LK', 'LK (Sri Lanka)'),
('LR', 'LR (Liberia)'),
('LS', 'LS (Lesotho)'),
('LT', 'LT (Lithuania)'),
('LU', 'LU (Luxembourg)'),
('LV', 'LV (Latvia)'),
('LY', 'LY (Libya)'),
('MA', 'MA (Morocco)'),
('MC', 'MC (Monaco)'),
('MD', 'MD (Moldova, Republic of)'),
('ME', 'ME (Montenegro)'),
('MF', 'MF (Saint Martin (French part))'),
('MG', 'MG (Madagascar)'),
('MH', 'MH (Marshall Islands)'),
('MK', 'MK (Macedonia, the Former Yugoslav Republic of)'),
('ML', 'ML (Mali)'),
('MM', 'MM (Myanmar)'),
('MN', 'MN (Mongolia)'),
('MO', 'MO (Macao)'),
('MP', 'MP (Northern Mariana Islands)'),
('MQ', 'MQ (Martinique)'),
('MR', 'MR (Mauritania)'),
('MS', 'MS (Montserrat)'),
('MT', 'MT (Malta)'),
('MU', 'MU (Mauritius)'),
('MV', 'MV (Maldives)'),
('MW', 'MW (Malawi)'),
('MX', 'MX (Mexico)'),
('MY', 'MY (Malaysia)'),
('MZ', 'MZ (Mozambique)'),
('NA', 'NA (Namibia)'),
('NC', 'NC (New Caledonia)'),
('NE', 'NE (Niger)'),
('NF', 'NF (Norfolk Island)'),
('NG', 'NG (Nigeria)'),
('NI', 'NI (Nicaragua)'),
('NL', 'NL (Netherlands)'),
('NO', 'NO (Norway)'),
('NP', 'NP (Nepal)'),
('NR', 'NR (Nauru)'),
('NU', 'NU (Niue)'),
('NZ', 'NZ (New Zealand)'),
('OM', 'OM (Oman)'),
('PA', 'PA (Panama)'),
('PE', 'PE (Peru)'),
('PF', 'PF (French Polynesia)'),
('PG', 'PG (Papua New Guinea)'),
('PH', 'PH (Philippines)'),
('PK', 'PK (Pakistan)'),
('PL', 'PL (Poland)'),
('PM', 'PM (Saint Pierre and Miquelon)'),
('PN', 'PN (Pitcairn)'),
('PR', 'PR (Puerto Rico)'),
('PS', 'PS (Palestine, State of)'),
('PT', 'PT (Portugal)'),
('PW', 'PW (Palau)'),
('PY', 'PY (Paraguay)'),
('QA', 'QA (Qatar)'),
('RE', 'RE (Réunion)'),
('RO', 'RO (Romania)'),
('RS', 'RS (Serbia)'),
('RU', 'RU (Russian Federation)'),
('RW', 'RW (Rwanda)'),
('SA', 'SA (Saudi Arabia)'),
('SB', 'SB (Solomon Islands)'),
('SC', 'SC (Seychelles)'),
('SD', 'SD (Sudan)'),
('SE', 'SE (Sweden)'),
('SG', 'SG (Singapore)'),
('SH', 'SH (Saint Helena, Ascension and Tristan da Cunha)'),
('SI', 'SI (Slovenia)'),
('SJ', 'SJ (Svalbard and Jan Mayen)'),
('SK', 'SK (Slovakia)'),
('SL', 'SL (Sierra Leone)'),
('SM', 'SM (San Marino)'),
('SN', 'SN (Senegal)'),
('SO', 'SO (Somalia)'),
('SR', 'SR (Suriname)'),
('SS', 'SS (South Sudan)'),
('ST', 'ST (Sao Tome and Principe)'),
('SV', 'SV (El Salvador)'),
('SX', 'SX (Sint Maarten (Dutch part))'),
('SY', 'SY (Syrian Arab Republic)'),
('SZ', 'SZ (Swaziland)'),
('TC', 'TC (Turks and Caicos Islands)'),
('TD', 'TD (Chad)'),
('TF', 'TF (French Southern Territories)'),
('TG', 'TG (Togo)'),
('TH', 'TH (Thailand)'),
('TJ', 'TJ (Tajikistan)'),
('TK', 'TK (Tokelau)'),
('TL', 'TL (Timor-Leste)'),
('TM', 'TM (Turkmenistan)'),
('TN', 'TN (Tunisia)'),
('TO', 'TO (Tonga)'),
('TR', 'TR (Turkey)'),
('TT', 'TT (Trinidad and Tobago)'),
('TV', 'TV (Tuvalu)'),
('TW', 'TW (Taiwan, Province of China)'),
('TZ', 'TZ (Tanzania, United Republic of)'),
('UA', 'UA (Ukraine)'),
('UG', 'UG (Uganda)'),
('UM', 'UM (United States Minor Outlying Islands)'),
('US', 'US (United States)'),
('UY', 'UY (Uruguay)'),
('UZ', 'UZ (Uzbekistan)'),
('VA', 'VA (Holy See (Vatican City State))'),
('VC', 'VC (Saint Vincent and the Grenadines)'),
('VE', 'VE (Venezuela, Bolivarian Republic of)'),
('VG', 'VG (Virgin Islands, British)'),
('VI', 'VI (Virgin Islands, U.S.)'),
('VN', 'VN (Viet Nam)'),
('VU', 'VU (Vanuatu)'),
('WF', 'WF (Wallis and Futuna)'),
('WS', 'WS (Samoa)'),
('YE', 'YE (Yemen)'),
('YT', 'YT (Mayotte)'),
('ZA', 'ZA (South Africa)'),
('ZM', 'ZM (Zambia)'),
('ZW', 'ZW (Zimbabwe)')
]

111557
netbox/extras/data/un_locode.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -114,7 +114,7 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
class Meta: class Meta:
model = CustomFieldChoiceSet model = CustomFieldChoiceSet
fields = [ fields = [
'id', 'name', 'description', 'order_alphabetically', 'id', 'name', 'description', 'base_choices', 'order_alphabetically',
] ]
def search(self, queryset, name, value): def search(self, queryset, name, value):

View File

@ -68,6 +68,10 @@ class CustomFieldChoiceSetBulkEditForm(BulkEditForm):
queryset=CustomFieldChoiceSet.objects.all(), queryset=CustomFieldChoiceSet.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
) )
base_choices = forms.ChoiceField(
choices=add_blank_choice(CustomFieldChoiceSetBaseChoices),
required=False
)
description = forms.CharField( description = forms.CharField(
required=False required=False
) )
@ -76,7 +80,7 @@ class CustomFieldChoiceSetBulkEditForm(BulkEditForm):
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
nullable_fields = ('description',) nullable_fields = ('base_choices', 'description')
class CustomLinkBulkEditForm(BulkEditForm): class CustomLinkBulkEditForm(BulkEditForm):

View File

@ -4,7 +4,7 @@ from django.contrib.postgres.forms import SimpleArrayField
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from extras.choices import CustomFieldVisibilityChoices, CustomFieldTypeChoices, JournalEntryKindChoices from extras.choices import *
from extras.models import * from extras.models import *
from extras.utils import FeatureQuery from extras.utils import FeatureQuery
from netbox.forms import NetBoxModelImportForm from netbox.forms import NetBoxModelImportForm
@ -68,6 +68,11 @@ class CustomFieldImportForm(CSVModelForm):
class CustomFieldChoiceSetImportForm(CSVModelForm): class CustomFieldChoiceSetImportForm(CSVModelForm):
base_choices = CSVChoiceField(
choices=CustomFieldChoiceSetBaseChoices,
required=False,
help_text=_('The base set of predefined choices to use (if any)')
)
extra_choices = SimpleArrayField( extra_choices = SimpleArrayField(
base_field=forms.CharField(), base_field=forms.CharField(),
required=False, required=False,

View File

@ -11,7 +11,9 @@ from extras.utils import FeatureQuery
from netbox.forms.base import NetBoxModelFilterSetForm from netbox.forms.base import NetBoxModelFilterSetForm
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField from utilities.forms.fields import (
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
)
from utilities.forms.widgets import APISelectMultiple, DateTimePicker from utilities.forms.widgets import APISelectMultiple, DateTimePicker
from virtualization.models import Cluster, ClusterGroup, ClusterType from virtualization.models import Cluster, ClusterGroup, ClusterType
from .mixins import SavedFiltersMixin from .mixins import SavedFiltersMixin
@ -88,7 +90,12 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm): class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'choice')), (None, ('q', 'filter_id')),
(_('Choices'), ('base_choices', 'choice')),
)
base_choices = forms.MultipleChoiceField(
choices=CustomFieldChoiceSetBaseChoices,
required=False
) )
choice = forms.CharField( choice = forms.CharField(
required=False required=False

View File

@ -19,7 +19,7 @@ from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, JSONField, SlugField, DynamicModelMultipleChoiceField, JSONField, SlugField,
) )
from utilities.forms.widgets import ArrayWidget from utilities.forms.widgets import ChoicesWidget
from virtualization.models import Cluster, ClusterGroup, ClusterType from virtualization.models import Cluster, ClusterGroup, ClusterType
@ -88,16 +88,22 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm): class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
extra_choices = forms.CharField( extra_choices = forms.CharField(
widget=ArrayWidget(), widget=ChoicesWidget(),
help_text=_('Enter one choice per line.')
) )
class Meta: class Meta:
model = CustomFieldChoiceSet model = CustomFieldChoiceSet
fields = ('name', 'description', 'extra_choices', 'order_alphabetically') fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically')
def clean_extra_choices(self): def clean_extra_choices(self):
return self.cleaned_data['extra_choices'].splitlines() data = []
for line in self.cleaned_data['extra_choices'].splitlines():
try:
value, label = line.split(',', maxsplit=1)
except ValueError:
value, label = line, line
data.append((value, label))
return data
class CustomLinkForm(BootstrapMixin, forms.ModelForm): class CustomLinkForm(BootstrapMixin, forms.ModelForm):

View File

@ -19,7 +19,7 @@ def create_choice_sets(apps, schema_editor):
for cf in choice_fields: for cf in choice_fields:
choiceset = CustomFieldChoiceSet.objects.create( choiceset = CustomFieldChoiceSet.objects.create(
name=f'{cf.name} Choices', name=f'{cf.name} Choices',
extra_choices=cf.choices extra_choices=tuple(zip(cf.choices, cf.choices)) # Convert list to tuple of two-tuples
) )
cf.choice_set = choiceset cf.choice_set = choiceset
@ -42,7 +42,8 @@ class Migration(migrations.Migration):
('last_updated', models.DateTimeField(auto_now=True, null=True)), ('last_updated', models.DateTimeField(auto_now=True, null=True)),
('name', models.CharField(max_length=100, unique=True)), ('name', models.CharField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=200)), ('description', models.CharField(blank=True, max_length=200)),
('extra_choices', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), size=None)), ('base_choices', models.CharField(blank=True, max_length=50)),
('extra_choices', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), size=2), blank=True, null=True, size=None)),
('order_alphabetically', models.BooleanField(default=False)), ('order_alphabetically', models.BooleanField(default=False)),
], ],
options={ options={

View File

@ -15,17 +15,18 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from extras.choices import * from extras.choices import *
from extras.data import CHOICE_SETS
from extras.utils import FeatureQuery from extras.utils import FeatureQuery
from netbox.models import ChangeLoggedModel from netbox.models import ChangeLoggedModel
from netbox.models.features import CloningMixin, ExportTemplatesMixin from netbox.models.features import CloningMixin, ExportTemplatesMixin
from netbox.search import FieldTypes from netbox.search import FieldTypes
from utilities import filters from utilities import filters
from utilities.forms.fields import ( from utilities.forms.fields import (
CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DynamicModelChoiceField, CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DynamicChoiceField,
DynamicModelMultipleChoiceField, JSONField, LaxURLField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, DynamicMultipleChoiceField, JSONField, LaxURLField,
) )
from utilities.forms.utils import add_blank_choice from utilities.forms.utils import add_blank_choice
from utilities.forms.widgets import DatePicker, DateTimePicker from utilities.forms.widgets import APISelect, APISelectMultiple, DatePicker, DateTimePicker
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.validators import validate_regex from utilities.validators import validate_regex
@ -410,7 +411,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Select # Select
elif self.type in (CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT): elif self.type in (CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT):
choices = [(c, c) for c in self.choices] choices = self.choice_set.choices
default_choice = self.default if self.default in self.choices else None default_choice = self.default if self.default in self.choices else None
if not required or default_choice is None: if not required or default_choice is None:
@ -421,11 +422,17 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
initial = default_choice initial = default_choice
if self.type == CustomFieldTypeChoices.TYPE_SELECT: if self.type == CustomFieldTypeChoices.TYPE_SELECT:
field_class = CSVChoiceField if for_csv_import else forms.ChoiceField field_class = CSVChoiceField if for_csv_import else DynamicChoiceField
field = field_class(choices=choices, required=required, initial=initial) widget_class = APISelect
else: else:
field_class = CSVMultipleChoiceField if for_csv_import else forms.MultipleChoiceField field_class = CSVMultipleChoiceField if for_csv_import else DynamicMultipleChoiceField
field = field_class(choices=choices, required=required, initial=initial) widget_class = APISelectMultiple
field = field_class(
choices=choices,
required=required,
initial=initial,
widget=widget_class(api_url=f'/api/extras/custom-field-choices/{self.choice_set.pk}/choices/')
)
# URL # URL
elif self.type == CustomFieldTypeChoices.TYPE_URL: elif self.type == CustomFieldTypeChoices.TYPE_URL:
@ -604,14 +611,14 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Validate selected choice # Validate selected choice
elif self.type == CustomFieldTypeChoices.TYPE_SELECT: elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
if value not in self.choices: if value not in [c[0] for c in self.choices]:
raise ValidationError( raise ValidationError(
f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}" f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}"
) )
# Validate all selected choices # Validate all selected choices
elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT: elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
if not set(value).issubset(self.choices): if not set(value).issubset([c[0] for c in self.choices]):
raise ValidationError( raise ValidationError(
f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}" f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}"
) )
@ -645,13 +652,23 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
max_length=200, max_length=200,
blank=True blank=True
) )
base_choices = models.CharField(
max_length=50,
choices=CustomFieldChoiceSetBaseChoices,
blank=True,
help_text=_('Base set of predefined choices (optional)')
)
extra_choices = ArrayField( extra_choices = ArrayField(
ArrayField(
base_field=models.CharField(max_length=100), base_field=models.CharField(max_length=100),
help_text=_('List of field choices') size=2
),
blank=True,
null=True
) )
order_alphabetically = models.BooleanField( order_alphabetically = models.BooleanField(
default=False, default=False,
help_text=_('Choices are automatically ordered alphabetically on save') help_text=_('Choices are automatically ordered alphabetically')
) )
clone_fields = ('extra_choices', 'order_alphabetically') clone_fields = ('extra_choices', 'order_alphabetically')
@ -667,16 +684,31 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
@property @property
def choices(self): def choices(self):
return self.extra_choices """
Returns a concatenation of the base and extra choices.
"""
if not hasattr(self, '_choices'):
self._choices = []
if self.base_choices:
self._choices.extend(CHOICE_SETS.get(self.base_choices))
if self.extra_choices:
self._choices.extend(self.extra_choices)
if self.order_alphabetically:
self._choices = sorted(self._choices, key=lambda x: x[0])
return self._choices
@property @property
def choices_count(self): def choices_count(self):
return len(self.choices) return len(self.choices)
def clean(self):
if not self.base_choices and not self.extra_choices:
raise ValidationError(_("Must define base or extra choices."))
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Sort choices if alphabetical ordering is enforced # Sort choices if alphabetical ordering is enforced
if self.order_alphabetically: if self.order_alphabetically:
self.extra_choices = sorted(self.choices) self.extra_choices = sorted(self.extra_choices, key=lambda x: x[0])
return super().save(*args, **kwargs) return super().save(*args, **kwargs)

View File

@ -66,10 +66,12 @@ class CustomFieldTable(NetBoxTable):
required = columns.BooleanColumn() required = columns.BooleanColumn()
ui_visibility = columns.ChoiceFieldColumn(verbose_name="UI visibility") ui_visibility = columns.ChoiceFieldColumn(verbose_name="UI visibility")
description = columns.MarkdownColumn() description = columns.MarkdownColumn()
choices = columns.ArrayColumn( choice_set = tables.Column(
linkify=True
)
choices = columns.ChoicesColumn(
max_items=10, max_items=10,
orderable=False, orderable=False
verbose_name=_('Choices')
) )
is_cloneable = columns.BooleanColumn() is_cloneable = columns.BooleanColumn()
@ -77,8 +79,8 @@ class CustomFieldTable(NetBoxTable):
model = CustomField model = CustomField
fields = ( fields = (
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description', 'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description',
'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'weight', 'choices', 'created', 'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'weight', 'choice_set', 'choices',
'last_updated', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description') default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
@ -87,11 +89,13 @@ class CustomFieldChoiceSetTable(NetBoxTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
choices = columns.ArrayColumn( base_choices = columns.ChoiceFieldColumn()
extra_choices = tables.TemplateColumn(
template_code="""{% for k, v in value.items %}{{ v }}{% if not forloop.last %}, {% endif %}{% endfor %}"""
)
choices = columns.ChoicesColumn(
max_items=10, max_items=10,
accessor=tables.A('extra_choices'), orderable=False
orderable=False,
verbose_name=_('Choices')
) )
choice_count = tables.TemplateColumn( choice_count = tables.TemplateColumn(
accessor=tables.A('extra_choices'), accessor=tables.A('extra_choices'),
@ -104,10 +108,10 @@ class CustomFieldChoiceSetTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = CustomFieldChoiceSet model = CustomFieldChoiceSet
fields = ( fields = (
'pk', 'id', 'name', 'description', 'choice_count', 'choices', 'order_alphabetically', 'created', 'pk', 'id', 'name', 'description', 'base_choices', 'extra_choices', 'choice_count', 'choices',
'last_updated', 'order_alphabetically', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'choice_count', 'description') default_columns = ('pk', 'name', 'base_choices', 'choice_count', 'description')
class CustomLinkTable(NetBoxTable): class CustomLinkTable(NetBoxTable):

View File

@ -139,15 +139,27 @@ class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase):
create_data = [ create_data = [
{ {
'name': 'Choice Set 4', 'name': 'Choice Set 4',
'extra_choices': ['4A', '4B', '4C'], 'extra_choices': [
['4A', 'Choice 1'],
['4B', 'Choice 2'],
['4C', 'Choice 3'],
],
}, },
{ {
'name': 'Choice Set 5', 'name': 'Choice Set 5',
'extra_choices': ['5A', '5B', '5C'], 'extra_choices': [
['5A', 'Choice 1'],
['5B', 'Choice 2'],
['5C', 'Choice 3'],
],
}, },
{ {
'name': 'Choice Set 6', 'name': 'Choice Set 6',
'extra_choices': ['6A', '6B', '6C'], 'extra_choices': [
['6A', 'Choice 1'],
['6B', 'Choice 2'],
['6C', 'Choice 3'],
],
}, },
] ]
bulk_update_data = { bulk_update_data = {
@ -155,7 +167,11 @@ class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase):
} }
update_data = { update_data = {
'name': 'Choice Set X', 'name': 'Choice Set X',
'extra_choices': ['X1', 'X2', 'X3'], 'extra_choices': [
['X1', 'Choice 1'],
['X2', 'Choice 2'],
['X3', 'Choice 3'],
],
'description': 'New description', 'description': 'New description',
} }

View File

@ -17,8 +17,8 @@ class ChangeLogViewTest(ModelViewTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
name='Custom Field Choice Set 1', name='Choice Set 1',
extra_choices=['Bar', 'Foo'] extra_choices=(('foo', 'Foo'), ('bar', 'Bar'))
) )
# Create a custom field on the Site model # Create a custom field on the Site model
@ -48,7 +48,7 @@ class ChangeLogViewTest(ModelViewTestCase):
'slug': 'site-1', 'slug': 'site-1',
'status': SiteStatusChoices.STATUS_ACTIVE, 'status': SiteStatusChoices.STATUS_ACTIVE,
'cf_cf1': 'ABC', 'cf_cf1': 'ABC',
'cf_cf2': 'Bar', 'cf_cf2': 'bar',
'tags': [tag.pk for tag in tags], 'tags': [tag.pk for tag in tags],
} }
@ -84,7 +84,7 @@ class ChangeLogViewTest(ModelViewTestCase):
'slug': 'site-x', 'slug': 'site-x',
'status': SiteStatusChoices.STATUS_PLANNED, 'status': SiteStatusChoices.STATUS_PLANNED,
'cf_cf1': 'DEF', 'cf_cf1': 'DEF',
'cf_cf2': 'Foo', 'cf_cf2': 'foo',
'tags': [tags[2].pk], 'tags': [tags[2].pk],
} }
@ -226,7 +226,7 @@ class ChangeLogAPITest(APITestCase):
# Create a select custom field on the Site model # Create a select custom field on the Site model
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
name='Choice Set 1', name='Choice Set 1',
extra_choices=['Bar', 'Foo'] extra_choices=(('foo', 'Foo'), ('bar', 'Bar'))
) )
cf_select = CustomField( cf_select = CustomField(
type=CustomFieldTypeChoices.TYPE_SELECT, type=CustomFieldTypeChoices.TYPE_SELECT,
@ -251,7 +251,7 @@ class ChangeLogAPITest(APITestCase):
'slug': 'site-1', 'slug': 'site-1',
'custom_fields': { 'custom_fields': {
'cf1': 'ABC', 'cf1': 'ABC',
'cf2': 'Bar', 'cf2': 'bar',
}, },
'tags': [ 'tags': [
{'name': 'Tag 1'}, {'name': 'Tag 1'},
@ -285,7 +285,7 @@ class ChangeLogAPITest(APITestCase):
'slug': 'site-x', 'slug': 'site-x',
'custom_fields': { 'custom_fields': {
'cf1': 'DEF', 'cf1': 'DEF',
'cf2': 'Foo', 'cf2': 'foo',
}, },
'tags': [ 'tags': [
{'name': 'Tag 3'} {'name': 'Tag 3'}

View File

@ -269,8 +269,12 @@ class CustomFieldTest(TestCase):
self.assertIsNone(instance.custom_field_data.get(cf.name)) self.assertIsNone(instance.custom_field_data.get(cf.name))
def test_select_field(self): def test_select_field(self):
CHOICES = ('Option A', 'Option B', 'Option C') CHOICES = (
value = CHOICES[1] ('a', 'Option A'),
('b', 'Option B'),
('c', 'Option C'),
)
value = 'a'
# Create a set of custom field choices # Create a set of custom field choices
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
@ -302,8 +306,12 @@ class CustomFieldTest(TestCase):
self.assertIsNone(instance.custom_field_data.get(cf.name)) self.assertIsNone(instance.custom_field_data.get(cf.name))
def test_multiselect_field(self): def test_multiselect_field(self):
CHOICES = ['Option A', 'Option B', 'Option C'] CHOICES = (
value = [CHOICES[1], CHOICES[2]] ('a', 'Option A'),
('b', 'Option B'),
('c', 'Option C'),
)
value = ['a', 'b']
# Create a set of custom field choices # Create a set of custom field choices
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
@ -453,7 +461,7 @@ class CustomFieldAPITest(APITestCase):
# Create a set of custom field choices # Create a set of custom field choices
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
name='Custom Field Choice Set 1', name='Custom Field Choice Set 1',
extra_choices=('Foo', 'Bar', 'Baz') extra_choices=(('foo', 'Foo'), ('bar', 'Bar'), ('baz', 'Baz'))
) )
custom_fields = ( custom_fields = (
@ -469,13 +477,13 @@ class CustomFieldAPITest(APITestCase):
CustomField( CustomField(
type=CustomFieldTypeChoices.TYPE_SELECT, type=CustomFieldTypeChoices.TYPE_SELECT,
name='select_field', name='select_field',
default='Foo', default='foo',
choice_set=choice_set choice_set=choice_set
), ),
CustomField( CustomField(
type=CustomFieldTypeChoices.TYPE_MULTISELECT, type=CustomFieldTypeChoices.TYPE_MULTISELECT,
name='multiselect_field', name='multiselect_field',
default=['Foo'], default=['foo'],
choice_set=choice_set choice_set=choice_set
), ),
CustomField( CustomField(
@ -514,8 +522,8 @@ class CustomFieldAPITest(APITestCase):
custom_fields[6].name: '2020-01-02 12:00:00', custom_fields[6].name: '2020-01-02 12:00:00',
custom_fields[7].name: 'http://example.com/2', custom_fields[7].name: 'http://example.com/2',
custom_fields[8].name: '{"foo": 1, "bar": 2}', custom_fields[8].name: '{"foo": 1, "bar": 2}',
custom_fields[9].name: 'Bar', custom_fields[9].name: 'bar',
custom_fields[10].name: ['Bar', 'Baz'], custom_fields[10].name: ['bar', 'baz'],
custom_fields[11].name: vlans[1].pk, custom_fields[11].name: vlans[1].pk,
custom_fields[12].name: [vlans[2].pk, vlans[3].pk], custom_fields[12].name: [vlans[2].pk, vlans[3].pk],
} }
@ -671,8 +679,8 @@ class CustomFieldAPITest(APITestCase):
'datetime_field': datetime.datetime(2020, 1, 2, 12, 0, 0), 'datetime_field': datetime.datetime(2020, 1, 2, 12, 0, 0),
'url_field': 'http://example.com/2', 'url_field': 'http://example.com/2',
'json_field': '{"foo": 1, "bar": 2}', 'json_field': '{"foo": 1, "bar": 2}',
'select_field': 'Bar', 'select_field': 'bar',
'multiselect_field': ['Bar', 'Baz'], 'multiselect_field': ['bar', 'baz'],
'object_field': VLAN.objects.get(vid=2).pk, 'object_field': VLAN.objects.get(vid=2).pk,
'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)), 'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)),
}, },
@ -799,8 +807,8 @@ class CustomFieldAPITest(APITestCase):
'datetime_field': datetime.datetime(2020, 1, 2, 12, 0, 0), 'datetime_field': datetime.datetime(2020, 1, 2, 12, 0, 0),
'url_field': 'http://example.com/2', 'url_field': 'http://example.com/2',
'json_field': '{"foo": 1, "bar": 2}', 'json_field': '{"foo": 1, "bar": 2}',
'select_field': 'Bar', 'select_field': 'bar',
'multiselect_field': ['Bar', 'Baz'], 'multiselect_field': ['bar', 'baz'],
'object_field': VLAN.objects.get(vid=2).pk, 'object_field': VLAN.objects.get(vid=2).pk,
'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)), 'multiobject_field': list(VLAN.objects.filter(vid__in=[3, 4]).values_list('pk', flat=True)),
} }
@ -1041,7 +1049,11 @@ class CustomFieldImportTest(TestCase):
# Create a set of custom field choices # Create a set of custom field choices
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
name='Custom Field Choice Set 1', name='Custom Field Choice Set 1',
extra_choices=('Choice A', 'Choice B', 'Choice C') extra_choices=(
('a', 'Option A'),
('b', 'Option B'),
('c', 'Option C'),
)
) )
custom_fields = ( custom_fields = (
@ -1067,8 +1079,8 @@ class CustomFieldImportTest(TestCase):
""" """
data = ( data = (
('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_decimal', 'cf_boolean', 'cf_date', 'cf_datetime', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'), ('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_decimal', 'cf_boolean', 'cf_date', 'cf_datetime', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'),
('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', '123.45', 'True', '2020-01-01', '2020-01-01 12:00:00', 'http://example.com/1', '{"foo": 123}', 'Choice A', '"Choice A,Choice B"'), ('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', '123.45', 'True', '2020-01-01', '2020-01-01 12:00:00', 'http://example.com/1', '{"foo": 123}', 'a', '"a,b"'),
('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', '456.78', 'False', '2020-01-02', '2020-01-02 12:00:00', 'http://example.com/2', '{"bar": 456}', 'Choice B', '"Choice B,Choice C"'), ('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', '456.78', 'False', '2020-01-02', '2020-01-02 12:00:00', 'http://example.com/2', '{"bar": 456}', 'b', '"b,c"'),
('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', '', '', ''), ('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', '', '', ''),
) )
csv_data = '\n'.join(','.join(row) for row in data) csv_data = '\n'.join(','.join(row) for row in data)
@ -1089,8 +1101,8 @@ class CustomFieldImportTest(TestCase):
self.assertEqual(site1.cf['datetime'].isoformat(), '2020-01-01T12:00:00+00:00') self.assertEqual(site1.cf['datetime'].isoformat(), '2020-01-01T12:00:00+00:00')
self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1') self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1')
self.assertEqual(site1.custom_field_data['json'], {"foo": 123}) self.assertEqual(site1.custom_field_data['json'], {"foo": 123})
self.assertEqual(site1.custom_field_data['select'], 'Choice A') self.assertEqual(site1.custom_field_data['select'], 'a')
self.assertEqual(site1.custom_field_data['multiselect'], ['Choice A', 'Choice B']) self.assertEqual(site1.custom_field_data['multiselect'], ['a', 'b'])
# Validate data for site 2 # Validate data for site 2
site2 = Site.objects.get(name='Site 2') site2 = Site.objects.get(name='Site 2')
@ -1104,8 +1116,8 @@ class CustomFieldImportTest(TestCase):
self.assertEqual(site2.cf['datetime'].isoformat(), '2020-01-02T12:00:00+00:00') self.assertEqual(site2.cf['datetime'].isoformat(), '2020-01-02T12:00:00+00:00')
self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2') self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2')
self.assertEqual(site2.custom_field_data['json'], {"bar": 456}) self.assertEqual(site2.custom_field_data['json'], {"bar": 456})
self.assertEqual(site2.custom_field_data['select'], 'Choice B') self.assertEqual(site2.custom_field_data['select'], 'b')
self.assertEqual(site2.custom_field_data['multiselect'], ['Choice B', 'Choice C']) self.assertEqual(site2.custom_field_data['multiselect'], ['b', 'c'])
# No custom field data should be set for site 3 # No custom field data should be set for site 3
site3 = Site.objects.get(name='Site 3') site3 = Site.objects.get(name='Site 3')
@ -1221,7 +1233,7 @@ class CustomFieldModelFilterTest(TestCase):
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
name='Custom Field Choice Set 1', name='Custom Field Choice Set 1',
extra_choices=['A', 'B', 'C', 'X'] extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'), ('x', 'X'))
) )
# Integer filtering # Integer filtering

View File

@ -14,8 +14,8 @@ class CustomFieldModelFormTest(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
obj_type = ContentType.objects.get_for_model(Site) obj_type = ContentType.objects.get_for_model(Site)
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
name='Custom Field Choice Set 1', name='Choice Set 1',
extra_choices=('A', 'B', 'C') extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'))
) )
cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT) cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT)

View File

@ -1,3 +1,4 @@
import json
import urllib.parse import urllib.parse
import uuid import uuid
@ -23,7 +24,11 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
site_ct = ContentType.objects.get_for_model(Site) site_ct = ContentType.objects.get_for_model(Site)
CustomFieldChoiceSet.objects.create( CustomFieldChoiceSet.objects.create(
name='Choice Set 1', name='Choice Set 1',
extra_choices=('A', 'B', 'C') extra_choices=(
('A', 'A'),
('B', 'B'),
('C', 'C'),
)
) )
custom_fields = ( custom_fields = (
@ -76,29 +81,38 @@ class CustomFieldChoiceSetTestCase(ViewTestCases.PrimaryObjectViewTestCase):
def setUpTestData(cls): def setUpTestData(cls):
choice_sets = ( choice_sets = (
CustomFieldChoiceSet(name='Choice Set 1', extra_choices=['1A', '1B', '1C', '1D', '1E']), CustomFieldChoiceSet(
CustomFieldChoiceSet(name='Choice Set 2', extra_choices=['2A', '2B', '2C', '2D', '2E']), name='Choice Set 1',
CustomFieldChoiceSet(name='Choice Set 3', extra_choices=['3A', '3B', '3C', '3D', '3E']), extra_choices=(('A1', 'Choice 1'), ('A2', 'Choice 2'), ('A3', 'Choice 3'))
),
CustomFieldChoiceSet(
name='Choice Set 2',
extra_choices=(('B1', 'Choice 1'), ('B2', 'Choice 2'), ('B3', 'Choice 3'))
),
CustomFieldChoiceSet(
name='Choice Set 3',
extra_choices=(('C1', 'Choice 1'), ('C2', 'Choice 2'), ('C3', 'Choice 3'))
),
) )
CustomFieldChoiceSet.objects.bulk_create(choice_sets) CustomFieldChoiceSet.objects.bulk_create(choice_sets)
cls.form_data = { cls.form_data = {
'name': 'Choice Set X', 'name': 'Choice Set X',
'extra_choices': 'X1,X2,X3,X4,X5', 'extra_choices': '\n'.join(['X1,Choice 1', 'X2,Choice 2', 'X3,Choice 3'])
} }
cls.csv_data = ( cls.csv_data = (
'name,extra_choices', 'name,extra_choices',
'Choice Set 4,"4A,4B,4C,4D,4E"', 'Choice Set 4,"D1,D2,D3"',
'Choice Set 5,"5A,5B,5C,5D,5E"', 'Choice Set 5,"E1,E2,E3"',
'Choice Set 6,"6A,6B,6C,6D,6E"', 'Choice Set 6,"F1,F2,F3"',
) )
cls.csv_update_data = ( cls.csv_update_data = (
'id,extra_choices', 'id,extra_choices',
f'{choice_sets[0].pk},"1X,1Y,1Z"', f'{choice_sets[0].pk},"A,B,C"',
f'{choice_sets[1].pk},"2X,2Y,2Z"', f'{choice_sets[1].pk},"A,B,C"',
f'{choice_sets[2].pk},"3X,3Y,3Z"', f'{choice_sets[2].pk},"A,B,C"',
) )
cls.bulk_edit_data = { cls.bulk_edit_data = {

View File

@ -24,11 +24,11 @@ class IPAddressAssignmentType(graphene.Union):
@classmethod @classmethod
def resolve_type(cls, instance, info): def resolve_type(cls, instance, info):
if type(instance) == Interface: if type(instance) is Interface:
return InterfaceType return InterfaceType
if type(instance) == FHRPGroup: if type(instance) is FHRPGroup:
return FHRPGroupType return FHRPGroupType
if type(instance) == VMInterface: if type(instance) is VMInterface:
return VMInterfaceType return VMInterfaceType
@ -42,11 +42,11 @@ class L2VPNAssignmentType(graphene.Union):
@classmethod @classmethod
def resolve_type(cls, instance, info): def resolve_type(cls, instance, info):
if type(instance) == Interface: if type(instance) is Interface:
return InterfaceType return InterfaceType
if type(instance) == VLAN: if type(instance) is VLAN:
return VLANType return VLANType
if type(instance) == VMInterface: if type(instance) is VMInterface:
return VMInterfaceType return VMInterfaceType
@ -59,9 +59,9 @@ class FHRPGroupInterfaceType(graphene.Union):
@classmethod @classmethod
def resolve_type(cls, instance, info): def resolve_type(cls, instance, info):
if type(instance) == Interface: if type(instance) is Interface:
return InterfaceType return InterfaceType
if type(instance) == VMInterface: if type(instance) is VMInterface:
return VMInterfaceType return VMInterfaceType
@ -79,17 +79,17 @@ class VLANGroupScopeType(graphene.Union):
@classmethod @classmethod
def resolve_type(cls, instance, info): def resolve_type(cls, instance, info):
if type(instance) == Cluster: if type(instance) is Cluster:
return ClusterType return ClusterType
if type(instance) == ClusterGroup: if type(instance) is ClusterGroup:
return ClusterGroupType return ClusterGroupType
if type(instance) == Location: if type(instance) is Location:
return LocationType return LocationType
if type(instance) == Rack: if type(instance) is Rack:
return RackType return RackType
if type(instance) == Region: if type(instance) is Region:
return RegionType return RegionType
if type(instance) == Site: if type(instance) is Site:
return SiteType return SiteType
if type(instance) == SiteGroup: if type(instance) is SiteGroup:
return SiteGroupType return SiteGroupType

View File

@ -129,7 +129,7 @@ def add_available_vlans(vlans, vlan_group=None):
}) })
vlans = list(vlans) + new_vlans vlans = list(vlans) + new_vlans
vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid']) vlans.sort(key=lambda v: v.vid if type(v) is VLAN else v['vid'])
return vlans return vlans

View File

@ -76,6 +76,18 @@ class ObjectPermissionMixin:
""" """
Return all permissions granted to the user by an ObjectPermission. Return all permissions granted to the user by an ObjectPermission.
""" """
# Initialize a dictionary mapping permission names to sets of constraints
perms = defaultdict(list)
# Collect any configured default permissions
for perm_name, constraints in settings.DEFAULT_PERMISSIONS.items():
constraints = constraints or tuple()
if type(constraints) not in (list, tuple):
raise ImproperlyConfigured(
f"Constraints for default permission {perm_name} must be defined as a list or tuple."
)
perms[perm_name].extend(constraints)
# Retrieve all assigned and enabled ObjectPermissions # Retrieve all assigned and enabled ObjectPermissions
object_permissions = ObjectPermission.objects.filter( object_permissions = ObjectPermission.objects.filter(
self.get_permission_filter(user_obj), self.get_permission_filter(user_obj),
@ -83,7 +95,6 @@ class ObjectPermissionMixin:
).order_by('id').distinct('id').prefetch_related('object_types') ).order_by('id').distinct('id').prefetch_related('object_types')
# Create a dictionary mapping permissions to their constraints # Create a dictionary mapping permissions to their constraints
perms = defaultdict(list)
for obj_perm in object_permissions: for obj_perm in object_permissions:
for object_type in obj_perm.object_types.all(): for object_type in obj_perm.object_types.all():
for action in obj_perm.actions: for action in obj_perm.actions:
@ -119,7 +130,7 @@ class ObjectPermissionMixin:
return True return True
# Sanity check: Ensure that the requested permission applies to the specified object # Sanity check: Ensure that the requested permission applies to the specified object
model = obj._meta.model model = obj._meta.concrete_model
if model._meta.label_lower != '.'.join((app_label, model_name)): if model._meta.label_lower != '.'.join((app_label, model_name)):
raise ValueError(f"Invalid permission {perm} for model {model}") raise ValueError(f"Invalid permission {perm} for model {model}")

View File

@ -39,6 +39,8 @@ REDIS = {
SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
DEFAULT_PERMISSIONS = {}
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': True 'disable_existing_loggers': True

View File

@ -99,6 +99,13 @@ DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a') DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
DEBUG = getattr(configuration, 'DEBUG', False) DEBUG = getattr(configuration, 'DEBUG', False)
DEFAULT_DASHBOARD = getattr(configuration, 'DEFAULT_DASHBOARD', None) DEFAULT_DASHBOARD = getattr(configuration, 'DEFAULT_DASHBOARD', None)
DEFAULT_PERMISSIONS = getattr(configuration, 'DEFAULT_PERMISSIONS', {
# Permit users to manage their own API tokens
'users.view_token': ({'user': '$user'},),
'users.add_token': ({'user': '$user'},),
'users.change_token': ({'user': '$user'},),
'users.delete_token': ({'user': '$user'},),
})
DEVELOPER = getattr(configuration, 'DEVELOPER', False) DEVELOPER = getattr(configuration, 'DEVELOPER', False)
DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs')) DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
EMAIL = getattr(configuration, 'EMAIL', {}) EMAIL = getattr(configuration, 'EMAIL', {})
@ -356,6 +363,7 @@ INSTALLED_APPS = [
'taggit', 'taggit',
'timezone_field', 'timezone_field',
'core', 'core',
'account',
'circuits', 'circuits',
'dcim', 'dcim',
'ipam', 'ipam',

View File

@ -9,13 +9,14 @@ from django.db.models import DateField, DateTimeField
from django.template import Context, Template from django.template import Context, Template
from django.urls import reverse from django.urls import reverse
from django.utils.dateparse import parse_date from django.utils.dateparse import parse_date
from django.utils.html import escape
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django_tables2.columns import library from django_tables2.columns import library
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from extras.choices import CustomFieldTypeChoices from extras.choices import CustomFieldTypeChoices
from utilities.permissions import get_permission_for_model
from utilities.templatetags.builtins.filters import render_markdown from utilities.templatetags.builtins.filters import render_markdown
from utilities.utils import content_type_identifier, content_type_name, get_viewname from utilities.utils import content_type_identifier, content_type_name, get_viewname
@ -24,6 +25,7 @@ __all__ = (
'ArrayColumn', 'ArrayColumn',
'BooleanColumn', 'BooleanColumn',
'ChoiceFieldColumn', 'ChoiceFieldColumn',
'ChoicesColumn',
'ColorColumn', 'ColorColumn',
'ColoredLabelColumn', 'ColoredLabelColumn',
'ContentTypeColumn', 'ContentTypeColumn',
@ -249,7 +251,7 @@ class ActionsColumn(tables.Column):
dropdown_links = [] dropdown_links = []
user = getattr(request, 'user', AnonymousUser()) user = getattr(request, 'user', AnonymousUser())
for idx, (action, attrs) in enumerate(self.actions.items()): for idx, (action, attrs) in enumerate(self.actions.items()):
permission = f'{model._meta.app_label}.{attrs.permission}_{model._meta.model_name}' permission = get_permission_for_model(model, attrs.permission)
if attrs.permission is None or user.has_perm(permission): if attrs.permission is None or user.has_perm(permission):
url = reverse(get_viewname(model, action), kwargs={'pk': record.pk}) url = reverse(get_viewname(model, action), kwargs={'pk': record.pk})
@ -598,16 +600,49 @@ class ArrayColumn(tables.Column):
""" """
List array items as a comma-separated list. List array items as a comma-separated list.
""" """
def __init__(self, *args, max_items=None, func=str, **kwargs):
self.max_items = max_items
self.func = func
super().__init__(*args, **kwargs)
def render(self, value):
omitted_count = 0
# Limit the returned items to the specified maximum number (if any)
if self.max_items:
omitted_count = len(value) - self.max_items
value = value[:self.max_items - 1]
# Apply custom processing function (if any) per item
if self.func:
value = [self.func(v) for v in value]
# Annotate omitted items (if applicable)
if omitted_count > 0:
value.append(f'({omitted_count} more)')
return ', '.join(value)
class ChoicesColumn(tables.Column):
"""
Display the human-friendly labels of a set of choices.
"""
def __init__(self, *args, max_items=None, **kwargs): def __init__(self, *args, max_items=None, **kwargs):
self.max_items = max_items self.max_items = max_items
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def render(self, value): def render(self, value):
omitted_count = 0
value = [v[1] for v in value]
# Limit the returned items to the specified maximum number (if any)
if self.max_items: if self.max_items:
# Limit the returned items to the specified maximum number omitted_count = len(value) - self.max_items
omitted = len(value) - self.max_items
value = value[:self.max_items - 1] value = value[:self.max_items - 1]
if omitted > 0:
value.append(f'({omitted} more)') # Annotate omitted items (if applicable)
if omitted_count > 0:
value.append(f'({omitted_count} more)')
return ', '.join(value) return ', '.join(value)

View File

@ -1,19 +1,18 @@
from django.conf import settings from django.conf import settings
from django.conf.urls import include from django.conf.urls import include
from django.urls import path, re_path from django.urls import path
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.static import serve from django.views.static import serve
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from account.views import LoginView, LogoutView
from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns
from netbox.api.views import APIRootView, StatusView from netbox.api.views import APIRootView, StatusView
from netbox.graphql.schema import schema from netbox.graphql.schema import schema
from netbox.graphql.views import GraphQLView from netbox.graphql.views import GraphQLView
from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx
from users.views import LoginView, LogoutView
from .admin import admin_site from .admin import admin_site
_patterns = [ _patterns = [
# Base views # Base views
@ -37,7 +36,7 @@ _patterns = [
path('wireless/', include('wireless.urls')), path('wireless/', include('wireless.urls')),
# Current user views # Current user views
path('user/', include('users.account_urls')), path('user/', include('account.urls')),
# HTMX views # HTMX views
path('htmx/object-selector/', htmx.ObjectSelectorView.as_view(), name='htmx_object_selector'), path('htmx/object-selector/', htmx.ObjectSelectorView.as_view(), name='htmx_object_selector'),

View File

@ -1,9 +1,10 @@
{% extends 'base/40x.html' %} {% extends 'base/40x.html' %}
{% load i18n %}
{% block title %}Access Denied{% endblock %} {% block title %}{% trans "Access Denied" %}{% endblock %}
{% block icon %}<i class="mdi mdi-lock"></i>{% endblock %} {% block icon %}<i class="mdi mdi-lock"></i>{% endblock %}
{% block message %} {% block message %}
You do not have permission to access this page. {% trans "You do not have permission to access this page" %}.
{% endblock %} {% endblock %}

View File

@ -1,9 +1,10 @@
{% extends 'base/40x.html' %} {% extends 'base/40x.html' %}
{% load i18n %}
{% block title %}Page Not Found{% endblock %} {% block title %}{% trans "Page Not Found" %}{% endblock %}
{% block icon %}<i class="mdi mdi-alert"></i>{% endblock %} {% block icon %}<i class="mdi mdi-alert"></i>{% endblock %}
{% block message %} {% block message %}
The requested page does not exist. {% trans "The requested page does not exist" %}.
{% endblock %} {% endblock %}

View File

@ -1,9 +1,10 @@
{% load i18n %}
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Server Error</title> <title>{% trans "Server Error" %}</title>
<link rel="stylesheet" href="{% static 'netbox-light.css'%}" /> <link rel="stylesheet" href="{% static 'netbox-light.css'%}" />
<meta charset="UTF-8"> <meta charset="UTF-8">
</head> </head>
@ -14,28 +15,28 @@
<div class="col col-md-6 offset-md-3"> <div class="col col-md-6 offset-md-3">
<div class="card border-danger mt-5"> <div class="card border-danger mt-5">
<h5 class="card-header"> <h5 class="card-header">
<i class="mdi mdi-alert"></i> Server Error <i class="mdi mdi-alert"></i> {% trans "Server Error" %}
</h5> </h5>
<div class="card-body"> <div class="card-body">
{% block message %} {% block message %}
<p> <p>
There was a problem with your request. Please contact an administrator. {% trans "There was a problem with your request. Please contact an administrator" %}.
</p> </p>
{% endblock %} {% endblock %}
<hr /> <hr />
<p> <p>
The complete exception is provided below: {% trans "The complete exception is provided below" %}:
</p> </p>
<pre class="block"><strong>{{ exception }}</strong><br /> <pre class="block"><strong>{{ exception }}</strong><br />
{{ error }} {{ error }}
Python version: {{ python_version }} {% trans "Python version" %}: {{ python_version }}
NetBox version: {{ netbox_version }}</pre> {% trans "NetBox version" %}: {{ netbox_version }}</pre>
<p> <p>
If further assistance is required, please post to the <a href="https://github.com/netbox-community/netbox/discussions">NetBox discussion forum</a> on GitHub. {% trans "If further assistance is required, please post to the" %} <a href="https://github.com/netbox-community/netbox/discussions">{% trans "NetBox discussion forum" %}</a> {% trans "on GitHub" %}.
</p> </p>
<div class="text-end"> <div class="text-end">
<a href="{% url 'home' %}" class="btn btn-primary">Home Page</a> <a href="{% url 'home' %}" class="btn btn-primary">{% trans "Home Page" %}</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,12 +1,12 @@
{% extends 'users/account/base.html' %} {% extends 'account/base.html' %}
{% load buttons %} {% load buttons %}
{% load helpers %} {% load helpers %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load i18n %}
{% block title %}Bookmarks{% endblock %} {% block title %}{% trans "Bookmarks" %}{% endblock %}
{% block content %} {% block content %}
<form method="post" class="form form-horizontal"> <form method="post" class="form form-horizontal">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="return_url" value="{% url 'account:bookmarks' %}" /> <input type="hidden" name="return_url" value="{% url 'account:bookmarks' %}" />

View File

@ -0,0 +1,21 @@
{% extends 'account/base.html' %}
{% load form_helpers %}
{% load i18n %}
{% block title %}{% trans "Change Password" %}{% endblock %}
{% block content %}
<form action="." method="post" class="form form-horizontal col-md-8 offset-md-2">
{% csrf_token %}
<div class="field-group">
<h5 class="text-center">{% trans "Password" %}</h5>
{% render_field form.old_password %}
{% render_field form.new_password1 %}
{% render_field form.new_password2 %}
</div>
<div class="text-end">
<a href="{% url 'account:profile' %}" class="btn btn-outline-danger">{% trans "Cancel" %}</a>
<button type="submit" name="_update" class="btn btn-primary">{% trans "Save" %}</button>
</div>
</form>
{% endblock %}

View File

@ -1,8 +1,9 @@
{% extends 'users/account/base.html' %} {% extends 'account/base.html' %}
{% load helpers %} {% load helpers %}
{% load form_helpers %} {% load form_helpers %}
{% load i18n %}
{% block title %}User Preferences{% endblock %} {% block title %}{% trans "User Preferences" %}{% endblock %}
{% block content %} {% block content %}
<form method="post" action="" id="preferences-update"> <form method="post" action="" id="preferences-update">
@ -25,7 +26,7 @@
{% if plugin_fields %} {% if plugin_fields %}
<div class="field-group my-5"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Plugins</h5> <h5 class="offset-sm-3">{% trans "Plugins" %}</h5>
</div> </div>
{% for name in plugin_fields %} {% for name in plugin_fields %}
{% render_field form|getfield:name %} {% render_field form|getfield:name %}
@ -37,23 +38,23 @@
{# Table configurations #} {# Table configurations #}
<div class="field-group my-5"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Table Configurations</h5> <h5 class="offset-sm-3">{% trans "Table Configurations" %}</h5>
</div> </div>
<div class="row"> <div class="row">
{% if request.user.config.data.tables %} {% if request.user.config.data.tables %}
<label class="col-sm-3 col-form-label text-lg-end"> <label class="col-sm-3 col-form-label text-lg-end">
Clear table preferences {% trans "Clear table preferences" %}
</label> </label>
<div class="col-sm-9"> <div class="col-sm-9">
<table class="table table-hover object-list"> <table class="table table-hover object-list">
<thead> <thead>
<tr> <tr>
<th> <th>
<input type="checkbox" class="toggle form-check-input" title="Toggle All"> <input type="checkbox" class="toggle form-check-input" title="{% trans "Toggle All" %}">
</th> </th>
<th>Table</th> <th>{% trans "Table" %}</th>
<th>Ordering</th> <th>{% trans "Ordering" %}</th>
<th>Columns</th> <th>{% trans "Columns" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -72,15 +73,14 @@
</div> </div>
{% else %} {% else %}
<div class="offset-sm-3"> <div class="offset-sm-3">
<p class="text-muted">None found</p> <p class="text-muted">{% trans "None found" %}</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="text-end my-3"> <div class="text-end my-3">
<a class="btn btn-outline-secondary" href="{% url 'account:preferences' %}">Cancel</a> <a class="btn btn-outline-secondary" href="{% url 'account:preferences' %}">{% trans "Cancel" %}</a>
<button type="submit" name="_update" class="btn btn-primary">Save </button> <button type="submit" name="_update" class="btn btn-primary">{% trans "Save" %}</button>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -1,22 +1,23 @@
{% extends 'users/account/base.html' %} {% extends 'account/base.html' %}
{% load helpers %} {% load helpers %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load i18n %}
{% block title %}User Profile{% endblock %} {% block title %}{% trans "User Profile" %}{% endblock %}
{% block content %} {% block content %}
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header">Account Details</h5> <h5 class="card-header">{% trans "Account Details" %}</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Username</th> <th scope="row">{% trans "Username" %}</th>
<td>{{ request.user.username }}</td> <td>{{ request.user.username }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Full Name</th> <th scope="row">{% trans "Full Name" %}</th>
<td> <td>
{% if request.user.first_name or request.user.last_name %} {% if request.user.first_name or request.user.last_name %}
{{ request.user.first_name }} {{ request.user.last_name }} {{ request.user.first_name }} {{ request.user.last_name }}
@ -26,19 +27,19 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Email</th> <th scope="row">{% trans "Email" %}</th>
<td>{{ request.user.email|placeholder }}</td> <td>{{ request.user.email|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Account Created</th> <th scope="row">{% trans "Account Created" %}</th>
<td>{{ request.user.date_joined|annotated_date }}</td> <td>{{ request.user.date_joined|annotated_date }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Superuser</th> <th scope="row">{% trans "Superuser" %}</th>
<td>{% checkmark request.user.is_superuser %}</td> <td>{% checkmark request.user.is_superuser %}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Admin Access</th> <th scope="row">{% trans "Admin Access" %}</th>
<td>{% checkmark request.user.is_staff %}</td> <td>{% checkmark request.user.is_staff %}</td>
</tr> </tr>
</table> </table>
@ -47,12 +48,12 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header">Assigned Groups</h5> <h5 class="card-header">{% trans "Assigned Groups" %}</h5>
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
{% for group in request.user.groups.all %} {% for group in request.user.groups.all %}
<li class="list-group-item">{{ group }}</li> <li class="list-group-item">{{ group }}</li>
{% empty %} {% empty %}
<li class="list-group-item text-muted">None</li> <li class="list-group-item text-muted">{% trans "None" %}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -62,7 +63,7 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="card"> <div class="card">
<h5 class="card-header text-center">Recent Activity</h5> <h5 class="card-header text-center">{% trans "Recent Activity" %}</h5>
<div class="card-body table-responsive"> <div class="card-body table-responsive">
{% render_table changelog_table 'inc/table.html' %} {% render_table changelog_table 'inc/table.html' %}
</div> </div>

View File

@ -0,0 +1,26 @@
{% extends 'account/base.html' %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block title %}{% trans "My API Tokens" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-12 text-end">
<a href="{% url 'account:usertoken_add' %}" class="btn btn-sm btn-primary my-3">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add a Token" %}
</a>
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<div class="card-body table-responsive">
{% render_table table 'inc/table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,4 +1,5 @@
{% extends "admin/index.html" %} {% extends "admin/index.html" %}
{% load i18n %}
{% block content_title %}{% endblock %} {% block content_title %}{% endblock %}
@ -6,16 +7,16 @@
{{ block.super }} {{ block.super }}
<div class="module"> <div class="module">
<table style="width: 100%"> <table style="width: 100%">
<caption>System</caption> <caption>{% trans "System" %}</caption>
<tbody> <tbody>
<tr> <tr>
<th> <th>
<a href="{% url 'rq_home' %}">Background Tasks</a> <a href="{% url 'rq_home' %}">{% trans "Background Tasks" %}</a>
</th> </th>
</tr> </tr>
<tr> <tr>
<th> <th>
<a href="{% url 'plugins_list' %}">Installed plugins</a> <a href="{% url 'plugins_list' %}">{% trans "Installed plugins" %}</a>
</th> </th>
</tr> </tr>
</tbody> </tbody>

View File

@ -1,4 +1,5 @@
{% extends 'base/layout.html' %} {% extends 'base/layout.html' %}
{% load i18n %}
{% block title %}{% endblock %} {% block title %}{% endblock %}
@ -13,7 +14,7 @@
{% block message %}{% endblock %} {% block message %}{% endblock %}
</div> </div>
<div class="card-footer text-end"> <div class="card-footer text-end">
<a href="{% url 'home' %}" class="btn btn-sm btn-primary">Home Page</a> <a href="{% url 'home' %}" class="btn btn-sm btn-primary">{% trans "Home Page" %}</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
{# Base template for (almost) all NetBox pages #} {# Base template for (almost) all NetBox pages #}
{% load static %} {% load static %}
{% load helpers %} {% load helpers %}
{% load i18n %}
<!DOCTYPE html> <!DOCTYPE html>
<html <html
lang="en" lang="en"
@ -24,7 +25,7 @@
/> />
{# Page title #} {# Page title #}
<title>{% block title %}Home{% endblock %} | NetBox</title> <title>{% block title %}{% trans "Home" %}{% endblock %} | NetBox</title>
<script <script
type="text/javascript" type="text/javascript"

View File

@ -2,6 +2,7 @@
{% extends 'base/base.html' %} {% extends 'base/base.html' %}
{% load helpers %} {% load helpers %}
{% load static %} {% load static %}
{% load i18n %}
{% comment %} {% comment %}
Blocks: Blocks:
@ -23,7 +24,7 @@ Blocks:
{# NetBox Logo, only visible when printing #} {# NetBox Logo, only visible when printing #}
<div class="p-2 printonly"> <div class="p-2 printonly">
<img src="{% static 'netbox_logo.svg' %}" alt="NetBox logo" width="200px" /> <img src="{% static 'netbox_logo.svg' %}" alt="{% trans "NetBox logo" %}" width="200px" />
</div> </div>
{# Top bar #} {# Top bar #}
@ -33,7 +34,7 @@ Blocks:
<div class="nav-mobile"> <div class="nav-mobile">
<div class="nav-mobile-top"> <div class="nav-mobile-top">
<a class="sidebar-logo p-2 d-block" href="{% url 'home' %}"> <a class="sidebar-logo p-2 d-block" href="{% url 'home' %}">
<img src="{% static 'netbox_logo.svg' %}" alt="NetBox logo" height="40" /> <img src="{% static 'netbox_logo.svg' %}" alt="{% trans "NetBox logo" %}" height="40" />
</a> </a>
<button type="button" aria-label="Toggle Navigation" class="navbar-toggler sidenav-toggle-mobile"> <button type="button" aria-label="Toggle Navigation" class="navbar-toggler sidenav-toggle-mobile">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
@ -72,14 +73,14 @@ Blocks:
{% if settings.DEBUG and not settings.DEVELOPER %} {% if settings.DEBUG and not settings.DEVELOPER %}
<div class="alert alert-warning text-center mx-3" role="alert"> <div class="alert alert-warning text-center mx-3" role="alert">
<strong><i class="mdi mdi-alert"></i> Debug mode is enabled.</strong> <strong><i class="mdi mdi-alert"></i> {% trans "Debug mode is enabled" %}.</strong>
Performance may be limited. Debugging should never be enabled on a production system. {% trans "Performance may be limited. Debugging should never be enabled on a production system" %}.
</div> </div>
{% endif %} {% endif %}
{% if config.MAINTENANCE_MODE and config.BANNER_MAINTENANCE %} {% if config.MAINTENANCE_MODE and config.BANNER_MAINTENANCE %}
<div class="alert alert-warning text-center mx-3" role="alert"> <div class="alert alert-warning text-center mx-3" role="alert">
<h5><i class="mdi mdi-alert"></i> Maintenance Mode</h5> <h5><i class="mdi mdi-alert"></i> {% trans "Maintenance Mode" %}</h5>
{{ config.BANNER_MAINTENANCE|escape }} {{ config.BANNER_MAINTENANCE|escape }}
</div> </div>
{% endif %} {% endif %}
@ -130,34 +131,34 @@ Blocks:
{% block footer_links %} {% block footer_links %}
{# Documentation #} {# Documentation #}
<a type="button" class="nav-link" href="{% static 'docs/' %}" target="_blank"> <a type="button" class="nav-link" href="{% static 'docs/' %}" target="_blank">
<i title="Docs" class="mdi mdi-book-open-variant text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i> <i title="{% trans "Docs" %}" class="mdi mdi-book-open-variant text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a> </a>
{# REST API #} {# REST API #}
<a type="button" class="nav-link" href="{% url 'api-root' %}" target="_blank"> <a type="button" class="nav-link" href="{% url 'api-root' %}" target="_blank">
<i title="REST API" class="mdi mdi-cloud-braces text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i> <i title="{% trans "REST API" %}" class="mdi mdi-cloud-braces text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a> </a>
{# API docs #} {# API docs #}
<a type="button" class="nav-link" href="{% url 'api_docs' %}" target="_blank"> <a type="button" class="nav-link" href="{% url 'api_docs' %}" target="_blank">
<i title="REST API documentation" class="mdi mdi-book text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i> <i title="{% trans "REST API documentation" %}" class="mdi mdi-book text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a> </a>
{# GraphQL API #} {# GraphQL API #}
{% if config.GRAPHQL_ENABLED %} {% if config.GRAPHQL_ENABLED %}
<a type="button" class="nav-link" href="{% url 'graphql' %}" target="_blank"> <a type="button" class="nav-link" href="{% url 'graphql' %}" target="_blank">
<i title="GraphQL API" class="mdi mdi-graphql text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i> <i title="{% trans "GraphQL API" %}" class="mdi mdi-graphql text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a> </a>
{% endif %} {% endif %}
{# GitHub #} {# GitHub #}
<a type="button" class="nav-link" href="https://github.com/netbox-community/netbox" target="_blank"> <a type="button" class="nav-link" href="https://github.com/netbox-community/netbox" target="_blank">
<i title="Source Code" class="mdi mdi-github text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i> <i title="{% trans "Source Code" %}" class="mdi mdi-github text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a> </a>
{# NetDev Slack #} {# NetDev Slack #}
<a type="button" class="nav-link" href="https://netdev.chat/" target="_blank"> <a type="button" class="nav-link" href="https://netdev.chat/" target="_blank">
<i title="Community" class="mdi mdi-slack text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i> <i title="{% trans "Community" %}" class="mdi mdi-slack text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
</a> </a>
{% endblock footer_links %} {% endblock footer_links %}
</nav> </nav>

View File

@ -1,5 +1,6 @@
{% load navigation %} {% load navigation %}
{% load static %} {% load static %}
{% load i18n %}
<nav class="sidenav noprint" id="sidenav" data-simplebar> <nav class="sidenav noprint" id="sidenav" data-simplebar>
<div class="sidenav-header"> <div class="sidenav-header">
@ -8,12 +9,12 @@
{# Full Logo #} {# Full Logo #}
<a class="sidenav-brand" href="{% url 'home' %}"> <a class="sidenav-brand" href="{% url 'home' %}">
<img src="{% static 'netbox_logo.svg' %}" height="48" class="sidenav-brand-img" alt="NetBox Logo"> <img src="{% static 'netbox_logo.svg' %}" height="48" class="sidenav-brand-img" alt="{% trans "NetBox Logo" %}">
</a> </a>
{# Icon Logo #} {# Icon Logo #}
<a class="sidenav-brand-icon" href="{% url 'home' %}"> <a class="sidenav-brand-icon" href="{% url 'home' %}">
<img src="{% static 'netbox_icon.svg' %}" height="32" class="sidenav-brand-img" alt="NetBox Logo"> <img src="{% static 'netbox_icon.svg' %}" height="32" class="sidenav-brand-img" alt="{% trans "NetBox Logo" %}">
</a> </a>
{# Pin/Unpin Toggle #} {# Pin/Unpin Toggle #}

View File

@ -1,6 +1,7 @@
{% extends 'generic/object.html' %} {% extends 'generic/object.html' %}
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load i18n %}
{% block breadcrumbs %} {% block breadcrumbs %}
{{ block.super }} {{ block.super }}
@ -11,31 +12,31 @@
<div class="row"> <div class="row">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header">Circuit</h5> <h5 class="card-header">{% trans "Circuit" %}</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Provider</th> <th scope="row">{% trans "Provider" %}</th>
<td>{{ object.provider|linkify }}</td> <td>{{ object.provider|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Account</th> <th scope="row">{% trans "Account" %}</th>
<td>{{ object.provider_account|linkify|placeholder }}</td> <td>{{ object.provider_account|linkify|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Circuit ID</th> <th scope="row">{% trans "Circuit ID" %}</th>
<td>{{ object.cid }}</td> <td>{{ object.cid }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Type</th> <th scope="row">{% trans "Type" %}</th>
<td>{{ object.type|linkify }}</td> <td>{{ object.type|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Status</th> <th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td> <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Tenant</th> <th scope="row">{% trans "Tenant" %}</th>
<td> <td>
{% if object.tenant.group %} {% if object.tenant.group %}
{{ object.tenant.group|linkify }} / {{ object.tenant.group|linkify }} /
@ -44,19 +45,19 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Install Date</th> <th scope="row">{% trans "Install Date" %}</th>
<td>{{ object.install_date|annotated_date|placeholder }}</td> <td>{{ object.install_date|annotated_date|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Termination Date</th> <th scope="row">{% trans "Termination Date" %}</th>
<td>{{ object.termination_date|annotated_date|placeholder }}</td> <td>{{ object.termination_date|annotated_date|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Commit Rate</th> <th scope="row">{% trans "Commit Rate" %}</th>
<td>{{ object.commit_rate|humanize_speed|placeholder }}</td> <td>{{ object.commit_rate|humanize_speed|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>
</tr> </tr>
</table> </table>

View File

@ -1,12 +1,13 @@
{% extends 'generic/confirmation_form.html' %} {% extends 'generic/confirmation_form.html' %}
{% load i18n %}
{% block title %}Swap Circuit Terminations{% endblock %} {% block title %}{% trans "Swap Circuit Terminations" %}{% endblock %}
{% block message %} {% block message %}
<p>Swap these terminations for circuit {{ circuit }}?</p> <p>{% blocktrans %}Swap these terminations for circuit {{ circuit }}?{% endblocktrans %}</p>
<ul> <ul>
<li> <li>
<strong>A side:</strong> <strong>{% trans "A side" %}:</strong>
{% if termination_a %} {% if termination_a %}
{{ termination_a.site }} {% if termination_a.interface %}- {{ termination_a.interface.device }} {{ termination_a.interface }}{% endif %} {{ termination_a.site }} {% if termination_a.interface %}- {{ termination_a.interface.device }} {{ termination_a.interface }}{% endif %}
{% else %} {% else %}
@ -14,7 +15,7 @@
{% endif %} {% endif %}
</li> </li>
<li> <li>
<strong>Z side:</strong> <strong>{% trans "Z side" %}:</strong>
{% if termination_z %} {% if termination_z %}
{{ termination_z.site }} {% if termination_z.interface %}- {{ termination_z.interface.device }} {{ termination_z.interface }}{% endif %} {{ termination_z.site }} {% if termination_z.interface %}- {{ termination_z.interface.device }} {{ termination_z.interface }}{% endif %}
{% else %} {% else %}

View File

@ -1,11 +1,12 @@
{% extends 'generic/object_edit.html' %} {% extends 'generic/object_edit.html' %}
{% load static %} {% load static %}
{% load form_helpers %} {% load form_helpers %}
{% load i18n %}
{% block form %} {% block form %}
<div class="field-group my-5"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Circuit Termination</h5> <h5 class="offset-sm-3">{% trans "Circuit Termination" %}</h5>
</div> </div>
{% render_field form.circuit %} {% render_field form.circuit %}
{% render_field form.term_side %} {% render_field form.term_side %}
@ -16,10 +17,10 @@
<div class="offset-sm-3"> <div class="offset-sm-3">
<ul class="nav nav-pills" role="tablist"> <ul class="nav nav-pills" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link{% if not providernetwork_tab_active %} active{% endif %}" role="tab" type="button" data-bs-target="#site" data-bs-toggle="tab">Site</button> <button class="nav-link{% if not providernetwork_tab_active %} active{% endif %}" role="tab" type="button" data-bs-target="#site" data-bs-toggle="tab">{% trans "Site" %}</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link{% if providernetwork_tab_active %} active{% endif %}" role="tab" type="button" data-bs-toggle="tab" data-bs-target="#providernetwork">Provider Network</button> <button class="nav-link{% if providernetwork_tab_active %} active{% endif %}" role="tab" type="button" data-bs-toggle="tab" data-bs-target="#providernetwork">{% trans "Provider Network" %}</button>
</li> </li>
</ul> </ul>
</div> </div>
@ -37,7 +38,7 @@
<div class="field-group my-5"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Termination Details</h5> <h5 class="offset-sm-3">{% trans "Termination Details" %}</h5>
</div> </div>
{% render_field form.port_speed %} {% render_field form.port_speed %}
{% render_field form.upstream_speed %} {% render_field form.upstream_speed %}
@ -49,7 +50,7 @@
{% if form.custom_fields %} {% if form.custom_fields %}
<div class="field-group mb-5"> <div class="field-group mb-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">{% trans "Custom Fields" %}</h5>
</div> </div>
{% render_custom_fields form %} {% render_custom_fields form %}
</div> </div>

View File

@ -2,11 +2,12 @@
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %} {% block extra_controls %}
{% if perms.circuits.add_circuit %} {% if perms.circuits.add_circuit %}
<a href="{% url 'circuits:circuit_add' %}?type={{ object.pk }}" class="btn btn-sm btn-primary"> <a href="{% url 'circuits:circuit_add' %}?type={{ object.pk }}" class="btn btn-sm btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Circuit <span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Circuit" %}
</a> </a>
{% endif %} {% endif %}
{% endblock extra_controls %} {% endblock extra_controls %}
@ -16,16 +17,16 @@
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">
Circuit Type {% trans "Circuit Type" %}
</h5> </h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Name</th> <th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td> <td>{{ object.name }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>
</tr> </tr>
</table> </table>

View File

@ -1,35 +1,36 @@
{% load helpers %} {% load helpers %}
{% load i18n %}
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<div class="float-md-end"> <div class="float-md-end">
{% if not termination and perms.circuits.add_circuittermination %} {% if not termination and perms.circuits.add_circuittermination %}
<a href="{% url 'circuits:circuittermination_add' %}?circuit={{ object.pk }}&term_side={{ side }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-success lh-1"> <a href="{% url 'circuits:circuittermination_add' %}?circuit={{ object.pk }}&term_side={{ side }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-success lh-1">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add <span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add" %}
</a> </a>
{% endif %} {% endif %}
{% if termination and perms.circuits.change_circuittermination %} {% if termination and perms.circuits.change_circuittermination %}
<a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning lh-1"> <a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning lh-1">
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit <span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
</a> </a>
<a href="{% url 'circuits:circuit_terminations_swap' pk=object.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary lh-1"> <a href="{% url 'circuits:circuit_terminations_swap' pk=object.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary lh-1">
<span class="mdi mdi-swap-vertical" aria-hidden="true"></span> Swap <span class="mdi mdi-swap-vertical" aria-hidden="true"></span> {% trans "Swap" %}
</a> </a>
{% endif %} {% endif %}
{% if termination and perms.circuits.delete_circuittermination %} {% if termination and perms.circuits.delete_circuittermination %}
<a href="{% url 'circuits:circuittermination_delete' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-danger lh-1"> <a href="{% url 'circuits:circuittermination_delete' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-danger lh-1">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Delete" %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
<h5>Termination {{ side }}</h5> <h5>{% blocktrans %}Termination {{ side }}{% endblocktrans %}</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if termination %} {% if termination %}
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
{% if termination.site %} {% if termination.site %}
<tr> <tr>
<td>Site</td> <td>{% trans "Site" %}</td>
<td> <td>
{% if termination.site.region %} {% if termination.site.region %}
{{ termination.site.region|linkify }} / {{ termination.site.region|linkify }} /
@ -38,13 +39,13 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Termination</td> <td>{% trans "Termination" %}</td>
<td> <td>
{% if termination.mark_connected %} {% if termination.mark_connected %}
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> <span class="text-success"><i class="mdi mdi-check-bold"></i></span>
<span class="text-muted">Marked as connected</span> <span class="text-muted">{% trans "Marked as connected" %}</span>
{% elif termination.cable %} {% elif termination.cable %}
<a class="d-block d-md-inline" href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a> to <a class="d-block d-md-inline" href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a> {% trans "to" %}
{% for peer in termination.link_peers %} {% for peer in termination.link_peers %}
{% if peer.device %} {% if peer.device %}
{{ peer.device|linkify }}<br/> {{ peer.device|linkify }}<br/>
@ -54,30 +55,30 @@
{{ peer|linkify }}{% if not forloop.last %},{% endif %} {{ peer|linkify }}{% if not forloop.last %},{% endif %}
{% endfor %} {% endfor %}
<div class="mt-1"> <div class="mt-1">
<a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace"> <a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-sm lh-1" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> Trace <i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> {% trans "Trace" %}
</a> </a>
{% if perms.dcim.change_cable %} {% if perms.dcim.change_cable %}
<a href="{% url 'dcim:cable_edit' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="Edit cable" class="btn btn-warning btn-sm"> <a href="{% url 'dcim:cable_edit' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="{% trans "Edit cable" %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Edit <i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> {% trans "Edit" %}
</a> </a>
{% endif %} {% endif %}
{% if perms.dcim.delete_cable %} {% if perms.dcim.delete_cable %}
<a href="{% url 'dcim:cable_delete' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="Remove cable" class="btn btn-danger btn-sm lh-1"> <a href="{% url 'dcim:cable_delete' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="{% trans "Remove cable" %}" class="btn btn-danger btn-sm lh-1">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> Disconnect <i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> {% trans "Disconnect" %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
{% elif perms.dcim.add_cable %} {% elif perms.dcim.add_cable %}
<div class="dropdown"> <div class="dropdown">
<button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">Interface</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">{% trans "Interface" %}</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">Front Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">{% trans "Front Port" %}</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">Rear Port</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">{% trans "Rear Port" %}</a></li>
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">Circuit Termination</a></li> <li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">{% trans "Circuit Termination" %}</a></li>
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
@ -85,16 +86,16 @@
</tr> </tr>
{% else %} {% else %}
<tr> <tr>
<td>Provider Network</td> <td>{% trans "Provider Network" %}</td>
<td>{{ termination.provider_network.provider|linkify }} / {{ termination.provider_network|linkify }}</td> <td>{{ termination.provider_network.provider|linkify }} / {{ termination.provider_network|linkify }}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<td>Speed</td> <td>{% trans "Speed" %}</td>
<td> <td>
{% if termination.port_speed and termination.upstream_speed %} {% if termination.port_speed and termination.upstream_speed %}
<i class="mdi mdi-arrow-down-bold" title="Downstream"></i> {{ termination.port_speed|humanize_speed }} &nbsp; <i class="mdi mdi-arrow-down-bold" title="{% trans "Downstream" %}"></i> {{ termination.port_speed|humanize_speed }} &nbsp;
<i class="mdi mdi-arrow-up-bold" title="Upstream"></i> {{ termination.upstream_speed|humanize_speed }} <i class="mdi mdi-arrow-up-bold" title="{% trans "Upstream" %}"></i> {{ termination.upstream_speed|humanize_speed }}
{% elif termination.port_speed %} {% elif termination.port_speed %}
{{ termination.port_speed|humanize_speed }} {{ termination.port_speed|humanize_speed }}
{% else %} {% else %}
@ -103,19 +104,19 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Cross-Connect</td> <td>{% trans "Cross-Connect" %}</td>
<td>{{ termination.xconnect_id|placeholder }}</td> <td>{{ termination.xconnect_id|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<td>Patch Panel/Port</td> <td>{% trans "Patch Panel/Port" %}</td>
<td>{{ termination.pp_info|placeholder }}</td> <td>{{ termination.pp_info|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<td>Description</td> <td>{% trans "Description" %}</td>
<td>{{ termination.description|placeholder }}</td> <td>{{ termination.description|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<td>Tags</td> <td>{% trans "Tags" %}</td>
<td> <td>
{% for tag in termination.tags.all %} {% for tag in termination.tags.all %}
{% tag tag %} {% tag tag %}
@ -150,7 +151,7 @@
{% endfor %} {% endfor %}
</table> </table>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">{% trans "None" %}</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -3,11 +3,12 @@
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %} {% block extra_controls %}
{% if perms.circuits.add_circuit %} {% if perms.circuits.add_circuit %}
<a href="{% url 'circuits:circuit_add' %}?provider={{ object.pk }}" class="btn btn-sm btn-primary"> <a href="{% url 'circuits:circuit_add' %}?provider={{ object.pk }}" class="btn btn-sm btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add circuit <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add circuit" %}
</a> </a>
{% endif %} {% endif %}
{% endblock extra_controls %} {% endblock extra_controls %}
@ -16,11 +17,11 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header">Provider</h5> <h5 class="card-header">{% trans "Provider" %}</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">ASNs</th> <th scope="row">{% trans "ASNs" %}</th>
<td> <td>
{% for asn in object.asns.all %} {% for asn in object.asns.all %}
{{ asn|linkify }}{% if not forloop.last %}, {% endif %} {{ asn|linkify }}{% if not forloop.last %}, {% endif %}
@ -30,7 +31,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>
</tr> </tr>
</table> </table>
@ -49,7 +50,7 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col col-md-12"> <div class="col col-md-12">
<div class="card"> <div class="card">
<h5 class="card-header">Provider Accounts</h5> <h5 class="card-header">{% trans "Provider Accounts" %}</h5>
<div class="card-body htmx-container table-responsive" <div class="card-body htmx-container table-responsive"
hx-get="{% url 'circuits:provideraccount_list' %}?provider_id={{ object.pk }}" hx-get="{% url 'circuits:provideraccount_list' %}?provider_id={{ object.pk }}"
hx-trigger="load" hx-trigger="load"
@ -57,7 +58,7 @@
</div> </div>
<div class="col col-md-12"> <div class="col col-md-12">
<div class="card"> <div class="card">
<h5 class="card-header">Circuits</h5> <h5 class="card-header">{% trans "Circuits" %}</h5>
<div class="card-body htmx-container table-responsive" <div class="card-body htmx-container table-responsive"
hx-get="{% url 'circuits:circuit_list' %}?provider_id={{ object.pk }}" hx-get="{% url 'circuits:circuit_list' %}?provider_id={{ object.pk }}"
hx-trigger="load" hx-trigger="load"

View File

@ -3,6 +3,7 @@
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %} {% block breadcrumbs %}
{{ block.super }} {{ block.super }}
@ -13,19 +14,19 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header">Provider Account</h5> <h5 class="card-header">{% trans "Provider Account" %}</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Provider</th> <th scope="row">{% trans "Provider" %}</th>
<td>{{ object.provider|linkify }}</td> <td>{{ object.provider|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Account</th> <th scope="row">{% trans "Account" %}</th>
<td>{{ object.account }}</td> <td>{{ object.account }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Name</th> <th scope="row">{% trans "Name" %}</th>
<td>{{ object.name|placeholder }}</td> <td>{{ object.name|placeholder }}</td>
</tr> </tr>
</table> </table>
@ -42,7 +43,7 @@
</div> </div>
<div class="col col-md-12"> <div class="col col-md-12">
<div class="card"> <div class="card">
<h5 class="card-header">Circuits</h5> <h5 class="card-header">{% trans "Circuits" %}</h5>
<div class="card-body htmx-container table-responsive" <div class="card-body htmx-container table-responsive"
hx-get="{% url 'circuits:circuit_list' %}?provider_account_id={{ object.pk }}" hx-get="{% url 'circuits:circuit_list' %}?provider_account_id={{ object.pk }}"
hx-trigger="load" hx-trigger="load"

View File

@ -3,6 +3,7 @@
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %} {% block breadcrumbs %}
{{ block.super }} {{ block.super }}
@ -14,24 +15,24 @@
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">
Provider Network {% trans "Provider Network" %}
</h5> </h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Provider</th> <th scope="row">{% trans "Provider" %}</th>
<td>{{ object.provider|linkify }}</td> <td>{{ object.provider|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Name</th> <th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td> <td>{{ object.name }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Service ID</th> <th scope="row">{% trans "Service ID" %}</th>
<td>{{ object.service_id|placeholder }}</td> <td>{{ object.service_id|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>
</tr> </tr>
</table> </table>
@ -50,7 +51,7 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col col-md-12"> <div class="col col-md-12">
<div class="card"> <div class="card">
<h5 class="card-header">Circuits</h5> <h5 class="card-header">{% trans "Circuits" %}</h5>
<div class="card-body htmx-container table-responsive" <div class="card-body htmx-container table-responsive"
hx-get="{% url 'circuits:circuit_list' %}?provider_network_id={{ object.pk }}" hx-get="{% url 'circuits:circuit_list' %}?provider_network_id={{ object.pk }}"
hx-trigger="load" hx-trigger="load"

View File

@ -4,6 +4,7 @@
{% load helpers %} {% load helpers %}
{% load perms %} {% load perms %}
{% load plugins %} {% load plugins %}
{% load i18n %}
{% block breadcrumbs %} {% block breadcrumbs %}
{{ block.super }} {{ block.super }}
@ -28,30 +29,30 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<h5 class="card-header">Data File</h5> <h5 class="card-header">{% trans "Data File" %}</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Source</th> <th scope="row">{% trans "Source" %}</th>
<td>{{ object.source|linkify }}</td> <td>{{ object.source|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Path</th> <th scope="row">{% trans "Path" %}</th>
<td> <td>
<span class="font-monospace" id="datafile_path">{{ object.path }}</span> <span class="font-monospace" id="datafile_path">{{ object.path }}</span>
{% copy_content "datafile_path" %} {% copy_content "datafile_path" %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Last Updated</th> <th scope="row">{% trans "Last Updated" %}</th>
<td>{{ object.last_updated }}</td> <td>{{ object.last_updated }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Size</th> <th scope="row">{% trans "Size" %}</th>
<td>{{ object.size }} byte{{ object.size|pluralize }}</td> <td>{{ object.size }} {% trans "bytes" %}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">SHA256 Hash</th> <th scope="row">{% trans "SHA256 Hash" %}</th>
<td> <td>
<span class="font-monospace" id="datafile_hash">{{ object.hash }}</span> <span class="font-monospace" id="datafile_hash">{{ object.hash }}</span>
{% copy_content "datafile_hash" %} {% copy_content "datafile_hash" %}
@ -61,7 +62,7 @@
</div> </div>
</div> </div>
<div class="card"> <div class="card">
<h5 class="card-header">Content</h5> <h5 class="card-header">{% trans "Content" %}</h5>
<div class="card-body"> <div class="card-body">
<pre>{{ object.data_as_string }}</pre> <pre>{{ object.data_as_string }}</pre>
</div> </div>

View File

@ -3,6 +3,7 @@
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %} {% block extra_controls %}
{% if perms.core.sync_datasource %} {% if perms.core.sync_datasource %}
@ -10,13 +11,13 @@
<form action="{% url 'core:datasource_sync' pk=object.pk %}" method="post"> <form action="{% url 'core:datasource_sync' pk=object.pk %}" method="post">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="btn btn-sm btn-primary"> <button type="submit" class="btn btn-sm btn-primary">
<i class="mdi mdi-sync" aria-hidden="true"></i> Sync <i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync" %}
</button> </button>
</form> </form>
{% else %} {% else %}
<span class="inline-block" tabindex="0" data-bs-toggle="tooltip" data-bs-delay="100" data-bs-placement="bottom"> <span class="inline-block" tabindex="0" data-bs-toggle="tooltip" data-bs-delay="100" data-bs-placement="bottom">
<button class="btn btn-sm btn-primary" disabled> <button class="btn btn-sm btn-primary" disabled>
<i class="mdi mdi-sync" aria-hidden="true"></i> Sync <i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync" %}
</button> </button>
</span> </span>
{% endif %} {% endif %}
@ -27,35 +28,35 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header">Data Source</h5> <h5 class="card-header">{% trans "Data Source" %}</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Name</th> <th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td> <td>{{ object.name }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Type</th> <th scope="row">{% trans "Type" %}</th>
<td>{{ object.get_type_display }}</td> <td>{{ object.get_type_display }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Enabled</th> <th scope="row">{% trans "Enabled" %}</th>
<td>{% checkmark object.enabled %}</td> <td>{% checkmark object.enabled %}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Status</th> <th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td> <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Last synced</th> <th scope="row">{% trans "Last synced" %}</th>
<td>{{ object.last_synced|placeholder }}</td> <td>{{ object.last_synced|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">URL</th> <th scope="row">{% trans "URL" %}</th>
<td> <td>
{% if not object.is_local %} {% if not object.is_local %}
<a href="{{ object.source_url }}">{{ object.source_url }}</a> <a href="{{ object.source_url }}">{{ object.source_url }}</a>
@ -65,7 +66,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Ignore rules</th> <th scope="row">{% trans "Ignore rules" %}</th>
<td> <td>
{% if object.ignore_rules %} {% if object.ignore_rules %}
<pre>{{ object.ignore_rules }}</pre> <pre>{{ object.ignore_rules }}</pre>
@ -82,7 +83,7 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header">Backend</h5> <h5 class="card-header">{% trans "Backend" %}</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
{% for name, field in object.get_backend.parameters.items %} {% for name, field in object.get_backend.parameters.items %}
@ -97,7 +98,7 @@
{% empty %} {% empty %}
<tr> <tr>
<td colspan="2" class="text-muted"> <td colspan="2" class="text-muted">
No parameters defined {% trans "No parameters defined" %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -112,7 +113,7 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col col-md-12"> <div class="col col-md-12">
<div class="card"> <div class="card">
<h5 class="card-header">Files</h5> <h5 class="card-header">{% trans "Files" %}</h5>
<div class="card-body htmx-container table-responsive" <div class="card-body htmx-container table-responsive"
hx-get="{% url 'core:datafile_list' %}?source_id={{ object.pk }}" hx-get="{% url 'core:datafile_list' %}?source_id={{ object.pk }}"
hx-trigger="load" hx-trigger="load"

View File

@ -2,6 +2,7 @@
{% load buttons %} {% load buttons %}
{% load helpers %} {% load helpers %}
{% load perms %} {% load perms %}
{% load i18n %}
{% block controls %} {% block controls %}
<div class="controls"> <div class="controls">
@ -17,25 +18,25 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header">Job</h5> <h5 class="card-header">{% trans "Job" %}</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Object Type</th> <th scope="row">{% trans "Object Type" %}</th>
<td> <td>
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object_type }}</a> <a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object_type }}</a>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Name</th> <th scope="row">{% trans "Name" %}</th>
<td>{{ object.name|placeholder }}</td> <td>{{ object.name|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Status</th> <th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display object.get_status_color %}</td> <td>{% badge object.get_status_display object.get_status_color %}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Created By</th> <th scope="row">{% trans "Created By" %}</th>
<td>{{ object.user|placeholder }}</td> <td>{{ object.user|placeholder }}</td>
</tr> </tr>
</table> </table>
@ -44,23 +45,23 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header">Scheduling</h5> <h5 class="card-header">{% trans "Scheduling" %}</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Created</th> <th scope="row">{% trans "Created" %}</th>
<td>{{ object.created|annotated_date }}</td> <td>{{ object.created|annotated_date }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Scheduled</th> <th scope="row">{% trans "Scheduled" %}</th>
<td>{{ object.scheduled|annotated_date|placeholder }}{% if object.interval %} (every {{ object.interval }} seconds){% endif %}</td> <td>{{ object.scheduled|annotated_date|placeholder }}{% if object.interval %} ({% blocktrans %}every {{ object.interval }} seconds{% endblocktrans %}){% endif %}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Started</th> <th scope="row">{% trans "Started" %}</th>
<td>{{ object.started|annotated_date|placeholder }}</td> <td>{{ object.started|annotated_date|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Completed</th> <th scope="row">{% trans "Completed" %}</th>
<td>{{ object.completed|annotated_date|placeholder }}</td> <td>{{ object.completed|annotated_date|placeholder }}</td>
</tr> </tr>
</table> </table>
@ -71,7 +72,7 @@
<div class="row"> <div class="row">
<div class="col col-12"> <div class="col col-12">
<div class="card"> <div class="card">
<h5 class="card-header">Data</h5> <h5 class="card-header">{% trans "Data" %}</h5>
<div class="card-body"> <div class="card-body">
<pre>{{ object.data|json }}</pre> <pre>{{ object.data|json }}</pre>
</div> </div>

View File

@ -1,10 +1,11 @@
{% extends 'generic/confirmation_form.html' %} {% extends 'generic/confirmation_form.html' %}
{% load helpers %} {% load helpers %}
{% load i18n %}
{% block title %}Disconnect {{ obj_type_plural|bettertitle }}{% endblock %} {% block title %}{% trans "Disconnect" %} {{ obj_type_plural|bettertitle }}{% endblock %}
{% block message %} {% block message %}
<p>Are you sure you want to disconnect these {{ selected_objects|length }} {{ obj_type_plural }}?</p> <p>{% blocktrans %}Are you sure you want to disconnect these {{ selected_objects|length }} {{ obj_type_plural }}?{% endblocktrans %}</p>
<ul> <ul>
{% for obj in selected_objects %} {% for obj in selected_objects %}
<li>{{ obj }}</li> <li>{{ obj }}</li>

View File

@ -3,24 +3,25 @@
{% load helpers %} {% load helpers %}
{% load perms %} {% load perms %}
{% load plugins %} {% load plugins %}
{% load i18n %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header">Cable</h5> <h5 class="card-header">{% trans "Cable" %}</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Type</th> <th scope="row">{% trans "Type" %}</th>
<td>{{ object.get_type_display|placeholder }}</td> <td>{{ object.get_type_display|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Status</th> <th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td> <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Tenant</th> <th scope="row">{% trans "Tenant" %}</th>
<td> <td>
{% if object.tenant.group %} {% if object.tenant.group %}
{{ object.tenant.group|linkify }} / {{ object.tenant.group|linkify }} /
@ -29,15 +30,15 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Label</th> <th scope="row">{% trans "Label" %}</th>
<td>{{ object.label|placeholder }}</td> <td>{{ object.label|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Color</th> <th scope="row">{% trans "Color" %}</th>
<td> <td>
{% if object.color %} {% if object.color %}
<span class="color-label" style="background-color: #{{ object.color }}">&nbsp;</span> <span class="color-label" style="background-color: #{{ object.color }}">&nbsp;</span>
@ -47,7 +48,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Length</th> <th scope="row">{% trans "Length" %}</th>
<td> <td>
{% if object.length %} {% if object.length %}
{{ object.length|floatformat }} {{ object.get_length_unit_display }} {{ object.length|floatformat }} {{ object.get_length_unit_display }}
@ -66,13 +67,13 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header">Termination A</h5> <h5 class="card-header">{% trans "Termination" %} A</h5>
<div class="card-body"> <div class="card-body">
{% include 'dcim/inc/cable_termination.html' with terminations=object.a_terminations %} {% include 'dcim/inc/cable_termination.html' with terminations=object.a_terminations %}
</div> </div>
</div> </div>
<div class="card"> <div class="card">
<h5 class="card-header">Termination B</h5> <h5 class="card-header">{% trans "Termination" %} B</h5>
<div class="card-body"> <div class="card-body">
{% include 'dcim/inc/cable_termination.html' with terminations=object.b_terminations %} {% include 'dcim/inc/cable_termination.html' with terminations=object.b_terminations %}
</div> </div>

View File

@ -2,13 +2,14 @@
{% load static %} {% load static %}
{% load helpers %} {% load helpers %}
{% load form_helpers %} {% load form_helpers %}
{% load i18n %}
{% block form %} {% block form %}
{# A side termination #} {# A side termination #}
<div class="field-group mb-5"> <div class="field-group mb-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">A Side</h5> <h5 class="offset-sm-3">{% trans "A Side" %}</h5>
</div> </div>
{% if 'termination_a_device' in form.fields %} {% if 'termination_a_device' in form.fields %}
{% render_field form.termination_a_device %} {% render_field form.termination_a_device %}
@ -25,7 +26,7 @@
{# B side termination #} {# B side termination #}
<div class="field-group mb-5"> <div class="field-group mb-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">B Side</h5> <h5 class="offset-sm-3">{% trans "B Side" %}</h5>
</div> </div>
{% if 'termination_b_device' in form.fields %} {% if 'termination_b_device' in form.fields %}
{% render_field form.termination_b_device %} {% render_field form.termination_b_device %}
@ -42,7 +43,7 @@
{# Cable attributes #} {# Cable attributes #}
<div class="field-group mb-5"> <div class="field-group mb-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Cable</h5> <h5 class="offset-sm-3">{% trans "Cable" %}</h5>
</div> </div>
{% render_field form.status %} {% render_field form.status %}
{% render_field form.type %} {% render_field form.type %}
@ -64,7 +65,7 @@
<div class="field-group mb-5"> <div class="field-group mb-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Tenancy</h5> <h5 class="offset-sm-3">{% trans "Tenancy" %}</h5>
</div> </div>
{% render_field form.tenant_group %} {% render_field form.tenant_group %}
{% render_field form.tenant %} {% render_field form.tenant %}
@ -73,7 +74,7 @@
{% if form.custom_fields %} {% if form.custom_fields %}
<div class="field-group mb-5"> <div class="field-group mb-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">{% trans "Custom Fields" %}</h5>
</div> </div>
{% render_custom_fields form %} {% render_custom_fields form %}
</div> </div>
@ -81,7 +82,7 @@
{% if form.comments %} {% if form.comments %}
<div class="field-group mb-5"> <div class="field-group mb-5">
<h5 class="text-center">Comments</h5> <h5 class="text-center">{% trans "Comments" %}</h5>
{% render_field form.comments %} {% render_field form.comments %}
</div> </div>
{% endif %} {% endif %}

View File

@ -1,7 +1,8 @@
{% extends 'base/layout.html' %} {% extends 'base/layout.html' %}
{% load helpers %} {% load helpers %}
{% load i18n %}
{% block title %}Cable Trace for {{ object|meta:"verbose_name"|bettertitle }} {{ object }}{% endblock %} {% block title %}{% blocktrans %}Cable Trace for {{ object|meta:"verbose_name"|bettertitle }} {{ object }}{% endblocktrans %}{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
@ -13,21 +14,21 @@
<object data="{{ svg_url }}" class="rack_elevation"></object> <object data="{{ svg_url }}" class="rack_elevation"></object>
<div> <div>
<a class="btn btn-outline-primary btn-sm my-3" href="{{ svg_url }}"> <a class="btn btn-outline-primary btn-sm my-3" href="{{ svg_url }}">
<i class="mdi mdi-file-download"></i> Download SVG <i class="mdi mdi-file-download"></i> {% trans "Download SVG" %}
</a> </a>
</div> </div>
</div> </div>
<div class="trace-end"> <div class="trace-end">
{% if path.is_split %} {% if path.is_split %}
<h3 class="text-danger">Path split!</h3> <h3 class="text-danger">{% trans "Path split" %}!</h3>
<p>Select a node below to continue:</p> <p>{% trans "Select a node below to continue" %}:</p>
<ul class="text-start"> <ul class="text-start">
{% for next_node in path.get_split_nodes %} {% for next_node in path.get_split_nodes %}
{% if next_node.cable %} {% if next_node.cable %}
{% with viewname=next_node|viewname:"trace" %} {% with viewname=next_node|viewname:"trace" %}
<li> <li>
<a href="{% url viewname pk=next_node.pk %}">{{ next_node|meta:"verbose_name"|bettertitle }} {{ next_node }}</a> <a href="{% url viewname pk=next_node.pk %}">{{ next_node|meta:"verbose_name"|bettertitle }} {{ next_node }}</a>
(Cable {{ next_node.cable|linkify }}) ({% trans "Cable" %} {{ next_node.cable|linkify }})
</li> </li>
{% endwith %} {% endwith %}
{% else %} {% else %}
@ -36,20 +37,20 @@
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}
<h3 class="text-center text-success">Trace Completed</h3> <h3 class="text-center text-success">{% trans "Trace Completed" %}</h3>
<table class="table"> <table class="table">
<tr> <tr>
<th scope="row">Total segments</th> <th scope="row">{% trans "Total segments" %}</th>
<td>{{ path.segment_count }}</td> <td>{{ path.segment_count }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Total length</th> <th scope="row">{% trans "Total length" %}</th>
<td> <td>
{% if total_length %} {% if total_length %}
{{ total_length|floatformat:"-2" }}{% if not is_definitive %}+{% endif %} Meters / {{ total_length|floatformat:"-2" }}{% if not is_definitive %}+{% endif %} {% trans "Meters" %} /
{{ total_length|meters_to_feet|floatformat:"-2" }} Feet {{ total_length|meters_to_feet|floatformat:"-2" }} {% trans "Feet" %}
{% else %} {% else %}
<span class="text-muted">N/A</span> <span class="text-muted">{% trans "N/A" %}</span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
@ -58,7 +59,7 @@
</div> </div>
{% else %} {% else %}
<h3 class="text-center text-muted my-3"> <h3 class="text-center text-muted my-3">
No paths found {% trans "No paths found" %}
</h3> </h3>
{% endif %} {% endif %}
</div> </div>
@ -67,15 +68,15 @@
<div class="col col-md-7"> <div class="col col-md-7">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">
Related Paths {% trans "Related Paths" %}
</h5> </h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th>Origin</th> <th>{% trans "Origin" %}</th>
<th>Destination</th> <th>{% trans "Destination" %}</th>
<th>Segments</th> <th>{% trans "Segments" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -88,7 +89,7 @@
{% if cablepath.destinations %} {% if cablepath.destinations %}
{{ cablepath.destinations|join:", " }} {{ cablepath.destinations|join:", " }}
{% else %} {% else %}
<span class="text-muted">Incomplete</span> <span class="text-muted">{% trans "Incomplete" %}</span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
@ -97,7 +98,7 @@
</tr> </tr>
{% empty %} {% empty %}
<td colspan="3" class="text-muted"> <td colspan="3" class="text-muted">
None found {% trans "None found" %}
</td> </td>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -1,6 +1,7 @@
{% extends 'generic/object.html' %} {% extends 'generic/object.html' %}
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load i18n %}
{% block breadcrumbs %} {% block breadcrumbs %}
{{ block.super }} {{ block.super }}
@ -14,36 +15,36 @@
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">
Console Port {% trans "Console Port" %}
</h5> </h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Device</th> <th scope="row">{% trans "Device" %}</th>
<td>{{ object.device|linkify }}</td> <td>{{ object.device|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Module</th> <th scope="row">{% trans "Module" %}</th>
<td>{{ object.module|linkify|placeholder }}</td> <td>{{ object.module|linkify|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Name</th> <th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td> <td>{{ object.name }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Label</th> <th scope="row">{% trans "Label" %}</th>
<td>{{ object.label|placeholder }}</td> <td>{{ object.label|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Type</th> <th scope="row">{% trans "Type" %}</th>
<td>{{ object.get_type_display }}</td> <td>{{ object.get_type_display }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Speed</th> <th scope="row">{% trans "Speed" %}</th>
<td>{{ object.get_speed_display }}</td> <td>{{ object.get_speed_display }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>
</tr> </tr>
</table> </table>
@ -55,29 +56,29 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header">Connection</h5> <h5 class="card-header">{% trans "Connection" %}</h5>
<div class="card-body"> <div class="card-body">
{% if object.mark_connected %} {% if object.mark_connected %}
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected <span class="text-success"><i class="mdi mdi-check-bold"></i></span> {% trans "Marked as connected" %}
{% elif object.cable %} {% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:consoleport_trace' %} {% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:consoleport_trace' %}
{% else %} {% else %}
<div class="text-muted"> <div class="text-muted">
Not Connected {% trans "Not Connected" %}
{% if perms.dcim.add_cable %} {% if perms.dcim.add_cable %}
<div class="dropdown float-end"> <div class="dropdown float-end">
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Server Port</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Console Server Port" %}</a>
</li> </li>
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Front Port" %}</a>
</li> </li>
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Rear Port" %}</a>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -1,8 +1,9 @@
{% extends 'generic/confirmation_form.html' %} {% extends 'generic/confirmation_form.html' %}
{% load form_helpers %} {% load form_helpers %}
{% load i18n %}
{% block title %}Delete console port {{ consoleport }}?{% endblock %} {% block title %}{% blocktrans %}Delete console port {{ consoleport }}?{% endblocktrans %}{% endblock %}
{% block message %} {% block message %}
<p>Are you sure you want to delete this console port from <strong>{{ consoleport.device }}</strong>?</p> <p>{% blocktrans %}Are you sure you want to delete this console port from <strong>{{ consoleport.device }}</strong>?{% endblocktrans %}</p>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,7 @@
{% extends 'generic/object.html' %} {% extends 'generic/object.html' %}
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load i18n %}
{% block breadcrumbs %} {% block breadcrumbs %}
{{ block.super }} {{ block.super }}
@ -14,36 +15,36 @@
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">
Console Server Port {% trans "Console Server Port" %}
</h5> </h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Device</th> <th scope="row">{% trans "Device" %}</th>
<td>{{ object.device|linkify }}</td> <td>{{ object.device|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Module</th> <th scope="row">{% trans "Module" %}</th>
<td>{{ object.module|linkify|placeholder }}</td> <td>{{ object.module|linkify|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Name</th> <th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td> <td>{{ object.name }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Label</th> <th scope="row">{% trans "Label" %}</th>
<td>{{ object.label|placeholder }}</td> <td>{{ object.label|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Type</th> <th scope="row">{% trans "Type" %}</th>
<td>{{ object.get_type_display|placeholder }}</td> <td>{{ object.get_type_display|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Speed</th> <th scope="row">{% trans "Speed" %}</th>
<td>{{ object.get_speed_display|placeholder }}</td> <td>{{ object.get_speed_display|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>
</tr> </tr>
</table> </table>
@ -55,29 +56,29 @@
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header">Connection</h5> <h5 class="card-header">{% trans "Connection" %}</h5>
<div class="card-body"> <div class="card-body">
{% if object.mark_connected %} {% if object.mark_connected %}
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> Marked as connected <span class="text-success"><i class="mdi mdi-check-bold"></i></span> {% trans "Marked as connected" %}
{% elif object.cable %} {% elif object.cable %}
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:consoleserverport_trace' %} {% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:consoleserverport_trace' %}
{% else %} {% else %}
<div class="text-muted"> <div class="text-muted">
Not Connected {% trans "Not Connected" %}
{% if perms.dcim.add_cable %} {% if perms.dcim.add_cable %}
<div class="dropdown float-end"> <div class="dropdown float-end">
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Port</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Console Port" %}</a>
</li> </li>
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Front Port" %}</a>
</li> </li>
<li> <li>
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a> <a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}" class="dropdown-item">{% trans "Rear Port" %}</a>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -1,8 +1,9 @@
{% extends 'generic/confirmation_form.html' %} {% extends 'generic/confirmation_form.html' %}
{% load form_helpers %} {% load form_helpers %}
{% load i18n %}
{% block title %}Delete console server port {{ consoleserverport }}?{% endblock %} {% block title %}{% blocktrans %}Delete console server port {{ consoleserverport }}?{% endblocktrans %}{% endblock %}
{% block message %} {% block message %}
<p>Are you sure you want to delete this console server port from <strong>{{ consoleserverport.device }}</strong>?</p> <p>{% blocktrans %}Are you sure you want to delete this console server port from <strong>{{ consoleserverport.device }}</strong>?{% endblocktrans %}</p>
{% endblock %} {% endblock %}

View File

@ -4,16 +4,17 @@
{% load static %} {% load static %}
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load i18n %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-12 col-xl-6"> <div class="col col-12 col-xl-6">
<div class="card"> <div class="card">
<h5 class="card-header">Device</h5> <h5 class="card-header">{% trans "Device" %}</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Region</th> <th scope="row">{% trans "Region" %}</th>
<td> <td>
{% if object.site.region %} {% if object.site.region %}
{% for region in object.site.region.get_ancestors %} {% for region in object.site.region.get_ancestors %}
@ -26,11 +27,11 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Site</th> <th scope="row">{% trans "Site" %}</th>
<td>{{ object.site|linkify }}</td> <td>{{ object.site|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Location</th> <th scope="row">{% trans "Location" %}</th>
<td> <td>
{% if object.location %} {% if object.location %}
{% for location in object.location.get_ancestors %} {% for location in object.location.get_ancestors %}
@ -43,12 +44,12 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Rack</th> <th scope="row">{% trans "Rack" %}</th>
<td class="position-relative"> <td class="position-relative">
{% if object.rack %} {% if object.rack %}
{{ object.rack|linkify }} {{ object.rack|linkify }}
<div class="position-absolute top-50 end-0 translate-middle-y noprint"> <div class="position-absolute top-50 end-0 translate-middle-y noprint">
<a href="{{ object.rack.get_absolute_url }}?device={{ object.pk }}" class="btn btn-primary btn-sm" title="Highlight device"> <a href="{{ object.rack.get_absolute_url }}?device={{ object.pk }}" class="btn btn-primary btn-sm" title="{% trans "Highlight device" %}">
<i class="mdi mdi-view-day-outline"></i> <i class="mdi mdi-view-day-outline"></i>
</a> </a>
</div> </div>
@ -58,7 +59,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Position</th> <th scope="row">{% trans "Position" %}</th>
<td> <td>
{% if object.parent_bay %} {% if object.parent_bay %}
{% with object.parent_bay.device as parent %} {% with object.parent_bay.device as parent %}
@ -70,20 +71,20 @@
{% elif object.rack and object.position %} {% elif object.rack and object.position %}
<span>U{{ object.position|floatformat }} / {{ object.get_face_display }}</span> <span>U{{ object.position|floatformat }} / {{ object.get_face_display }}</span>
{% elif object.rack and object.device_type.u_height %} {% elif object.rack and object.device_type.u_height %}
<span class="badge bg-warning">Not racked</span> <span class="badge bg-warning">{% trans "Not racked" %}</span>
{% else %} {% else %}
{{ ''|placeholder }} {{ ''|placeholder }}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">GPS Coordinates</th> <th scope="row">{% trans "GPS Coordinates" %}</th>
<td class="position-relative"> <td class="position-relative">
{% if object.latitude and object.longitude %} {% if object.latitude and object.longitude %}
{% if config.MAPS_URL %} {% if config.MAPS_URL %}
<div class="position-absolute top-50 end-0 translate-middle-y noprint"> <div class="position-absolute top-50 end-0 translate-middle-y noprint">
<a href="{{ config.MAPS_URL }}{{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary btn-sm"> <a href="{{ config.MAPS_URL }}{{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary btn-sm">
<i class="mdi mdi-map-marker"></i> Map It <i class="mdi mdi-map-marker"></i> {% trans "Map It" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}
@ -94,7 +95,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Tenant</th> <th scope="row">{% trans "Tenant" %}</th>
<td> <td>
{% if object.tenant.group %} {% if object.tenant.group %}
{{ object.tenant.group|linkify }} / {{ object.tenant.group|linkify }} /
@ -103,31 +104,31 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Device Type</th> <th scope="row">{% trans "Device Type" %}</th>
<td> <td>
{{ object.device_type|linkify:"get_full_name" }} ({{ object.device_type.u_height|floatformat }}U) {{ object.device_type|linkify:"get_full_name" }} ({{ object.device_type.u_height|floatformat }}U)
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td> <td>{{ object.description|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Airflow</th> <th scope="row">{% trans "Airflow" %}</th>
<td> <td>
{{ object.get_airflow_display|placeholder }} {{ object.get_airflow_display|placeholder }}
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Serial Number</th> <th scope="row">{% trans "Serial Number" %}</th>
<td class="font-monospace">{{ object.serial|placeholder }}</td> <td class="font-monospace">{{ object.serial|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Asset Tag</th> <th scope="row">{% trans "Asset Tag" %}</th>
<td class="font-monospace">{{ object.asset_tag|placeholder }}</td> <td class="font-monospace">{{ object.asset_tag|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Config Template</th> <th scope="row">{% trans "Config Template" %}</th>
<td>{{ object.config_template|linkify|placeholder }}</td> <td>{{ object.config_template|linkify|placeholder }}</td>
</tr> </tr>
</table> </table>
@ -136,15 +137,15 @@
{% if vc_members %} {% if vc_members %}
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">
Virtual Chassis {% trans "Virtual Chassis" %}
</h5> </h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th>Device</th> <th>{% trans "Device" %}</th>
<th>Position</th> <th>{% trans "Position" %}</th>
<th>Master</th> <th>{% trans "Master" %}</th>
<th>Priority</th> <th>{% trans "Priority" %}</th>
</tr> </tr>
{% for vc_member in vc_members %} {% for vc_member in vc_members %}
<tr{% if vc_member == object %} class="info"{% endif %}> <tr{% if vc_member == object %} class="info"{% endif %}>
@ -166,7 +167,7 @@
</div> </div>
<div class="card-footer text-end noprint"> <div class="card-footer text-end noprint">
<a href="{{ object.virtual_chassis.get_absolute_url }}" class="btn btn-primary btn-sm"> <a href="{{ object.virtual_chassis.get_absolute_url }}" class="btn btn-primary btn-sm">
<span class="mdi mdi-arrow-right-bold" aria-hidden="true"></span> View Virtual Chassis <span class="mdi mdi-arrow-right-bold" aria-hidden="true"></span> {% trans "View Virtual Chassis" %}
</a> </a>
</div> </div>
</div> </div>
@ -175,7 +176,7 @@
{% include 'inc/panels/tags.html' %} {% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %} {% include 'inc/panels/comments.html' %}
<div class="card"> <div class="card">
<h5 class="card-header">Virtual Device Contexts</h5> <h5 class="card-header">{% trans "Virtual Device Contexts" %}</h5>
<div class="card-body htmx-container table-responsive" <div class="card-body htmx-container table-responsive"
hx-get="{% url 'dcim:virtualdevicecontext_list' %}?device_id={{ object.pk }}" hx-get="{% url 'dcim:virtualdevicecontext_list' %}?device_id={{ object.pk }}"
hx-trigger="load" hx-trigger="load"
@ -183,7 +184,7 @@
{% if perms.dcim.add_virtualdevicecontext %} {% if perms.dcim.add_virtualdevicecontext %}
<div class="card-footer text-end noprint"> <div class="card-footer text-end noprint">
<a href="{% url 'dcim:virtualdevicecontext_add' %}?device={{ object.pk }}" class="btn btn-sm btn-primary"> <a href="{% url 'dcim:virtualdevicecontext_add' %}?device={{ object.pk }}" class="btn btn-sm btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Create VDC <span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Create VDC" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}
@ -192,30 +193,30 @@
</div> </div>
<div class="col col-12 col-xl-6"> <div class="col col-12 col-xl-6">
<div class="card"> <div class="card">
<h5 class="card-header">Management</h5> <h5 class="card-header">{% trans "Management" %}</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Status</th> <th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td> <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Role</th> <th scope="row">{% trans "Role" %}</th>
<td>{{ object.device_role|linkify }}</td> <td>{{ object.device_role|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Platform</th> <th scope="row">{% trans "Platform" %}</th>
<td>{{ object.platform|linkify|placeholder }}</td> <td>{{ object.platform|linkify|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Primary IPv4</th> <th scope="row">{% trans "Primary IPv4" %}</th>
<td> <td>
{% if object.primary_ip4 %} {% if object.primary_ip4 %}
<a href="{{ object.primary_ip4.get_absolute_url }}" id="primary_ip4">{{ object.primary_ip4.address.ip }}</a> <a href="{{ object.primary_ip4.get_absolute_url }}" id="primary_ip4">{{ object.primary_ip4.address.ip }}</a>
{% if object.primary_ip4.nat_inside %} {% if object.primary_ip4.nat_inside %}
(NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>) ({% trans "NAT for" %} <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
{% elif object.primary_ip4.nat_outside.exists %} {% elif object.primary_ip4.nat_outside.exists %}
(NAT: {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}) ({% trans "NAT" %}: {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %} {% endif %}
{% copy_content "primary_ip4" %} {% copy_content "primary_ip4" %}
{% else %} {% else %}
@ -224,14 +225,14 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Primary IPv6</th> <th scope="row">{% trans "Primary IPv6" %}</th>
<td> <td>
{% if object.primary_ip6 %} {% if object.primary_ip6 %}
<a href="{{ object.primary_ip6.get_absolute_url }}" id="primary_ip6">{{ object.primary_ip6.address.ip }}</a> <a href="{{ object.primary_ip6.get_absolute_url }}" id="primary_ip6">{{ object.primary_ip6.address.ip }}</a>
{% if object.primary_ip6.nat_inside %} {% if object.primary_ip6.nat_inside %}
(NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>) ({% trans "NAT for" %} <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
{% elif object.primary_ip6.nat_outside.exists %} {% elif object.primary_ip6.nat_outside.exists %}
(NAT: {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}) ({% trans "NAT" %}: {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %} {% endif %}
{% copy_content "primary_ip6" %} {% copy_content "primary_ip6" %}
{% else %} {% else %}
@ -252,7 +253,7 @@
</tr> </tr>
{% if object.cluster %} {% if object.cluster %}
<tr> <tr>
<th>Cluster</th> <th>{% trans "Cluster" %}</th>
<td> <td>
{% if object.cluster.group %} {% if object.cluster.group %}
{{ object.cluster.group|linkify }} / {{ object.cluster.group|linkify }} /
@ -267,25 +268,25 @@
{% if object.powerports.exists and object.poweroutlets.exists %} {% if object.powerports.exists and object.poweroutlets.exists %}
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">
Power Utilization {% trans "Power Utilization" %}
</h5> </h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover"> <table class="table table-hover">
<tr> <tr>
<th>Input</th> <th>{% trans "Input" %}</th>
<th>Outlets</th> <th>{% trans "Outlets" %}</th>
<th>Allocated</th> <th>{% trans "Allocated" %}</th>
<th>Available</th> <th>{% trans "Available" %}</th>
<th>Utilization</th> <th>{% trans "Utilization" %}</th>
</tr> </tr>
{% for powerport in object.powerports.all %} {% for powerport in object.powerports.all %}
{% with utilization=powerport.get_power_draw powerfeed=powerport.connected_endpoints.0 %} {% with utilization=powerport.get_power_draw powerfeed=powerport.connected_endpoints.0 %}
<tr> <tr>
<td>{{ powerport }}</td> <td>{{ powerport }}</td>
<td>{{ utilization.outlet_count }}</td> <td>{{ utilization.outlet_count }}</td>
<td>{{ utilization.allocated }}VA</td> <td>{{ utilization.allocated }}{% trans "VA" %}</td>
{% if powerfeed.available_power %} {% if powerfeed.available_power %}
<td>{{ powerfeed.available_power }}VA</td> <td>{{ powerfeed.available_power }}{% trans "VA" %}</td>
<td>{% utilization_graph utilization.allocated|percentage:powerfeed.available_power %}</td> <td>{% utilization_graph utilization.allocated|percentage:powerfeed.available_power %}</td>
{% else %} {% else %}
<td class="text-muted">&mdash;</td> <td class="text-muted">&mdash;</td>
@ -294,12 +295,12 @@
</tr> </tr>
{% for leg in utilization.legs %} {% for leg in utilization.legs %}
<tr> <tr>
<td style="padding-left: 20px">Leg {{ leg.name }}</td> <td style="padding-left: 20px">{% blocktrans %}Leg {{ leg.name }}{% endblocktrans %}</td>
<td>{{ leg.outlet_count }}</td> <td>{{ leg.outlet_count }}</td>
<td>{{ leg.allocated }}</td> <td>{{ leg.allocated }}</td>
{% if powerfeed.available_power %} {% if powerfeed.available_power %}
{% with phase_available=powerfeed.available_power|divide:3 %} {% with phase_available=powerfeed.available_power|divide:3 %}
<td>{{ phase_available }}VA</td> <td>{{ phase_available }}{% trans "VA" %}</td>
<td>{% utilization_graph leg.allocated|percentage:phase_available %}</td> <td>{% utilization_graph leg.allocated|percentage:phase_available %}</td>
{% endwith %} {% endwith %}
{% else %} {% else %}
@ -315,7 +316,7 @@
</div> </div>
{% endif %} {% endif %}
<div class="card"> <div class="card">
<h5 class="card-header">Services</h5> <h5 class="card-header">{% trans "Services" %}</h5>
<div class="card-body htmx-container table-responsive" <div class="card-body htmx-container table-responsive"
hx-get="{% url 'ipam:service_list' %}?device_id={{ object.pk }}" hx-get="{% url 'ipam:service_list' %}?device_id={{ object.pk }}"
hx-trigger="load" hx-trigger="load"
@ -323,27 +324,27 @@
{% if perms.ipam.add_service %} {% if perms.ipam.add_service %}
<div class="card-footer text-end noprint"> <div class="card-footer text-end noprint">
<a href="{% url 'ipam:service_add' %}?device={{ object.pk }}" class="btn btn-sm btn-primary"> <a href="{% url 'ipam:service_add' %}?device={{ object.pk }}" class="btn btn-sm btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add a service <span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add a service" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% include 'inc/panels/image_attachments.html' %} {% include 'inc/panels/image_attachments.html' %}
<div class="card"> <div class="card">
<h5 class="card-header">Dimensions</h5> <h5 class="card-header">{% trans "Dimensions" %}</h5>
<div class="card-body table-responsive"> <div class="card-body table-responsive">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Height</th> <th scope="row">{% trans "Height" %}</th>
<td> <td>
{{ object.device_type.u_height }}U {{ object.device_type.u_height }}U
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Weight</th> <th scope="row">{% trans "Weight" %}</th>
<td> <td>
{% if object.total_weight %} {% if object.total_weight %}
{{ object.total_weight|floatformat }} Kilograms {{ object.total_weight|floatformat }} {% trans "Kilograms" %}
{% else %} {% else %}
{{ ''|placeholder }} {{ ''|placeholder }}
{% endif %} {% endif %}
@ -356,13 +357,13 @@
<div class="row" style="margin-bottom: 20px"> <div class="row" style="margin-bottom: 20px">
<div class="col col-md-6 col-sm-6 col-xs-12 text-center"> <div class="col col-md-6 col-sm-6 col-xs-12 text-center">
<div style="margin-left: 30px"> <div style="margin-left: 30px">
<h4>Front</h4> <h4>{% trans "Front" %}</h4>
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' extra_params=svg_extra %} {% include 'dcim/inc/rack_elevation.html' with object=object.rack face='front' extra_params=svg_extra %}
</div> </div>
</div> </div>
<div class="col col-md-6 col-sm-6 col-xs-12 text-center"> <div class="col col-md-6 col-sm-6 col-xs-12 text-center">
<div style="margin-left: 30px"> <div style="margin-left: 30px">
<h4>Rear</h4> <h4>{% trans "Rear" %}</h4>
{% include 'dcim/inc/rack_elevation.html' with object=object.rack face='rear' extra_params=svg_extra %} {% include 'dcim/inc/rack_elevation.html' with object=object.rack face='rear' extra_params=svg_extra %}
</div> </div>
</div> </div>

View File

@ -3,6 +3,7 @@
{% load static %} {% load static %}
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load i18n %}
{% block breadcrumbs %} {% block breadcrumbs %}
{{ block.super }} {{ block.super }}
@ -17,38 +18,38 @@
{% if perms.dcim.change_device %} {% if perms.dcim.change_device %}
<div class="dropdown"> <div class="dropdown">
<button id="add-components" type="button" class="btn btn-sm btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button id="add-components" type="button" class="btn btn-sm btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Components <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Components" %}
</button> </button>
<ul class="dropdown-menu" aria-labeled-by="add-components"> <ul class="dropdown-menu" aria-labeled-by="add-components">
{% if perms.dcim.add_consoleport %} {% if perms.dcim.add_consoleport %}
<li><a class="dropdown-item" href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Console Ports</a></li> <li><a class="dropdown-item" href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">{% trans "Console Ports" %}</a></li>
{% endif %} {% endif %}
{% if perms.dcim.add_consoleserverport %} {% if perms.dcim.add_consoleserverport %}
<li><a class="dropdown-item" href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Server Ports</a></li> <li><a class="dropdown-item" href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">{% trans "Console Server Ports" %}</a></li>
{% endif %} {% endif %}
{% if perms.dcim.add_powerport %} {% if perms.dcim.add_powerport %}
<li><a class="dropdown-item" href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Ports</a></li> <li><a class="dropdown-item" href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}">{% trans "Power Ports" %}</a></li>
{% endif %} {% endif %}
{% if perms.dcim.add_poweroutlet %} {% if perms.dcim.add_poweroutlet %}
<li><a class="dropdown-item" href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}">Power Outlets</a></li> <li><a class="dropdown-item" href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}">{% trans "Power Outlets" %}</a></li>
{% endif %} {% endif %}
{% if perms.dcim.add_interface %} {% if perms.dcim.add_interface %}
<li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Interfaces</a></li> <li><a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">{% trans "Interfaces" %}</a></li>
{% endif %} {% endif %}
{% if perms.dcim.add_frontport %} {% if perms.dcim.add_frontport %}
<li><a class="dropdown-item" href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Front Ports</a></li> <li><a class="dropdown-item" href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">{% trans "Front Ports" %}</a></li>
{% endif %} {% endif %}
{% if perms.dcim.add_rearport %} {% if perms.dcim.add_rearport %}
<li><a class="dropdown-item" href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Rear Ports</a></li> <li><a class="dropdown-item" href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">{% trans "Rear Ports" %}</a></li>
{% endif %} {% endif %}
{% if perms.dcim.add_devicebay %} {% if perms.dcim.add_devicebay %}
<li><a class="dropdown-item" href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}">Module Bays</a></li> <li><a class="dropdown-item" href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}">{% trans "Module Bays" %}</a></li>
{% endif %} {% endif %}
{% if perms.dcim.add_devicebay %} {% if perms.dcim.add_devicebay %}
<li><a class="dropdown-item" href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}">Device Bays</a></li> <li><a class="dropdown-item" href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}">{% trans "Device Bays" %}</a></li>
{% endif %} {% endif %}
{% if perms.dcim.add_inventoryitem %} {% if perms.dcim.add_inventoryitem %}
<li><a class="dropdown-item" href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}">Inventory Items</a></li> <li><a class="dropdown-item" href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}">{% trans "Inventory Items" %}</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>

View File

@ -2,6 +2,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load helpers %} {% load helpers %}
{% load static %} {% load static %}
{% load i18n %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsolePortTable_config" %} {% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsolePortTable_config" %}
@ -20,22 +21,22 @@
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-warning btn-sm"> <button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit <i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
</button> </button>
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-warning btn-sm"> <button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button> </button>
</div> </div>
{% endif %} {% endif %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
{% if 'bulk_delete' in actions %} {% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-danger btn-sm"> <button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</button> </button>
{% endif %} {% endif %}
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-danger btn-sm"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>
{% endif %} {% endif %}
</div> </div>
@ -43,7 +44,7 @@
{% if perms.dcim.add_consoleport %} {% if perms.dcim.add_consoleport %}
<div class="bulk-button-group"> <div class="bulk-button-group">
<a href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-sm btn-primary"> <a href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-sm btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Port <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Console Port" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -2,6 +2,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load helpers %} {% load helpers %}
{% load static %} {% load static %}
{% load i18n %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsoleServerPortTable_config" %} {% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsoleServerPortTable_config" %}
@ -20,22 +21,22 @@
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-warning btn-sm"> <button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit <i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
</button> </button>
<button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-warning btn-sm"> <button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button> </button>
</div> </div>
{% endif %} {% endif %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
{% if 'bulk_delete' in actions %} {% if 'bulk_delete' in actions %}
<button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-danger btn-sm"> <button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</button> </button>
{% endif %} {% endif %}
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-danger btn-sm"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>
{% endif %} {% endif %}
</div> </div>
@ -43,7 +44,7 @@
{% if perms.dcim.add_consoleserverport %} {% if perms.dcim.add_consoleserverport %}
<div class="bulk-button-group"> <div class="bulk-button-group">
<a href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-primary btn-sm"> <a href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Server Ports <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Console Server Ports" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -2,6 +2,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load helpers %} {% load helpers %}
{% load static %} {% load static %}
{% load i18n %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceDeviceBayTable_config" %} {% include 'inc/table_controls_htmx.html' with table_modal="DeviceDeviceBayTable_config" %}
@ -20,23 +21,23 @@
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:devicebay_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-warning btn-sm"> <button type="submit" name="_edit" formaction="{% url 'dcim:devicebay_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit <i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
</button> </button>
<button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm"> <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button> </button>
</div> </div>
{% endif %} {% endif %}
{% if 'bulk_delete' in actions %} {% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-danger btn-sm"> <button type="submit" name="_delete" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</button> </button>
{% endif %} {% endif %}
</div> </div>
{% if perms.dcim.add_devicebay %} {% if perms.dcim.add_devicebay %}
<div class="bulk-button-group"> <div class="bulk-button-group">
<a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-primary btn-sm"> <a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Device Bays <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Device Bays" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -2,6 +2,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load helpers %} {% load helpers %}
{% load static %} {% load static %}
{% load i18n %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceFrontPortTable_config" %} {% include 'inc/table_controls_htmx.html' with table_modal="DeviceFrontPortTable_config" %}
@ -20,22 +21,22 @@
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-warning btn-sm"> <button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit <i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
</button> </button>
<button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-warning btn-sm"> <button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button> </button>
</div> </div>
{% endif %} {% endif %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
{% if 'bulk_delete' in actions %} {% if 'bulk_delete' in actions %}
<button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-danger btn-sm"> <button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</button> </button>
{% endif %} {% endif %}
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-danger btn-sm"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>
{% endif %} {% endif %}
</div> </div>
@ -43,7 +44,7 @@
{% if perms.dcim.add_frontport %} {% if perms.dcim.add_frontport %}
<div class="bulk-button-group"> <div class="bulk-button-group">
<a href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-primary btn-sm"> <a href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add front ports <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add front ports" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -1,12 +1,13 @@
{% extends 'inc/table_controls_htmx.html' %} {% extends 'inc/table_controls_htmx.html' %}
{% load i18n %}
{% block extra_table_controls %} {% block extra_table_controls %}
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="mdi mdi-eye"></i> <i class="mdi mdi-eye"></i>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button> <button type="button" class="dropdown-item toggle-enabled" data-state="show">{% trans "Hide Enabled" %}</button>
<button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button> <button type="button" class="dropdown-item toggle-disabled" data-state="show">{% trans "Hide Disabled" %}</button>
<button type="button" class="dropdown-item toggle-virtual" data-state="show">Hide Virtual</button> <button type="button" class="dropdown-item toggle-virtual" data-state="show">{% trans "Hide Virtual" %}</button>
</ul> </ul>
{% endblock extra_table_controls %} {% endblock extra_table_controls %}

View File

@ -2,6 +2,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load helpers %} {% load helpers %}
{% load static %} {% load static %}
{% load i18n %}
{% block content %} {% block content %}
{% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %} {% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %}
@ -22,12 +23,12 @@
<button type="submit" name="_edit" <button type="submit" name="_edit"
formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}" formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-warning btn-sm"> class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit <i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
</button> </button>
<button type="submit" name="_rename" <button type="submit" name="_rename"
formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-outline-warning btn-sm"> class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button> </button>
</div> </div>
{% endif %} {% endif %}
@ -36,14 +37,14 @@
<button type="submit" name="_delete" <button type="submit" name="_delete"
formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-danger btn-sm"> class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</button> </button>
{% endif %} {% endif %}
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<button type="submit" name="_disconnect" <button type="submit" name="_disconnect"
formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-outline-danger btn-sm"> class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>
{% endif %} {% endif %}
</div> </div>
@ -52,7 +53,7 @@
<div class="bulk-button-group"> <div class="bulk-button-group">
<a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}" <a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
class="btn btn-primary btn-sm"> class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Interfaces <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Interfaces" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -2,6 +2,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load helpers %} {% load helpers %}
{% load static %} {% load static %}
{% load i18n %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceInventoryItemTable_config" %} {% include 'inc/table_controls_htmx.html' with table_modal="DeviceInventoryItemTable_config" %}
@ -20,23 +21,23 @@
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:inventoryitem_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-warning btn-sm"> <button type="submit" name="_edit" formaction="{% url 'dcim:inventoryitem_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit <i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
</button> </button>
<button type="submit" name="_rename" formaction="{% url 'dcim:inventoryitem_bulk_rename' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-outline-warning btn-sm"> <button type="submit" name="_rename" formaction="{% url 'dcim:inventoryitem_bulk_rename' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button> </button>
</div> </div>
{% endif %} {% endif %}
{% if 'bulk_delete' in actions %} {% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'dcim:inventoryitem_bulk_delete' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-danger btn-sm"> <button type="submit" name="_delete" formaction="{% url 'dcim:inventoryitem_bulk_delete' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</button> </button>
{% endif %} {% endif %}
</div> </div>
{% if perms.dcim.add_inventoryitem %} {% if perms.dcim.add_inventoryitem %}
<div class="bulk-button-group"> <div class="bulk-button-group">
<a href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-primary btn-sm"> <a href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Inventory Item <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Inventory Item" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -2,6 +2,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load helpers %} {% load helpers %}
{% load static %} {% load static %}
{% load i18n %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceModuleBayTable_config" %} {% include 'inc/table_controls_htmx.html' with table_modal="DeviceModuleBayTable_config" %}
@ -20,23 +21,23 @@
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:modulebay_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-warning btn-sm"> <button type="submit" name="_edit" formaction="{% url 'dcim:modulebay_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit <i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
</button> </button>
<button type="submit" name="_rename" formaction="{% url 'dcim:modulebay_bulk_rename' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm"> <button type="submit" name="_rename" formaction="{% url 'dcim:modulebay_bulk_rename' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button> </button>
</div> </div>
{% endif %} {% endif %}
{% if 'bulk_delete' in actions %} {% if 'bulk_delete' in actions %}
<button type="submit" formaction="{% url 'dcim:modulebay_bulk_delete' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-danger btn-sm"> <button type="submit" formaction="{% url 'dcim:modulebay_bulk_delete' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</button> </button>
{% endif %} {% endif %}
</div> </div>
{% if perms.dcim.add_modulebay %} {% if perms.dcim.add_modulebay %}
<div class="bulk-button-group"> <div class="bulk-button-group">
<a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-primary btn-sm"> <a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Module Bays <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Module Bays" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -2,6 +2,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load helpers %} {% load helpers %}
{% load static %} {% load static %}
{% load i18n %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerOutletTable_config" %} {% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerOutletTable_config" %}
@ -20,22 +21,22 @@
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-warning btn-sm"> <button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit <i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
</button> </button>
<button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-warning btn-sm"> <button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button> </button>
</div> </div>
{% endif %} {% endif %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
{% if 'bulk_delete' in actions %} {% if 'bulk_delete' in actions %}
<button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-danger btn-sm"> <button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</button> </button>
{% endif %} {% endif %}
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-danger btn-sm"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>
{% endif %} {% endif %}
</div> </div>
@ -43,7 +44,7 @@
{% if perms.dcim.add_poweroutlet %} {% if perms.dcim.add_poweroutlet %}
<div class="bulk-button-group"> <div class="bulk-button-group">
<a href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-primary btn-sm"> <a href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Outlets <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Power Outlets" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -2,6 +2,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load helpers %} {% load helpers %}
{% load static %} {% load static %}
{% load i18n %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerPortTable_config" %} {% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerPortTable_config" %}
@ -20,22 +21,22 @@
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-warning btn-sm"> <button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit <i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
</button> </button>
<button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-warning btn-sm"> <button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button> </button>
</div> </div>
{% endif %} {% endif %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
{% if 'bulk_delete' in actions %} {% if 'bulk_delete' in actions %}
<button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-danger btn-sm"> <button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</button> </button>
{% endif %} {% endif %}
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-danger btn-sm"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>
{% endif %} {% endif %}
</div> </div>
@ -43,7 +44,7 @@
{% if perms.dcim.add_powerport %} {% if perms.dcim.add_powerport %}
<div class="bulk-button-group"> <div class="bulk-button-group">
<a href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-sm btn-primary"> <a href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-sm btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Port <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Power Port" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -2,6 +2,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load static %} {% load static %}
{% load helpers %} {% load helpers %}
{% load i18n %}
{% block content %} {% block content %}
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceRearPortTable_config" %} {% include 'inc/table_controls_htmx.html' with table_modal="DeviceRearPortTable_config" %}
@ -20,22 +21,22 @@
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-warning btn-sm"> <button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit <i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
</button> </button>
<button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-warning btn-sm"> <button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button> </button>
</div> </div>
{% endif %} {% endif %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
{% if 'bulk_delete' in actions %} {% if 'bulk_delete' in actions %}
<button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-danger btn-sm"> <button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</button> </button>
{% endif %} {% endif %}
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-danger btn-sm"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>
{% endif %} {% endif %}
</div> </div>
@ -43,7 +44,7 @@
{% if perms.dcim.add_rearport %} {% if perms.dcim.add_rearport %}
<div class="bulk-button-group"> <div class="bulk-button-group">
<a href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-primary btn-sm"> <a href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add rear ports <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add rear ports" %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -1,25 +1,26 @@
{% extends 'dcim/device/base.html' %} {% extends 'dcim/device/base.html' %}
{% load static %} {% load static %}
{% load i18n %}
{% block title %}{{ object }} - Config{% endblock %} {% block title %}{{ object }} - {% trans "Config" %}{% endblock %}
{% block content %} {% block content %}
<div class="row mb-3"> <div class="row mb-3">
<div class="col-5"> <div class="col-5">
<div class="card"> <div class="card">
<h5 class="card-header">Config Template</h5> <h5 class="card-header">{% trans "Config Template" %}</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<th scope="row">Config Template</th> <th scope="row">{% trans "Config Template" %}</th>
<td>{{ config_template|linkify|placeholder }}</td> <td>{{ config_template|linkify|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Data Source</th> <th scope="row">{% trans "Data Source" %}</th>
<td>{{ config_template.data_file.source|linkify|placeholder }}</td> <td>{{ config_template.data_file.source|linkify|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Data File</th> <th scope="row">{% trans "Data File" %}</th>
<td>{{ config_template.data_file|linkify|placeholder }}</td> <td>{{ config_template.data_file|linkify|placeholder }}</td>
</tr> </tr>
</table> </table>
@ -33,7 +34,7 @@
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="renderConfigHeading"> <h2 class="accordion-header" id="renderConfigHeading">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsedRenderConfig" aria-expanded="false" aria-controls="collapsedRenderConfig"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsedRenderConfig" aria-expanded="false" aria-controls="collapsedRenderConfig">
Context Data {% trans "Context Data" %}
</button> </button>
</h2> </h2>
<div id="collapsedRenderConfig" class="accordion-collapse collapse" aria-labelledby="renderConfigHeading" data-bs-parent="#renderConfig"> <div id="collapsedRenderConfig" class="accordion-collapse collapse" aria-labelledby="renderConfigHeading" data-bs-parent="#renderConfig">
@ -53,15 +54,15 @@
<div class="card-header"> <div class="card-header">
<div class="float-end"> <div class="float-end">
<a href="?export=True" class="btn btn-sm btn-primary" role="button"> <a href="?export=True" class="btn btn-sm btn-primary" role="button">
<i class="mdi mdi-download" aria-hidden="true"></i> Download <i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
</a> </a>
</div> </div>
<h5>Rendered Config</h5> <h5>{% trans "Rendered Config" %}</h5>
</div> </div>
{% if config_template %} {% if config_template %}
<pre class="card-body">{{ rendered_config }}</pre> <pre class="card-body">{{ rendered_config }}</pre>
{% else %} {% else %}
<div class="card-body text-muted">No configuration template found</div> <div class="card-body text-muted">{% trans "No configuration template found" %}</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -1,12 +1,13 @@
{% extends 'generic/object_edit.html' %} {% extends 'generic/object_edit.html' %}
{% load form_helpers %} {% load form_helpers %}
{% load i18n %}
{% block form %} {% block form %}
{% render_errors form %} {% render_errors form %}
<div class="field-group my-5"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Device</h5> <h5 class="offset-sm-3">{% trans "Device" %}</h5>
</div> </div>
{% render_field form.name %} {% render_field form.name %}
{% render_field form.device_role %} {% render_field form.device_role %}
@ -16,7 +17,7 @@
<div class="field-group my-5"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Hardware</h5> <h5 class="offset-sm-3">{% trans "Hardware" %}</h5>
</div> </div>
{% render_field form.device_type %} {% render_field form.device_type %}
{% render_field form.airflow %} {% render_field form.airflow %}
@ -26,7 +27,7 @@
<div class="field-group my-5"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Location</h5> <h5 class="offset-sm-3">{% trans "Location" %}</h5>
</div> </div>
{% render_field form.site %} {% render_field form.site %}
{% render_field form.location %} {% render_field form.location %}
@ -34,18 +35,18 @@
{% if object.device_type.is_child_device and object.parent_bay %} {% if object.device_type.is_child_device and object.parent_bay %}
<div class="row mb-3"> <div class="row mb-3">
<label class="col-sm-3 col-form-label">Parent Device</label> <label class="col-sm-3 col-form-label">{% trans "Parent Device" %}</label>
<div class="col"> <div class="col">
<input class="form-control" value="{{ object.parent_bay.device }}" disabled /> <input class="form-control" value="{{ object.parent_bay.device }}" disabled />
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<label class="col-sm-3 col-form-label">Parent Bay</label> <label class="col-sm-3 col-form-label">{% trans "Parent Bay" %}</label>
<div class="col"> <div class="col">
<div class="input-group"> <div class="input-group">
<input class="form-control" value="{{ object.parent_bay.name }}" disabled /> <input class="form-control" value="{{ object.parent_bay.name }}" disabled />
<a href="{% url 'dcim:devicebay_depopulate' pk=object.parent_bay.pk %}" title="Regenerate Slug" class="btn btn-danger d-inline-flex align-items-center"> <a href="{% url 'dcim:devicebay_depopulate' pk=object.parent_bay.pk %}" title="{% trans "Regenerate Slug" %}" class="btn btn-danger d-inline-flex align-items-center">
<i class="mdi mdi-close-thick"></i>&nbsp;Remove <i class="mdi mdi-close-thick"></i>&nbsp;{% trans "Remove" %}
</a> </a>
</div> </div>
</div> </div>
@ -60,7 +61,7 @@
<div class="field-group my-5"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Management</h5> <h5 class="offset-sm-3">{% trans "Management" %}</h5>
</div> </div>
{% render_field form.status %} {% render_field form.status %}
{% render_field form.platform %} {% render_field form.platform %}
@ -74,14 +75,14 @@
<div class="field-group my-5"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Virtualization</h5> <h5 class="offset-sm-3">{% trans "Virtualization" %}</h5>
</div> </div>
{% render_field form.cluster %} {% render_field form.cluster %}
</div> </div>
<div class="field-group my-5"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Tenancy</h5> <h5 class="offset-sm-3">{% trans "Tenancy" %}</h5>
</div> </div>
{% render_field form.tenant_group %} {% render_field form.tenant_group %}
{% render_field form.tenant %} {% render_field form.tenant %}
@ -89,7 +90,7 @@
<div class="field-group my-5"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Virtual Chassis</h5> <h5 class="offset-sm-3">{% trans "Virtual Chassis" %}</h5>
</div> </div>
{% render_field form.virtual_chassis %} {% render_field form.virtual_chassis %}
{% render_field form.vc_position %} {% render_field form.vc_position %}
@ -99,14 +100,14 @@
{% if form.custom_fields %} {% if form.custom_fields %}
<div class="field-group my-5"> <div class="field-group my-5">
<div class="row mb-2"> <div class="row mb-2">
<h5 class="offset-sm-3">Custom Fields</h5> <h5 class="offset-sm-3">{% trans "Custom Fields" %}</h5>
</div> </div>
{% render_custom_fields form %} {% render_custom_fields form %}
</div> </div>
{% endif %} {% endif %}
<div class="field-group my-5"> <div class="field-group my-5">
<h5 class="text-center">Local Config Context Data</h5> <h5 class="text-center">{% trans "Local Config Context Data" %}</h5>
{% render_field form.local_context_data %} {% render_field form.local_context_data %}
</div> </div>

View File

@ -1,73 +1,74 @@
{% extends 'generic/object_list.html' %} {% extends 'generic/object_list.html' %}
{% load buttons %} {% load buttons %}
{% load i18n %}
{% block bulk_buttons %} {% block bulk_buttons %}
{% if perms.dcim.change_device %} {% if perms.dcim.change_device %}
<div class="dropdown"> <div class="dropdown">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Components <i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Components" %}
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% if perms.dcim.add_consoleport %} {% if perms.dcim.add_consoleport %}
<li> <li>
<button type="submit" formaction="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item"> <button type="submit" formaction="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
Console Ports {% trans "Console Ports" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_consoleserverport %} {% if perms.dcim.add_consoleserverport %}
<li> <li>
<button type="submit" formaction="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item "> <button type="submit" formaction="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item ">
Console Server Ports {% trans "Console Server Ports" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_powerport %} {% if perms.dcim.add_powerport %}
<li> <li>
<button type="submit" formaction="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item"> <button type="submit" formaction="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
Power Ports {% trans "Power Ports" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_poweroutlet %} {% if perms.dcim.add_poweroutlet %}
<li> <li>
<button type="submit" formaction="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item"> <button type="submit" formaction="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
Power Outlets {% trans "Power Outlets" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_interface %} {% if perms.dcim.add_interface %}
<li> <li>
<button type="submit" formaction="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" <button type="submit" formaction="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}"
class="dropdown-item">Interfaces class="dropdown-item">{% trans "Interfaces" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_rearport %} {% if perms.dcim.add_rearport %}
<li> <li>
<button type="submit" formaction="{% url 'dcim:device_bulk_add_rearport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item"> <button type="submit" formaction="{% url 'dcim:device_bulk_add_rearport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
Rear Ports {% trans "Rear Ports" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_devicebay %} {% if perms.dcim.add_devicebay %}
<li> <li>
<button type="submit" formaction="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item"> <button type="submit" formaction="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
Device Bays {% trans "Device Bays" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_modulebay %} {% if perms.dcim.add_modulebay %}
<li> <li>
<button type="submit" formaction="{% url 'dcim:device_bulk_add_modulebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item"> <button type="submit" formaction="{% url 'dcim:device_bulk_add_modulebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
Module Bays {% trans "Module Bays" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_inventoryitem %} {% if perms.dcim.add_inventoryitem %}
<li> <li>
<button type="submit" formaction="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item"> <button type="submit" formaction="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
Inventory Items {% trans "Inventory Items" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
@ -78,7 +79,7 @@
<div class="btn-group" role="group"> <div class="btn-group" role="group">
{% bulk_edit_button model query_params=request.GET %} {% bulk_edit_button model query_params=request.GET %}
<button type="submit" name="_rename" formaction="{% url 'dcim:device_bulk_rename' %}?return_url={% url 'dcim:device_list' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-outline-warning btn-sm"> <button type="submit" name="_rename" formaction="{% url 'dcim:device_bulk_rename' %}?return_url={% url 'dcim:device_list' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-outline-warning btn-sm">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button> </button>
</div> </div>
{% endif %} {% endif %}

Some files were not shown because too many files have changed in this diff Show More