mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 00:36:11 -06:00
Merge branch 'feature' into 13149-form-gettext
This commit is contained in:
commit
406cee91cc
@ -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.
|
||||
|
||||
### 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 |
|
||||
| ----------- | ----------- |
|
||||
|
@ -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
|
||||
|
||||
Default: Empty list
|
||||
|
@ -1,6 +1,8 @@
|
||||
# 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
|
||||
|
||||
@ -8,9 +10,17 @@ Single- and multi-selection [custom fields documentation](../../customization/cu
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
#### 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))
|
||||
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
@ -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
|
||||
* [#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
|
||||
* [#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
|
||||
|
||||
### 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
|
||||
* [#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
|
||||
|
0
netbox/account/__init__.py
Normal file
0
netbox/account/__init__.py
Normal 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
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('users', '0004_netboxgroup_netboxuser'),
|
||||
]
|
||||
@ -15,10 +17,10 @@ class Migration(migrations.Migration):
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'token',
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
'verbose_name': 'token',
|
||||
},
|
||||
bases=('users.token',),
|
||||
),
|
0
netbox/account/migrations/__init__.py
Normal file
0
netbox/account/migrations/__init__.py
Normal file
15
netbox/account/models.py
Normal file
15
netbox/account/models.py
Normal 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
55
netbox/account/tables.py
Normal 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',
|
||||
)
|
@ -13,6 +13,6 @@ urlpatterns = [
|
||||
path('password/', views.ChangePasswordView.as_view(), name='change_password'),
|
||||
path('api-tokens/', views.UserTokenListView.as_view(), name='usertoken_list'),
|
||||
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
298
netbox/account/views.py
Normal 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'
|
@ -1236,6 +1236,10 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
choices=PowerFeedPhaseChoices,
|
||||
default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE,
|
||||
)
|
||||
tenant = NestedTenantSerializer(
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
@ -1243,5 +1247,5 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
'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',
|
||||
'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',
|
||||
]
|
||||
|
@ -6,7 +6,6 @@ from netaddr import AddrFormatError, EUI, eui64_unix_expanded, mac_unix_expanded
|
||||
from .lookups import PathContains
|
||||
|
||||
__all__ = (
|
||||
'ASNField',
|
||||
'MACAddressField',
|
||||
'PathField',
|
||||
'WWNField',
|
||||
|
@ -1880,7 +1880,7 @@ class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
||||
class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet, TenancyFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='power_panel__site__region',
|
||||
|
@ -854,6 +854,10 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect
|
||||
)
|
||||
tenant = DynamicModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
@ -865,10 +869,10 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
model = PowerFeed
|
||||
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'))
|
||||
)
|
||||
nullable_fields = ('location', 'description', 'comments')
|
||||
nullable_fields = ('location', 'tenant', 'description', 'comments')
|
||||
|
||||
|
||||
#
|
||||
|
@ -1291,6 +1291,12 @@ class PowerFeedImportForm(NetBoxModelImportForm):
|
||||
required=False,
|
||||
help_text=_('Rack')
|
||||
)
|
||||
tenant = CSVModelChoiceField(
|
||||
queryset=Tenant.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False,
|
||||
help_text=_('Assigned tenant')
|
||||
)
|
||||
status = CSVChoiceField(
|
||||
label=_('Status'),
|
||||
choices=PowerFeedStatusChoices,
|
||||
@ -1316,7 +1322,7 @@ class PowerFeedImportForm(NetBoxModelImportForm):
|
||||
model = PowerFeed
|
||||
fields = (
|
||||
'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):
|
||||
|
@ -1020,11 +1020,12 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class PowerFeedFilterForm(NetBoxModelFilterSetForm):
|
||||
class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = PowerFeed
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
(_('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')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
|
@ -675,7 +675,7 @@ class PowerPanelForm(NetBoxModelForm):
|
||||
]
|
||||
|
||||
|
||||
class PowerFeedForm(NetBoxModelForm):
|
||||
class PowerFeedForm(TenancyForm, NetBoxModelForm):
|
||||
power_panel = DynamicModelChoiceField(
|
||||
label=_('Power panel'),
|
||||
queryset=PowerPanel.objects.all(),
|
||||
@ -694,13 +694,14 @@ class PowerFeedForm(NetBoxModelForm):
|
||||
fieldsets = (
|
||||
(_('Power Feed'), ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
|
||||
(_('Characteristics'), ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
|
||||
(_('Tenancy'), ('tenant_group', 'tenant')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = [
|
||||
'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'
|
||||
]
|
||||
|
||||
|
||||
|
@ -53,23 +53,23 @@ class LinkPeerType(graphene.Union):
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) == CircuitTermination:
|
||||
if type(instance) is CircuitTermination:
|
||||
return CircuitTerminationType
|
||||
if type(instance) == ConsolePortType:
|
||||
if type(instance) is ConsolePortType:
|
||||
return ConsolePortType
|
||||
if type(instance) == ConsoleServerPort:
|
||||
if type(instance) is ConsoleServerPort:
|
||||
return ConsoleServerPortType
|
||||
if type(instance) == FrontPort:
|
||||
if type(instance) is FrontPort:
|
||||
return FrontPortType
|
||||
if type(instance) == Interface:
|
||||
if type(instance) is Interface:
|
||||
return InterfaceType
|
||||
if type(instance) == PowerFeed:
|
||||
if type(instance) is PowerFeed:
|
||||
return PowerFeedType
|
||||
if type(instance) == PowerOutlet:
|
||||
if type(instance) is PowerOutlet:
|
||||
return PowerOutletType
|
||||
if type(instance) == PowerPort:
|
||||
if type(instance) is PowerPort:
|
||||
return PowerPortType
|
||||
if type(instance) == RearPort:
|
||||
if type(instance) is RearPort:
|
||||
return RearPortType
|
||||
|
||||
|
||||
@ -89,23 +89,23 @@ class CableTerminationTerminationType(graphene.Union):
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) == CircuitTermination:
|
||||
if type(instance) is CircuitTermination:
|
||||
return CircuitTerminationType
|
||||
if type(instance) == ConsolePortType:
|
||||
if type(instance) is ConsolePortType:
|
||||
return ConsolePortType
|
||||
if type(instance) == ConsoleServerPort:
|
||||
if type(instance) is ConsoleServerPort:
|
||||
return ConsoleServerPortType
|
||||
if type(instance) == FrontPort:
|
||||
if type(instance) is FrontPort:
|
||||
return FrontPortType
|
||||
if type(instance) == Interface:
|
||||
if type(instance) is Interface:
|
||||
return InterfaceType
|
||||
if type(instance) == PowerFeed:
|
||||
if type(instance) is PowerFeed:
|
||||
return PowerFeedType
|
||||
if type(instance) == PowerOutlet:
|
||||
if type(instance) is PowerOutlet:
|
||||
return PowerOutletType
|
||||
if type(instance) == PowerPort:
|
||||
if type(instance) is PowerPort:
|
||||
return PowerPortType
|
||||
if type(instance) == RearPort:
|
||||
if type(instance) is RearPort:
|
||||
return RearPortType
|
||||
|
||||
|
||||
@ -123,19 +123,19 @@ class InventoryItemTemplateComponentType(graphene.Union):
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) == ConsolePortTemplate:
|
||||
if type(instance) is ConsolePortTemplate:
|
||||
return ConsolePortTemplateType
|
||||
if type(instance) == ConsoleServerPortTemplate:
|
||||
if type(instance) is ConsoleServerPortTemplate:
|
||||
return ConsoleServerPortTemplateType
|
||||
if type(instance) == FrontPortTemplate:
|
||||
if type(instance) is FrontPortTemplate:
|
||||
return FrontPortTemplateType
|
||||
if type(instance) == InterfaceTemplate:
|
||||
if type(instance) is InterfaceTemplate:
|
||||
return InterfaceTemplateType
|
||||
if type(instance) == PowerOutletTemplate:
|
||||
if type(instance) is PowerOutletTemplate:
|
||||
return PowerOutletTemplateType
|
||||
if type(instance) == PowerPortTemplate:
|
||||
if type(instance) is PowerPortTemplate:
|
||||
return PowerPortTemplateType
|
||||
if type(instance) == RearPortTemplate:
|
||||
if type(instance) is RearPortTemplate:
|
||||
return RearPortTemplateType
|
||||
|
||||
|
||||
@ -153,17 +153,17 @@ class InventoryItemComponentType(graphene.Union):
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) == ConsolePort:
|
||||
if type(instance) is ConsolePort:
|
||||
return ConsolePortType
|
||||
if type(instance) == ConsoleServerPort:
|
||||
if type(instance) is ConsoleServerPort:
|
||||
return ConsoleServerPortType
|
||||
if type(instance) == FrontPort:
|
||||
if type(instance) is FrontPort:
|
||||
return FrontPortType
|
||||
if type(instance) == Interface:
|
||||
if type(instance) is Interface:
|
||||
return InterfaceType
|
||||
if type(instance) == PowerOutlet:
|
||||
if type(instance) is PowerOutlet:
|
||||
return PowerOutletType
|
||||
if type(instance) == PowerPort:
|
||||
if type(instance) is PowerPort:
|
||||
return PowerPortType
|
||||
if type(instance) == RearPort:
|
||||
if type(instance) is RearPort:
|
||||
return RearPortType
|
||||
|
20
netbox/dcim/migrations/0180_powerfeed_tenant.py
Normal file
20
netbox/dcim/migrations/0180_powerfeed_tenant.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -131,10 +131,17 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
|
||||
default=0,
|
||||
editable=False
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
to='tenancy.Tenant',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='power_feeds',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
|
||||
'max_utilization',
|
||||
'max_utilization', 'tenant',
|
||||
)
|
||||
prerequisite_models = (
|
||||
'dcim.PowerPanel',
|
||||
|
@ -1,6 +1,6 @@
|
||||
import django_tables2 as tables
|
||||
from dcim.models import PowerFeed, PowerPanel
|
||||
from tenancy.tables import ContactsColumnMixin
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
|
||||
@ -51,7 +51,7 @@ class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
|
||||
|
||||
# We're not using PathEndpointTable for PowerFeed because power connections
|
||||
# cannot traverse pass-through ports.
|
||||
class PowerFeedTable(CableTerminationTable):
|
||||
class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
@ -69,6 +69,9 @@ class PowerFeedTable(CableTerminationTable):
|
||||
available_power = tables.Column(
|
||||
verbose_name='Available power (VA)'
|
||||
)
|
||||
tenant = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
comments = columns.MarkdownColumn()
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:powerfeed_list'
|
||||
@ -78,8 +81,8 @@ class PowerFeedTable(CableTerminationTable):
|
||||
model = PowerFeed
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
||||
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power',
|
||||
'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power', 'tenant',
|
||||
'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
|
||||
|
@ -4419,6 +4419,21 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
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 = (
|
||||
PowerPanel(name='Power Panel 1', site=sites[0]),
|
||||
PowerPanel(name='Power Panel 2', site=sites[1]),
|
||||
@ -4427,9 +4442,44 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
PowerPanel.objects.bulk_create(power_panels)
|
||||
|
||||
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(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),
|
||||
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),
|
||||
PowerFeed(
|
||||
power_panel=power_panels[0],
|
||||
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)
|
||||
|
||||
@ -4520,6 +4570,20 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'connected': False}
|
||||
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):
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
|
@ -131,12 +131,16 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
||||
|
||||
class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
|
||||
base_choices = ChoiceField(
|
||||
choices=CustomFieldChoiceSetBaseChoices,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomFieldChoiceSet
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'description', 'extra_choices', 'order_alphabetically', 'choices_count',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
|
||||
'choices_count', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_rq.queues import get_connection
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
@ -63,6 +64,26 @@ class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
|
||||
serializer_class = serializers.CustomFieldChoiceSetSerializer
|
||||
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
|
||||
|
@ -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
|
||||
#
|
||||
|
9
netbox/extras/data/__init__.py
Normal file
9
netbox/extras/data/__init__.py
Normal 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
9768
netbox/extras/data/iata.py
Normal file
File diff suppressed because it is too large
Load Diff
253
netbox/extras/data/iso_3166.py
Normal file
253
netbox/extras/data/iso_3166.py
Normal 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
111557
netbox/extras/data/un_locode.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -114,7 +114,7 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
|
||||
class Meta:
|
||||
model = CustomFieldChoiceSet
|
||||
fields = [
|
||||
'id', 'name', 'description', 'order_alphabetically',
|
||||
'id', 'name', 'description', 'base_choices', 'order_alphabetically',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
|
@ -68,6 +68,10 @@ class CustomFieldChoiceSetBulkEditForm(BulkEditForm):
|
||||
queryset=CustomFieldChoiceSet.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
base_choices = forms.ChoiceField(
|
||||
choices=add_blank_choice(CustomFieldChoiceSetBaseChoices),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
@ -76,7 +80,7 @@ class CustomFieldChoiceSetBulkEditForm(BulkEditForm):
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
|
||||
nullable_fields = ('description',)
|
||||
nullable_fields = ('base_choices', 'description')
|
||||
|
||||
|
||||
class CustomLinkBulkEditForm(BulkEditForm):
|
||||
|
@ -4,7 +4,7 @@ from django.contrib.postgres.forms import SimpleArrayField
|
||||
from django.utils.safestring import mark_safe
|
||||
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.utils import FeatureQuery
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
@ -68,6 +68,11 @@ class CustomFieldImportForm(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(
|
||||
base_field=forms.CharField(),
|
||||
required=False,
|
||||
|
@ -11,7 +11,9 @@ from extras.utils import FeatureQuery
|
||||
from netbox.forms.base import NetBoxModelFilterSetForm
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
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 virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
from .mixins import SavedFiltersMixin
|
||||
@ -88,7 +90,12 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
||||
|
||||
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
|
||||
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(
|
||||
required=False
|
||||
|
@ -19,7 +19,7 @@ from utilities.forms.fields import (
|
||||
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField, JSONField, SlugField,
|
||||
)
|
||||
from utilities.forms.widgets import ArrayWidget
|
||||
from utilities.forms.widgets import ChoicesWidget
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
|
||||
@ -88,16 +88,22 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
||||
|
||||
class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
|
||||
extra_choices = forms.CharField(
|
||||
widget=ArrayWidget(),
|
||||
help_text=_('Enter one choice per line.')
|
||||
widget=ChoicesWidget(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomFieldChoiceSet
|
||||
fields = ('name', 'description', 'extra_choices', 'order_alphabetically')
|
||||
fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically')
|
||||
|
||||
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):
|
||||
|
@ -19,7 +19,7 @@ def create_choice_sets(apps, schema_editor):
|
||||
for cf in choice_fields:
|
||||
choiceset = CustomFieldChoiceSet.objects.create(
|
||||
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
|
||||
|
||||
@ -42,7 +42,8 @@ class Migration(migrations.Migration):
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('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)),
|
||||
],
|
||||
options={
|
||||
|
@ -15,17 +15,18 @@ from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from extras.choices import *
|
||||
from extras.data import CHOICE_SETS
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import CloningMixin, ExportTemplatesMixin
|
||||
from netbox.search import FieldTypes
|
||||
from utilities import filters
|
||||
from utilities.forms.fields import (
|
||||
CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField, JSONField, LaxURLField,
|
||||
CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DynamicChoiceField,
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, DynamicMultipleChoiceField, JSONField, LaxURLField,
|
||||
)
|
||||
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.validators import validate_regex
|
||||
|
||||
@ -410,7 +411,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
|
||||
# Select
|
||||
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
|
||||
|
||||
if not required or default_choice is None:
|
||||
@ -421,11 +422,17 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
initial = default_choice
|
||||
|
||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
field_class = CSVChoiceField if for_csv_import else forms.ChoiceField
|
||||
field = field_class(choices=choices, required=required, initial=initial)
|
||||
field_class = CSVChoiceField if for_csv_import else DynamicChoiceField
|
||||
widget_class = APISelect
|
||||
else:
|
||||
field_class = CSVMultipleChoiceField if for_csv_import else forms.MultipleChoiceField
|
||||
field = field_class(choices=choices, required=required, initial=initial)
|
||||
field_class = CSVMultipleChoiceField if for_csv_import else DynamicMultipleChoiceField
|
||||
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
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_URL:
|
||||
@ -604,14 +611,14 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
|
||||
# Validate selected choice
|
||||
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(
|
||||
f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}"
|
||||
)
|
||||
|
||||
# Validate all selected choices
|
||||
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(
|
||||
f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}"
|
||||
)
|
||||
@ -645,13 +652,23 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
base_choices = models.CharField(
|
||||
max_length=50,
|
||||
choices=CustomFieldChoiceSetBaseChoices,
|
||||
blank=True,
|
||||
help_text=_('Base set of predefined choices (optional)')
|
||||
)
|
||||
extra_choices = ArrayField(
|
||||
ArrayField(
|
||||
base_field=models.CharField(max_length=100),
|
||||
help_text=_('List of field choices')
|
||||
size=2
|
||||
),
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
order_alphabetically = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_('Choices are automatically ordered alphabetically on save')
|
||||
help_text=_('Choices are automatically ordered alphabetically')
|
||||
)
|
||||
|
||||
clone_fields = ('extra_choices', 'order_alphabetically')
|
||||
@ -667,16 +684,31 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
|
||||
|
||||
@property
|
||||
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
|
||||
def choices_count(self):
|
||||
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):
|
||||
|
||||
# Sort choices if alphabetical ordering is enforced
|
||||
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)
|
||||
|
@ -66,10 +66,12 @@ class CustomFieldTable(NetBoxTable):
|
||||
required = columns.BooleanColumn()
|
||||
ui_visibility = columns.ChoiceFieldColumn(verbose_name="UI visibility")
|
||||
description = columns.MarkdownColumn()
|
||||
choices = columns.ArrayColumn(
|
||||
choice_set = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
choices = columns.ChoicesColumn(
|
||||
max_items=10,
|
||||
orderable=False,
|
||||
verbose_name=_('Choices')
|
||||
orderable=False
|
||||
)
|
||||
is_cloneable = columns.BooleanColumn()
|
||||
|
||||
@ -77,8 +79,8 @@ class CustomFieldTable(NetBoxTable):
|
||||
model = CustomField
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description',
|
||||
'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'weight', 'choices', 'created',
|
||||
'last_updated',
|
||||
'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'weight', 'choice_set', 'choices',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
|
||||
|
||||
@ -87,11 +89,13 @@ class CustomFieldChoiceSetTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
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,
|
||||
accessor=tables.A('extra_choices'),
|
||||
orderable=False,
|
||||
verbose_name=_('Choices')
|
||||
orderable=False
|
||||
)
|
||||
choice_count = tables.TemplateColumn(
|
||||
accessor=tables.A('extra_choices'),
|
||||
@ -104,10 +108,10 @@ class CustomFieldChoiceSetTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CustomFieldChoiceSet
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'description', 'choice_count', 'choices', 'order_alphabetically', 'created',
|
||||
'last_updated',
|
||||
'pk', 'id', 'name', 'description', 'base_choices', 'extra_choices', 'choice_count', 'choices',
|
||||
'order_alphabetically', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'choice_count', 'description')
|
||||
default_columns = ('pk', 'name', 'base_choices', 'choice_count', 'description')
|
||||
|
||||
|
||||
class CustomLinkTable(NetBoxTable):
|
||||
|
@ -139,15 +139,27 @@ class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase):
|
||||
create_data = [
|
||||
{
|
||||
'name': 'Choice Set 4',
|
||||
'extra_choices': ['4A', '4B', '4C'],
|
||||
'extra_choices': [
|
||||
['4A', 'Choice 1'],
|
||||
['4B', 'Choice 2'],
|
||||
['4C', 'Choice 3'],
|
||||
],
|
||||
},
|
||||
{
|
||||
'name': 'Choice Set 5',
|
||||
'extra_choices': ['5A', '5B', '5C'],
|
||||
'extra_choices': [
|
||||
['5A', 'Choice 1'],
|
||||
['5B', 'Choice 2'],
|
||||
['5C', 'Choice 3'],
|
||||
],
|
||||
},
|
||||
{
|
||||
'name': 'Choice Set 6',
|
||||
'extra_choices': ['6A', '6B', '6C'],
|
||||
'extra_choices': [
|
||||
['6A', 'Choice 1'],
|
||||
['6B', 'Choice 2'],
|
||||
['6C', 'Choice 3'],
|
||||
],
|
||||
},
|
||||
]
|
||||
bulk_update_data = {
|
||||
@ -155,7 +167,11 @@ class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase):
|
||||
}
|
||||
update_data = {
|
||||
'name': 'Choice Set X',
|
||||
'extra_choices': ['X1', 'X2', 'X3'],
|
||||
'extra_choices': [
|
||||
['X1', 'Choice 1'],
|
||||
['X2', 'Choice 2'],
|
||||
['X3', 'Choice 3'],
|
||||
],
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
@ -17,8 +17,8 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
name='Custom Field Choice Set 1',
|
||||
extra_choices=['Bar', 'Foo']
|
||||
name='Choice Set 1',
|
||||
extra_choices=(('foo', 'Foo'), ('bar', 'Bar'))
|
||||
)
|
||||
|
||||
# Create a custom field on the Site model
|
||||
@ -48,7 +48,7 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
'slug': 'site-1',
|
||||
'status': SiteStatusChoices.STATUS_ACTIVE,
|
||||
'cf_cf1': 'ABC',
|
||||
'cf_cf2': 'Bar',
|
||||
'cf_cf2': 'bar',
|
||||
'tags': [tag.pk for tag in tags],
|
||||
}
|
||||
|
||||
@ -84,7 +84,7 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
'slug': 'site-x',
|
||||
'status': SiteStatusChoices.STATUS_PLANNED,
|
||||
'cf_cf1': 'DEF',
|
||||
'cf_cf2': 'Foo',
|
||||
'cf_cf2': 'foo',
|
||||
'tags': [tags[2].pk],
|
||||
}
|
||||
|
||||
@ -226,7 +226,7 @@ class ChangeLogAPITest(APITestCase):
|
||||
# Create a select custom field on the Site model
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
name='Choice Set 1',
|
||||
extra_choices=['Bar', 'Foo']
|
||||
extra_choices=(('foo', 'Foo'), ('bar', 'Bar'))
|
||||
)
|
||||
cf_select = CustomField(
|
||||
type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||
@ -251,7 +251,7 @@ class ChangeLogAPITest(APITestCase):
|
||||
'slug': 'site-1',
|
||||
'custom_fields': {
|
||||
'cf1': 'ABC',
|
||||
'cf2': 'Bar',
|
||||
'cf2': 'bar',
|
||||
},
|
||||
'tags': [
|
||||
{'name': 'Tag 1'},
|
||||
@ -285,7 +285,7 @@ class ChangeLogAPITest(APITestCase):
|
||||
'slug': 'site-x',
|
||||
'custom_fields': {
|
||||
'cf1': 'DEF',
|
||||
'cf2': 'Foo',
|
||||
'cf2': 'foo',
|
||||
},
|
||||
'tags': [
|
||||
{'name': 'Tag 3'}
|
||||
|
@ -269,8 +269,12 @@ class CustomFieldTest(TestCase):
|
||||
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||
|
||||
def test_select_field(self):
|
||||
CHOICES = ('Option A', 'Option B', 'Option C')
|
||||
value = CHOICES[1]
|
||||
CHOICES = (
|
||||
('a', 'Option A'),
|
||||
('b', 'Option B'),
|
||||
('c', 'Option C'),
|
||||
)
|
||||
value = 'a'
|
||||
|
||||
# Create a set of custom field choices
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
@ -302,8 +306,12 @@ class CustomFieldTest(TestCase):
|
||||
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||
|
||||
def test_multiselect_field(self):
|
||||
CHOICES = ['Option A', 'Option B', 'Option C']
|
||||
value = [CHOICES[1], CHOICES[2]]
|
||||
CHOICES = (
|
||||
('a', 'Option A'),
|
||||
('b', 'Option B'),
|
||||
('c', 'Option C'),
|
||||
)
|
||||
value = ['a', 'b']
|
||||
|
||||
# Create a set of custom field choices
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
@ -453,7 +461,7 @@ class CustomFieldAPITest(APITestCase):
|
||||
# Create a set of custom field choices
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
name='Custom Field Choice Set 1',
|
||||
extra_choices=('Foo', 'Bar', 'Baz')
|
||||
extra_choices=(('foo', 'Foo'), ('bar', 'Bar'), ('baz', 'Baz'))
|
||||
)
|
||||
|
||||
custom_fields = (
|
||||
@ -469,13 +477,13 @@ class CustomFieldAPITest(APITestCase):
|
||||
CustomField(
|
||||
type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||
name='select_field',
|
||||
default='Foo',
|
||||
default='foo',
|
||||
choice_set=choice_set
|
||||
),
|
||||
CustomField(
|
||||
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
||||
name='multiselect_field',
|
||||
default=['Foo'],
|
||||
default=['foo'],
|
||||
choice_set=choice_set
|
||||
),
|
||||
CustomField(
|
||||
@ -514,8 +522,8 @@ class CustomFieldAPITest(APITestCase):
|
||||
custom_fields[6].name: '2020-01-02 12:00:00',
|
||||
custom_fields[7].name: 'http://example.com/2',
|
||||
custom_fields[8].name: '{"foo": 1, "bar": 2}',
|
||||
custom_fields[9].name: 'Bar',
|
||||
custom_fields[10].name: ['Bar', 'Baz'],
|
||||
custom_fields[9].name: 'bar',
|
||||
custom_fields[10].name: ['bar', 'baz'],
|
||||
custom_fields[11].name: vlans[1].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),
|
||||
'url_field': 'http://example.com/2',
|
||||
'json_field': '{"foo": 1, "bar": 2}',
|
||||
'select_field': 'Bar',
|
||||
'multiselect_field': ['Bar', 'Baz'],
|
||||
'select_field': 'bar',
|
||||
'multiselect_field': ['bar', 'baz'],
|
||||
'object_field': VLAN.objects.get(vid=2).pk,
|
||||
'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),
|
||||
'url_field': 'http://example.com/2',
|
||||
'json_field': '{"foo": 1, "bar": 2}',
|
||||
'select_field': 'Bar',
|
||||
'multiselect_field': ['Bar', 'Baz'],
|
||||
'select_field': 'bar',
|
||||
'multiselect_field': ['bar', 'baz'],
|
||||
'object_field': VLAN.objects.get(vid=2).pk,
|
||||
'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
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
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 = (
|
||||
@ -1067,8 +1079,8 @@ class CustomFieldImportTest(TestCase):
|
||||
"""
|
||||
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'),
|
||||
('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 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 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}', 'b', '"b,c"'),
|
||||
('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', '', '', ''),
|
||||
)
|
||||
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.custom_field_data['url'], 'http://example.com/1')
|
||||
self.assertEqual(site1.custom_field_data['json'], {"foo": 123})
|
||||
self.assertEqual(site1.custom_field_data['select'], 'Choice A')
|
||||
self.assertEqual(site1.custom_field_data['multiselect'], ['Choice A', 'Choice B'])
|
||||
self.assertEqual(site1.custom_field_data['select'], 'a')
|
||||
self.assertEqual(site1.custom_field_data['multiselect'], ['a', 'b'])
|
||||
|
||||
# Validate data for 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.custom_field_data['url'], 'http://example.com/2')
|
||||
self.assertEqual(site2.custom_field_data['json'], {"bar": 456})
|
||||
self.assertEqual(site2.custom_field_data['select'], 'Choice B')
|
||||
self.assertEqual(site2.custom_field_data['multiselect'], ['Choice B', 'Choice C'])
|
||||
self.assertEqual(site2.custom_field_data['select'], 'b')
|
||||
self.assertEqual(site2.custom_field_data['multiselect'], ['b', 'c'])
|
||||
|
||||
# No custom field data should be set for site 3
|
||||
site3 = Site.objects.get(name='Site 3')
|
||||
@ -1221,7 +1233,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
name='Custom Field Choice Set 1',
|
||||
extra_choices=['A', 'B', 'C', 'X']
|
||||
extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'), ('x', 'X'))
|
||||
)
|
||||
|
||||
# Integer filtering
|
||||
|
@ -14,8 +14,8 @@ class CustomFieldModelFormTest(TestCase):
|
||||
def setUpTestData(cls):
|
||||
obj_type = ContentType.objects.get_for_model(Site)
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
name='Custom Field Choice Set 1',
|
||||
extra_choices=('A', 'B', 'C')
|
||||
name='Choice Set 1',
|
||||
extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'))
|
||||
)
|
||||
|
||||
cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT)
|
||||
|
@ -1,3 +1,4 @@
|
||||
import json
|
||||
import urllib.parse
|
||||
import uuid
|
||||
|
||||
@ -23,7 +24,11 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
CustomFieldChoiceSet.objects.create(
|
||||
name='Choice Set 1',
|
||||
extra_choices=('A', 'B', 'C')
|
||||
extra_choices=(
|
||||
('A', 'A'),
|
||||
('B', 'B'),
|
||||
('C', 'C'),
|
||||
)
|
||||
)
|
||||
|
||||
custom_fields = (
|
||||
@ -76,29 +81,38 @@ class CustomFieldChoiceSetTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
def setUpTestData(cls):
|
||||
|
||||
choice_sets = (
|
||||
CustomFieldChoiceSet(name='Choice Set 1', extra_choices=['1A', '1B', '1C', '1D', '1E']),
|
||||
CustomFieldChoiceSet(name='Choice Set 2', extra_choices=['2A', '2B', '2C', '2D', '2E']),
|
||||
CustomFieldChoiceSet(name='Choice Set 3', extra_choices=['3A', '3B', '3C', '3D', '3E']),
|
||||
CustomFieldChoiceSet(
|
||||
name='Choice Set 1',
|
||||
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)
|
||||
|
||||
cls.form_data = {
|
||||
'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 = (
|
||||
'name,extra_choices',
|
||||
'Choice Set 4,"4A,4B,4C,4D,4E"',
|
||||
'Choice Set 5,"5A,5B,5C,5D,5E"',
|
||||
'Choice Set 6,"6A,6B,6C,6D,6E"',
|
||||
'Choice Set 4,"D1,D2,D3"',
|
||||
'Choice Set 5,"E1,E2,E3"',
|
||||
'Choice Set 6,"F1,F2,F3"',
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
'id,extra_choices',
|
||||
f'{choice_sets[0].pk},"1X,1Y,1Z"',
|
||||
f'{choice_sets[1].pk},"2X,2Y,2Z"',
|
||||
f'{choice_sets[2].pk},"3X,3Y,3Z"',
|
||||
f'{choice_sets[0].pk},"A,B,C"',
|
||||
f'{choice_sets[1].pk},"A,B,C"',
|
||||
f'{choice_sets[2].pk},"A,B,C"',
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
|
@ -24,11 +24,11 @@ class IPAddressAssignmentType(graphene.Union):
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) == Interface:
|
||||
if type(instance) is Interface:
|
||||
return InterfaceType
|
||||
if type(instance) == FHRPGroup:
|
||||
if type(instance) is FHRPGroup:
|
||||
return FHRPGroupType
|
||||
if type(instance) == VMInterface:
|
||||
if type(instance) is VMInterface:
|
||||
return VMInterfaceType
|
||||
|
||||
|
||||
@ -42,11 +42,11 @@ class L2VPNAssignmentType(graphene.Union):
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) == Interface:
|
||||
if type(instance) is Interface:
|
||||
return InterfaceType
|
||||
if type(instance) == VLAN:
|
||||
if type(instance) is VLAN:
|
||||
return VLANType
|
||||
if type(instance) == VMInterface:
|
||||
if type(instance) is VMInterface:
|
||||
return VMInterfaceType
|
||||
|
||||
|
||||
@ -59,9 +59,9 @@ class FHRPGroupInterfaceType(graphene.Union):
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) == Interface:
|
||||
if type(instance) is Interface:
|
||||
return InterfaceType
|
||||
if type(instance) == VMInterface:
|
||||
if type(instance) is VMInterface:
|
||||
return VMInterfaceType
|
||||
|
||||
|
||||
@ -79,17 +79,17 @@ class VLANGroupScopeType(graphene.Union):
|
||||
|
||||
@classmethod
|
||||
def resolve_type(cls, instance, info):
|
||||
if type(instance) == Cluster:
|
||||
if type(instance) is Cluster:
|
||||
return ClusterType
|
||||
if type(instance) == ClusterGroup:
|
||||
if type(instance) is ClusterGroup:
|
||||
return ClusterGroupType
|
||||
if type(instance) == Location:
|
||||
if type(instance) is Location:
|
||||
return LocationType
|
||||
if type(instance) == Rack:
|
||||
if type(instance) is Rack:
|
||||
return RackType
|
||||
if type(instance) == Region:
|
||||
if type(instance) is Region:
|
||||
return RegionType
|
||||
if type(instance) == Site:
|
||||
if type(instance) is Site:
|
||||
return SiteType
|
||||
if type(instance) == SiteGroup:
|
||||
if type(instance) is SiteGroup:
|
||||
return SiteGroupType
|
||||
|
@ -129,7 +129,7 @@ def add_available_vlans(vlans, vlan_group=None):
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
|
@ -76,6 +76,18 @@ class ObjectPermissionMixin:
|
||||
"""
|
||||
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
|
||||
object_permissions = ObjectPermission.objects.filter(
|
||||
self.get_permission_filter(user_obj),
|
||||
@ -83,7 +95,6 @@ class ObjectPermissionMixin:
|
||||
).order_by('id').distinct('id').prefetch_related('object_types')
|
||||
|
||||
# Create a dictionary mapping permissions to their constraints
|
||||
perms = defaultdict(list)
|
||||
for obj_perm in object_permissions:
|
||||
for object_type in obj_perm.object_types.all():
|
||||
for action in obj_perm.actions:
|
||||
@ -119,7 +130,7 @@ class ObjectPermissionMixin:
|
||||
return True
|
||||
|
||||
# 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)):
|
||||
raise ValueError(f"Invalid permission {perm} for model {model}")
|
||||
|
||||
|
@ -39,6 +39,8 @@ REDIS = {
|
||||
|
||||
SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
|
||||
DEFAULT_PERMISSIONS = {}
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': True
|
||||
|
@ -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')
|
||||
DEBUG = getattr(configuration, 'DEBUG', False)
|
||||
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)
|
||||
DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
|
||||
EMAIL = getattr(configuration, 'EMAIL', {})
|
||||
@ -356,6 +363,7 @@ INSTALLED_APPS = [
|
||||
'taggit',
|
||||
'timezone_field',
|
||||
'core',
|
||||
'account',
|
||||
'circuits',
|
||||
'dcim',
|
||||
'ipam',
|
||||
|
@ -9,13 +9,14 @@ from django.db.models import DateField, DateTimeField
|
||||
from django.template import Context, Template
|
||||
from django.urls import reverse
|
||||
from django.utils.dateparse import parse_date
|
||||
from django.utils.html import escape
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django_tables2.columns import library
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from extras.choices import CustomFieldTypeChoices
|
||||
from utilities.permissions import get_permission_for_model
|
||||
from utilities.templatetags.builtins.filters import render_markdown
|
||||
from utilities.utils import content_type_identifier, content_type_name, get_viewname
|
||||
|
||||
@ -24,6 +25,7 @@ __all__ = (
|
||||
'ArrayColumn',
|
||||
'BooleanColumn',
|
||||
'ChoiceFieldColumn',
|
||||
'ChoicesColumn',
|
||||
'ColorColumn',
|
||||
'ColoredLabelColumn',
|
||||
'ContentTypeColumn',
|
||||
@ -249,7 +251,7 @@ class ActionsColumn(tables.Column):
|
||||
dropdown_links = []
|
||||
user = getattr(request, 'user', AnonymousUser())
|
||||
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):
|
||||
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.
|
||||
"""
|
||||
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):
|
||||
self.max_items = max_items
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
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:
|
||||
# Limit the returned items to the specified maximum number
|
||||
omitted = len(value) - self.max_items
|
||||
omitted_count = len(value) - self.max_items
|
||||
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)
|
||||
|
@ -1,19 +1,18 @@
|
||||
from django.conf import settings
|
||||
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.static import serve
|
||||
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 netbox.api.views import APIRootView, StatusView
|
||||
from netbox.graphql.schema import schema
|
||||
from netbox.graphql.views import GraphQLView
|
||||
from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx
|
||||
from users.views import LoginView, LogoutView
|
||||
from .admin import admin_site
|
||||
|
||||
|
||||
_patterns = [
|
||||
|
||||
# Base views
|
||||
@ -37,7 +36,7 @@ _patterns = [
|
||||
path('wireless/', include('wireless.urls')),
|
||||
|
||||
# Current user views
|
||||
path('user/', include('users.account_urls')),
|
||||
path('user/', include('account.urls')),
|
||||
|
||||
# HTMX views
|
||||
path('htmx/object-selector/', htmx.ObjectSelectorView.as_view(), name='htmx_object_selector'),
|
||||
|
@ -1,9 +1,10 @@
|
||||
{% 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 message %}
|
||||
You do not have permission to access this page.
|
||||
{% trans "You do not have permission to access this page" %}.
|
||||
{% endblock %}
|
||||
|
@ -1,9 +1,10 @@
|
||||
{% 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 message %}
|
||||
The requested page does not exist.
|
||||
{% trans "The requested page does not exist" %}.
|
||||
{% endblock %}
|
||||
|
@ -1,9 +1,10 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Server Error</title>
|
||||
<title>{% trans "Server Error" %}</title>
|
||||
<link rel="stylesheet" href="{% static 'netbox-light.css'%}" />
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
@ -14,28 +15,28 @@
|
||||
<div class="col col-md-6 offset-md-3">
|
||||
<div class="card border-danger mt-5">
|
||||
<h5 class="card-header">
|
||||
<i class="mdi mdi-alert"></i> Server Error
|
||||
<i class="mdi mdi-alert"></i> {% trans "Server Error" %}
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
{% block message %}
|
||||
<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>
|
||||
{% endblock %}
|
||||
<hr />
|
||||
<p>
|
||||
The complete exception is provided below:
|
||||
{% trans "The complete exception is provided below" %}:
|
||||
</p>
|
||||
<pre class="block"><strong>{{ exception }}</strong><br />
|
||||
{{ error }}
|
||||
|
||||
Python version: {{ python_version }}
|
||||
NetBox version: {{ netbox_version }}</pre>
|
||||
{% trans "Python version" %}: {{ python_version }}
|
||||
{% trans "NetBox version" %}: {{ netbox_version }}</pre>
|
||||
<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>
|
||||
<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>
|
||||
|
@ -1,12 +1,12 @@
|
||||
{% extends 'users/account/base.html' %}
|
||||
{% extends 'account/base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Bookmarks{% endblock %}
|
||||
{% block title %}{% trans "Bookmarks" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="return_url" value="{% url 'account:bookmarks' %}" />
|
21
netbox/templates/account/password.html
Normal file
21
netbox/templates/account/password.html
Normal 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 %}
|
@ -1,8 +1,9 @@
|
||||
{% extends 'users/account/base.html' %}
|
||||
{% extends 'account/base.html' %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}User Preferences{% endblock %}
|
||||
{% block title %}{% trans "User Preferences" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" action="" id="preferences-update">
|
||||
@ -25,7 +26,7 @@
|
||||
{% if plugin_fields %}
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Plugins</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Plugins" %}</h5>
|
||||
</div>
|
||||
{% for name in plugin_fields %}
|
||||
{% render_field form|getfield:name %}
|
||||
@ -37,23 +38,23 @@
|
||||
{# Table configurations #}
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Table Configurations</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Table Configurations" %}</h5>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% if request.user.config.data.tables %}
|
||||
<label class="col-sm-3 col-form-label text-lg-end">
|
||||
Clear table preferences
|
||||
{% trans "Clear table preferences" %}
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<table class="table table-hover object-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<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>Table</th>
|
||||
<th>Ordering</th>
|
||||
<th>Columns</th>
|
||||
<th>{% trans "Table" %}</th>
|
||||
<th>{% trans "Ordering" %}</th>
|
||||
<th>{% trans "Columns" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -72,15 +73,14 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="offset-sm-3">
|
||||
<p class="text-muted">None found</p>
|
||||
<p class="text-muted">{% trans "None found" %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-end my-3">
|
||||
<a class="btn btn-outline-secondary" href="{% url 'account:preferences' %}">Cancel</a>
|
||||
<button type="submit" name="_update" class="btn btn-primary">Save </button>
|
||||
<a class="btn btn-outline-secondary" href="{% url 'account:preferences' %}">{% trans "Cancel" %}</a>
|
||||
<button type="submit" name="_update" class="btn btn-primary">{% trans "Save" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
@ -1,22 +1,23 @@
|
||||
{% extends 'users/account/base.html' %}
|
||||
{% extends 'account/base.html' %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}User Profile{% endblock %}
|
||||
{% block title %}{% trans "User Profile" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Account Details</h5>
|
||||
<h5 class="card-header">{% trans "Account Details" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Username</th>
|
||||
<th scope="row">{% trans "Username" %}</th>
|
||||
<td>{{ request.user.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Full Name</th>
|
||||
<th scope="row">{% trans "Full Name" %}</th>
|
||||
<td>
|
||||
{% if request.user.first_name or request.user.last_name %}
|
||||
{{ request.user.first_name }} {{ request.user.last_name }}
|
||||
@ -26,19 +27,19 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Email</th>
|
||||
<th scope="row">{% trans "Email" %}</th>
|
||||
<td>{{ request.user.email|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Account Created</th>
|
||||
<th scope="row">{% trans "Account Created" %}</th>
|
||||
<td>{{ request.user.date_joined|annotated_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Superuser</th>
|
||||
<th scope="row">{% trans "Superuser" %}</th>
|
||||
<td>{% checkmark request.user.is_superuser %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Admin Access</th>
|
||||
<th scope="row">{% trans "Admin Access" %}</th>
|
||||
<td>{% checkmark request.user.is_staff %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -47,12 +48,12 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<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">
|
||||
{% for group in request.user.groups.all %}
|
||||
<li class="list-group-item">{{ group }}</li>
|
||||
{% empty %}
|
||||
<li class="list-group-item text-muted">None</li>
|
||||
<li class="list-group-item text-muted">{% trans "None" %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
@ -62,7 +63,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<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">
|
||||
{% render_table changelog_table 'inc/table.html' %}
|
||||
</div>
|
26
netbox/templates/account/token_list.html
Normal file
26
netbox/templates/account/token_list.html
Normal 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 %}
|
@ -1,4 +1,5 @@
|
||||
{% extends "admin/index.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content_title %}{% endblock %}
|
||||
|
||||
@ -6,16 +7,16 @@
|
||||
{{ block.super }}
|
||||
<div class="module">
|
||||
<table style="width: 100%">
|
||||
<caption>System</caption>
|
||||
<caption>{% trans "System" %}</caption>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
<a href="{% url 'rq_home' %}">Background Tasks</a>
|
||||
<a href="{% url 'rq_home' %}">{% trans "Background Tasks" %}</a>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<a href="{% url 'plugins_list' %}">Installed plugins</a>
|
||||
<a href="{% url 'plugins_list' %}">{% trans "Installed plugins" %}</a>
|
||||
</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -1,4 +1,5 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% endblock %}
|
||||
|
||||
@ -13,7 +14,7 @@
|
||||
{% block message %}{% endblock %}
|
||||
</div>
|
||||
<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>
|
||||
|
@ -1,6 +1,7 @@
|
||||
{# Base template for (almost) all NetBox pages #}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
lang="en"
|
||||
@ -24,7 +25,7 @@
|
||||
/>
|
||||
|
||||
{# Page title #}
|
||||
<title>{% block title %}Home{% endblock %} | NetBox</title>
|
||||
<title>{% block title %}{% trans "Home" %}{% endblock %} | NetBox</title>
|
||||
|
||||
<script
|
||||
type="text/javascript"
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% extends 'base/base.html' %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% comment %}
|
||||
Blocks:
|
||||
@ -23,7 +24,7 @@ Blocks:
|
||||
|
||||
{# NetBox Logo, only visible when printing #}
|
||||
<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>
|
||||
|
||||
{# Top bar #}
|
||||
@ -33,7 +34,7 @@ Blocks:
|
||||
<div class="nav-mobile">
|
||||
<div class="nav-mobile-top">
|
||||
<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>
|
||||
<button type="button" aria-label="Toggle Navigation" class="navbar-toggler sidenav-toggle-mobile">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
@ -72,14 +73,14 @@ Blocks:
|
||||
|
||||
{% if settings.DEBUG and not settings.DEVELOPER %}
|
||||
<div class="alert alert-warning text-center mx-3" role="alert">
|
||||
<strong><i class="mdi mdi-alert"></i> Debug mode is enabled.</strong>
|
||||
Performance may be limited. Debugging should never be enabled on a production system.
|
||||
<strong><i class="mdi mdi-alert"></i> {% trans "Debug mode is enabled" %}.</strong>
|
||||
{% trans "Performance may be limited. Debugging should never be enabled on a production system" %}.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if config.MAINTENANCE_MODE and config.BANNER_MAINTENANCE %}
|
||||
<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 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -130,34 +131,34 @@ Blocks:
|
||||
{% block footer_links %}
|
||||
{# Documentation #}
|
||||
<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>
|
||||
|
||||
{# REST API #}
|
||||
<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>
|
||||
|
||||
{# API docs #}
|
||||
<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>
|
||||
|
||||
{# GraphQL API #}
|
||||
{% if config.GRAPHQL_ENABLED %}
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
{# GitHub #}
|
||||
<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>
|
||||
|
||||
{# NetDev Slack #}
|
||||
<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>
|
||||
{% endblock footer_links %}
|
||||
</nav>
|
||||
|
@ -1,5 +1,6 @@
|
||||
{% load navigation %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<nav class="sidenav noprint" id="sidenav" data-simplebar>
|
||||
<div class="sidenav-header">
|
||||
@ -8,12 +9,12 @@
|
||||
|
||||
{# Full Logo #}
|
||||
<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>
|
||||
|
||||
{# Icon Logo #}
|
||||
<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>
|
||||
|
||||
{# Pin/Unpin Toggle #}
|
||||
|
@ -1,6 +1,7 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
@ -11,31 +12,31 @@
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Circuit</h5>
|
||||
<h5 class="card-header">{% trans "Circuit" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Provider</th>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>{{ object.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Account</th>
|
||||
<th scope="row">{% trans "Account" %}</th>
|
||||
<td>{{ object.provider_account|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Circuit ID</th>
|
||||
<th scope="row">{% trans "Circuit ID" %}</th>
|
||||
<td>{{ object.cid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Type</th>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>{{ object.type|linkify }}</td>
|
||||
</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>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Tenant</th>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
@ -44,19 +45,19 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Install Date</th>
|
||||
<th scope="row">{% trans "Install Date" %}</th>
|
||||
<td>{{ object.install_date|annotated_date|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Termination Date</th>
|
||||
<th scope="row">{% trans "Termination Date" %}</th>
|
||||
<td>{{ object.termination_date|annotated_date|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Commit Rate</th>
|
||||
<th scope="row">{% trans "Commit Rate" %}</th>
|
||||
<td>{{ object.commit_rate|humanize_speed|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -1,12 +1,13 @@
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Swap Circuit Terminations{% endblock %}
|
||||
{% block title %}{% trans "Swap Circuit Terminations" %}{% endblock %}
|
||||
|
||||
{% block message %}
|
||||
<p>Swap these terminations for circuit {{ circuit }}?</p>
|
||||
<p>{% blocktrans %}Swap these terminations for circuit {{ circuit }}?{% endblocktrans %}</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>A side:</strong>
|
||||
<strong>{% trans "A side" %}:</strong>
|
||||
{% if termination_a %}
|
||||
{{ termination_a.site }} {% if termination_a.interface %}- {{ termination_a.interface.device }} {{ termination_a.interface }}{% endif %}
|
||||
{% else %}
|
||||
@ -14,7 +15,7 @@
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Z side:</strong>
|
||||
<strong>{% trans "Z side" %}:</strong>
|
||||
{% if termination_z %}
|
||||
{{ termination_z.site }} {% if termination_z.interface %}- {{ termination_z.interface.device }} {{ termination_z.interface }}{% endif %}
|
||||
{% else %}
|
||||
|
@ -1,11 +1,12 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
{% load static %}
|
||||
{% load form_helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form %}
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Circuit Termination</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Circuit Termination" %}</h5>
|
||||
</div>
|
||||
{% render_field form.circuit %}
|
||||
{% render_field form.term_side %}
|
||||
@ -16,10 +17,10 @@
|
||||
<div class="offset-sm-3">
|
||||
<ul class="nav nav-pills" role="tablist">
|
||||
<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 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>
|
||||
</ul>
|
||||
</div>
|
||||
@ -37,7 +38,7 @@
|
||||
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Termination Details</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Termination Details" %}</h5>
|
||||
</div>
|
||||
{% render_field form.port_speed %}
|
||||
{% render_field form.upstream_speed %}
|
||||
@ -49,7 +50,7 @@
|
||||
{% if form.custom_fields %}
|
||||
<div class="field-group mb-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Custom Fields" %}</h5>
|
||||
</div>
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
|
@ -2,11 +2,12 @@
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.circuits.add_circuit %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
@ -16,16 +17,16 @@
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Circuit Type
|
||||
{% trans "Circuit Type" %}
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -1,35 +1,36 @@
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="float-md-end">
|
||||
{% 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">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
<span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
|
||||
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
|
||||
</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">
|
||||
<span class="mdi mdi-swap-vertical" aria-hidden="true"></span> Swap
|
||||
<span class="mdi mdi-swap-vertical" aria-hidden="true"></span> {% trans "Swap" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h5>Termination {{ side }}</h5>
|
||||
<h5>{% blocktrans %}Termination {{ side }}{% endblocktrans %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if termination %}
|
||||
<table class="table table-hover attr-table">
|
||||
{% if termination.site %}
|
||||
<tr>
|
||||
<td>Site</td>
|
||||
<td>{% trans "Site" %}</td>
|
||||
<td>
|
||||
{% if termination.site.region %}
|
||||
{{ termination.site.region|linkify }} /
|
||||
@ -38,13 +39,13 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Termination</td>
|
||||
<td>{% trans "Termination" %}</td>
|
||||
<td>
|
||||
{% if termination.mark_connected %}
|
||||
<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 %}
|
||||
<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 %}
|
||||
{% if peer.device %}
|
||||
{{ peer.device|linkify }}<br/>
|
||||
@ -54,30 +55,30 @@
|
||||
{{ peer|linkify }}{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
<div class="mt-1">
|
||||
<a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> 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> {% trans "Trace" %}
|
||||
</a>
|
||||
{% 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">
|
||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Edit
|
||||
<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> {% trans "Edit" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> Disconnect
|
||||
<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> {% trans "Disconnect" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<div class="dropdown">
|
||||
<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>
|
||||
<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.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.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=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=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 }}">{% 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 }}">{% 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 }}">{% trans "Circuit Termination" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -85,16 +86,16 @@
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td>Provider Network</td>
|
||||
<td>{% trans "Provider Network" %}</td>
|
||||
<td>{{ termination.provider_network.provider|linkify }} / {{ termination.provider_network|linkify }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Speed</td>
|
||||
<td>{% trans "Speed" %}</td>
|
||||
<td>
|
||||
{% if termination.port_speed and termination.upstream_speed %}
|
||||
<i class="mdi mdi-arrow-down-bold" title="Downstream"></i> {{ termination.port_speed|humanize_speed }}
|
||||
<i class="mdi mdi-arrow-up-bold" title="Upstream"></i> {{ termination.upstream_speed|humanize_speed }}
|
||||
<i class="mdi mdi-arrow-down-bold" title="{% trans "Downstream" %}"></i> {{ termination.port_speed|humanize_speed }}
|
||||
<i class="mdi mdi-arrow-up-bold" title="{% trans "Upstream" %}"></i> {{ termination.upstream_speed|humanize_speed }}
|
||||
{% elif termination.port_speed %}
|
||||
{{ termination.port_speed|humanize_speed }}
|
||||
{% else %}
|
||||
@ -103,19 +104,19 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cross-Connect</td>
|
||||
<td>{% trans "Cross-Connect" %}</td>
|
||||
<td>{{ termination.xconnect_id|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Patch Panel/Port</td>
|
||||
<td>{% trans "Patch Panel/Port" %}</td>
|
||||
<td>{{ termination.pp_info|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{% trans "Description" %}</td>
|
||||
<td>{{ termination.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tags</td>
|
||||
<td>{% trans "Tags" %}</td>
|
||||
<td>
|
||||
{% for tag in termination.tags.all %}
|
||||
{% tag tag %}
|
||||
@ -150,7 +151,7 @@
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
<span class="text-muted">{% trans "None" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,11 +3,12 @@
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.circuits.add_circuit %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
@ -16,11 +17,11 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Provider</h5>
|
||||
<h5 class="card-header">{% trans "Provider" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">ASNs</th>
|
||||
<th scope="row">{% trans "ASNs" %}</th>
|
||||
<td>
|
||||
{% for asn in object.asns.all %}
|
||||
{{ asn|linkify }}{% if not forloop.last %}, {% endif %}
|
||||
@ -30,7 +31,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -49,7 +50,7 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<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"
|
||||
hx-get="{% url 'circuits:provideraccount_list' %}?provider_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
@ -57,7 +58,7 @@
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Circuits</h5>
|
||||
<h5 class="card-header">{% trans "Circuits" %}</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'circuits:circuit_list' %}?provider_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
|
@ -3,6 +3,7 @@
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
@ -13,19 +14,19 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Provider Account</h5>
|
||||
<h5 class="card-header">{% trans "Provider Account" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Provider</th>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>{{ object.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Account</th>
|
||||
<th scope="row">{% trans "Account" %}</th>
|
||||
<td>{{ object.account }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -42,7 +43,7 @@
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Circuits</h5>
|
||||
<h5 class="card-header">{% trans "Circuits" %}</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'circuits:circuit_list' %}?provider_account_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
|
@ -3,6 +3,7 @@
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
@ -14,24 +15,24 @@
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Provider Network
|
||||
{% trans "Provider Network" %}
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Provider</th>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>{{ object.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Service ID</th>
|
||||
<th scope="row">{% trans "Service ID" %}</th>
|
||||
<td>{{ object.service_id|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -50,7 +51,7 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Circuits</h5>
|
||||
<h5 class="card-header">{% trans "Circuits" %}</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'circuits:circuit_list' %}?provider_network_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
|
@ -4,6 +4,7 @@
|
||||
{% load helpers %}
|
||||
{% load perms %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
@ -28,30 +29,30 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Data File</h5>
|
||||
<h5 class="card-header">{% trans "Data File" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Source</th>
|
||||
<th scope="row">{% trans "Source" %}</th>
|
||||
<td>{{ object.source|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Path</th>
|
||||
<th scope="row">{% trans "Path" %}</th>
|
||||
<td>
|
||||
<span class="font-monospace" id="datafile_path">{{ object.path }}</span>
|
||||
{% copy_content "datafile_path" %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Last Updated</th>
|
||||
<th scope="row">{% trans "Last Updated" %}</th>
|
||||
<td>{{ object.last_updated }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Size</th>
|
||||
<td>{{ object.size }} byte{{ object.size|pluralize }}</td>
|
||||
<th scope="row">{% trans "Size" %}</th>
|
||||
<td>{{ object.size }} {% trans "bytes" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">SHA256 Hash</th>
|
||||
<th scope="row">{% trans "SHA256 Hash" %}</th>
|
||||
<td>
|
||||
<span class="font-monospace" id="datafile_hash">{{ object.hash }}</span>
|
||||
{% copy_content "datafile_hash" %}
|
||||
@ -61,7 +62,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">Content</h5>
|
||||
<h5 class="card-header">{% trans "Content" %}</h5>
|
||||
<div class="card-body">
|
||||
<pre>{{ object.data_as_string }}</pre>
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.core.sync_datasource %}
|
||||
@ -10,13 +11,13 @@
|
||||
<form action="{% url 'core:datasource_sync' pk=object.pk %}" method="post">
|
||||
{% csrf_token %}
|
||||
<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>
|
||||
</form>
|
||||
{% else %}
|
||||
<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>
|
||||
<i class="mdi mdi-sync" aria-hidden="true"></i> Sync
|
||||
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync" %}
|
||||
</button>
|
||||
</span>
|
||||
{% endif %}
|
||||
@ -27,35 +28,35 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Data Source</h5>
|
||||
<h5 class="card-header">{% trans "Data Source" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Type</th>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>{{ object.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Enabled</th>
|
||||
<th scope="row">{% trans "Enabled" %}</th>
|
||||
<td>{% checkmark object.enabled %}</td>
|
||||
</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>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Last synced</th>
|
||||
<th scope="row">{% trans "Last synced" %}</th>
|
||||
<td>{{ object.last_synced|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">URL</th>
|
||||
<th scope="row">{% trans "URL" %}</th>
|
||||
<td>
|
||||
{% if not object.is_local %}
|
||||
<a href="{{ object.source_url }}">{{ object.source_url }}</a>
|
||||
@ -65,7 +66,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Ignore rules</th>
|
||||
<th scope="row">{% trans "Ignore rules" %}</th>
|
||||
<td>
|
||||
{% if object.ignore_rules %}
|
||||
<pre>{{ object.ignore_rules }}</pre>
|
||||
@ -82,7 +83,7 @@
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Backend</h5>
|
||||
<h5 class="card-header">{% trans "Backend" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
{% for name, field in object.get_backend.parameters.items %}
|
||||
@ -97,7 +98,7 @@
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="2" class="text-muted">
|
||||
No parameters defined
|
||||
{% trans "No parameters defined" %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -112,7 +113,7 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Files</h5>
|
||||
<h5 class="card-header">{% trans "Files" %}</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'core:datafile_list' %}?source_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load perms %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block controls %}
|
||||
<div class="controls">
|
||||
@ -17,25 +18,25 @@
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Job</h5>
|
||||
<h5 class="card-header">{% trans "Job" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Object Type</th>
|
||||
<th scope="row">{% trans "Object Type" %}</th>
|
||||
<td>
|
||||
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object_type }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Status</th>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Created By</th>
|
||||
<th scope="row">{% trans "Created By" %}</th>
|
||||
<td>{{ object.user|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -44,23 +45,23 @@
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Scheduling</h5>
|
||||
<h5 class="card-header">{% trans "Scheduling" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Created</th>
|
||||
<th scope="row">{% trans "Created" %}</th>
|
||||
<td>{{ object.created|annotated_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Scheduled</th>
|
||||
<td>{{ object.scheduled|annotated_date|placeholder }}{% if object.interval %} (every {{ object.interval }} seconds){% endif %}</td>
|
||||
<th scope="row">{% trans "Scheduled" %}</th>
|
||||
<td>{{ object.scheduled|annotated_date|placeholder }}{% if object.interval %} ({% blocktrans %}every {{ object.interval }} seconds{% endblocktrans %}){% endif %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Started</th>
|
||||
<th scope="row">{% trans "Started" %}</th>
|
||||
<td>{{ object.started|annotated_date|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Completed</th>
|
||||
<th scope="row">{% trans "Completed" %}</th>
|
||||
<td>{{ object.completed|annotated_date|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -71,7 +72,7 @@
|
||||
<div class="row">
|
||||
<div class="col col-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Data</h5>
|
||||
<h5 class="card-header">{% trans "Data" %}</h5>
|
||||
<div class="card-body">
|
||||
<pre>{{ object.data|json }}</pre>
|
||||
</div>
|
||||
|
@ -1,10 +1,11 @@
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Disconnect {{ obj_type_plural|bettertitle }}{% endblock %}
|
||||
{% block title %}{% trans "Disconnect" %} {{ obj_type_plural|bettertitle }}{% endblock %}
|
||||
|
||||
{% 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>
|
||||
{% for obj in selected_objects %}
|
||||
<li>{{ obj }}</li>
|
||||
|
@ -3,24 +3,25 @@
|
||||
{% load helpers %}
|
||||
{% load perms %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Cable</h5>
|
||||
<h5 class="card-header">{% trans "Cable" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Type</th>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>{{ object.get_type_display|placeholder }}</td>
|
||||
</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>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Tenant</th>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
@ -29,15 +30,15 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Label</th>
|
||||
<th scope="row">{% trans "Label" %}</th>
|
||||
<td>{{ object.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Color</th>
|
||||
<th scope="row">{% trans "Color" %}</th>
|
||||
<td>
|
||||
{% if object.color %}
|
||||
<span class="color-label" style="background-color: #{{ object.color }}"> </span>
|
||||
@ -47,7 +48,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Length</th>
|
||||
<th scope="row">{% trans "Length" %}</th>
|
||||
<td>
|
||||
{% if object.length %}
|
||||
{{ object.length|floatformat }} {{ object.get_length_unit_display }}
|
||||
@ -66,13 +67,13 @@
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Termination A</h5>
|
||||
<h5 class="card-header">{% trans "Termination" %} A</h5>
|
||||
<div class="card-body">
|
||||
{% include 'dcim/inc/cable_termination.html' with terminations=object.a_terminations %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">Termination B</h5>
|
||||
<h5 class="card-header">{% trans "Termination" %} B</h5>
|
||||
<div class="card-body">
|
||||
{% include 'dcim/inc/cable_termination.html' with terminations=object.b_terminations %}
|
||||
</div>
|
||||
|
@ -2,13 +2,14 @@
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form %}
|
||||
|
||||
{# A side termination #}
|
||||
<div class="field-group mb-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">A Side</h5>
|
||||
<h5 class="offset-sm-3">{% trans "A Side" %}</h5>
|
||||
</div>
|
||||
{% if 'termination_a_device' in form.fields %}
|
||||
{% render_field form.termination_a_device %}
|
||||
@ -25,7 +26,7 @@
|
||||
{# B side termination #}
|
||||
<div class="field-group mb-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">B Side</h5>
|
||||
<h5 class="offset-sm-3">{% trans "B Side" %}</h5>
|
||||
</div>
|
||||
{% if 'termination_b_device' in form.fields %}
|
||||
{% render_field form.termination_b_device %}
|
||||
@ -42,7 +43,7 @@
|
||||
{# Cable attributes #}
|
||||
<div class="field-group mb-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Cable</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Cable" %}</h5>
|
||||
</div>
|
||||
{% render_field form.status %}
|
||||
{% render_field form.type %}
|
||||
@ -64,7 +65,7 @@
|
||||
|
||||
<div class="field-group mb-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Tenancy</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Tenancy" %}</h5>
|
||||
</div>
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
@ -73,7 +74,7 @@
|
||||
{% if form.custom_fields %}
|
||||
<div class="field-group mb-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Custom Fields" %}</h5>
|
||||
</div>
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
@ -81,7 +82,7 @@
|
||||
|
||||
{% if form.comments %}
|
||||
<div class="field-group mb-5">
|
||||
<h5 class="text-center">Comments</h5>
|
||||
<h5 class="text-center">{% trans "Comments" %}</h5>
|
||||
{% render_field form.comments %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -1,7 +1,8 @@
|
||||
{% extends 'base/layout.html' %}
|
||||
{% 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 %}
|
||||
<div class="row">
|
||||
@ -13,21 +14,21 @@
|
||||
<object data="{{ svg_url }}" class="rack_elevation"></object>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="trace-end">
|
||||
{% if path.is_split %}
|
||||
<h3 class="text-danger">Path split!</h3>
|
||||
<p>Select a node below to continue:</p>
|
||||
<h3 class="text-danger">{% trans "Path split" %}!</h3>
|
||||
<p>{% trans "Select a node below to continue" %}:</p>
|
||||
<ul class="text-start">
|
||||
{% for next_node in path.get_split_nodes %}
|
||||
{% if next_node.cable %}
|
||||
{% with viewname=next_node|viewname:"trace" %}
|
||||
<li>
|
||||
<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>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
@ -36,20 +37,20 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<h3 class="text-center text-success">Trace Completed</h3>
|
||||
<h3 class="text-center text-success">{% trans "Trace Completed" %}</h3>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th scope="row">Total segments</th>
|
||||
<th scope="row">{% trans "Total segments" %}</th>
|
||||
<td>{{ path.segment_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Total length</th>
|
||||
<th scope="row">{% trans "Total length" %}</th>
|
||||
<td>
|
||||
{% if total_length %}
|
||||
{{ total_length|floatformat:"-2" }}{% if not is_definitive %}+{% endif %} Meters /
|
||||
{{ total_length|meters_to_feet|floatformat:"-2" }} Feet
|
||||
{{ total_length|floatformat:"-2" }}{% if not is_definitive %}+{% endif %} {% trans "Meters" %} /
|
||||
{{ total_length|meters_to_feet|floatformat:"-2" }} {% trans "Feet" %}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
<span class="text-muted">{% trans "N/A" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@ -58,7 +59,7 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<h3 class="text-center text-muted my-3">
|
||||
No paths found
|
||||
{% trans "No paths found" %}
|
||||
</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -67,15 +68,15 @@
|
||||
<div class="col col-md-7">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Related Paths
|
||||
{% trans "Related Paths" %}
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Origin</th>
|
||||
<th>Destination</th>
|
||||
<th>Segments</th>
|
||||
<th>{% trans "Origin" %}</th>
|
||||
<th>{% trans "Destination" %}</th>
|
||||
<th>{% trans "Segments" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -88,7 +89,7 @@
|
||||
{% if cablepath.destinations %}
|
||||
{{ cablepath.destinations|join:", " }}
|
||||
{% else %}
|
||||
<span class="text-muted">Incomplete</span>
|
||||
<span class="text-muted">{% trans "Incomplete" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
@ -97,7 +98,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<td colspan="3" class="text-muted">
|
||||
None found
|
||||
{% trans "None found" %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -1,6 +1,7 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
@ -14,36 +15,36 @@
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Console Port
|
||||
{% trans "Console Port" %}
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Device</th>
|
||||
<th scope="row">{% trans "Device" %}</th>
|
||||
<td>{{ object.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Module</th>
|
||||
<th scope="row">{% trans "Module" %}</th>
|
||||
<td>{{ object.module|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Label</th>
|
||||
<th scope="row">{% trans "Label" %}</th>
|
||||
<td>{{ object.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Type</th>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>{{ object.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Speed</th>
|
||||
<th scope="row">{% trans "Speed" %}</th>
|
||||
<td>{{ object.get_speed_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -55,29 +56,29 @@
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Connection</h5>
|
||||
<h5 class="card-header">{% trans "Connection" %}</h5>
|
||||
<div class="card-body">
|
||||
{% 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 %}
|
||||
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:consoleport_trace' %}
|
||||
{% else %}
|
||||
<div class="text-muted">
|
||||
Not Connected
|
||||
{% trans "Not Connected" %}
|
||||
{% if perms.dcim.add_cable %}
|
||||
<div class="dropdown float-end">
|
||||
<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>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -1,8 +1,9 @@
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Delete console port {{ consoleport }}?{% endblock %}
|
||||
{% block title %}{% blocktrans %}Delete console port {{ consoleport }}?{% endblocktrans %}{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
|
@ -1,6 +1,7 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
@ -14,36 +15,36 @@
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Console Server Port
|
||||
{% trans "Console Server Port" %}
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Device</th>
|
||||
<th scope="row">{% trans "Device" %}</th>
|
||||
<td>{{ object.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Module</th>
|
||||
<th scope="row">{% trans "Module" %}</th>
|
||||
<td>{{ object.module|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Label</th>
|
||||
<th scope="row">{% trans "Label" %}</th>
|
||||
<td>{{ object.label|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Type</th>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>{{ object.get_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Speed</th>
|
||||
<th scope="row">{% trans "Speed" %}</th>
|
||||
<td>{{ object.get_speed_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -55,29 +56,29 @@
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Connection</h5>
|
||||
<h5 class="card-header">{% trans "Connection" %}</h5>
|
||||
<div class="card-body">
|
||||
{% 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 %}
|
||||
{% include 'dcim/inc/connection_endpoints.html' with trace_url='dcim:consoleserverport_trace' %}
|
||||
{% else %}
|
||||
<div class="text-muted">
|
||||
Not Connected
|
||||
{% trans "Not Connected" %}
|
||||
{% if perms.dcim.add_cable %}
|
||||
<div class="dropdown float-end">
|
||||
<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>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -1,8 +1,9 @@
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
|
@ -4,16 +4,17 @@
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-xl-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Device</h5>
|
||||
<h5 class="card-header">{% trans "Device" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Region</th>
|
||||
<th scope="row">{% trans "Region" %}</th>
|
||||
<td>
|
||||
{% if object.site.region %}
|
||||
{% for region in object.site.region.get_ancestors %}
|
||||
@ -26,11 +27,11 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Site</th>
|
||||
<th scope="row">{% trans "Site" %}</th>
|
||||
<td>{{ object.site|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Location</th>
|
||||
<th scope="row">{% trans "Location" %}</th>
|
||||
<td>
|
||||
{% if object.location %}
|
||||
{% for location in object.location.get_ancestors %}
|
||||
@ -43,12 +44,12 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Rack</th>
|
||||
<th scope="row">{% trans "Rack" %}</th>
|
||||
<td class="position-relative">
|
||||
{% if object.rack %}
|
||||
{{ object.rack|linkify }}
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
@ -58,7 +59,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Position</th>
|
||||
<th scope="row">{% trans "Position" %}</th>
|
||||
<td>
|
||||
{% if object.parent_bay %}
|
||||
{% with object.parent_bay.device as parent %}
|
||||
@ -70,20 +71,20 @@
|
||||
{% elif object.rack and object.position %}
|
||||
<span>U{{ object.position|floatformat }} / {{ object.get_face_display }}</span>
|
||||
{% 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 %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">GPS Coordinates</th>
|
||||
<th scope="row">{% trans "GPS Coordinates" %}</th>
|
||||
<td class="position-relative">
|
||||
{% if object.latitude and object.longitude %}
|
||||
{% if config.MAPS_URL %}
|
||||
<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">
|
||||
<i class="mdi mdi-map-marker"></i> Map It
|
||||
<i class="mdi mdi-map-marker"></i> {% trans "Map It" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -94,7 +95,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Tenant</th>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
@ -103,31 +104,31 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Device Type</th>
|
||||
<th scope="row">{% trans "Device Type" %}</th>
|
||||
<td>
|
||||
{{ object.device_type|linkify:"get_full_name" }} ({{ object.device_type.u_height|floatformat }}U)
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Airflow</th>
|
||||
<th scope="row">{% trans "Airflow" %}</th>
|
||||
<td>
|
||||
{{ object.get_airflow_display|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Serial Number</th>
|
||||
<th scope="row">{% trans "Serial Number" %}</th>
|
||||
<td class="font-monospace">{{ object.serial|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Asset Tag</th>
|
||||
<th scope="row">{% trans "Asset Tag" %}</th>
|
||||
<td class="font-monospace">{{ object.asset_tag|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Config Template</th>
|
||||
<th scope="row">{% trans "Config Template" %}</th>
|
||||
<td>{{ object.config_template|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -136,15 +137,15 @@
|
||||
{% if vc_members %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Virtual Chassis
|
||||
{% trans "Virtual Chassis" %}
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th>Device</th>
|
||||
<th>Position</th>
|
||||
<th>Master</th>
|
||||
<th>Priority</th>
|
||||
<th>{% trans "Device" %}</th>
|
||||
<th>{% trans "Position" %}</th>
|
||||
<th>{% trans "Master" %}</th>
|
||||
<th>{% trans "Priority" %}</th>
|
||||
</tr>
|
||||
{% for vc_member in vc_members %}
|
||||
<tr{% if vc_member == object %} class="info"{% endif %}>
|
||||
@ -166,7 +167,7 @@
|
||||
</div>
|
||||
<div class="card-footer text-end noprint">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -175,7 +176,7 @@
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
<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"
|
||||
hx-get="{% url 'dcim:virtualdevicecontext_list' %}?device_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
@ -183,7 +184,7 @@
|
||||
{% if perms.dcim.add_virtualdevicecontext %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -192,30 +193,30 @@
|
||||
</div>
|
||||
<div class="col col-12 col-xl-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Management</h5>
|
||||
<h5 class="card-header">{% trans "Management" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Role</th>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>{{ object.device_role|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Platform</th>
|
||||
<th scope="row">{% trans "Platform" %}</th>
|
||||
<td>{{ object.platform|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Primary IPv4</th>
|
||||
<th scope="row">{% trans "Primary IPv4" %}</th>
|
||||
<td>
|
||||
{% if object.primary_ip4 %}
|
||||
<a href="{{ object.primary_ip4.get_absolute_url }}" id="primary_ip4">{{ object.primary_ip4.address.ip }}</a>
|
||||
{% 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 %}
|
||||
(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 %}
|
||||
{% copy_content "primary_ip4" %}
|
||||
{% else %}
|
||||
@ -224,14 +225,14 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Primary IPv6</th>
|
||||
<th scope="row">{% trans "Primary IPv6" %}</th>
|
||||
<td>
|
||||
{% if object.primary_ip6 %}
|
||||
<a href="{{ object.primary_ip6.get_absolute_url }}" id="primary_ip6">{{ object.primary_ip6.address.ip }}</a>
|
||||
{% 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 %}
|
||||
(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 %}
|
||||
{% copy_content "primary_ip6" %}
|
||||
{% else %}
|
||||
@ -252,7 +253,7 @@
|
||||
</tr>
|
||||
{% if object.cluster %}
|
||||
<tr>
|
||||
<th>Cluster</th>
|
||||
<th>{% trans "Cluster" %}</th>
|
||||
<td>
|
||||
{% if object.cluster.group %}
|
||||
{{ object.cluster.group|linkify }} /
|
||||
@ -267,25 +268,25 @@
|
||||
{% if object.powerports.exists and object.poweroutlets.exists %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Power Utilization
|
||||
{% trans "Power Utilization" %}
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th>Input</th>
|
||||
<th>Outlets</th>
|
||||
<th>Allocated</th>
|
||||
<th>Available</th>
|
||||
<th>Utilization</th>
|
||||
<th>{% trans "Input" %}</th>
|
||||
<th>{% trans "Outlets" %}</th>
|
||||
<th>{% trans "Allocated" %}</th>
|
||||
<th>{% trans "Available" %}</th>
|
||||
<th>{% trans "Utilization" %}</th>
|
||||
</tr>
|
||||
{% for powerport in object.powerports.all %}
|
||||
{% with utilization=powerport.get_power_draw powerfeed=powerport.connected_endpoints.0 %}
|
||||
<tr>
|
||||
<td>{{ powerport }}</td>
|
||||
<td>{{ utilization.outlet_count }}</td>
|
||||
<td>{{ utilization.allocated }}VA</td>
|
||||
<td>{{ utilization.allocated }}{% trans "VA" %}</td>
|
||||
{% 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>
|
||||
{% else %}
|
||||
<td class="text-muted">—</td>
|
||||
@ -294,12 +295,12 @@
|
||||
</tr>
|
||||
{% for leg in utilization.legs %}
|
||||
<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.allocated }}</td>
|
||||
{% if powerfeed.available_power %}
|
||||
{% 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>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
@ -315,7 +316,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Services</h5>
|
||||
<h5 class="card-header">{% trans "Services" %}</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'ipam:service_list' %}?device_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
@ -323,27 +324,27 @@
|
||||
{% if perms.ipam.add_service %}
|
||||
<div class="card-footer text-end noprint">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">Dimensions</h5>
|
||||
<h5 class="card-header">{% trans "Dimensions" %}</h5>
|
||||
<div class="card-body table-responsive">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Height</th>
|
||||
<th scope="row">{% trans "Height" %}</th>
|
||||
<td>
|
||||
{{ object.device_type.u_height }}U
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Weight</th>
|
||||
<th scope="row">{% trans "Weight" %}</th>
|
||||
<td>
|
||||
{% if object.total_weight %}
|
||||
{{ object.total_weight|floatformat }} Kilograms
|
||||
{{ object.total_weight|floatformat }} {% trans "Kilograms" %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
@ -356,13 +357,13 @@
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<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 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<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 %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
@ -17,38 +18,38 @@
|
||||
{% if perms.dcim.change_device %}
|
||||
<div class="dropdown">
|
||||
<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>
|
||||
<ul class="dropdown-menu" aria-labeled-by="add-components">
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsolePortTable_config" %}
|
||||
@ -20,22 +21,22 @@
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<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">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</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">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="btn-group" role="group">
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -43,7 +44,7 @@
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsoleServerPortTable_config" %}
|
||||
@ -20,22 +21,22 @@
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<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">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</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">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="btn-group" role="group">
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -43,7 +44,7 @@
|
||||
{% if perms.dcim.add_consoleserverport %}
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceDeviceBayTable_config" %}
|
||||
@ -20,23 +21,23 @@
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<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">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</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">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceFrontPortTable_config" %}
|
||||
@ -20,22 +21,22 @@
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<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">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</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">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="btn-group" role="group">
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -43,7 +44,7 @@
|
||||
{% if perms.dcim.add_frontport %}
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -1,12 +1,13 @@
|
||||
{% extends 'inc/table_controls_htmx.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_table_controls %}
|
||||
<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>
|
||||
</button>
|
||||
<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-disabled" data-state="show">Hide Disabled</button>
|
||||
<button type="button" class="dropdown-item toggle-virtual" data-state="show">Hide Virtual</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">{% trans "Hide Disabled" %}</button>
|
||||
<button type="button" class="dropdown-item toggle-virtual" data-state="show">{% trans "Hide Virtual" %}</button>
|
||||
</ul>
|
||||
{% endblock extra_table_controls %}
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %}
|
||||
@ -22,12 +23,12 @@
|
||||
<button type="submit" name="_edit"
|
||||
formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' 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 type="submit" name="_rename"
|
||||
formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={% url 'dcim:device_interfaces' 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>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -36,14 +37,14 @@
|
||||
<button type="submit" name="_delete"
|
||||
formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={% url 'dcim:device_interfaces' 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>
|
||||
{% endif %}
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={% url 'dcim:device_interfaces' 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -52,7 +53,7 @@
|
||||
<div class="bulk-button-group">
|
||||
<a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
|
||||
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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceInventoryItemTable_config" %}
|
||||
@ -20,23 +21,23 @@
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<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">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</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">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceModuleBayTable_config" %}
|
||||
@ -20,23 +21,23 @@
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<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">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</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">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if perms.dcim.add_modulebay %}
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerOutletTable_config" %}
|
||||
@ -20,22 +21,22 @@
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<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">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</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">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="btn-group" role="group">
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -43,7 +44,7 @@
|
||||
{% if perms.dcim.add_poweroutlet %}
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerPortTable_config" %}
|
||||
@ -20,22 +21,22 @@
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<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">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</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">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="btn-group" role="group">
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -43,7 +44,7 @@
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="DeviceRearPortTable_config" %}
|
||||
@ -20,22 +21,22 @@
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<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">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</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">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="btn-group" role="group">
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -43,7 +44,7 @@
|
||||
{% if perms.dcim.add_rearport %}
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -1,25 +1,26 @@
|
||||
{% extends 'dcim/device/base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{{ object }} - Config{% endblock %}
|
||||
{% block title %}{{ object }} - {% trans "Config" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-5">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Config Template</h5>
|
||||
<h5 class="card-header">{% trans "Config Template" %}</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Config Template</th>
|
||||
<th scope="row">{% trans "Config Template" %}</th>
|
||||
<td>{{ config_template|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data Source</th>
|
||||
<th scope="row">{% trans "Data Source" %}</th>
|
||||
<td>{{ config_template.data_file.source|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Data File</th>
|
||||
<th scope="row">{% trans "Data File" %}</th>
|
||||
<td>{{ config_template.data_file|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -33,7 +34,7 @@
|
||||
<div class="accordion-item">
|
||||
<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">
|
||||
Context Data
|
||||
{% trans "Context Data" %}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapsedRenderConfig" class="accordion-collapse collapse" aria-labelledby="renderConfigHeading" data-bs-parent="#renderConfig">
|
||||
@ -53,15 +54,15 @@
|
||||
<div class="card-header">
|
||||
<div class="float-end">
|
||||
<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>
|
||||
</div>
|
||||
<h5>Rendered Config</h5>
|
||||
<h5>{% trans "Rendered Config" %}</h5>
|
||||
</div>
|
||||
{% if config_template %}
|
||||
<pre class="card-body">{{ rendered_config }}</pre>
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,12 +1,13 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
{% load form_helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form %}
|
||||
{% render_errors form %}
|
||||
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Device</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Device" %}</h5>
|
||||
</div>
|
||||
{% render_field form.name %}
|
||||
{% render_field form.device_role %}
|
||||
@ -16,7 +17,7 @@
|
||||
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Hardware</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Hardware" %}</h5>
|
||||
</div>
|
||||
{% render_field form.device_type %}
|
||||
{% render_field form.airflow %}
|
||||
@ -26,7 +27,7 @@
|
||||
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Location</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Location" %}</h5>
|
||||
</div>
|
||||
{% render_field form.site %}
|
||||
{% render_field form.location %}
|
||||
@ -34,18 +35,18 @@
|
||||
|
||||
{% if object.device_type.is_child_device and object.parent_bay %}
|
||||
<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">
|
||||
<input class="form-control" value="{{ object.parent_bay.device }}" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<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="input-group">
|
||||
<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">
|
||||
<i class="mdi mdi-close-thick"></i> Remove
|
||||
<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> {% trans "Remove" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -60,7 +61,7 @@
|
||||
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Management</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Management" %}</h5>
|
||||
</div>
|
||||
{% render_field form.status %}
|
||||
{% render_field form.platform %}
|
||||
@ -74,14 +75,14 @@
|
||||
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Virtualization</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Virtualization" %}</h5>
|
||||
</div>
|
||||
{% render_field form.cluster %}
|
||||
</div>
|
||||
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Tenancy</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Tenancy" %}</h5>
|
||||
</div>
|
||||
{% render_field form.tenant_group %}
|
||||
{% render_field form.tenant %}
|
||||
@ -89,7 +90,7 @@
|
||||
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Virtual Chassis</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Virtual Chassis" %}</h5>
|
||||
</div>
|
||||
{% render_field form.virtual_chassis %}
|
||||
{% render_field form.vc_position %}
|
||||
@ -99,14 +100,14 @@
|
||||
{% if form.custom_fields %}
|
||||
<div class="field-group my-5">
|
||||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||
<h5 class="offset-sm-3">{% trans "Custom Fields" %}</h5>
|
||||
</div>
|
||||
{% render_custom_fields form %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<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 %}
|
||||
</div>
|
||||
|
||||
|
@ -1,73 +1,74 @@
|
||||
{% extends 'generic/object_list.html' %}
|
||||
{% load buttons %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block bulk_buttons %}
|
||||
{% if perms.dcim.change_device %}
|
||||
<div class="dropdown">
|
||||
<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>
|
||||
<ul class="dropdown-menu">
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_consoleserverport %}
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_poweroutlet %}
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_rearport %}
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_modulebay %}
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
{% endif %}
|
||||
@ -78,7 +79,7 @@
|
||||
<div class="btn-group" role="group">
|
||||
{% 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">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user