Merge branch 'feature' into 13150-table-translation

This commit is contained in:
Jeremy Stretch 2023-07-31 13:18:52 -04:00
commit dad962c3d8
420 changed files with 127202 additions and 3510 deletions

View File

@ -14,7 +14,7 @@ body:
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v3.5.6 placeholder: v3.5.7
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v3.5.6 placeholder: v3.5.7
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

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

View File

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

View File

@ -38,7 +38,7 @@ An example hierarchy might look like this:
* 100.64.16.1/24 (address) * 100.64.16.1/24 (address)
* 100.64.16.2/24 (address) * 100.64.16.2/24 (address)
* 100.64.16.3/24 (address) * 100.64.16.3/24 (address)
* 100.64.16.9/24 (prefix) * 100.64.19.0/24 (prefix)
* 100.64.32.0/20 (prefix) * 100.64.32.0/20 (prefix)
* 100.64.32.1/24 (address) * 100.64.32.1/24 (address)
* 100.64.32.10-99/24 (range) * 100.64.32.10-99/24 (range)

View File

@ -55,6 +55,9 @@ Within the shell, enter the following commands to create the database and user (
CREATE DATABASE netbox; CREATE DATABASE netbox;
CREATE USER netbox WITH PASSWORD 'J5brHrAXFLQSif0K'; CREATE USER netbox WITH PASSWORD 'J5brHrAXFLQSif0K';
ALTER DATABASE netbox OWNER TO netbox; ALTER DATABASE netbox OWNER TO netbox;
-- the next two commands are needed on PostgreSQL 15 and later
\connect netbox;
GRANT CREATE ON SCHEMA public TO netbox;
``` ```
!!! danger "Use a strong password" !!! danger "Use a strong password"

View File

@ -48,36 +48,40 @@ Download the [latest stable release](https://github.com/netbox-community/netbox/
Download and extract the latest version: Download and extract the latest version:
```no-highlight ```no-highlight
wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz # Set $NEWVER to the NetBox version being installed
sudo tar -xzf vX.Y.Z.tar.gz -C /opt NEWVER=3.5.0
sudo ln -sfn /opt/netbox-X.Y.Z/ /opt/netbox wget https://github.com/netbox-community/netbox/archive/v$NEWVER.tar.gz
sudo tar -xzf v$NEWVER.tar.gz -C /opt
sudo ln -sfn /opt/netbox-$NEWVER/ /opt/netbox
``` ```
Copy `local_requirements.txt`, `configuration.py`, and `ldap_config.py` (if present) from the current installation to the new version: Copy `local_requirements.txt`, `configuration.py`, and `ldap_config.py` (if present) from the current installation to the new version:
```no-highlight ```no-highlight
sudo cp /opt/netbox-X.Y.Z/local_requirements.txt /opt/netbox/ # Set $OLDVER to the NetBox version currently installed
sudo cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/ NEWVER=3.4.9
sudo cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/ sudo cp /opt/netbox-$OLDVER/local_requirements.txt /opt/netbox/
sudo cp /opt/netbox-$OLDVER/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/
sudo cp /opt/netbox-$OLDVER/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/
``` ```
Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.) Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.)
```no-highlight ```no-highlight
sudo cp -pr /opt/netbox-X.Y.Z/netbox/media/ /opt/netbox/netbox/ sudo cp -pr /opt/netbox-$OLDVER/netbox/media/ /opt/netbox/netbox/
``` ```
Also make sure to copy or link any custom scripts and reports that you've made. Note that if these are stored outside the project root, you will not need to copy them. (Check the `SCRIPTS_ROOT` and `REPORTS_ROOT` parameters in the configuration file above if you're unsure.) Also make sure to copy or link any custom scripts and reports that you've made. Note that if these are stored outside the project root, you will not need to copy them. (Check the `SCRIPTS_ROOT` and `REPORTS_ROOT` parameters in the configuration file above if you're unsure.)
```no-highlight ```no-highlight
sudo cp -r /opt/netbox-X.Y.Z/netbox/scripts /opt/netbox/netbox/ sudo cp -r /opt/netbox-$OLDVER/netbox/scripts /opt/netbox/netbox/
sudo cp -r /opt/netbox-X.Y.Z/netbox/reports /opt/netbox/netbox/ sudo cp -r /opt/netbox-$OLDVER/netbox/reports /opt/netbox/netbox/
``` ```
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well: If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
```no-highlight ```no-highlight
sudo cp /opt/netbox-X.Y.Z/gunicorn.py /opt/netbox/ sudo cp /opt/netbox-$OLDVER/gunicorn.py /opt/netbox/
``` ```
### Option B: Clone the Git Repository ### Option B: Clone the Git Repository

View File

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

View File

@ -1,6 +1,26 @@
# NetBox v3.5 # NetBox v3.5
## v3.5.7 (FUTURE) ## v3.5.8 (FUTURE)
---
## v3.5.7 (2023-07-28)
### Enhancements
* [#11803](https://github.com/netbox-community/netbox/issues/11803) - Move non-rack devices list to a separate tab under the rack view
* [#12625](https://github.com/netbox-community/netbox/issues/12625) - Mask sensitive parameters when viewing a configured data source
* [#13009](https://github.com/netbox-community/netbox/issues/13009) - Add IEC 10609-1 and NBR 14136 power port & outlet types
* [#13097](https://github.com/netbox-community/netbox/issues/13097) - Implement a faster initial poll for report & script results
* [#13234](https://github.com/netbox-community/netbox/issues/13234) - Add 100GBASE-X-DSFP and 100GBASE-X-SFPDD interface types
### Bug Fixes
* [#13051](https://github.com/netbox-community/netbox/issues/13051) - Fix Markdown support for table cell alignment
* [#13167](https://github.com/netbox-community/netbox/issues/13167) - Fix missing script results when fetched via REST API
* [#13233](https://github.com/netbox-community/netbox/issues/13233) - Remove extraneous VLAN group field from bulk edit form for interfaces
* [#13237](https://github.com/netbox-community/netbox/issues/13237) - Permit unauthenticated access to content types REST API endpoint when `LOGIN_REQUIRED` is false
* [#13285](https://github.com/netbox-community/netbox/issues/13285) - Fix exception when importing device type missing rack unit height value
--- ---

View File

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

View File

View File

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

View File

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

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

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

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

View File

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

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

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

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
from circuits.models import * from circuits.models import *
@ -26,12 +26,11 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label=_('Comments')
)
model = Provider model = Provider
fieldsets = ( fieldsets = (
@ -44,16 +43,16 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm): class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
provider = DynamicModelChoiceField( provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label=_('Comments')
)
model = ProviderAccount model = ProviderAccount
fieldsets = ( fieldsets = (
@ -66,6 +65,7 @@ class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm): class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
provider = DynamicModelChoiceField( provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
required=False required=False
) )
@ -75,12 +75,11 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
label=_('Service ID') label=_('Service ID')
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label=_('Comments')
)
model = ProviderNetwork model = ProviderNetwork
fieldsets = ( fieldsets = (
@ -93,6 +92,7 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm): class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
@ -106,14 +106,17 @@ class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
class CircuitBulkEditForm(NetBoxModelBulkEditForm): class CircuitBulkEditForm(NetBoxModelBulkEditForm):
type = DynamicModelChoiceField( type = DynamicModelChoiceField(
label=_('Type'),
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
required=False required=False
) )
provider = DynamicModelChoiceField( provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
required=False required=False
) )
provider_account = DynamicModelChoiceField( provider_account = DynamicModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(), queryset=ProviderAccount.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -121,19 +124,23 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
} }
) )
status = forms.ChoiceField( status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(CircuitStatusChoices), choices=add_blank_choice(CircuitStatusChoices),
required=False, required=False,
initial='' initial=''
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False
) )
install_date = forms.DateField( install_date = forms.DateField(
label=_('Install date'),
required=False, required=False,
widget=DatePicker() widget=DatePicker()
) )
termination_date = forms.DateField( termination_date = forms.DateField(
label=_('Termination date'),
required=False, required=False,
widget=DatePicker() widget=DatePicker()
) )
@ -145,18 +152,17 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
) )
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=100, max_length=100,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label=_('Comments')
)
model = Circuit model = Circuit
fieldsets = ( fieldsets = (
('Circuit', ('provider', 'type', 'status', 'description')), (_('Circuit'), ('provider', 'type', 'status', 'description')),
('Service Parameters', ('provider_account', 'install_date', 'termination_date', 'commit_rate')), (_('Service Parameters'), ('provider_account', 'install_date', 'termination_date', 'commit_rate')),
('Tenancy', ('tenant',)), (_('Tenancy'), ('tenant',)),
) )
nullable_fields = ( nullable_fields = (
'tenant', 'commit_rate', 'description', 'comments', 'tenant', 'commit_rate', 'description', 'comments',

View File

@ -3,7 +3,7 @@ from django import forms
from circuits.choices import CircuitStatusChoices from circuits.choices import CircuitStatusChoices
from circuits.models import * from circuits.models import *
from dcim.models import Site from dcim.models import Site
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelImportForm from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import BootstrapMixin from utilities.forms import BootstrapMixin
@ -31,6 +31,7 @@ class ProviderImportForm(NetBoxModelImportForm):
class ProviderAccountImportForm(NetBoxModelImportForm): class ProviderAccountImportForm(NetBoxModelImportForm):
provider = CSVModelChoiceField( provider = CSVModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Assigned provider') help_text=_('Assigned provider')
@ -45,6 +46,7 @@ class ProviderAccountImportForm(NetBoxModelImportForm):
class ProviderNetworkImportForm(NetBoxModelImportForm): class ProviderNetworkImportForm(NetBoxModelImportForm):
provider = CSVModelChoiceField( provider = CSVModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Assigned provider') help_text=_('Assigned provider')
@ -67,26 +69,31 @@ class CircuitTypeImportForm(NetBoxModelImportForm):
class CircuitImportForm(NetBoxModelImportForm): class CircuitImportForm(NetBoxModelImportForm):
provider = CSVModelChoiceField( provider = CSVModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Assigned provider') help_text=_('Assigned provider')
) )
provider_account = CSVModelChoiceField( provider_account = CSVModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(), queryset=ProviderAccount.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Assigned provider account'), help_text=_('Assigned provider account'),
required=False required=False
) )
type = CSVModelChoiceField( type = CSVModelChoiceField(
label=_('Type'),
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Type of circuit') help_text=_('Type of circuit')
) )
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'),
choices=CircuitStatusChoices, choices=CircuitStatusChoices,
help_text=_('Operational status') help_text=_('Operational status')
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -103,11 +110,13 @@ class CircuitImportForm(NetBoxModelImportForm):
class CircuitTerminationImportForm(BootstrapMixin, forms.ModelForm): class CircuitTerminationImportForm(BootstrapMixin, forms.ModelForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='name', to_field_name='name',
required=False required=False
) )
provider_network = CSVModelChoiceField( provider_network = CSVModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(), queryset=ProviderNetwork.objects.all(),
to_field_name='name', to_field_name='name',
required=False required=False

View File

@ -23,9 +23,9 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Provider model = Provider
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id')),
('ASN', ('asn',)), (_('ASN'), ('asn',)),
('Contacts', ('contact', 'contact_role', 'contact_group')), (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -62,7 +62,7 @@ class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
model = ProviderAccount model = ProviderAccount
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('provider_id', 'account')), (_('Attributes'), ('provider_id', 'account')),
) )
provider_id = DynamicModelMultipleChoiceField( provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
@ -70,6 +70,7 @@ class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
label=_('Provider') label=_('Provider')
) )
account = forms.CharField( account = forms.CharField(
label=_('Account'),
required=False required=False
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -79,7 +80,7 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
model = ProviderNetwork model = ProviderNetwork
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('provider_id', 'service_id')), (_('Attributes'), ('provider_id', 'service_id')),
) )
provider_id = DynamicModelMultipleChoiceField( provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
@ -87,6 +88,7 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
label=_('Provider') label=_('Provider')
) )
service_id = forms.CharField( service_id = forms.CharField(
label=_('Service id'),
max_length=100, max_length=100,
required=False required=False
) )
@ -102,11 +104,11 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
model = Circuit model = Circuit
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Provider', ('provider_id', 'provider_account_id', 'provider_network_id')), (_('Provider'), ('provider_id', 'provider_account_id', 'provider_network_id')),
('Attributes', ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')), (_('Attributes'), ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
('Location', ('region_id', 'site_group_id', 'site_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')), (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
) )
type_id = DynamicModelMultipleChoiceField( type_id = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
@ -135,6 +137,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
label=_('Provider network') label=_('Provider network')
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
choices=CircuitStatusChoices, choices=CircuitStatusChoices,
required=False required=False
) )
@ -158,10 +161,12 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
label=_('Site') label=_('Site')
) )
install_date = forms.DateField( install_date = forms.DateField(
label=_('Install date'),
required=False, required=False,
widget=DatePicker widget=DatePicker
) )
termination_date = forms.DateField( termination_date = forms.DateField(
label=_('Termination date'),
required=False, required=False,
widget=DatePicker widget=DatePicker
) )

View File

@ -1,4 +1,4 @@
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
from circuits.models import * from circuits.models import *
@ -29,7 +29,7 @@ class ProviderForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Provider', ('name', 'slug', 'asns', 'description', 'tags')), (_('Provider'), ('name', 'slug', 'asns', 'description', 'tags')),
) )
class Meta: class Meta:
@ -41,6 +41,7 @@ class ProviderForm(NetBoxModelForm):
class ProviderAccountForm(NetBoxModelForm): class ProviderAccountForm(NetBoxModelForm):
provider = DynamicModelChoiceField( provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all() queryset=Provider.objects.all()
) )
comments = CommentField() comments = CommentField()
@ -54,12 +55,13 @@ class ProviderAccountForm(NetBoxModelForm):
class ProviderNetworkForm(NetBoxModelForm): class ProviderNetworkForm(NetBoxModelForm):
provider = DynamicModelChoiceField( provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all() queryset=Provider.objects.all()
) )
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Provider Network', ('provider', 'name', 'service_id', 'description', 'tags')), (_('Provider Network'), ('provider', 'name', 'service_id', 'description', 'tags')),
) )
class Meta: class Meta:
@ -73,7 +75,7 @@ class CircuitTypeForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
('Circuit Type', ( (_('Circuit Type'), (
'name', 'slug', 'description', 'tags', 'name', 'slug', 'description', 'tags',
)), )),
) )
@ -87,10 +89,12 @@ class CircuitTypeForm(NetBoxModelForm):
class CircuitForm(TenancyForm, NetBoxModelForm): class CircuitForm(TenancyForm, NetBoxModelForm):
provider = DynamicModelChoiceField( provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
selector=True selector=True
) )
provider_account = DynamicModelChoiceField( provider_account = DynamicModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(), queryset=ProviderAccount.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -103,9 +107,9 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Circuit', ('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags')), (_('Circuit'), ('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags')),
('Service Parameters', ('install_date', 'termination_date', 'commit_rate')), (_('Service Parameters'), ('install_date', 'termination_date', 'commit_rate')),
('Tenancy', ('tenant_group', 'tenant')), (_('Tenancy'), ('tenant_group', 'tenant')),
) )
class Meta: class Meta:
@ -125,15 +129,18 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
class CircuitTerminationForm(NetBoxModelForm): class CircuitTerminationForm(NetBoxModelForm):
circuit = DynamicModelChoiceField( circuit = DynamicModelChoiceField(
label=_('Circuit'),
queryset=Circuit.objects.all(), queryset=Circuit.objects.all(),
selector=True selector=True
) )
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
selector=True selector=True
) )
provider_network = DynamicModelChoiceField( provider_network = DynamicModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(), queryset=ProviderNetwork.objects.all(),
required=False, required=False,
selector=True selector=True

View File

@ -2,7 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from circuits.choices import * from circuits.choices import *
from dcim.models import CabledObjectModel from dcim.models import CabledObjectModel
@ -34,8 +34,8 @@ class Circuit(PrimaryModel):
""" """
cid = models.CharField( cid = models.CharField(
max_length=100, max_length=100,
verbose_name='Circuit ID', verbose_name=_('circuit ID'),
help_text=_("Unique circuit ID") help_text=_('Unique circuit ID')
) )
provider = models.ForeignKey( provider = models.ForeignKey(
to='circuits.Provider', to='circuits.Provider',
@ -55,6 +55,7 @@ class Circuit(PrimaryModel):
related_name='circuits' related_name='circuits'
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=CircuitStatusChoices, choices=CircuitStatusChoices,
default=CircuitStatusChoices.STATUS_ACTIVE default=CircuitStatusChoices.STATUS_ACTIVE
@ -69,17 +70,17 @@ class Circuit(PrimaryModel):
install_date = models.DateField( install_date = models.DateField(
blank=True, blank=True,
null=True, null=True,
verbose_name='Installed' verbose_name=_('installed')
) )
termination_date = models.DateField( termination_date = models.DateField(
blank=True, blank=True,
null=True, null=True,
verbose_name='Terminates' verbose_name=_('terminates')
) )
commit_rate = models.PositiveIntegerField( commit_rate = models.PositiveIntegerField(
blank=True, blank=True,
null=True, null=True,
verbose_name='Commit rate (Kbps)', verbose_name=_('commit rate (Kbps)'),
help_text=_("Committed rate") help_text=_("Committed rate")
) )
@ -162,7 +163,7 @@ class CircuitTermination(
term_side = models.CharField( term_side = models.CharField(
max_length=1, max_length=1,
choices=CircuitTerminationSideChoices, choices=CircuitTerminationSideChoices,
verbose_name='Termination' verbose_name=_('termination')
) )
site = models.ForeignKey( site = models.ForeignKey(
to='dcim.Site', to='dcim.Site',
@ -179,30 +180,31 @@ class CircuitTermination(
null=True null=True
) )
port_speed = models.PositiveIntegerField( port_speed = models.PositiveIntegerField(
verbose_name='Port speed (Kbps)', verbose_name=_('port speed (Kbps)'),
blank=True, blank=True,
null=True, null=True,
help_text=_("Physical circuit speed") help_text=_('Physical circuit speed')
) )
upstream_speed = models.PositiveIntegerField( upstream_speed = models.PositiveIntegerField(
blank=True, blank=True,
null=True, null=True,
verbose_name='Upstream speed (Kbps)', verbose_name=_('upstream speed (Kbps)'),
help_text=_('Upstream speed, if different from port speed') help_text=_('Upstream speed, if different from port speed')
) )
xconnect_id = models.CharField( xconnect_id = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
verbose_name='Cross-connect ID', verbose_name=_('cross-connect ID'),
help_text=_("ID of the local cross-connect") help_text=_('ID of the local cross-connect')
) )
pp_info = models.CharField( pp_info = models.CharField(
max_length=100, max_length=100,
blank=True, blank=True,
verbose_name='Patch panel/port(s)', verbose_name=_('patch panel/port(s)'),
help_text=_("Patch panel ID and port number(s)") help_text=_('Patch panel ID and port number(s)')
) )
description = models.CharField( description = models.CharField(
verbose_name=_('description'),
max_length=200, max_length=200,
blank=True blank=True
) )

View File

@ -2,7 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from netbox.models import PrimaryModel from netbox.models import PrimaryModel
@ -19,11 +19,13 @@ class Provider(PrimaryModel):
stores information pertinent to the user's relationship with the Provider. stores information pertinent to the user's relationship with the Provider.
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100, max_length=100,
unique=True, unique=True,
help_text=_("Full name of the provider") help_text=_('Full name of the provider')
) )
slug = models.SlugField( slug = models.SlugField(
verbose_name=_('slug'),
max_length=100, max_length=100,
unique=True unique=True
) )
@ -61,9 +63,10 @@ class ProviderAccount(PrimaryModel):
) )
account = models.CharField( account = models.CharField(
max_length=100, max_length=100,
verbose_name='Account ID' verbose_name=_('account ID')
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100, max_length=100,
blank=True blank=True
) )
@ -104,6 +107,7 @@ class ProviderNetwork(PrimaryModel):
unimportant to the user. unimportant to the user.
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100 max_length=100
) )
provider = models.ForeignKey( provider = models.ForeignKey(
@ -114,7 +118,7 @@ class ProviderNetwork(PrimaryModel):
service_id = models.CharField( service_id = models.CharField(
max_length=100, max_length=100,
blank=True, blank=True,
verbose_name='Service ID' verbose_name=_('service ID')
) )
class Meta: class Meta:

View File

@ -41,6 +41,7 @@ def register_backend(name):
class DataBackend: class DataBackend:
parameters = {} parameters = {}
sensitive_parameters = []
def __init__(self, url, **kwargs): def __init__(self, url, **kwargs):
self.url = url self.url = url
@ -86,6 +87,7 @@ class GitBackend(DataBackend):
widget=forms.TextInput(attrs={'class': 'form-control'}) widget=forms.TextInput(attrs={'class': 'form-control'})
) )
} }
sensitive_parameters = ['password']
@contextmanager @contextmanager
def fetch(self): def fetch(self):
@ -135,6 +137,7 @@ class S3Backend(DataBackend):
widget=forms.TextInput(attrs={'class': 'form-control'}) widget=forms.TextInput(attrs={'class': 'form-control'})
), ),
} }
sensitive_parameters = ['aws_secret_access_key']
REGION_REGEX = r's3\.([a-z0-9-]+)\.amazonaws\.com' REGION_REGEX = r's3\.([a-z0-9-]+)\.amazonaws\.com'

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from core.choices import DataSourceTypeChoices from core.choices import DataSourceTypeChoices
from core.models import * from core.models import *
@ -15,6 +15,7 @@ __all__ = (
class DataSourceBulkEditForm(NetBoxModelBulkEditForm): class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
type = forms.ChoiceField( type = forms.ChoiceField(
label=_('Type'),
choices=add_blank_choice(DataSourceTypeChoices), choices=add_blank_choice(DataSourceTypeChoices),
required=False, required=False,
initial='' initial=''
@ -25,16 +26,17 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
label=_('Enforce unique space') label=_('Enforce unique space')
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label=_('Comments')
)
parameters = forms.JSONField( parameters = forms.JSONField(
label=_('Parameters'),
required=False required=False
) )
ignore_rules = forms.CharField( ignore_rules = forms.CharField(
label=_('Ignore rules'),
required=False, required=False,
widget=forms.Textarea() widget=forms.Textarea()
) )

View File

@ -1,7 +1,7 @@
from django import forms from django import forms
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from core.choices import * from core.choices import *
from core.models import * from core.models import *
@ -23,17 +23,20 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm):
model = DataSource model = DataSource
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
('Data Source', ('type', 'status')), (_('Data Source'), ('type', 'status')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'),
choices=DataSourceTypeChoices, choices=DataSourceTypeChoices,
required=False required=False
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
choices=DataSourceStatusChoices, choices=DataSourceStatusChoices,
required=False required=False
) )
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
@ -45,7 +48,7 @@ class DataFileFilterForm(NetBoxModelFilterSetForm):
model = DataFile model = DataFile
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
('File', ('source_id',)), (_('File'), ('source_id',)),
) )
source_id = DynamicModelMultipleChoiceField( source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(), queryset=DataSource.objects.all(),
@ -57,8 +60,8 @@ class DataFileFilterForm(NetBoxModelFilterSetForm):
class JobFilterForm(SavedFiltersMixin, FilterForm): class JobFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
('Attributes', ('object_type', 'status')), (_('Attributes'), ('object_type', 'status')),
('Creation', ( (_('Creation'), (
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before', 'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
'started__after', 'completed__before', 'completed__after', 'user', 'started__after', 'completed__before', 'completed__after', 'user',
)), )),
@ -69,38 +72,47 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
required=False, required=False,
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
choices=JobStatusChoices, choices=JobStatusChoices,
required=False required=False
) )
created__after = forms.DateTimeField( created__after = forms.DateTimeField(
label=_('Created after'),
required=False, required=False,
widget=DateTimePicker() widget=DateTimePicker()
) )
created__before = forms.DateTimeField( created__before = forms.DateTimeField(
label=_('Created before'),
required=False, required=False,
widget=DateTimePicker() widget=DateTimePicker()
) )
scheduled__after = forms.DateTimeField( scheduled__after = forms.DateTimeField(
label=_('Scheduled after'),
required=False, required=False,
widget=DateTimePicker() widget=DateTimePicker()
) )
scheduled__before = forms.DateTimeField( scheduled__before = forms.DateTimeField(
label=_('Scheduled before'),
required=False, required=False,
widget=DateTimePicker() widget=DateTimePicker()
) )
started__after = forms.DateTimeField( started__after = forms.DateTimeField(
label=_('Started after'),
required=False, required=False,
widget=DateTimePicker() widget=DateTimePicker()
) )
started__before = forms.DateTimeField( started__before = forms.DateTimeField(
label=_('Started before'),
required=False, required=False,
widget=DateTimePicker() widget=DateTimePicker()
) )
completed__after = forms.DateTimeField( completed__after = forms.DateTimeField(
label=_('Completed after'),
required=False, required=False,
widget=DateTimePicker() widget=DateTimePicker()
) )
completed__before = forms.DateTimeField( completed__before = forms.DateTimeField(
label=_('Completed before'),
required=False, required=False,
widget=DateTimePicker() widget=DateTimePicker()
) )

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from core.models import DataFile, DataSource from core.models import DataFile, DataSource
from utilities.forms.fields import DynamicModelChoiceField from utilities.forms.fields import DynamicModelChoiceField

View File

@ -1,6 +1,7 @@
import copy import copy
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _
from core.forms.mixins import SyncedDataMixin from core.forms.mixins import SyncedDataMixin
from core.models import * from core.models import *
@ -38,11 +39,11 @@ class DataSourceForm(NetBoxModelForm):
@property @property
def fieldsets(self): def fieldsets(self):
fieldsets = [ fieldsets = [
('Source', ('name', 'type', 'source_url', 'enabled', 'description', 'tags', 'ignore_rules')), (_('Source'), ('name', 'type', 'source_url', 'enabled', 'description', 'tags', 'ignore_rules')),
] ]
if self.backend_fields: if self.backend_fields:
fieldsets.append( fieldsets.append(
('Backend Parameters', self.backend_fields) (_('Backend Parameters'), self.backend_fields)
) )
return fieldsets return fieldsets
@ -79,8 +80,8 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
) )
fieldsets = ( fieldsets = (
('File Upload', ('upload_file',)), (_('File Upload'), ('upload_file',)),
('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')), (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
) )
class Meta: class Meta:

View File

@ -39,10 +39,12 @@ class DataSource(JobsMixin, PrimaryModel):
A remote source, such as a git repository, from which DataFiles are synchronized. A remote source, such as a git repository, from which DataFiles are synchronized.
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100, max_length=100,
unique=True unique=True
) )
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=DataSourceTypeChoices, choices=DataSourceTypeChoices,
default=DataSourceTypeChoices.LOCAL default=DataSourceTypeChoices.LOCAL
@ -52,23 +54,28 @@ class DataSource(JobsMixin, PrimaryModel):
verbose_name=_('URL') verbose_name=_('URL')
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=DataSourceStatusChoices, choices=DataSourceStatusChoices,
default=DataSourceStatusChoices.NEW, default=DataSourceStatusChoices.NEW,
editable=False editable=False
) )
enabled = models.BooleanField( enabled = models.BooleanField(
verbose_name=_('enabled'),
default=True default=True
) )
ignore_rules = models.TextField( ignore_rules = models.TextField(
verbose_name=_('ignore rules'),
blank=True, blank=True,
help_text=_("Patterns (one per line) matching files to ignore when syncing") help_text=_("Patterns (one per line) matching files to ignore when syncing")
) )
parameters = models.JSONField( parameters = models.JSONField(
verbose_name=_('parameters'),
blank=True, blank=True,
null=True null=True
) )
last_synced = models.DateTimeField( last_synced = models.DateTimeField(
verbose_name=_('last synced'),
blank=True, blank=True,
null=True, null=True,
editable=False editable=False
@ -239,9 +246,11 @@ class DataFile(models.Model):
updated, or deleted only by calling DataSource.sync(). updated, or deleted only by calling DataSource.sync().
""" """
created = models.DateTimeField( created = models.DateTimeField(
verbose_name=_('created'),
auto_now_add=True auto_now_add=True
) )
last_updated = models.DateTimeField( last_updated = models.DateTimeField(
verbose_name=_('last updated'),
editable=False editable=False
) )
source = models.ForeignKey( source = models.ForeignKey(
@ -251,20 +260,23 @@ class DataFile(models.Model):
editable=False editable=False
) )
path = models.CharField( path = models.CharField(
verbose_name=_('path'),
max_length=1000, max_length=1000,
editable=False, editable=False,
help_text=_("File path relative to the data source's root") help_text=_("File path relative to the data source's root")
) )
size = models.PositiveIntegerField( size = models.PositiveIntegerField(
editable=False editable=False,
verbose_name=_('size')
) )
hash = models.CharField( hash = models.CharField(
verbose_name=_('hash'),
max_length=64, max_length=64,
editable=False, editable=False,
validators=[ validators=[
RegexValidator(regex='^[0-9a-f]{64}$', message=_("Length must be 64 hexadecimal characters.")) RegexValidator(regex='^[0-9a-f]{64}$', message=_("Length must be 64 hexadecimal characters."))
], ],
help_text=_("SHA256 hash of the file data") help_text=_('SHA256 hash of the file data')
) )
data = models.BinaryField() data = models.BinaryField()

View File

@ -23,20 +23,24 @@ class ManagedFile(SyncedDataMixin, models.Model):
to provide additional functionality. to provide additional functionality.
""" """
created = models.DateTimeField( created = models.DateTimeField(
verbose_name=_('created'),
auto_now_add=True auto_now_add=True
) )
last_updated = models.DateTimeField( last_updated = models.DateTimeField(
verbose_name=_('last updated'),
editable=False, editable=False,
blank=True, blank=True,
null=True null=True
) )
file_root = models.CharField( file_root = models.CharField(
verbose_name=_('file root'),
max_length=1000, max_length=1000,
choices=ManagedFileRootPathChoices choices=ManagedFileRootPathChoices
) )
file_path = models.FilePathField( file_path = models.FilePathField(
verbose_name=_('file path'),
editable=False, editable=False,
help_text=_("File path relative to the designated root path") help_text=_('File path relative to the designated root path')
) )
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()

View File

@ -43,28 +43,34 @@ class Job(models.Model):
for_concrete_model=False for_concrete_model=False
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=200 max_length=200
) )
created = models.DateTimeField( created = models.DateTimeField(
verbose_name=_('created'),
auto_now_add=True auto_now_add=True
) )
scheduled = models.DateTimeField( scheduled = models.DateTimeField(
verbose_name=_('scheduled'),
null=True, null=True,
blank=True blank=True
) )
interval = models.PositiveIntegerField( interval = models.PositiveIntegerField(
verbose_name=_('interval'),
blank=True, blank=True,
null=True, null=True,
validators=( validators=(
MinValueValidator(1), MinValueValidator(1),
), ),
help_text=_("Recurrence interval (in minutes)") help_text=_('Recurrence interval (in minutes)')
) )
started = models.DateTimeField( started = models.DateTimeField(
verbose_name=_('started'),
null=True, null=True,
blank=True blank=True
) )
completed = models.DateTimeField( completed = models.DateTimeField(
verbose_name=_('completed'),
null=True, null=True,
blank=True blank=True
) )
@ -76,15 +82,18 @@ class Job(models.Model):
null=True null=True
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=30, max_length=30,
choices=JobStatusChoices, choices=JobStatusChoices,
default=JobStatusChoices.STATUS_PENDING default=JobStatusChoices.STATUS_PENDING
) )
data = models.JSONField( data = models.JSONField(
verbose_name=_('data'),
null=True, null=True,
blank=True blank=True
) )
job_id = models.UUIDField( job_id = models.UUIDField(
verbose_name=_('job ID'),
unique=True unique=True
) )

View File

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

View File

@ -318,6 +318,10 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h' TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h'
TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h' TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h'
TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h' TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h'
# IEC 60906-1
TYPE_IEC_60906_1 = 'iec-60906-1'
TYPE_NBR_14136_10A = 'nbr-14136-10a'
TYPE_NBR_14136_20A = 'nbr-14136-20a'
# NEMA non-locking # NEMA non-locking
TYPE_NEMA_115P = 'nema-1-15p' TYPE_NEMA_115P = 'nema-1-15p'
TYPE_NEMA_515P = 'nema-5-15p' TYPE_NEMA_515P = 'nema-5-15p'
@ -429,6 +433,11 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_IEC_3PNE6H, '3P+N+E 6H'), (TYPE_IEC_3PNE6H, '3P+N+E 6H'),
(TYPE_IEC_3PNE9H, '3P+N+E 9H'), (TYPE_IEC_3PNE9H, '3P+N+E 9H'),
)), )),
('IEC 60906-1', (
(TYPE_IEC_60906_1, 'IEC 60906-1'),
(TYPE_NBR_14136_10A, '2P+T 10A (NBR 14136)'),
(TYPE_NBR_14136_20A, '2P+T 20A (NBR 14136)'),
)),
('NEMA (Non-locking)', ( ('NEMA (Non-locking)', (
(TYPE_NEMA_115P, 'NEMA 1-15P'), (TYPE_NEMA_115P, 'NEMA 1-15P'),
(TYPE_NEMA_515P, 'NEMA 5-15P'), (TYPE_NEMA_515P, 'NEMA 5-15P'),
@ -553,6 +562,10 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h' TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h'
TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h' TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h'
TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h' TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h'
# IEC 60906-1
TYPE_IEC_60906_1 = 'iec-60906-1'
TYPE_NBR_14136_10A = 'nbr-14136-10a'
TYPE_NBR_14136_20A = 'nbr-14136-20a'
# NEMA non-locking # NEMA non-locking
TYPE_NEMA_115R = 'nema-1-15r' TYPE_NEMA_115R = 'nema-1-15r'
TYPE_NEMA_515R = 'nema-5-15r' TYPE_NEMA_515R = 'nema-5-15r'
@ -657,6 +670,11 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_IEC_3PNE6H, '3P+N+E 6H'), (TYPE_IEC_3PNE6H, '3P+N+E 6H'),
(TYPE_IEC_3PNE9H, '3P+N+E 9H'), (TYPE_IEC_3PNE9H, '3P+N+E 9H'),
)), )),
('IEC 60906-1', (
(TYPE_IEC_60906_1, 'IEC 60906-1'),
(TYPE_NBR_14136_10A, '2P+T 10A (NBR 14136)'),
(TYPE_NBR_14136_20A, '2P+T 20A (NBR 14136)'),
)),
('NEMA (Non-locking)', ( ('NEMA (Non-locking)', (
(TYPE_NEMA_115R, 'NEMA 1-15R'), (TYPE_NEMA_115R, 'NEMA 1-15R'),
(TYPE_NEMA_515R, 'NEMA 5-15R'), (TYPE_NEMA_515R, 'NEMA 5-15R'),
@ -809,6 +827,8 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_100GE_CFP4 = '100gbase-x-cfp4' TYPE_100GE_CFP4 = '100gbase-x-cfp4'
TYPE_100GE_CXP = '100gbase-x-cxp' TYPE_100GE_CXP = '100gbase-x-cxp'
TYPE_100GE_CPAK = '100gbase-x-cpak' TYPE_100GE_CPAK = '100gbase-x-cpak'
TYPE_100GE_DSFP = '100gbase-x-dsfp'
TYPE_100GE_SFP_DD = '100gbase-x-sfpdd'
TYPE_100GE_QSFP28 = '100gbase-x-qsfp28' TYPE_100GE_QSFP28 = '100gbase-x-qsfp28'
TYPE_100GE_QSFP_DD = '100gbase-x-qsfpdd' TYPE_100GE_QSFP_DD = '100gbase-x-qsfpdd'
TYPE_200GE_CFP2 = '200gbase-x-cfp2' TYPE_200GE_CFP2 = '200gbase-x-cfp2'
@ -959,6 +979,8 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_100GE_CFP4, 'CFP4 (100GE)'), (TYPE_100GE_CFP4, 'CFP4 (100GE)'),
(TYPE_100GE_CXP, 'CXP (100GE)'), (TYPE_100GE_CXP, 'CXP (100GE)'),
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'), (TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
(TYPE_100GE_DSFP, 'DSFP (100GE)'),
(TYPE_100GE_SFP_DD, 'SFP-DD (100GE)'),
(TYPE_100GE_QSFP28, 'QSFP28 (100GE)'), (TYPE_100GE_QSFP28, 'QSFP28 (100GE)'),
(TYPE_100GE_QSFP_DD, 'QSFP-DD (100GE)'), (TYPE_100GE_QSFP_DD, 'QSFP-DD (100GE)'),
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'), (TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),

View File

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

View File

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

View File

@ -1,7 +1,7 @@
from django import forms from django import forms
from dcim.models import * from dcim.models import *
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from extras.forms import CustomFieldsMixin from extras.forms import CustomFieldsMixin
from extras.models import Tag from extras.models import Tag
from utilities.forms import BootstrapMixin, form_from_model from utilities.forms import BootstrapMixin, form_from_model
@ -32,10 +32,12 @@ class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentCre
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=100, max_length=100,
required=False required=False
) )
tags = DynamicModelMultipleChoiceField( tags = DynamicModelMultipleChoiceField(
label=_('Tags'),
queryset=Tag.objects.all(), queryset=Tag.objects.all(),
required=False required=False
) )

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.forms.array import SimpleArrayField from django.contrib.postgres.forms.array import SimpleArrayField
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
@ -56,6 +56,7 @@ __all__ = (
class RegionImportForm(NetBoxModelImportForm): class RegionImportForm(NetBoxModelImportForm):
parent = CSVModelChoiceField( parent = CSVModelChoiceField(
label=_('Parent'),
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -69,6 +70,7 @@ class RegionImportForm(NetBoxModelImportForm):
class SiteGroupImportForm(NetBoxModelImportForm): class SiteGroupImportForm(NetBoxModelImportForm):
parent = CSVModelChoiceField( parent = CSVModelChoiceField(
label=_('Parent'),
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -82,22 +84,26 @@ class SiteGroupImportForm(NetBoxModelImportForm):
class SiteImportForm(NetBoxModelImportForm): class SiteImportForm(NetBoxModelImportForm):
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'),
choices=SiteStatusChoices, choices=SiteStatusChoices,
help_text=_('Operational status') help_text=_('Operational status')
) )
region = CSVModelChoiceField( region = CSVModelChoiceField(
label=_('Region'),
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Assigned region') help_text=_('Assigned region')
) )
group = CSVModelChoiceField( group = CSVModelChoiceField(
label=_('Group'),
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Assigned group') help_text=_('Assigned group')
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -119,11 +125,13 @@ class SiteImportForm(NetBoxModelImportForm):
class LocationImportForm(NetBoxModelImportForm): class LocationImportForm(NetBoxModelImportForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Assigned site') help_text=_('Assigned site')
) )
parent = CSVModelChoiceField( parent = CSVModelChoiceField(
label=_('Parent'),
queryset=Location.objects.all(), queryset=Location.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -133,10 +141,12 @@ class LocationImportForm(NetBoxModelImportForm):
} }
) )
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'),
choices=LocationStatusChoices, choices=LocationStatusChoices,
help_text=_('Operational status') help_text=_('Operational status')
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -161,45 +171,54 @@ class RackRoleImportForm(NetBoxModelImportForm):
class RackImportForm(NetBoxModelImportForm): class RackImportForm(NetBoxModelImportForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='name' to_field_name='name'
) )
location = CSVModelChoiceField( location = CSVModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(), queryset=Location.objects.all(),
required=False, required=False,
to_field_name='name' to_field_name='name'
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Name of assigned tenant') help_text=_('Name of assigned tenant')
) )
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'),
choices=RackStatusChoices, choices=RackStatusChoices,
help_text=_('Operational status') help_text=_('Operational status')
) )
role = CSVModelChoiceField( role = CSVModelChoiceField(
label=_('Role'),
queryset=RackRole.objects.all(), queryset=RackRole.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Name of assigned role') help_text=_('Name of assigned role')
) )
type = CSVChoiceField( type = CSVChoiceField(
label=_('Type'),
choices=RackTypeChoices, choices=RackTypeChoices,
required=False, required=False,
help_text=_('Rack type') help_text=_('Rack type')
) )
width = forms.ChoiceField( width = forms.ChoiceField(
label=_('Width'),
choices=RackWidthChoices, choices=RackWidthChoices,
help_text=_('Rail-to-rail width (in inches)') help_text=_('Rail-to-rail width (in inches)')
) )
outer_unit = CSVChoiceField( outer_unit = CSVChoiceField(
label=_('Outer unit'),
choices=RackDimensionUnitChoices, choices=RackDimensionUnitChoices,
required=False, required=False,
help_text=_('Unit for outer dimensions') help_text=_('Unit for outer dimensions')
) )
weight_unit = CSVChoiceField( weight_unit = CSVChoiceField(
label=_('Weight unit'),
choices=WeightUnitChoices, choices=WeightUnitChoices,
required=False, required=False,
help_text=_('Unit for rack weights') help_text=_('Unit for rack weights')
@ -225,27 +244,32 @@ class RackImportForm(NetBoxModelImportForm):
class RackReservationImportForm(NetBoxModelImportForm): class RackReservationImportForm(NetBoxModelImportForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Parent site') help_text=_('Parent site')
) )
location = CSVModelChoiceField( location = CSVModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(), queryset=Location.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_("Rack's location (if any)") help_text=_("Rack's location (if any)")
) )
rack = CSVModelChoiceField( rack = CSVModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Rack') help_text=_('Rack')
) )
units = SimpleArrayField( units = SimpleArrayField(
label=_('Units'),
base_field=forms.IntegerField(), base_field=forms.IntegerField(),
required=True, required=True,
help_text=_('Comma-separated list of individual unit numbers') help_text=_('Comma-separated list of individual unit numbers')
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -282,21 +306,25 @@ class ManufacturerImportForm(NetBoxModelImportForm):
class DeviceTypeImportForm(NetBoxModelImportForm): class DeviceTypeImportForm(NetBoxModelImportForm):
manufacturer = forms.ModelChoiceField( manufacturer = forms.ModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('The manufacturer which produces this device type') help_text=_('The manufacturer which produces this device type')
) )
default_platform = forms.ModelChoiceField( default_platform = forms.ModelChoiceField(
label=_('Default platform'),
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_('The default platform for devices of this type (optional)') help_text=_('The default platform for devices of this type (optional)')
) )
weight = forms.DecimalField( weight = forms.DecimalField(
label=_('Weight'),
required=False, required=False,
help_text=_('Device weight'), help_text=_('Device weight'),
) )
weight_unit = CSVChoiceField( weight_unit = CSVChoiceField(
label=_('Weight unit'),
choices=WeightUnitChoices, choices=WeightUnitChoices,
required=False, required=False,
help_text=_('Unit for device weight') help_text=_('Unit for device weight')
@ -312,14 +340,17 @@ class DeviceTypeImportForm(NetBoxModelImportForm):
class ModuleTypeImportForm(NetBoxModelImportForm): class ModuleTypeImportForm(NetBoxModelImportForm):
manufacturer = forms.ModelChoiceField( manufacturer = forms.ModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
to_field_name='name' to_field_name='name'
) )
weight = forms.DecimalField( weight = forms.DecimalField(
label=_('Weight'),
required=False, required=False,
help_text=_('Module weight'), help_text=_('Module weight'),
) )
weight_unit = CSVChoiceField( weight_unit = CSVChoiceField(
label=_('Weight unit'),
choices=WeightUnitChoices, choices=WeightUnitChoices,
required=False, required=False,
help_text=_('Unit for module weight') help_text=_('Unit for module weight')
@ -332,6 +363,7 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
class DeviceRoleImportForm(NetBoxModelImportForm): class DeviceRoleImportForm(NetBoxModelImportForm):
config_template = CSVModelChoiceField( config_template = CSVModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(), queryset=ConfigTemplate.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
@ -350,12 +382,14 @@ class DeviceRoleImportForm(NetBoxModelImportForm):
class PlatformImportForm(NetBoxModelImportForm): class PlatformImportForm(NetBoxModelImportForm):
slug = SlugField() slug = SlugField()
manufacturer = CSVModelChoiceField( manufacturer = CSVModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Limit platform assignments to this manufacturer') help_text=_('Limit platform assignments to this manufacturer')
) )
config_template = CSVModelChoiceField( config_template = CSVModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(), queryset=ConfigTemplate.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
@ -371,43 +405,51 @@ class PlatformImportForm(NetBoxModelImportForm):
class BaseDeviceImportForm(NetBoxModelImportForm): class BaseDeviceImportForm(NetBoxModelImportForm):
device_role = CSVModelChoiceField( device_role = CSVModelChoiceField(
label=_('Device role'),
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Assigned role') help_text=_('Assigned role')
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Assigned tenant') help_text=_('Assigned tenant')
) )
manufacturer = CSVModelChoiceField( manufacturer = CSVModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Device type manufacturer') help_text=_('Device type manufacturer')
) )
device_type = CSVModelChoiceField( device_type = CSVModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
to_field_name='model', to_field_name='model',
help_text=_('Device type model') help_text=_('Device type model')
) )
platform = CSVModelChoiceField( platform = CSVModelChoiceField(
label=_('Platform'),
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Assigned platform') help_text=_('Assigned platform')
) )
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'),
choices=DeviceStatusChoices, choices=DeviceStatusChoices,
help_text=_('Operational status') help_text=_('Operational status')
) )
virtual_chassis = CSVModelChoiceField( virtual_chassis = CSVModelChoiceField(
label=_('Virtual chassis'),
queryset=VirtualChassis.objects.all(), queryset=VirtualChassis.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_('Virtual chassis') help_text=_('Virtual chassis')
) )
cluster = CSVModelChoiceField( cluster = CSVModelChoiceField(
label=_('Cluster'),
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
@ -430,45 +472,53 @@ class BaseDeviceImportForm(NetBoxModelImportForm):
class DeviceImportForm(BaseDeviceImportForm): class DeviceImportForm(BaseDeviceImportForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Assigned site') help_text=_('Assigned site')
) )
location = CSVModelChoiceField( location = CSVModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(), queryset=Location.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_("Assigned location (if any)") help_text=_("Assigned location (if any)")
) )
rack = CSVModelChoiceField( rack = CSVModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_("Assigned rack (if any)") help_text=_("Assigned rack (if any)")
) )
face = CSVChoiceField( face = CSVChoiceField(
label=_('Face'),
choices=DeviceFaceChoices, choices=DeviceFaceChoices,
required=False, required=False,
help_text=_('Mounted rack face') help_text=_('Mounted rack face')
) )
parent = CSVModelChoiceField( parent = CSVModelChoiceField(
label=_('Parent'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_('Parent device (for child devices)') help_text=_('Parent device (for child devices)')
) )
device_bay = CSVModelChoiceField( device_bay = CSVModelChoiceField(
label=_('Device bay'),
queryset=DeviceBay.objects.all(), queryset=DeviceBay.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_('Device bay in which this device is installed (for child devices)') help_text=_('Device bay in which this device is installed (for child devices)')
) )
airflow = CSVChoiceField( airflow = CSVChoiceField(
label=_('Airflow'),
choices=DeviceAirflowChoices, choices=DeviceAirflowChoices,
required=False, required=False,
help_text=_('Airflow direction') help_text=_('Airflow direction')
) )
config_template = CSVModelChoiceField( config_template = CSVModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(), queryset=ConfigTemplate.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
@ -523,29 +573,35 @@ class DeviceImportForm(BaseDeviceImportForm):
class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm): class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('The device in which this module is installed') help_text=_('The device in which this module is installed')
) )
module_bay = CSVModelChoiceField( module_bay = CSVModelChoiceField(
label=_('Module bay'),
queryset=ModuleBay.objects.all(), queryset=ModuleBay.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('The module bay in which this module is installed') help_text=_('The module bay in which this module is installed')
) )
module_type = CSVModelChoiceField( module_type = CSVModelChoiceField(
label=_('Module type'),
queryset=ModuleType.objects.all(), queryset=ModuleType.objects.all(),
to_field_name='model', to_field_name='model',
help_text=_('The type of module') help_text=_('The type of module')
) )
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'),
choices=ModuleStatusChoices, choices=ModuleStatusChoices,
help_text=_('Operational status') help_text=_('Operational status')
) )
replicate_components = forms.BooleanField( replicate_components = forms.BooleanField(
label=_('Replicate components'),
required=False, required=False,
help_text=_('Automatically populate components associated with this module type (enabled by default)') help_text=_('Automatically populate components associated with this module type (enabled by default)')
) )
adopt_components = forms.BooleanField( adopt_components = forms.BooleanField(
label=_('Adopt components'),
required=False, required=False,
help_text=_('Adopt already existing components') help_text=_('Adopt already existing components')
) )
@ -579,15 +635,18 @@ class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
class ConsolePortImportForm(NetBoxModelImportForm): class ConsolePortImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
) )
type = CSVChoiceField( type = CSVChoiceField(
label=_('Type'),
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
required=False, required=False,
help_text=_('Port type') help_text=_('Port type')
) )
speed = CSVTypedChoiceField( speed = CSVTypedChoiceField(
label=_('Speed'),
choices=ConsolePortSpeedChoices, choices=ConsolePortSpeedChoices,
coerce=int, coerce=int,
empty_value=None, empty_value=None,
@ -602,15 +661,18 @@ class ConsolePortImportForm(NetBoxModelImportForm):
class ConsoleServerPortImportForm(NetBoxModelImportForm): class ConsoleServerPortImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
) )
type = CSVChoiceField( type = CSVChoiceField(
label=_('Type'),
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
required=False, required=False,
help_text=_('Port type') help_text=_('Port type')
) )
speed = CSVTypedChoiceField( speed = CSVTypedChoiceField(
label=_('Speed'),
choices=ConsolePortSpeedChoices, choices=ConsolePortSpeedChoices,
coerce=int, coerce=int,
empty_value=None, empty_value=None,
@ -625,10 +687,12 @@ class ConsoleServerPortImportForm(NetBoxModelImportForm):
class PowerPortImportForm(NetBoxModelImportForm): class PowerPortImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
) )
type = CSVChoiceField( type = CSVChoiceField(
label=_('Type'),
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
required=False, required=False,
help_text=_('Port type') help_text=_('Port type')
@ -643,21 +707,25 @@ class PowerPortImportForm(NetBoxModelImportForm):
class PowerOutletImportForm(NetBoxModelImportForm): class PowerOutletImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
) )
type = CSVChoiceField( type = CSVChoiceField(
label=_('Type'),
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
required=False, required=False,
help_text=_('Outlet type') help_text=_('Outlet type')
) )
power_port = CSVModelChoiceField( power_port = CSVModelChoiceField(
label=_('Power port'),
queryset=PowerPort.objects.all(), queryset=PowerPort.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Local power port which feeds this outlet') help_text=_('Local power port which feeds this outlet')
) )
feed_leg = CSVChoiceField( feed_leg = CSVChoiceField(
label=_('Feed lag'),
choices=PowerOutletFeedLegChoices, choices=PowerOutletFeedLegChoices,
required=False, required=False,
help_text=_('Electrical phase (for three-phase circuits)') help_text=_('Electrical phase (for three-phase circuits)')
@ -692,63 +760,75 @@ class PowerOutletImportForm(NetBoxModelImportForm):
class InterfaceImportForm(NetBoxModelImportForm): class InterfaceImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
) )
parent = CSVModelChoiceField( parent = CSVModelChoiceField(
label=_('Parent'),
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Parent interface') help_text=_('Parent interface')
) )
bridge = CSVModelChoiceField( bridge = CSVModelChoiceField(
label=_('Bridge'),
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Bridged interface') help_text=_('Bridged interface')
) )
lag = CSVModelChoiceField( lag = CSVModelChoiceField(
label=_('Lag'),
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Parent LAG interface') help_text=_('Parent LAG interface')
) )
vdcs = CSVModelMultipleChoiceField( vdcs = CSVModelMultipleChoiceField(
label=_('Vdcs'),
queryset=VirtualDeviceContext.objects.all(), queryset=VirtualDeviceContext.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text='VDC names separated by commas, encased with double quotes (e.g. "vdc1, vdc2, vdc3")' help_text=_('VDC names separated by commas, encased with double quotes (e.g. "vdc1, vdc2, vdc3")')
) )
type = CSVChoiceField( type = CSVChoiceField(
label=_('Type'),
choices=InterfaceTypeChoices, choices=InterfaceTypeChoices,
help_text=_('Physical medium') help_text=_('Physical medium')
) )
duplex = CSVChoiceField( duplex = CSVChoiceField(
label=_('Duplex'),
choices=InterfaceDuplexChoices, choices=InterfaceDuplexChoices,
required=False required=False
) )
poe_mode = CSVChoiceField( poe_mode = CSVChoiceField(
label=_('Poe mode'),
choices=InterfacePoEModeChoices, choices=InterfacePoEModeChoices,
required=False, required=False,
help_text=_('PoE mode') help_text=_('PoE mode')
) )
poe_type = CSVChoiceField( poe_type = CSVChoiceField(
label=_('Poe type'),
choices=InterfacePoETypeChoices, choices=InterfacePoETypeChoices,
required=False, required=False,
help_text=_('PoE type') help_text=_('PoE type')
) )
mode = CSVChoiceField( mode = CSVChoiceField(
label=_('Mode'),
choices=InterfaceModeChoices, choices=InterfaceModeChoices,
required=False, required=False,
help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)') help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)')
) )
vrf = CSVModelChoiceField( vrf = CSVModelChoiceField(
label=_('VRF'),
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
to_field_name='rd', to_field_name='rd',
help_text=_('Assigned VRF') help_text=_('Assigned VRF')
) )
rf_role = CSVChoiceField( rf_role = CSVChoiceField(
label=_('Rf role'),
choices=WirelessRoleChoices, choices=WirelessRoleChoices,
required=False, required=False,
help_text=_('Wireless role (AP/station)') help_text=_('Wireless role (AP/station)')
@ -792,15 +872,18 @@ class InterfaceImportForm(NetBoxModelImportForm):
class FrontPortImportForm(NetBoxModelImportForm): class FrontPortImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
) )
rear_port = CSVModelChoiceField( rear_port = CSVModelChoiceField(
label=_('Rear port'),
queryset=RearPort.objects.all(), queryset=RearPort.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Corresponding rear port') help_text=_('Corresponding rear port')
) )
type = CSVChoiceField( type = CSVChoiceField(
label=_('Type'),
choices=PortTypeChoices, choices=PortTypeChoices,
help_text=_('Physical medium classification') help_text=_('Physical medium classification')
) )
@ -837,10 +920,12 @@ class FrontPortImportForm(NetBoxModelImportForm):
class RearPortImportForm(NetBoxModelImportForm): class RearPortImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
) )
type = CSVChoiceField( type = CSVChoiceField(
label=_('Type'),
help_text=_('Physical medium classification'), help_text=_('Physical medium classification'),
choices=PortTypeChoices, choices=PortTypeChoices,
) )
@ -852,6 +937,7 @@ class RearPortImportForm(NetBoxModelImportForm):
class ModuleBayImportForm(NetBoxModelImportForm): class ModuleBayImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
) )
@ -863,10 +949,12 @@ class ModuleBayImportForm(NetBoxModelImportForm):
class DeviceBayImportForm(NetBoxModelImportForm): class DeviceBayImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
) )
installed_device = CSVModelChoiceField( installed_device = CSVModelChoiceField(
label=_('Installed device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -909,32 +997,38 @@ class DeviceBayImportForm(NetBoxModelImportForm):
class InventoryItemImportForm(NetBoxModelImportForm): class InventoryItemImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name' to_field_name='name'
) )
role = CSVModelChoiceField( role = CSVModelChoiceField(
label=_('Role'),
queryset=InventoryItemRole.objects.all(), queryset=InventoryItemRole.objects.all(),
to_field_name='name', to_field_name='name',
required=False required=False
) )
manufacturer = CSVModelChoiceField( manufacturer = CSVModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
to_field_name='name', to_field_name='name',
required=False required=False
) )
parent = CSVModelChoiceField( parent = CSVModelChoiceField(
label=_('Parent'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_('Parent inventory item') help_text=_('Parent inventory item')
) )
component_type = CSVContentTypeField( component_type = CSVContentTypeField(
label=_('Component type'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=MODULAR_COMPONENT_MODELS, limit_choices_to=MODULAR_COMPONENT_MODELS,
required=False, required=False,
help_text=_('Component Type') help_text=_('Component Type')
) )
component_name = forms.CharField( component_name = forms.CharField(
label=_('Compnent name'),
required=False, required=False,
help_text=_('Component Name') help_text=_('Component Name')
) )
@ -1002,52 +1096,62 @@ class InventoryItemRoleImportForm(NetBoxModelImportForm):
class CableImportForm(NetBoxModelImportForm): class CableImportForm(NetBoxModelImportForm):
# Termination A # Termination A
side_a_device = CSVModelChoiceField( side_a_device = CSVModelChoiceField(
label=_('Side a device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Side A device') help_text=_('Side A device')
) )
side_a_type = CSVContentTypeField( side_a_type = CSVContentTypeField(
label=_('Side a type'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=CABLE_TERMINATION_MODELS, limit_choices_to=CABLE_TERMINATION_MODELS,
help_text=_('Side A type') help_text=_('Side A type')
) )
side_a_name = forms.CharField( side_a_name = forms.CharField(
label=_('Side a name'),
help_text=_('Side A component name') help_text=_('Side A component name')
) )
# Termination B # Termination B
side_b_device = CSVModelChoiceField( side_b_device = CSVModelChoiceField(
label=_('Side b device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Side B device') help_text=_('Side B device')
) )
side_b_type = CSVContentTypeField( side_b_type = CSVContentTypeField(
label=_('Side b type'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=CABLE_TERMINATION_MODELS, limit_choices_to=CABLE_TERMINATION_MODELS,
help_text=_('Side B type') help_text=_('Side B type')
) )
side_b_name = forms.CharField( side_b_name = forms.CharField(
label=_('Side b name'),
help_text=_('Side B component name') help_text=_('Side B component name')
) )
# Cable attributes # Cable attributes
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'),
choices=LinkStatusChoices, choices=LinkStatusChoices,
required=False, required=False,
help_text=_('Connection status') help_text=_('Connection status')
) )
type = CSVChoiceField( type = CSVChoiceField(
label=_('Type'),
choices=CableTypeChoices, choices=CableTypeChoices,
required=False, required=False,
help_text=_('Physical medium classification') help_text=_('Physical medium classification')
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Assigned tenant') help_text=_('Assigned tenant')
) )
length_unit = CSVChoiceField( length_unit = CSVChoiceField(
label=_('Length unit'),
choices=CableLengthUnitChoices, choices=CableLengthUnitChoices,
required=False, required=False,
help_text=_('Length unit') help_text=_('Length unit')
@ -1110,6 +1214,7 @@ class CableImportForm(NetBoxModelImportForm):
class VirtualChassisImportForm(NetBoxModelImportForm): class VirtualChassisImportForm(NetBoxModelImportForm):
master = CSVModelChoiceField( master = CSVModelChoiceField(
label=_('Master'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
@ -1127,11 +1232,13 @@ class VirtualChassisImportForm(NetBoxModelImportForm):
class PowerPanelImportForm(NetBoxModelImportForm): class PowerPanelImportForm(NetBoxModelImportForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Name of parent site') help_text=_('Name of parent site')
) )
location = CSVModelChoiceField( location = CSVModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(), queryset=Location.objects.all(),
required=False, required=False,
to_field_name='name' to_field_name='name'
@ -1153,40 +1260,54 @@ class PowerPanelImportForm(NetBoxModelImportForm):
class PowerFeedImportForm(NetBoxModelImportForm): class PowerFeedImportForm(NetBoxModelImportForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Assigned site') help_text=_('Assigned site')
) )
power_panel = CSVModelChoiceField( power_panel = CSVModelChoiceField(
label=_('Power panel'),
queryset=PowerPanel.objects.all(), queryset=PowerPanel.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Upstream power panel') help_text=_('Upstream power panel')
) )
location = CSVModelChoiceField( location = CSVModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(), queryset=Location.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_("Rack's location (if any)") help_text=_("Rack's location (if any)")
) )
rack = CSVModelChoiceField( rack = CSVModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_('Rack') help_text=_('Rack')
) )
tenant = CSVModelChoiceField(
queryset=Tenant.objects.all(),
to_field_name='name',
required=False,
help_text=_('Assigned tenant')
)
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'),
choices=PowerFeedStatusChoices, choices=PowerFeedStatusChoices,
help_text=_('Operational status') help_text=_('Operational status')
) )
type = CSVChoiceField( type = CSVChoiceField(
label=_('Type'),
choices=PowerFeedTypeChoices, choices=PowerFeedTypeChoices,
help_text=_('Primary or redundant') help_text=_('Primary or redundant')
) )
supply = CSVChoiceField( supply = CSVChoiceField(
label=_('Supply'),
choices=PowerFeedSupplyChoices, choices=PowerFeedSupplyChoices,
help_text=_('Supply type (AC/DC)') help_text=_('Supply type (AC/DC)')
) )
phase = CSVChoiceField( phase = CSVChoiceField(
label=_('Phase'),
choices=PowerFeedPhaseChoices, choices=PowerFeedPhaseChoices,
help_text=_('Single or three-phase') help_text=_('Single or three-phase')
) )
@ -1195,7 +1316,7 @@ class PowerFeedImportForm(NetBoxModelImportForm):
model = PowerFeed model = PowerFeed
fields = ( fields = (
'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
'voltage', 'amperage', 'max_utilization', 'description', 'comments', 'tags', 'voltage', 'amperage', 'max_utilization', 'tenant', 'description', 'comments', 'tags',
) )
def __init__(self, data=None, *args, **kwargs): def __init__(self, data=None, *args, **kwargs):
@ -1222,11 +1343,13 @@ class PowerFeedImportForm(NetBoxModelImportForm):
class VirtualDeviceContextImportForm(NetBoxModelImportForm): class VirtualDeviceContextImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name', to_field_name='name',
help_text='Assigned role' help_text='Assigned role'
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
@ -47,7 +47,7 @@ class InterfaceCommonForm(forms.Form):
# Untagged interfaces cannot be assigned tagged VLANs # Untagged interfaces cannot be assigned tagged VLANs
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans: if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
raise forms.ValidationError({ raise forms.ValidationError({
'mode': "An access interface cannot have tagged VLANs assigned." 'mode': _("An access interface cannot have tagged VLANs assigned.")
}) })
# Remove all tagged VLAN assignments from "tagged all" interfaces # Remove all tagged VLAN assignments from "tagged all" interfaces
@ -61,8 +61,10 @@ class InterfaceCommonForm(forms.Form):
if invalid_vlans: if invalid_vlans:
raise forms.ValidationError({ raise forms.ValidationError({
'tagged_vlans': f"The tagged VLANs ({', '.join(invalid_vlans)}) must belong to the same site as " 'tagged_vlans': _(
f"the interface's parent device/VM, or they must be global" "The tagged VLANs ({vlans}) must belong to the same site as the interface's parent device/VM, "
"or they must be global"
).format(vlans=', '.join(invalid_vlans))
}) })
@ -105,7 +107,7 @@ class ModuleCommonForm(forms.Form):
# Installing modules with placeholders require that the bay has a position value # Installing modules with placeholders require that the bay has a position value
if MODULE_TOKEN in template.name and not module_bay.position: if MODULE_TOKEN in template.name and not module_bay.position:
raise forms.ValidationError( raise forms.ValidationError(
"Cannot install module with placeholder values in a module bay with no position defined" _("Cannot install module with placeholder values in a module bay with no position defined.")
) )
resolved_name = template.name.replace(MODULE_TOKEN, module_bay.position) resolved_name = template.name.replace(MODULE_TOKEN, module_bay.position)
@ -114,12 +116,17 @@ class ModuleCommonForm(forms.Form):
# It is not possible to adopt components already belonging to a module # It is not possible to adopt components already belonging to a module
if adopt_components and existing_item and existing_item.module: if adopt_components and existing_item and existing_item.module:
raise forms.ValidationError( raise forms.ValidationError(
f"Cannot adopt {template.component_model.__name__} '{resolved_name}' as it already belongs " _("Cannot adopt {name} '{resolved_name}' as it already belongs to a module").format(
f"to a module" name=template.component_model.__name__,
resolved_name=resolved_name
)
) )
# If we are not adopting components we error if the component exists # If we are not adopting components we error if the component exists
if not adopt_components and resolved_name in installed_components: if not adopt_components and resolved_name in installed_components:
raise forms.ValidationError( raise forms.ValidationError(
f"{template.component_model.__name__} - {resolved_name} already exists" _("{name} - {resolved_name} already exists").format(
name=template.component_model.__name__,
resolved_name=resolved_name
)
) )

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from circuits.models import Circuit, CircuitTermination from circuits.models import Circuit, CircuitTermination
from dcim.models import * from dcim.models import *

View File

@ -1,6 +1,6 @@
from django import forms from django import forms
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
@ -56,9 +56,11 @@ __all__ = (
class DeviceComponentFilterForm(NetBoxModelFilterSetForm): class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
name = forms.CharField( name = forms.CharField(
label=_('Name'),
required=False required=False
) )
label = forms.CharField( label = forms.CharField(
label=_('Label'),
required=False required=False
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
@ -130,7 +132,7 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Region model = Region
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag', 'parent_id')), (None, ('q', 'filter_id', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')) (_('Contacts'), ('contact', 'contact_role', 'contact_group'))
) )
parent_id = DynamicModelMultipleChoiceField( parent_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -144,7 +146,7 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = SiteGroup model = SiteGroup
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag', 'parent_id')), (None, ('q', 'filter_id', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')) (_('Contacts'), ('contact', 'contact_role', 'contact_group'))
) )
parent_id = DynamicModelMultipleChoiceField( parent_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
@ -158,11 +160,12 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
model = Site model = Site
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')), (_('Attributes'), ('status', 'region_id', 'group_id', 'asn_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')), (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
choices=SiteStatusChoices, choices=SiteStatusChoices,
required=False required=False
) )
@ -188,9 +191,9 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
model = Location model = Location
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')), (_('Attributes'), ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')), (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -221,6 +224,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
label=_('Parent') label=_('Parent')
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
choices=LocationStatusChoices, choices=LocationStatusChoices,
required=False required=False
) )
@ -236,12 +240,12 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
model = Rack model = Rack
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Function', ('status', 'role_id')), (_('Function'), ('status', 'role_id')),
('Hardware', ('type', 'width', 'serial', 'asset_tag')), (_('Hardware'), ('type', 'width', 'serial', 'asset_tag')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')), (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
('Weight', ('weight', 'max_weight', 'weight_unit')), (_('Weight'), ('weight', 'max_weight', 'weight_unit')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -271,14 +275,17 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
label=_('Location') label=_('Location')
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
choices=RackStatusChoices, choices=RackStatusChoices,
required=False required=False
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'),
choices=RackTypeChoices, choices=RackTypeChoices,
required=False required=False
) )
width = forms.MultipleChoiceField( width = forms.MultipleChoiceField(
label=_('Width'),
choices=RackWidthChoices, choices=RackWidthChoices,
required=False required=False
) )
@ -289,21 +296,26 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
label=_('Role') label=_('Role')
) )
serial = forms.CharField( serial = forms.CharField(
label=_('Serial'),
required=False required=False
) )
asset_tag = forms.CharField( asset_tag = forms.CharField(
label=_('Asset tag'),
required=False required=False
) )
tag = TagFilterField(model) tag = TagFilterField(model)
weight = forms.DecimalField( weight = forms.DecimalField(
label=_('Weight'),
required=False, required=False,
min_value=1 min_value=1
) )
max_weight = forms.IntegerField( max_weight = forms.IntegerField(
label=_('Max weight'),
required=False, required=False,
min_value=1 min_value=1
) )
weight_unit = forms.ChoiceField( weight_unit = forms.ChoiceField(
label=_('Weight unit'),
choices=add_blank_choice(WeightUnitChoices), choices=add_blank_choice(WeightUnitChoices),
required=False required=False
) )
@ -312,12 +324,12 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
class RackElevationFilterForm(RackFilterForm): class RackElevationFilterForm(RackFilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'id')),
('Function', ('status', 'role_id')), (_('Function'), ('status', 'role_id')),
('Hardware', ('type', 'width', 'serial', 'asset_tag')), (_('Hardware'), ('type', 'width', 'serial', 'asset_tag')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')), (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
('Weight', ('weight', 'max_weight', 'weight_unit')), (_('Weight'), ('weight', 'max_weight', 'weight_unit')),
) )
id = DynamicModelMultipleChoiceField( id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
@ -334,9 +346,9 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = RackReservation model = RackReservation
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('User', ('user_id',)), (_('User'), ('user_id',)),
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Rack'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -390,7 +402,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Manufacturer model = Manufacturer
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Contacts', ('contact', 'contact_role', 'contact_group')) (_('Contacts'), ('contact', 'contact_role', 'contact_group'))
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -399,13 +411,13 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
model = DeviceType model = DeviceType
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Hardware', ('manufacturer_id', 'default_platform_id', 'part_number', 'subdevice_role', 'airflow')), (_('Hardware'), ('manufacturer_id', 'default_platform_id', 'part_number', 'subdevice_role', 'airflow')),
('Images', ('has_front_image', 'has_rear_image')), (_('Images'), ('has_front_image', 'has_rear_image')),
('Components', ( (_('Components'), (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items', 'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
)), )),
('Weight', ('weight', 'weight_unit')), (_('Weight'), ('weight', 'weight_unit')),
) )
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -418,98 +430,103 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
label=_('Default platform') label=_('Default platform')
) )
part_number = forms.CharField( part_number = forms.CharField(
label=_('Part number'),
required=False required=False
) )
subdevice_role = forms.MultipleChoiceField( subdevice_role = forms.MultipleChoiceField(
label=_('Subdevice role'),
choices=add_blank_choice(SubdeviceRoleChoices), choices=add_blank_choice(SubdeviceRoleChoices),
required=False required=False
) )
airflow = forms.MultipleChoiceField( airflow = forms.MultipleChoiceField(
label=_('Airflow'),
choices=add_blank_choice(DeviceAirflowChoices), choices=add_blank_choice(DeviceAirflowChoices),
required=False required=False
) )
has_front_image = forms.NullBooleanField( has_front_image = forms.NullBooleanField(
required=False, required=False,
label='Has a front image', label=_('Has a front image'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
has_rear_image = forms.NullBooleanField( has_rear_image = forms.NullBooleanField(
required=False, required=False,
label='Has a rear image', label=_('Has a rear image'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
console_ports = forms.NullBooleanField( console_ports = forms.NullBooleanField(
required=False, required=False,
label='Has console ports', label=_('Has console ports'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
console_server_ports = forms.NullBooleanField( console_server_ports = forms.NullBooleanField(
required=False, required=False,
label='Has console server ports', label=_('Has console server ports'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
power_ports = forms.NullBooleanField( power_ports = forms.NullBooleanField(
required=False, required=False,
label='Has power ports', label=_('Has power ports'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
power_outlets = forms.NullBooleanField( power_outlets = forms.NullBooleanField(
required=False, required=False,
label='Has power outlets', label=_('Has power outlets'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
interfaces = forms.NullBooleanField( interfaces = forms.NullBooleanField(
required=False, required=False,
label='Has interfaces', label=_('Has interfaces'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
pass_through_ports = forms.NullBooleanField( pass_through_ports = forms.NullBooleanField(
required=False, required=False,
label='Has pass-through ports', label=_('Has pass-through ports'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
device_bays = forms.NullBooleanField( device_bays = forms.NullBooleanField(
required=False, required=False,
label='Has device bays', label=_('Has device bays'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
module_bays = forms.NullBooleanField( module_bays = forms.NullBooleanField(
required=False, required=False,
label='Has module bays', label=_('Has module bays'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
inventory_items = forms.NullBooleanField( inventory_items = forms.NullBooleanField(
required=False, required=False,
label='Has inventory items', label=_('Has inventory items'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
tag = TagFilterField(model) tag = TagFilterField(model)
weight = forms.DecimalField( weight = forms.DecimalField(
label=_('Weight'),
required=False required=False
) )
weight_unit = forms.ChoiceField( weight_unit = forms.ChoiceField(
label=_('Weight unit'),
choices=add_blank_choice(WeightUnitChoices), choices=add_blank_choice(WeightUnitChoices),
required=False required=False
) )
@ -519,12 +536,12 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
model = ModuleType model = ModuleType
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Hardware', ('manufacturer_id', 'part_number')), (_('Hardware'), ('manufacturer_id', 'part_number')),
('Components', ( (_('Components'), (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', 'pass_through_ports',
)), )),
('Weight', ('weight', 'weight_unit')), (_('Weight'), ('weight', 'weight_unit')),
) )
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -533,55 +550,58 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
fetch_trigger='open' fetch_trigger='open'
) )
part_number = forms.CharField( part_number = forms.CharField(
label=_('Part number'),
required=False required=False
) )
console_ports = forms.NullBooleanField( console_ports = forms.NullBooleanField(
required=False, required=False,
label='Has console ports', label=_('Has console ports'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
console_server_ports = forms.NullBooleanField( console_server_ports = forms.NullBooleanField(
required=False, required=False,
label='Has console server ports', label=_('Has console server ports'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
power_ports = forms.NullBooleanField( power_ports = forms.NullBooleanField(
required=False, required=False,
label='Has power ports', label=_('Has power ports'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
power_outlets = forms.NullBooleanField( power_outlets = forms.NullBooleanField(
required=False, required=False,
label='Has power outlets', label=_('Has power outlets'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
interfaces = forms.NullBooleanField( interfaces = forms.NullBooleanField(
required=False, required=False,
label='Has interfaces', label=_('Has interfaces'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
pass_through_ports = forms.NullBooleanField( pass_through_ports = forms.NullBooleanField(
required=False, required=False,
label='Has pass-through ports', label=_('Has pass-through ports'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
tag = TagFilterField(model) tag = TagFilterField(model)
weight = forms.DecimalField( weight = forms.DecimalField(
label=_('Weight'),
required=False required=False
) )
weight_unit = forms.ChoiceField( weight_unit = forms.ChoiceField(
label=_('Weight unit'),
choices=add_blank_choice(WeightUnitChoices), choices=add_blank_choice(WeightUnitChoices),
required=False required=False
) )
@ -621,15 +641,17 @@ class DeviceFilterForm(
model = Device model = Device
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')), (_('Operation'), ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')), (_('Hardware'), ('manufacturer_id', 'device_type_id', 'platform_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')), (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
('Components', ( (_('Components'), (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports', 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
)), )),
('Miscellaneous', ('has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data')) (_('Miscellaneous'), (
'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
))
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -694,22 +716,26 @@ class DeviceFilterForm(
label=_('Platform') label=_('Platform')
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
choices=DeviceStatusChoices, choices=DeviceStatusChoices,
required=False required=False
) )
airflow = forms.MultipleChoiceField( airflow = forms.MultipleChoiceField(
label=_('Airflow'),
choices=add_blank_choice(DeviceAirflowChoices), choices=add_blank_choice(DeviceAirflowChoices),
required=False required=False
) )
serial = forms.CharField( serial = forms.CharField(
label=_('Serial'),
required=False required=False
) )
asset_tag = forms.CharField( asset_tag = forms.CharField(
label=_('Asset tag'),
required=False required=False
) )
mac_address = forms.CharField( mac_address = forms.CharField(
required=False, required=False,
label='MAC address' label=_('MAC address')
) )
config_template_id = DynamicModelMultipleChoiceField( config_template_id = DynamicModelMultipleChoiceField(
queryset=ConfigTemplate.objects.all(), queryset=ConfigTemplate.objects.all(),
@ -718,7 +744,7 @@ class DeviceFilterForm(
) )
has_primary_ip = forms.NullBooleanField( has_primary_ip = forms.NullBooleanField(
required=False, required=False,
label='Has a primary IP', label=_('Has a primary IP'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
@ -732,49 +758,49 @@ class DeviceFilterForm(
) )
virtual_chassis_member = forms.NullBooleanField( virtual_chassis_member = forms.NullBooleanField(
required=False, required=False,
label='Virtual chassis member', label=_('Virtual chassis member'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
console_ports = forms.NullBooleanField( console_ports = forms.NullBooleanField(
required=False, required=False,
label='Has console ports', label=_('Has console ports'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
console_server_ports = forms.NullBooleanField( console_server_ports = forms.NullBooleanField(
required=False, required=False,
label='Has console server ports', label=_('Has console server ports'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
power_ports = forms.NullBooleanField( power_ports = forms.NullBooleanField(
required=False, required=False,
label='Has power ports', label=_('Has power ports'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
power_outlets = forms.NullBooleanField( power_outlets = forms.NullBooleanField(
required=False, required=False,
label='Has power outlets', label=_('Has power outlets'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
interfaces = forms.NullBooleanField( interfaces = forms.NullBooleanField(
required=False, required=False,
label='Has interfaces', label=_('Has interfaces'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
pass_through_ports = forms.NullBooleanField( pass_through_ports = forms.NullBooleanField(
required=False, required=False,
label='Has pass-through ports', label=_('Has pass-through ports'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
@ -789,8 +815,8 @@ class VirtualDeviceContextFilterForm(
model = VirtualDeviceContext model = VirtualDeviceContext
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('device', 'status', 'has_primary_ip')), (_('Attributes'), ('device', 'status', 'has_primary_ip')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
) )
device = DynamicModelMultipleChoiceField( device = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
@ -799,12 +825,13 @@ class VirtualDeviceContextFilterForm(
fetch_trigger='open' fetch_trigger='open'
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
required=False, required=False,
choices=add_blank_choice(VirtualDeviceContextStatusChoices) choices=add_blank_choice(VirtualDeviceContextStatusChoices)
) )
has_primary_ip = forms.NullBooleanField( has_primary_ip = forms.NullBooleanField(
required=False, required=False,
label='Has a primary IP', label=_('Has a primary IP'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
@ -816,7 +843,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
model = Module model = Module
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Hardware', ('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag')), (_('Hardware'), ('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag')),
) )
manufacturer_id = DynamicModelMultipleChoiceField( manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
@ -834,13 +861,16 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
fetch_trigger='open' fetch_trigger='open'
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
choices=ModuleStatusChoices, choices=ModuleStatusChoices,
required=False required=False
) )
serial = forms.CharField( serial = forms.CharField(
label=_('Serial'),
required=False required=False
) )
asset_tag = forms.CharField( asset_tag = forms.CharField(
label=_('Asset tag'),
required=False required=False
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -850,8 +880,8 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VirtualChassis model = VirtualChassis
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -879,9 +909,9 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Cable model = Cable
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Location', ('site_id', 'location_id', 'rack_id', 'device_id')), (_('Location'), ('site_id', 'location_id', 'rack_id', 'device_id')),
('Attributes', ('type', 'status', 'color', 'length', 'length_unit')), (_('Attributes'), ('type', 'status', 'color', 'length', 'length_unit')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -927,20 +957,25 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
label=_('Device') label=_('Device')
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'),
choices=add_blank_choice(CableTypeChoices), choices=add_blank_choice(CableTypeChoices),
required=False required=False
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
required=False, required=False,
choices=add_blank_choice(LinkStatusChoices) choices=add_blank_choice(LinkStatusChoices)
) )
color = ColorField( color = ColorField(
label=_('Color'),
required=False required=False
) )
length = forms.IntegerField( length = forms.IntegerField(
label=_('Length'),
required=False required=False
) )
length_unit = forms.ChoiceField( length_unit = forms.ChoiceField(
label=_('Length unit'),
choices=add_blank_choice(CableLengthUnitChoices), choices=add_blank_choice(CableLengthUnitChoices),
required=False required=False
) )
@ -951,8 +986,8 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = PowerPanel model = PowerPanel
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')), (_('Contacts'), ('contact', 'contact_role', 'contact_group')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -985,12 +1020,13 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class PowerFeedFilterForm(NetBoxModelFilterSetForm): class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = PowerFeed model = PowerFeed
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
('Attributes', ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
(_('Attributes'), ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -1029,28 +1065,35 @@ class PowerFeedFilterForm(NetBoxModelFilterSetForm):
label=_('Rack') label=_('Rack')
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
choices=PowerFeedStatusChoices, choices=PowerFeedStatusChoices,
required=False required=False
) )
type = forms.ChoiceField( type = forms.ChoiceField(
label=_('Type'),
choices=add_blank_choice(PowerFeedTypeChoices), choices=add_blank_choice(PowerFeedTypeChoices),
required=False required=False
) )
supply = forms.ChoiceField( supply = forms.ChoiceField(
label=_('Supply'),
choices=add_blank_choice(PowerFeedSupplyChoices), choices=add_blank_choice(PowerFeedSupplyChoices),
required=False required=False
) )
phase = forms.ChoiceField( phase = forms.ChoiceField(
label=_('Phase'),
choices=add_blank_choice(PowerFeedPhaseChoices), choices=add_blank_choice(PowerFeedPhaseChoices),
required=False required=False
) )
voltage = forms.IntegerField( voltage = forms.IntegerField(
label=_('Voltage'),
required=False required=False
) )
amperage = forms.IntegerField( amperage = forms.IntegerField(
label=_('Amperage'),
required=False required=False
) )
max_utilization = forms.IntegerField( max_utilization = forms.IntegerField(
label=_('Max utilization'),
required=False required=False
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -1062,12 +1105,14 @@ class PowerFeedFilterForm(NetBoxModelFilterSetForm):
class CabledFilterForm(forms.Form): class CabledFilterForm(forms.Form):
cabled = forms.NullBooleanField( cabled = forms.NullBooleanField(
label=_('Cabled'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
occupied = forms.NullBooleanField( occupied = forms.NullBooleanField(
label=_('Occupied'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
@ -1077,6 +1122,7 @@ class CabledFilterForm(forms.Form):
class PathEndpointFilterForm(CabledFilterForm): class PathEndpointFilterForm(CabledFilterForm):
connected = forms.NullBooleanField( connected = forms.NullBooleanField(
label=_('Connected'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
@ -1088,16 +1134,18 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = ConsolePort model = ConsolePort
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'type', 'speed')), (_('Attributes'), ('name', 'label', 'type', 'speed')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
('Connection', ('cabled', 'connected', 'occupied')), (_('Connection'), ('cabled', 'connected', 'occupied')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'),
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
required=False required=False
) )
speed = forms.MultipleChoiceField( speed = forms.MultipleChoiceField(
label=_('Speed'),
choices=ConsolePortSpeedChoices, choices=ConsolePortSpeedChoices,
required=False required=False
) )
@ -1108,16 +1156,18 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
model = ConsoleServerPort model = ConsoleServerPort
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'type', 'speed')), (_('Attributes'), ('name', 'label', 'type', 'speed')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
('Connection', ('cabled', 'connected', 'occupied')), (_('Connection'), ('cabled', 'connected', 'occupied')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'),
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
required=False required=False
) )
speed = forms.MultipleChoiceField( speed = forms.MultipleChoiceField(
label=_('Speed'),
choices=ConsolePortSpeedChoices, choices=ConsolePortSpeedChoices,
required=False required=False
) )
@ -1128,12 +1178,13 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerPort model = PowerPort
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'type')), (_('Attributes'), ('name', 'label', 'type')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
('Connection', ('cabled', 'connected', 'occupied')), (_('Connection'), ('cabled', 'connected', 'occupied')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'),
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
required=False required=False
) )
@ -1144,12 +1195,13 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerOutlet model = PowerOutlet
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'type')), (_('Attributes'), ('name', 'label', 'type')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
('Connection', ('cabled', 'connected', 'occupied')), (_('Connection'), ('cabled', 'connected', 'occupied')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'),
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
required=False required=False
) )
@ -1160,13 +1212,13 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = Interface model = Interface
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')), (_('Attributes'), ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
('Addressing', ('vrf_id', 'l2vpn_id', 'mac_address', 'wwn')), (_('Addressing'), ('vrf_id', 'l2vpn_id', 'mac_address', 'wwn')),
('PoE', ('poe_mode', 'poe_type')), (_('PoE'), ('poe_mode', 'poe_type')),
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')), (_('Wireless'), ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
('Connection', ('cabled', 'connected', 'occupied')), (_('Connection'), ('cabled', 'connected', 'occupied')),
) )
vdc_id = DynamicModelMultipleChoiceField( vdc_id = DynamicModelMultipleChoiceField(
queryset=VirtualDeviceContext.objects.all(), queryset=VirtualDeviceContext.objects.all(),
@ -1177,30 +1229,36 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
label=_('Virtual Device Context') label=_('Virtual Device Context')
) )
kind = forms.MultipleChoiceField( kind = forms.MultipleChoiceField(
label=_('Kind'),
choices=InterfaceKindChoices, choices=InterfaceKindChoices,
required=False required=False
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'),
choices=InterfaceTypeChoices, choices=InterfaceTypeChoices,
required=False required=False
) )
speed = forms.IntegerField( speed = forms.IntegerField(
label=_('Speed'),
required=False, required=False,
widget=NumberWithOptions( widget=NumberWithOptions(
options=InterfaceSpeedChoices options=InterfaceSpeedChoices
) )
) )
duplex = forms.MultipleChoiceField( duplex = forms.MultipleChoiceField(
label=_('Duplex'),
choices=InterfaceDuplexChoices, choices=InterfaceDuplexChoices,
required=False required=False
) )
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
mgmt_only = forms.NullBooleanField( mgmt_only = forms.NullBooleanField(
label=_('Mgmt only'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
@ -1208,50 +1266,50 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
) )
mac_address = forms.CharField( mac_address = forms.CharField(
required=False, required=False,
label='MAC address' label=_('MAC address')
) )
wwn = forms.CharField( wwn = forms.CharField(
required=False, required=False,
label='WWN' label=_('WWN')
) )
poe_mode = forms.MultipleChoiceField( poe_mode = forms.MultipleChoiceField(
choices=InterfacePoEModeChoices, choices=InterfacePoEModeChoices,
required=False, required=False,
label='PoE mode' label=_('PoE mode')
) )
poe_type = forms.MultipleChoiceField( poe_type = forms.MultipleChoiceField(
choices=InterfacePoETypeChoices, choices=InterfacePoETypeChoices,
required=False, required=False,
label='PoE type' label=_('PoE type')
) )
rf_role = forms.MultipleChoiceField( rf_role = forms.MultipleChoiceField(
choices=WirelessRoleChoices, choices=WirelessRoleChoices,
required=False, required=False,
label='Wireless role' label=_('Wireless role')
) )
rf_channel = forms.MultipleChoiceField( rf_channel = forms.MultipleChoiceField(
choices=WirelessChannelChoices, choices=WirelessChannelChoices,
required=False, required=False,
label='Wireless channel' label=_('Wireless channel')
) )
rf_channel_frequency = forms.IntegerField( rf_channel_frequency = forms.IntegerField(
required=False, required=False,
label='Channel frequency (MHz)' label=_('Channel frequency (MHz)')
) )
rf_channel_width = forms.IntegerField( rf_channel_width = forms.IntegerField(
required=False, required=False,
label='Channel width (MHz)' label=_('Channel width (MHz)')
) )
tx_power = forms.IntegerField( tx_power = forms.IntegerField(
required=False, required=False,
label='Transmit power (dBm)', label=_('Transmit power (dBm)'),
min_value=0, min_value=0,
max_value=127 max_value=127
) )
vrf_id = DynamicModelMultipleChoiceField( vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
label='VRF' label=_('VRF')
) )
l2vpn_id = DynamicModelMultipleChoiceField( l2vpn_id = DynamicModelMultipleChoiceField(
queryset=L2VPN.objects.all(), queryset=L2VPN.objects.all(),
@ -1264,17 +1322,19 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm): class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'type', 'color')), (_('Attributes'), ('name', 'label', 'type', 'color')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
('Cable', ('cabled', 'occupied')), (_('Cable'), ('cabled', 'occupied')),
) )
model = FrontPort model = FrontPort
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'),
choices=PortTypeChoices, choices=PortTypeChoices,
required=False required=False
) )
color = ColorField( color = ColorField(
label=_('Color'),
required=False required=False
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -1284,16 +1344,18 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
model = RearPort model = RearPort
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'type', 'color')), (_('Attributes'), ('name', 'label', 'type', 'color')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
('Cable', ('cabled', 'occupied')), (_('Cable'), ('cabled', 'occupied')),
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
label=_('Type'),
choices=PortTypeChoices, choices=PortTypeChoices,
required=False required=False
) )
color = ColorField( color = ColorField(
label=_('Color'),
required=False required=False
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -1303,12 +1365,13 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
model = ModuleBay model = ModuleBay
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'position')), (_('Attributes'), ('name', 'label', 'position')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
) )
tag = TagFilterField(model) tag = TagFilterField(model)
position = forms.CharField( position = forms.CharField(
label=_('Position'),
required=False required=False
) )
@ -1317,9 +1380,9 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
model = DeviceBay model = DeviceBay
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label')), (_('Attributes'), ('name', 'label')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -1328,9 +1391,9 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
model = InventoryItem model = InventoryItem
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')), (_('Attributes'), ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')), (_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
) )
role_id = DynamicModelMultipleChoiceField( role_id = DynamicModelMultipleChoiceField(
queryset=InventoryItemRole.objects.all(), queryset=InventoryItemRole.objects.all(),
@ -1344,12 +1407,15 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
label=_('Manufacturer') label=_('Manufacturer')
) )
serial = forms.CharField( serial = forms.CharField(
label=_('Serial'),
required=False required=False
) )
asset_tag = forms.CharField( asset_tag = forms.CharField(
label=_('Asset tag'),
required=False required=False
) )
discovered = forms.NullBooleanField( discovered = forms.NullBooleanField(
label=_('Discovered'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES

View File

@ -1,4 +1,5 @@
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _
__all__ = ( __all__ = (
'BaseVCMemberFormSet', 'BaseVCMemberFormSet',
@ -16,6 +17,8 @@ class BaseVCMemberFormSet(forms.BaseModelFormSet):
vc_position = form.cleaned_data.get('vc_position') vc_position = form.cleaned_data.get('vc_position')
if vc_position: if vc_position:
if vc_position in vc_position_list: if vc_position in vc_position_list:
error_msg = f"A virtual chassis member already exists in position {vc_position}." error_msg = _("A virtual chassis member already exists in position {vc_position}.").format(
vc_position=vc_position
)
form.add_error('vc_position', error_msg) form.add_error('vc_position', error_msg)
vc_position_list.append(vc_position) vc_position_list.append(vc_position)

View File

@ -1,7 +1,7 @@
from django import forms from django import forms
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from timezone_field import TimeZoneFormField from timezone_field import TimeZoneFormField
from dcim.choices import * from dcim.choices import *
@ -70,13 +70,14 @@ __all__ = (
class RegionForm(NetBoxModelForm): class RegionForm(NetBoxModelForm):
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False required=False
) )
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
('Region', ( (_('Region'), (
'parent', 'name', 'slug', 'description', 'tags', 'parent', 'name', 'slug', 'description', 'tags',
)), )),
) )
@ -90,13 +91,14 @@ class RegionForm(NetBoxModelForm):
class SiteGroupForm(NetBoxModelForm): class SiteGroupForm(NetBoxModelForm):
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
required=False required=False
) )
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
('Site Group', ( (_('Site Group'), (
'parent', 'name', 'slug', 'description', 'tags', 'parent', 'name', 'slug', 'description', 'tags',
)), )),
) )
@ -110,10 +112,12 @@ class SiteGroupForm(NetBoxModelForm):
class SiteForm(TenancyForm, NetBoxModelForm): class SiteForm(TenancyForm, NetBoxModelForm):
region = DynamicModelChoiceField( region = DynamicModelChoiceField(
label=_('Region'),
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False required=False
) )
group = DynamicModelChoiceField( group = DynamicModelChoiceField(
label=_('Group'),
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
required=False required=False
) )
@ -124,17 +128,18 @@ class SiteForm(TenancyForm, NetBoxModelForm):
) )
slug = SlugField() slug = SlugField()
time_zone = TimeZoneFormField( time_zone = TimeZoneFormField(
label=_('Time zone'),
choices=add_blank_choice(TimeZoneFormField().choices), choices=add_blank_choice(TimeZoneFormField().choices),
required=False required=False
) )
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Site', ( (_('Site'), (
'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags', 'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags',
)), )),
('Tenancy', ('tenant_group', 'tenant')), (_('Tenancy'), ('tenant_group', 'tenant')),
('Contact Info', ('physical_address', 'shipping_address', 'latitude', 'longitude')), (_('Contact Info'), ('physical_address', 'shipping_address', 'latitude', 'longitude')),
) )
class Meta: class Meta:
@ -159,10 +164,12 @@ class SiteForm(TenancyForm, NetBoxModelForm):
class LocationForm(TenancyForm, NetBoxModelForm): class LocationForm(TenancyForm, NetBoxModelForm):
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
selector=True selector=True
) )
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=Location.objects.all(), queryset=Location.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -172,8 +179,8 @@ class LocationForm(TenancyForm, NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
('Location', ('site', 'parent', 'name', 'slug', 'status', 'description', 'tags')), (_('Location'), ('site', 'parent', 'name', 'slug', 'status', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')), (_('Tenancy'), ('tenant_group', 'tenant')),
) )
class Meta: class Meta:
@ -187,7 +194,7 @@ class RackRoleForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
('Rack Role', ( (_('Rack Role'), (
'name', 'slug', 'color', 'description', 'tags', 'name', 'slug', 'color', 'description', 'tags',
)), )),
) )
@ -201,10 +208,12 @@ class RackRoleForm(NetBoxModelForm):
class RackForm(TenancyForm, NetBoxModelForm): class RackForm(TenancyForm, NetBoxModelForm):
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
selector=True selector=True
) )
location = DynamicModelChoiceField( location = DynamicModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(), queryset=Location.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -212,6 +221,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
} }
) )
role = DynamicModelChoiceField( role = DynamicModelChoiceField(
label=_('Role'),
queryset=RackRole.objects.all(), queryset=RackRole.objects.all(),
required=False required=False
) )
@ -228,14 +238,17 @@ class RackForm(TenancyForm, NetBoxModelForm):
class RackReservationForm(TenancyForm, NetBoxModelForm): class RackReservationForm(TenancyForm, NetBoxModelForm):
rack = DynamicModelChoiceField( rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
selector=True selector=True
) )
units = NumericArrayField( units = NumericArrayField(
label=_('Units'),
base_field=forms.IntegerField(), base_field=forms.IntegerField(),
help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.") help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.")
) )
user = forms.ModelChoiceField( user = forms.ModelChoiceField(
label=_('User'),
queryset=get_user_model().objects.order_by( queryset=get_user_model().objects.order_by(
'username' 'username'
) )
@ -243,8 +256,8 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Reservation', ('rack', 'units', 'user', 'description', 'tags')), (_('Reservation'), ('rack', 'units', 'user', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')), (_('Tenancy'), ('tenant_group', 'tenant')),
) )
class Meta: class Meta:
@ -258,7 +271,7 @@ class ManufacturerForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
('Manufacturer', ( (_('Manufacturer'), (
'name', 'slug', 'description', 'tags', 'name', 'slug', 'description', 'tags',
)), )),
) )
@ -272,23 +285,26 @@ class ManufacturerForm(NetBoxModelForm):
class DeviceTypeForm(NetBoxModelForm): class DeviceTypeForm(NetBoxModelForm):
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all() queryset=Manufacturer.objects.all()
) )
default_platform = DynamicModelChoiceField( default_platform = DynamicModelChoiceField(
label=_('Default platform'),
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
required=False required=False
) )
slug = SlugField( slug = SlugField(
label=_('Slug'),
slug_source='model' slug_source='model'
) )
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Device Type', ('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags')), (_('Device Type'), ('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags')),
('Chassis', ( (_('Chassis'), (
'u_height', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'u_height', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
)), )),
('Images', ('front_image', 'rear_image')), (_('Images'), ('front_image', 'rear_image')),
) )
class Meta: class Meta:
@ -310,13 +326,14 @@ class DeviceTypeForm(NetBoxModelForm):
class ModuleTypeForm(NetBoxModelForm): class ModuleTypeForm(NetBoxModelForm):
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all() queryset=Manufacturer.objects.all()
) )
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Module Type', ('manufacturer', 'model', 'part_number', 'description', 'tags')), (_('Module Type'), ('manufacturer', 'model', 'part_number', 'description', 'tags')),
('Weight', ('weight', 'weight_unit')) (_('Weight'), ('weight', 'weight_unit'))
) )
class Meta: class Meta:
@ -328,13 +345,14 @@ class ModuleTypeForm(NetBoxModelForm):
class DeviceRoleForm(NetBoxModelForm): class DeviceRoleForm(NetBoxModelForm):
config_template = DynamicModelChoiceField( config_template = DynamicModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(), queryset=ConfigTemplate.objects.all(),
required=False required=False
) )
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
('Device Role', ( (_('Device Role'), (
'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags',
)), )),
) )
@ -348,19 +366,22 @@ class DeviceRoleForm(NetBoxModelForm):
class PlatformForm(NetBoxModelForm): class PlatformForm(NetBoxModelForm):
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False required=False
) )
config_template = DynamicModelChoiceField( config_template = DynamicModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(), queryset=ConfigTemplate.objects.all(),
required=False required=False
) )
slug = SlugField( slug = SlugField(
label=_('Slug'),
max_length=64 max_length=64
) )
fieldsets = ( fieldsets = (
('Platform', ('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags')), (_('Platform'), ('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags')),
) )
class Meta: class Meta:
@ -372,10 +393,12 @@ class PlatformForm(NetBoxModelForm):
class DeviceForm(TenancyForm, NetBoxModelForm): class DeviceForm(TenancyForm, NetBoxModelForm):
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
selector=True selector=True
) )
location = DynamicModelChoiceField( location = DynamicModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(), queryset=Location.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -386,6 +409,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
} }
) )
rack = DynamicModelChoiceField( rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -394,6 +418,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
} }
) )
position = forms.DecimalField( position = forms.DecimalField(
label=_('Position'),
required=False, required=False,
help_text=_("The lowest-numbered unit occupied by the device"), help_text=_("The lowest-numbered unit occupied by the device"),
widget=APISelect( widget=APISelect(
@ -405,17 +430,21 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
) )
) )
device_type = DynamicModelChoiceField( device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
selector=True selector=True
) )
device_role = DynamicModelChoiceField( device_role = DynamicModelChoiceField(
label=_('Device role'),
queryset=DeviceRole.objects.all() queryset=DeviceRole.objects.all()
) )
platform = DynamicModelChoiceField( platform = DynamicModelChoiceField(
label=_('Platform'),
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
required=False required=False
) )
cluster = DynamicModelChoiceField( cluster = DynamicModelChoiceField(
label=_('Cluster'),
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
required=False, required=False,
selector=True selector=True
@ -426,6 +455,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
label='' label=''
) )
virtual_chassis = DynamicModelChoiceField( virtual_chassis = DynamicModelChoiceField(
label=_('Virtual chassis'),
queryset=VirtualChassis.objects.all(), queryset=VirtualChassis.objects.all(),
required=False, required=False,
selector=True selector=True
@ -441,6 +471,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
help_text=_("The priority of the device in the virtual chassis") help_text=_("The priority of the device in the virtual chassis")
) )
config_template = DynamicModelChoiceField( config_template = DynamicModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(), queryset=ConfigTemplate.objects.all(),
required=False required=False
) )
@ -518,36 +549,41 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
class ModuleForm(ModuleCommonForm, NetBoxModelForm): class ModuleForm(ModuleCommonForm, NetBoxModelForm):
device = DynamicModelChoiceField( device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
initial_params={ initial_params={
'modulebays': '$module_bay' 'modulebays': '$module_bay'
} }
) )
module_bay = DynamicModelChoiceField( module_bay = DynamicModelChoiceField(
label=_('Module bay'),
queryset=ModuleBay.objects.all(), queryset=ModuleBay.objects.all(),
query_params={ query_params={
'device_id': '$device' 'device_id': '$device'
} }
) )
module_type = DynamicModelChoiceField( module_type = DynamicModelChoiceField(
label=_('Module type'),
queryset=ModuleType.objects.all(), queryset=ModuleType.objects.all(),
selector=True selector=True
) )
comments = CommentField() comments = CommentField()
replicate_components = forms.BooleanField( replicate_components = forms.BooleanField(
label=_('Replicate components'),
required=False, required=False,
initial=True, initial=True,
help_text=_("Automatically populate components associated with this module type") help_text=_("Automatically populate components associated with this module type")
) )
adopt_components = forms.BooleanField( adopt_components = forms.BooleanField(
label=_('Adopt components'),
required=False, required=False,
initial=False, initial=False,
help_text=_("Adopt already existing components") help_text=_("Adopt already existing components")
) )
fieldsets = ( fieldsets = (
('Module', ('device', 'module_bay', 'module_type', 'status', 'description', 'tags')), (_('Module'), ('device', 'module_bay', 'module_type', 'status', 'description', 'tags')),
('Hardware', ( (_('Hardware'), (
'serial', 'asset_tag', 'replicate_components', 'adopt_components', 'serial', 'asset_tag', 'replicate_components', 'adopt_components',
)), )),
) )
@ -581,17 +617,19 @@ class CableForm(TenancyForm, NetBoxModelForm):
] ]
error_messages = { error_messages = {
'length': { 'length': {
'max_value': 'Maximum length is 32767 (any unit)' 'max_value': _('Maximum length is 32767 (any unit)')
} }
} }
class PowerPanelForm(NetBoxModelForm): class PowerPanelForm(NetBoxModelForm):
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
selector=True selector=True
) )
location = DynamicModelChoiceField( location = DynamicModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(), queryset=Location.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -611,12 +649,14 @@ class PowerPanelForm(NetBoxModelForm):
] ]
class PowerFeedForm(NetBoxModelForm): class PowerFeedForm(TenancyForm, NetBoxModelForm):
power_panel = DynamicModelChoiceField( power_panel = DynamicModelChoiceField(
label=_('Power panel'),
queryset=PowerPanel.objects.all(), queryset=PowerPanel.objects.all(),
selector=True selector=True
) )
rack = DynamicModelChoiceField( rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
required=False, required=False,
selector=True selector=True
@ -624,15 +664,16 @@ class PowerFeedForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Power Feed', ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')), (_('Power Feed'), ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')), (_('Characteristics'), ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
(_('Tenancy'), ('tenant_group', 'tenant')),
) )
class Meta: class Meta:
model = PowerFeed model = PowerFeed
fields = [ fields = [
'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
'max_utilization', 'description', 'comments', 'tags', 'max_utilization', 'tenant_group', 'tenant', 'description', 'comments', 'tags'
] ]
@ -642,6 +683,7 @@ class PowerFeedForm(NetBoxModelForm):
class VirtualChassisForm(NetBoxModelForm): class VirtualChassisForm(NetBoxModelForm):
master = forms.ModelChoiceField( master = forms.ModelChoiceField(
label=_('Master'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
) )
@ -705,6 +747,7 @@ class DeviceVCMembershipForm(forms.ModelForm):
class VCMemberSelectForm(BootstrapMixin, forms.Form): class VCMemberSelectForm(BootstrapMixin, forms.Form):
device = DynamicModelChoiceField( device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
query_params={ query_params={
'virtual_chassis_id': 'null', 'virtual_chassis_id': 'null',
@ -727,6 +770,7 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
class ComponentTemplateForm(BootstrapMixin, forms.ModelForm): class ComponentTemplateForm(BootstrapMixin, forms.ModelForm):
device_type = DynamicModelChoiceField( device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all() queryset=DeviceType.objects.all()
) )
@ -740,10 +784,12 @@ class ComponentTemplateForm(BootstrapMixin, forms.ModelForm):
class ModularComponentTemplateForm(ComponentTemplateForm): class ModularComponentTemplateForm(ComponentTemplateForm):
device_type = DynamicModelChoiceField( device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all().all(), queryset=DeviceType.objects.all().all(),
required=False required=False
) )
module_type = DynamicModelChoiceField( module_type = DynamicModelChoiceField(
label=_('Module type'),
queryset=ModuleType.objects.all(), queryset=ModuleType.objects.all(),
required=False required=False
) )
@ -796,6 +842,7 @@ class PowerPortTemplateForm(ModularComponentTemplateForm):
class PowerOutletTemplateForm(ModularComponentTemplateForm): class PowerOutletTemplateForm(ModularComponentTemplateForm):
power_port = DynamicModelChoiceField( power_port = DynamicModelChoiceField(
label=_('Power port'),
queryset=PowerPortTemplate.objects.all(), queryset=PowerPortTemplate.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -816,6 +863,7 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm):
class InterfaceTemplateForm(ModularComponentTemplateForm): class InterfaceTemplateForm(ModularComponentTemplateForm):
bridge = DynamicModelChoiceField( bridge = DynamicModelChoiceField(
label=_('Bridge'),
queryset=InterfaceTemplate.objects.all(), queryset=InterfaceTemplate.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -826,8 +874,8 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
fieldsets = ( fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge')), (None, ('device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge')),
('PoE', ('poe_mode', 'poe_type')), (_('PoE'), ('poe_mode', 'poe_type')),
('Wireless', ('rf_role',)) (_('Wireless'), ('rf_role',)),
) )
class Meta: class Meta:
@ -839,6 +887,7 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
class FrontPortTemplateForm(ModularComponentTemplateForm): class FrontPortTemplateForm(ModularComponentTemplateForm):
rear_port = DynamicModelChoiceField( rear_port = DynamicModelChoiceField(
label=_('Rear port'),
queryset=RearPortTemplate.objects.all(), queryset=RearPortTemplate.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -900,6 +949,7 @@ class DeviceBayTemplateForm(ComponentTemplateForm):
class InventoryItemTemplateForm(ComponentTemplateForm): class InventoryItemTemplateForm(ComponentTemplateForm):
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=InventoryItemTemplate.objects.all(), queryset=InventoryItemTemplate.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -907,10 +957,12 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
} }
) )
role = DynamicModelChoiceField( role = DynamicModelChoiceField(
label=_('Role'),
queryset=InventoryItemRole.objects.all(), queryset=InventoryItemRole.objects.all(),
required=False required=False
) )
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False required=False
) )
@ -946,6 +998,7 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
class DeviceComponentForm(NetBoxModelForm): class DeviceComponentForm(NetBoxModelForm):
device = DynamicModelChoiceField( device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
selector=True selector=True
) )
@ -960,6 +1013,7 @@ class DeviceComponentForm(NetBoxModelForm):
class ModularDeviceComponentForm(DeviceComponentForm): class ModularDeviceComponentForm(DeviceComponentForm):
module = DynamicModelChoiceField( module = DynamicModelChoiceField(
label=_('Module'),
queryset=Module.objects.all(), queryset=Module.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -1016,6 +1070,7 @@ class PowerPortForm(ModularDeviceComponentForm):
class PowerOutletForm(ModularDeviceComponentForm): class PowerOutletForm(ModularDeviceComponentForm):
power_port = DynamicModelChoiceField( power_port = DynamicModelChoiceField(
label=_('Power port'),
queryset=PowerPort.objects.all(), queryset=PowerPort.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -1042,7 +1097,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
vdcs = DynamicModelMultipleChoiceField( vdcs = DynamicModelMultipleChoiceField(
queryset=VirtualDeviceContext.objects.all(), queryset=VirtualDeviceContext.objects.all(),
required=False, required=False,
label='Virtual Device Contexts', label=_('Virtual device contexts'),
query_params={ query_params={
'device_id': '$device', 'device_id': '$device',
} }
@ -1120,13 +1175,13 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
) )
fieldsets = ( fieldsets = (
('Interface', ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')), (_('Interface'), ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
('Addressing', ('vrf', 'mac_address', 'wwn')), (_('Addressing'), ('vrf', 'mac_address', 'wwn')),
('Operation', ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')), (_('Operation'), ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
('Related Interfaces', ('parent', 'bridge', 'lag')), (_('Related Interfaces'), ('parent', 'bridge', 'lag')),
('PoE', ('poe_mode', 'poe_type')), (_('PoE'), ('poe_mode', 'poe_type')),
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')), (_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
('Wireless', ( (_('Wireless'), (
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
)), )),
) )
@ -1232,6 +1287,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
class InventoryItemForm(DeviceComponentForm): class InventoryItemForm(DeviceComponentForm):
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=InventoryItem.objects.all(), queryset=InventoryItem.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -1239,10 +1295,12 @@ class InventoryItemForm(DeviceComponentForm):
} }
) )
role = DynamicModelChoiceField( role = DynamicModelChoiceField(
label=_('Role'),
queryset=InventoryItemRole.objects.all(), queryset=InventoryItemRole.objects.all(),
required=False required=False
) )
manufacturer = DynamicModelChoiceField( manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
required=False required=False
) )
@ -1306,8 +1364,8 @@ class InventoryItemForm(DeviceComponentForm):
) )
fieldsets = ( fieldsets = (
('Inventory Item', ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')), (_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')), (_('Hardware'), ('manufacturer', 'part_id', 'serial', 'asset_tag')),
) )
class Meta: class Meta:
@ -1358,7 +1416,7 @@ class InventoryItemForm(DeviceComponentForm):
) if self.cleaned_data[field] ) if self.cleaned_data[field]
] ]
if len(selected_objects) > 1: if len(selected_objects) > 1:
raise forms.ValidationError("An InventoryItem can only be assigned to a single component.") raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component."))
elif selected_objects: elif selected_objects:
self.instance.component = self.cleaned_data[selected_objects[0]] self.instance.component = self.cleaned_data[selected_objects[0]]
else: else:
@ -1372,7 +1430,7 @@ class InventoryItemRoleForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
('Inventory Item Role', ( (_('Inventory Item Role'), (
'name', 'slug', 'color', 'description', 'tags', 'name', 'slug', 'color', 'description', 'tags',
)), )),
) )
@ -1386,12 +1444,13 @@ class InventoryItemRoleForm(NetBoxModelForm):
class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm): class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
device = DynamicModelChoiceField( device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
selector=True selector=True
) )
primary_ip4 = DynamicModelChoiceField( primary_ip4 = DynamicModelChoiceField(
queryset=IPAddress.objects.all(), queryset=IPAddress.objects.all(),
label='Primary IPv4', label=_('Primary IPv4'),
required=False, required=False,
query_params={ query_params={
'device_id': '$device', 'device_id': '$device',
@ -1400,7 +1459,7 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
) )
primary_ip6 = DynamicModelChoiceField( primary_ip6 = DynamicModelChoiceField(
queryset=IPAddress.objects.all(), queryset=IPAddress.objects.all(),
label='Primary IPv6', label=_('Primary IPv6'),
required=False, required=False,
query_params={ query_params={
'device_id': '$device', 'device_id': '$device',
@ -1409,8 +1468,8 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
) )
fieldsets = ( fieldsets = (
('Virtual Device Context', ('device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')), (_('Virtual Device Context'), ('device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')),
('Tenancy', ('tenant_group', 'tenant')) (_('Tenancy'), ('tenant_group', 'tenant'))
) )
class Meta: class Meta:

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from dcim.models import * from dcim.models import *
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
@ -38,8 +38,11 @@ class ComponentCreateForm(forms.Form):
Subclass this form when facilitating the creation of one or more component or component template objects based on Subclass this form when facilitating the creation of one or more component or component template objects based on
a name pattern. a name pattern.
""" """
name = ExpandableNameField() name = ExpandableNameField(
label=_('Name'),
)
label = ExpandableNameField( label = ExpandableNameField(
label=_('Label'),
required=False, required=False,
help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)') help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
) )
@ -57,8 +60,9 @@ class ComponentCreateForm(forms.Form):
value_count = len(self.cleaned_data[field_name]) value_count = len(self.cleaned_data[field_name])
if self.cleaned_data[field_name] and value_count != pattern_count: if self.cleaned_data[field_name] and value_count != pattern_count:
raise forms.ValidationError({ raise forms.ValidationError({
field_name: f'The provided pattern specifies {value_count} values, but {pattern_count} are ' field_name: _(
f'expected.' "The provided pattern specifies {value_count} values, but {pattern_count} are expected."
).format(value_count=value_count, pattern_count=pattern_count)
}, code='label_pattern_mismatch') }, code='label_pattern_mismatch')
@ -222,12 +226,14 @@ class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if 'module' in self.fields: if 'module' in self.fields:
self.fields['name'].help_text += ' The string <code>{module}</code> will be replaced with the position ' \ self.fields['name'].help_text += _(
'of the assigned module, if any' "The string <code>{module}</code> will be replaced with the position of the assigned module, if any."
)
class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm): class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
device = DynamicModelChoiceField( device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
selector=True, selector=True,
widget=APISelect( widget=APISelect(
@ -329,6 +335,7 @@ class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm
class VirtualChassisCreateForm(NetBoxModelForm): class VirtualChassisCreateForm(NetBoxModelForm):
region = DynamicModelChoiceField( region = DynamicModelChoiceField(
label=_('Region'),
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
initial_params={ initial_params={
@ -336,6 +343,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
} }
) )
site_group = DynamicModelChoiceField( site_group = DynamicModelChoiceField(
label=_('Site group'),
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
required=False, required=False,
initial_params={ initial_params={
@ -343,6 +351,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
} }
) )
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -351,6 +360,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
} }
) )
rack = DynamicModelChoiceField( rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
required=False, required=False,
null_option='None', null_option='None',
@ -359,6 +369,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
} }
) )
members = DynamicModelMultipleChoiceField( members = DynamicModelMultipleChoiceField(
label=_('Members'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -367,6 +378,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
} }
) )
initial_position = forms.IntegerField( initial_position = forms.IntegerField(
label=_('Initial position'),
initial=1, initial=1,
required=False, required=False,
help_text=_('Position of the first member device. Increases by one for each additional member.') help_text=_('Position of the first member device. Increases by one for each additional member.')
@ -383,7 +395,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None: if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
raise forms.ValidationError({ raise forms.ValidationError({
'initial_position': "A position must be specified for the first VC member." 'initial_position': _("A position must be specified for the first VC member.")
}) })
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices
from dcim.models import * from dcim.models import *
@ -57,6 +57,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
class PowerOutletTemplateImportForm(ComponentTemplateImportForm): class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
power_port = forms.ModelChoiceField( power_port = forms.ModelChoiceField(
label=_('Power port'),
queryset=PowerPortTemplate.objects.all(), queryset=PowerPortTemplate.objects.all(),
to_field_name='name', to_field_name='name',
required=False required=False
@ -85,6 +86,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
class InterfaceTemplateImportForm(ComponentTemplateImportForm): class InterfaceTemplateImportForm(ComponentTemplateImportForm):
type = forms.ChoiceField( type = forms.ChoiceField(
label=_('Type'),
choices=InterfaceTypeChoices.CHOICES choices=InterfaceTypeChoices.CHOICES
) )
poe_mode = forms.ChoiceField( poe_mode = forms.ChoiceField(
@ -113,9 +115,11 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
class FrontPortTemplateImportForm(ComponentTemplateImportForm): class FrontPortTemplateImportForm(ComponentTemplateImportForm):
type = forms.ChoiceField( type = forms.ChoiceField(
label=_('Type'),
choices=PortTypeChoices.CHOICES choices=PortTypeChoices.CHOICES
) )
rear_port = forms.ModelChoiceField( rear_port = forms.ModelChoiceField(
label=_('Rear port'),
queryset=RearPortTemplate.objects.all(), queryset=RearPortTemplate.objects.all(),
to_field_name='name' to_field_name='name'
) )
@ -143,6 +147,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
class RearPortTemplateImportForm(ComponentTemplateImportForm): class RearPortTemplateImportForm(ComponentTemplateImportForm):
type = forms.ChoiceField( type = forms.ChoiceField(
label=_('Type'),
choices=PortTypeChoices.CHOICES choices=PortTypeChoices.CHOICES
) )
@ -173,15 +178,18 @@ class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
class InventoryItemTemplateImportForm(ComponentTemplateImportForm): class InventoryItemTemplateImportForm(ComponentTemplateImportForm):
parent = forms.ModelChoiceField( parent = forms.ModelChoiceField(
label=_('Parent'),
queryset=InventoryItemTemplate.objects.all(), queryset=InventoryItemTemplate.objects.all(),
required=False required=False
) )
role = forms.ModelChoiceField( role = forms.ModelChoiceField(
label=_('Role'),
queryset=InventoryItemRole.objects.all(), queryset=InventoryItemRole.objects.all(),
to_field_name='name', to_field_name='name',
required=False required=False
) )
manufacturer = forms.ModelChoiceField( manufacturer = forms.ModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
to_field_name='name', to_field_name='name',
required=False required=False

View File

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

View File

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

View File

@ -8,6 +8,7 @@ from django.db import models
from django.db.models import Sum from django.db.models import Sum
from django.dispatch import Signal from django.dispatch import Signal
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
@ -40,11 +41,13 @@ class Cable(PrimaryModel):
A physical connection between two endpoints. A physical connection between two endpoints.
""" """
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=CableTypeChoices, choices=CableTypeChoices,
blank=True blank=True
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=LinkStatusChoices, choices=LinkStatusChoices,
default=LinkStatusChoices.STATUS_CONNECTED default=LinkStatusChoices.STATUS_CONNECTED
@ -57,19 +60,23 @@ class Cable(PrimaryModel):
null=True null=True
) )
label = models.CharField( label = models.CharField(
verbose_name=_('label'),
max_length=100, max_length=100,
blank=True blank=True
) )
color = ColorField( color = ColorField(
verbose_name=_('color'),
blank=True blank=True
) )
length = models.DecimalField( length = models.DecimalField(
verbose_name=_('length'),
max_digits=8, max_digits=8,
decimal_places=2, decimal_places=2,
blank=True, blank=True,
null=True null=True
) )
length_unit = models.CharField( length_unit = models.CharField(
verbose_name=_('length unit'),
max_length=50, max_length=50,
choices=CableLengthUnitChoices, choices=CableLengthUnitChoices,
blank=True, blank=True,
@ -235,7 +242,7 @@ class CableTermination(ChangeLoggedModel):
cable_end = models.CharField( cable_end = models.CharField(
max_length=1, max_length=1,
choices=CableEndChoices, choices=CableEndChoices,
verbose_name='End' verbose_name=_('end')
) )
termination_type = models.ForeignKey( termination_type = models.ForeignKey(
to=ContentType, to=ContentType,
@ -403,15 +410,19 @@ class CablePath(models.Model):
`_nodes` retains a flattened list of all nodes within the path to enable simple filtering. `_nodes` retains a flattened list of all nodes within the path to enable simple filtering.
""" """
path = models.JSONField( path = models.JSONField(
verbose_name=_('path'),
default=list default=list
) )
is_active = models.BooleanField( is_active = models.BooleanField(
verbose_name=_('is active'),
default=False default=False
) )
is_complete = models.BooleanField( is_complete = models.BooleanField(
verbose_name=_('is complete'),
default=False default=False
) )
is_split = models.BooleanField( is_split = models.BooleanField(
verbose_name=_('is split'),
default=False default=False
) )
_nodes = PathField() _nodes = PathField()

View File

@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import * from dcim.choices import *
@ -41,10 +41,11 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
related_name='%(class)ss' related_name='%(class)ss'
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=64, max_length=64,
help_text=""" help_text=_(
{module} is accepted as a substitution for the module bay position when attached to a module type. "{module} is accepted as a substitution for the module bay position when attached to a module type."
""" )
) )
_name = NaturalOrderingField( _name = NaturalOrderingField(
target_field='name', target_field='name',
@ -52,11 +53,13 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
blank=True blank=True
) )
label = models.CharField( label = models.CharField(
verbose_name=_('label'),
max_length=64, max_length=64,
blank=True, blank=True,
help_text=_("Physical label") help_text=_('Physical label')
) )
description = models.CharField( description = models.CharField(
verbose_name=_('description'),
max_length=200, max_length=200,
blank=True blank=True
) )
@ -98,7 +101,7 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
if self.pk is not None and self._original_device_type != self.device_type_id: if self.pk is not None and self._original_device_type != self.device_type_id:
raise ValidationError({ raise ValidationError({
"device_type": "Component templates cannot be moved to a different device type." "device_type": _("Component templates cannot be moved to a different device type.")
}) })
@ -149,11 +152,11 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
# A component template must belong to a DeviceType *or* to a ModuleType # A component template must belong to a DeviceType *or* to a ModuleType
if self.device_type and self.module_type: if self.device_type and self.module_type:
raise ValidationError( raise ValidationError(
"A component template cannot be associated with both a device type and a module type." _("A component template cannot be associated with both a device type and a module type.")
) )
if not self.device_type and not self.module_type: if not self.device_type and not self.module_type:
raise ValidationError( raise ValidationError(
"A component template must be associated with either a device type or a module type." _("A component template must be associated with either a device type or a module type.")
) )
def resolve_name(self, module): def resolve_name(self, module):
@ -172,6 +175,7 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
A template for a ConsolePort to be created for a new Device. A template for a ConsolePort to be created for a new Device.
""" """
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
blank=True blank=True
@ -201,6 +205,7 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
A template for a ConsoleServerPort to be created for a new Device. A template for a ConsoleServerPort to be created for a new Device.
""" """
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
blank=True blank=True
@ -231,21 +236,24 @@ class PowerPortTemplate(ModularComponentTemplateModel):
A template for a PowerPort to be created for a new Device. A template for a PowerPort to be created for a new Device.
""" """
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
blank=True blank=True
) )
maximum_draw = models.PositiveIntegerField( maximum_draw = models.PositiveIntegerField(
verbose_name=_('maximum draw'),
blank=True, blank=True,
null=True, null=True,
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
help_text=_("Maximum power draw (watts)") help_text=_('Maximum power draw (watts)')
) )
allocated_draw = models.PositiveIntegerField( allocated_draw = models.PositiveIntegerField(
verbose_name=_('allocated draw'),
blank=True, blank=True,
null=True, null=True,
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
help_text=_("Allocated power draw (watts)") help_text=_('Allocated power draw (watts)')
) )
component_model = PowerPort component_model = PowerPort
@ -267,7 +275,7 @@ class PowerPortTemplate(ModularComponentTemplateModel):
if self.maximum_draw is not None and self.allocated_draw is not None: if self.maximum_draw is not None and self.allocated_draw is not None:
if self.allocated_draw > self.maximum_draw: if self.allocated_draw > self.maximum_draw:
raise ValidationError({ raise ValidationError({
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)." 'allocated_draw': _("Allocated draw cannot exceed the maximum draw ({maximum_draw}W).").format(maximum_draw=self.maximum_draw)
}) })
def to_yaml(self): def to_yaml(self):
@ -286,6 +294,7 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
A template for a PowerOutlet to be created for a new Device. A template for a PowerOutlet to be created for a new Device.
""" """
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
blank=True blank=True
@ -298,10 +307,11 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
related_name='poweroutlet_templates' related_name='poweroutlet_templates'
) )
feed_leg = models.CharField( feed_leg = models.CharField(
verbose_name=_('feed leg'),
max_length=50, max_length=50,
choices=PowerOutletFeedLegChoices, choices=PowerOutletFeedLegChoices,
blank=True, blank=True,
help_text=_("Phase (for three-phase feeds)") help_text=_('Phase (for three-phase feeds)')
) )
component_model = PowerOutlet component_model = PowerOutlet
@ -313,11 +323,11 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
if self.power_port: if self.power_port:
if self.device_type and self.power_port.device_type != self.device_type: if self.device_type and self.power_port.device_type != self.device_type:
raise ValidationError( raise ValidationError(
f"Parent power port ({self.power_port}) must belong to the same device type" _("Parent power port ({power_port}) must belong to the same device type").format(power_port=self.power_port)
) )
if self.module_type and self.power_port.module_type != self.module_type: if self.module_type and self.power_port.module_type != self.module_type:
raise ValidationError( raise ValidationError(
f"Parent power port ({self.power_port}) must belong to the same module type" _("Parent power port ({power_port}) must belong to the same module type").format(power_port=self.power_port)
) )
def instantiate(self, **kwargs): def instantiate(self, **kwargs):
@ -359,15 +369,17 @@ class InterfaceTemplate(ModularComponentTemplateModel):
blank=True blank=True
) )
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=InterfaceTypeChoices choices=InterfaceTypeChoices
) )
enabled = models.BooleanField( enabled = models.BooleanField(
verbose_name=_('enabled'),
default=True default=True
) )
mgmt_only = models.BooleanField( mgmt_only = models.BooleanField(
default=False, default=False,
verbose_name='Management only' verbose_name=_('management only')
) )
bridge = models.ForeignKey( bridge = models.ForeignKey(
to='self', to='self',
@ -375,25 +387,25 @@ class InterfaceTemplate(ModularComponentTemplateModel):
related_name='bridge_interfaces', related_name='bridge_interfaces',
null=True, null=True,
blank=True, blank=True,
verbose_name='Bridge interface' verbose_name=_('bridge interface')
) )
poe_mode = models.CharField( poe_mode = models.CharField(
max_length=50, max_length=50,
choices=InterfacePoEModeChoices, choices=InterfacePoEModeChoices,
blank=True, blank=True,
verbose_name='PoE mode' verbose_name=_('PoE mode')
) )
poe_type = models.CharField( poe_type = models.CharField(
max_length=50, max_length=50,
choices=InterfacePoETypeChoices, choices=InterfacePoETypeChoices,
blank=True, blank=True,
verbose_name='PoE type' verbose_name=_('PoE type')
) )
rf_role = models.CharField( rf_role = models.CharField(
max_length=30, max_length=30,
choices=WirelessRoleChoices, choices=WirelessRoleChoices,
blank=True, blank=True,
verbose_name='Wireless role' verbose_name=_('wireless role')
) )
component_model = Interface component_model = Interface
@ -403,14 +415,14 @@ class InterfaceTemplate(ModularComponentTemplateModel):
if self.bridge: if self.bridge:
if self.pk and self.bridge_id == self.pk: if self.pk and self.bridge_id == self.pk:
raise ValidationError({'bridge': "An interface cannot be bridged to itself."}) raise ValidationError({'bridge': _("An interface cannot be bridged to itself.")})
if self.device_type and self.device_type != self.bridge.device_type: if self.device_type and self.device_type != self.bridge.device_type:
raise ValidationError({ raise ValidationError({
'bridge': f"Bridge interface ({self.bridge}) must belong to the same device type" 'bridge': _("Bridge interface ({bridge}) must belong to the same device type").format(bridge=self.bridge)
}) })
if self.module_type and self.module_type != self.bridge.module_type: if self.module_type and self.module_type != self.bridge.module_type:
raise ValidationError({ raise ValidationError({
'bridge': f"Bridge interface ({self.bridge}) must belong to the same module type" 'bridge': _("Bridge interface ({bridge}) must belong to the same module type").format(bridge=self.bridge)
}) })
if self.rf_role and self.type not in WIRELESS_IFACE_TYPES: if self.rf_role and self.type not in WIRELESS_IFACE_TYPES:
@ -452,10 +464,12 @@ class FrontPortTemplate(ModularComponentTemplateModel):
Template for a pass-through port on the front of a new Device. Template for a pass-through port on the front of a new Device.
""" """
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=PortTypeChoices choices=PortTypeChoices
) )
color = ColorField( color = ColorField(
verbose_name=_('color'),
blank=True blank=True
) )
rear_port = models.ForeignKey( rear_port = models.ForeignKey(
@ -464,6 +478,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
related_name='frontport_templates' related_name='frontport_templates'
) )
rear_port_position = models.PositiveSmallIntegerField( rear_port_position = models.PositiveSmallIntegerField(
verbose_name=_('rear port position'),
default=1, default=1,
validators=[ validators=[
MinValueValidator(REARPORT_POSITIONS_MIN), MinValueValidator(REARPORT_POSITIONS_MIN),
@ -497,13 +512,13 @@ class FrontPortTemplate(ModularComponentTemplateModel):
# Validate rear port assignment # Validate rear port assignment
if self.rear_port.device_type != self.device_type: if self.rear_port.device_type != self.device_type:
raise ValidationError( raise ValidationError(
"Rear port ({}) must belong to the same device type".format(self.rear_port) _("Rear port ({}) must belong to the same device type").format(self.rear_port)
) )
# Validate rear port position assignment # Validate rear port position assignment
if self.rear_port_position > self.rear_port.positions: if self.rear_port_position > self.rear_port.positions:
raise ValidationError( raise ValidationError(
"Invalid rear port position ({}); rear port {} has only {} positions".format( _("Invalid rear port position ({}); rear port {} has only {} positions").format(
self.rear_port_position, self.rear_port.name, self.rear_port.positions self.rear_port_position, self.rear_port.name, self.rear_port.positions
) )
) )
@ -545,13 +560,16 @@ class RearPortTemplate(ModularComponentTemplateModel):
Template for a pass-through port on the rear of a new Device. Template for a pass-through port on the rear of a new Device.
""" """
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=PortTypeChoices choices=PortTypeChoices
) )
color = ColorField( color = ColorField(
verbose_name=_('color'),
blank=True blank=True
) )
positions = models.PositiveSmallIntegerField( positions = models.PositiveSmallIntegerField(
verbose_name=_('positions'),
default=1, default=1,
validators=[ validators=[
MinValueValidator(REARPORT_POSITIONS_MIN), MinValueValidator(REARPORT_POSITIONS_MIN),
@ -588,6 +606,7 @@ class ModuleBayTemplate(ComponentTemplateModel):
A template for a ModuleBay to be created for a new parent Device. A template for a ModuleBay to be created for a new parent Device.
""" """
position = models.CharField( position = models.CharField(
verbose_name=_('position'),
max_length=30, max_length=30,
blank=True, blank=True,
help_text=_('Identifier to reference when renaming installed components') help_text=_('Identifier to reference when renaming installed components')
@ -630,7 +649,7 @@ class DeviceBayTemplate(ComponentTemplateModel):
def clean(self): def clean(self):
if self.device_type and self.device_type.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT: if self.device_type and self.device_type.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT:
raise ValidationError( raise ValidationError(
f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays." _("Subdevice role of device type ({device_type}) must be set to \"parent\" to allow device bays.").format(device_type=self.device_type)
) )
def to_yaml(self): def to_yaml(self):
@ -685,7 +704,7 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
) )
part_id = models.CharField( part_id = models.CharField(
max_length=50, max_length=50,
verbose_name='Part ID', verbose_name=_('part ID'),
blank=True, blank=True,
help_text=_('Manufacturer-assigned part identifier') help_text=_('Manufacturer-assigned part identifier')
) )

View File

@ -7,7 +7,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Sum from django.db.models import Sum
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import * from dcim.choices import *
@ -52,6 +52,7 @@ class ComponentModel(NetBoxModel):
related_name='%(class)ss' related_name='%(class)ss'
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=64 max_length=64
) )
_name = NaturalOrderingField( _name = NaturalOrderingField(
@ -60,11 +61,13 @@ class ComponentModel(NetBoxModel):
blank=True blank=True
) )
label = models.CharField( label = models.CharField(
verbose_name=_('label'),
max_length=64, max_length=64,
blank=True, blank=True,
help_text=_("Physical label") help_text=_('Physical label')
) )
description = models.CharField( description = models.CharField(
verbose_name=_('description'),
max_length=200, max_length=200,
blank=True blank=True
) )
@ -101,7 +104,7 @@ class ComponentModel(NetBoxModel):
# Check list of Modules that allow device field to be changed # Check list of Modules that allow device field to be changed
if (type(self) not in [InventoryItem]) and (self.pk is not None) and (self._original_device != self.device_id): if (type(self) not in [InventoryItem]) and (self.pk is not None) and (self._original_device != self.device_id):
raise ValidationError({ raise ValidationError({
"device": "Components cannot be moved to a different device." "device": _("Components cannot be moved to a different device.")
}) })
@property @property
@ -140,13 +143,15 @@ class CabledObjectModel(models.Model):
null=True null=True
) )
cable_end = models.CharField( cable_end = models.CharField(
verbose_name=_('cable end'),
max_length=1, max_length=1,
blank=True, blank=True,
choices=CableEndChoices choices=CableEndChoices
) )
mark_connected = models.BooleanField( mark_connected = models.BooleanField(
verbose_name=_('mark connected'),
default=False, default=False,
help_text=_("Treat as if a cable is connected") help_text=_('Treat as if a cable is connected')
) )
cable_terminations = GenericRelation( cable_terminations = GenericRelation(
@ -164,15 +169,15 @@ class CabledObjectModel(models.Model):
if self.cable and not self.cable_end: if self.cable and not self.cable_end:
raise ValidationError({ raise ValidationError({
"cable_end": "Must specify cable end (A or B) when attaching a cable." "cable_end": _("Must specify cable end (A or B) when attaching a cable.")
}) })
if self.cable_end and not self.cable: if self.cable_end and not self.cable:
raise ValidationError({ raise ValidationError({
"cable_end": "Cable end must not be set without a cable." "cable_end": _("Cable end must not be set without a cable.")
}) })
if self.mark_connected and self.cable: if self.mark_connected and self.cable:
raise ValidationError({ raise ValidationError({
"mark_connected": "Cannot mark as connected with a cable attached." "mark_connected": _("Cannot mark as connected with a cable attached.")
}) })
@property @property
@ -195,7 +200,9 @@ class CabledObjectModel(models.Model):
@property @property
def parent_object(self): def parent_object(self):
raise NotImplementedError(f"{self.__class__.__name__} models must declare a parent_object property") raise NotImplementedError(
_("{class_name} models must declare a parent_object property").format(class_name=self.__class__.__name__)
)
@property @property
def opposite_cable_end(self): def opposite_cable_end(self):
@ -275,12 +282,14 @@ class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
""" """
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
blank=True, blank=True,
help_text=_('Physical port type') help_text=_('Physical port type')
) )
speed = models.PositiveIntegerField( speed = models.PositiveIntegerField(
verbose_name=_('speed'),
choices=ConsolePortSpeedChoices, choices=ConsolePortSpeedChoices,
blank=True, blank=True,
null=True, null=True,
@ -298,12 +307,14 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint,
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
""" """
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
blank=True, blank=True,
help_text=_('Physical port type') help_text=_('Physical port type')
) )
speed = models.PositiveIntegerField( speed = models.PositiveIntegerField(
verbose_name=_('speed'),
choices=ConsolePortSpeedChoices, choices=ConsolePortSpeedChoices,
blank=True, blank=True,
null=True, null=True,
@ -325,22 +336,25 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracking
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
""" """
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
blank=True, blank=True,
help_text=_('Physical port type') help_text=_('Physical port type')
) )
maximum_draw = models.PositiveIntegerField( maximum_draw = models.PositiveIntegerField(
verbose_name=_('maximum draw'),
blank=True, blank=True,
null=True, null=True,
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
help_text=_("Maximum power draw (watts)") help_text=_("Maximum power draw (watts)")
) )
allocated_draw = models.PositiveIntegerField( allocated_draw = models.PositiveIntegerField(
verbose_name=_('allocated draw'),
blank=True, blank=True,
null=True, null=True,
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
help_text=_("Allocated power draw (watts)") help_text=_('Allocated power draw (watts)')
) )
clone_fields = ('device', 'module', 'maximum_draw', 'allocated_draw') clone_fields = ('device', 'module', 'maximum_draw', 'allocated_draw')
@ -354,7 +368,9 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracking
if self.maximum_draw is not None and self.allocated_draw is not None: if self.maximum_draw is not None and self.allocated_draw is not None:
if self.allocated_draw > self.maximum_draw: if self.allocated_draw > self.maximum_draw:
raise ValidationError({ raise ValidationError({
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)." 'allocated_draw': _(
"Allocated draw cannot exceed the maximum draw ({maximum_draw}W)."
).format(maximum_draw=self.maximum_draw)
}) })
def get_downstream_powerports(self, leg=None): def get_downstream_powerports(self, leg=None):
@ -434,6 +450,7 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
A physical power outlet (output) within a Device which provides power to a PowerPort. A physical power outlet (output) within a Device which provides power to a PowerPort.
""" """
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
blank=True, blank=True,
@ -447,10 +464,11 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
related_name='poweroutlets' related_name='poweroutlets'
) )
feed_leg = models.CharField( feed_leg = models.CharField(
verbose_name=_('feed leg'),
max_length=50, max_length=50,
choices=PowerOutletFeedLegChoices, choices=PowerOutletFeedLegChoices,
blank=True, blank=True,
help_text=_("Phase (for three-phase feeds)") help_text=_('Phase (for three-phase feeds)')
) )
clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg') clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg')
@ -463,7 +481,9 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
# Validate power port assignment # Validate power port assignment
if self.power_port and self.power_port.device != self.device: if self.power_port and self.power_port.device != self.device:
raise ValidationError(f"Parent power port ({self.power_port}) must belong to the same device") raise ValidationError(
_("Parent power port ({power_port}) must belong to the same device").format(power_port=self.power_port)
)
# #
@ -475,12 +495,13 @@ class BaseInterface(models.Model):
Abstract base class for fields shared by dcim.Interface and virtualization.VMInterface. Abstract base class for fields shared by dcim.Interface and virtualization.VMInterface.
""" """
enabled = models.BooleanField( enabled = models.BooleanField(
verbose_name=_('enabled'),
default=True default=True
) )
mac_address = MACAddressField( mac_address = MACAddressField(
null=True, null=True,
blank=True, blank=True,
verbose_name='MAC Address' verbose_name=_('MAC address')
) )
mtu = models.PositiveIntegerField( mtu = models.PositiveIntegerField(
blank=True, blank=True,
@ -489,13 +510,14 @@ class BaseInterface(models.Model):
MinValueValidator(INTERFACE_MTU_MIN), MinValueValidator(INTERFACE_MTU_MIN),
MaxValueValidator(INTERFACE_MTU_MAX) MaxValueValidator(INTERFACE_MTU_MAX)
], ],
verbose_name='MTU' verbose_name=_('MTU')
) )
mode = models.CharField( mode = models.CharField(
verbose_name=_('mode'),
max_length=50, max_length=50,
choices=InterfaceModeChoices, choices=InterfaceModeChoices,
blank=True, blank=True,
help_text=_("IEEE 802.1Q tagging strategy") help_text=_('IEEE 802.1Q tagging strategy')
) )
parent = models.ForeignKey( parent = models.ForeignKey(
to='self', to='self',
@ -503,7 +525,7 @@ class BaseInterface(models.Model):
related_name='child_interfaces', related_name='child_interfaces',
null=True, null=True,
blank=True, blank=True,
verbose_name='Parent interface' verbose_name=_('parent interface')
) )
bridge = models.ForeignKey( bridge = models.ForeignKey(
to='self', to='self',
@ -511,7 +533,7 @@ class BaseInterface(models.Model):
related_name='bridge_interfaces', related_name='bridge_interfaces',
null=True, null=True,
blank=True, blank=True,
verbose_name='Bridge interface' verbose_name=_('bridge interface')
) )
class Meta: class Meta:
@ -559,23 +581,25 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
related_name='member_interfaces', related_name='member_interfaces',
null=True, null=True,
blank=True, blank=True,
verbose_name='Parent LAG' verbose_name=_('parent LAG')
) )
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=InterfaceTypeChoices choices=InterfaceTypeChoices
) )
mgmt_only = models.BooleanField( mgmt_only = models.BooleanField(
default=False, default=False,
verbose_name='Management only', verbose_name=_('management only'),
help_text=_('This interface is used only for out-of-band management') help_text=_('This interface is used only for out-of-band management')
) )
speed = models.PositiveIntegerField( speed = models.PositiveIntegerField(
blank=True, blank=True,
null=True, null=True,
verbose_name='Speed (Kbps)' verbose_name=_('speed (Kbps)')
) )
duplex = models.CharField( duplex = models.CharField(
verbose_name=_('duplex'),
max_length=50, max_length=50,
blank=True, blank=True,
null=True, null=True,
@ -584,27 +608,27 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
wwn = WWNField( wwn = WWNField(
null=True, null=True,
blank=True, blank=True,
verbose_name='WWN', verbose_name=_('WWN'),
help_text=_('64-bit World Wide Name') help_text=_('64-bit World Wide Name')
) )
rf_role = models.CharField( rf_role = models.CharField(
max_length=30, max_length=30,
choices=WirelessRoleChoices, choices=WirelessRoleChoices,
blank=True, blank=True,
verbose_name='Wireless role' verbose_name=_('wireless role')
) )
rf_channel = models.CharField( rf_channel = models.CharField(
max_length=50, max_length=50,
choices=WirelessChannelChoices, choices=WirelessChannelChoices,
blank=True, blank=True,
verbose_name='Wireless channel' verbose_name=_('wireless channel')
) )
rf_channel_frequency = models.DecimalField( rf_channel_frequency = models.DecimalField(
max_digits=7, max_digits=7,
decimal_places=2, decimal_places=2,
blank=True, blank=True,
null=True, null=True,
verbose_name='Channel frequency (MHz)', verbose_name=_('channel frequency (MHz)'),
help_text=_("Populated by selected channel (if set)") help_text=_("Populated by selected channel (if set)")
) )
rf_channel_width = models.DecimalField( rf_channel_width = models.DecimalField(
@ -612,26 +636,26 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
decimal_places=3, decimal_places=3,
blank=True, blank=True,
null=True, null=True,
verbose_name='Channel width (MHz)', verbose_name=('channel width (MHz)'),
help_text=_("Populated by selected channel (if set)") help_text=_("Populated by selected channel (if set)")
) )
tx_power = models.PositiveSmallIntegerField( tx_power = models.PositiveSmallIntegerField(
blank=True, blank=True,
null=True, null=True,
validators=(MaxValueValidator(127),), validators=(MaxValueValidator(127),),
verbose_name='Transmit power (dBm)' verbose_name=_('transmit power (dBm)')
) )
poe_mode = models.CharField( poe_mode = models.CharField(
max_length=50, max_length=50,
choices=InterfacePoEModeChoices, choices=InterfacePoEModeChoices,
blank=True, blank=True,
verbose_name='PoE mode' verbose_name=_('PoE mode')
) )
poe_type = models.CharField( poe_type = models.CharField(
max_length=50, max_length=50,
choices=InterfacePoETypeChoices, choices=InterfacePoETypeChoices,
blank=True, blank=True,
verbose_name='PoE type' verbose_name=_('PoE type')
) )
wireless_link = models.ForeignKey( wireless_link = models.ForeignKey(
to='wireless.WirelessLink', to='wireless.WirelessLink',
@ -644,7 +668,7 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
to='wireless.WirelessLAN', to='wireless.WirelessLAN',
related_name='interfaces', related_name='interfaces',
blank=True, blank=True,
verbose_name='Wireless LANs' verbose_name=_('wireless LANs')
) )
untagged_vlan = models.ForeignKey( untagged_vlan = models.ForeignKey(
to='ipam.VLAN', to='ipam.VLAN',
@ -652,13 +676,13 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
related_name='interfaces_as_untagged', related_name='interfaces_as_untagged',
null=True, null=True,
blank=True, blank=True,
verbose_name='Untagged VLAN' verbose_name=_('untagged VLAN')
) )
tagged_vlans = models.ManyToManyField( tagged_vlans = models.ManyToManyField(
to='ipam.VLAN', to='ipam.VLAN',
related_name='interfaces_as_tagged', related_name='interfaces_as_tagged',
blank=True, blank=True,
verbose_name='Tagged VLANs' verbose_name=_('tagged VLANs')
) )
vrf = models.ForeignKey( vrf = models.ForeignKey(
to='ipam.VRF', to='ipam.VRF',
@ -666,7 +690,7 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
related_name='interfaces', related_name='interfaces',
null=True, null=True,
blank=True, blank=True,
verbose_name='VRF' verbose_name=_('VRF')
) )
ip_addresses = GenericRelation( ip_addresses = GenericRelation(
to='ipam.IPAddress', to='ipam.IPAddress',
@ -704,77 +728,98 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
# Virtual Interfaces cannot have a Cable attached # Virtual Interfaces cannot have a Cable attached
if self.is_virtual and self.cable: if self.is_virtual and self.cable:
raise ValidationError({ raise ValidationError({
'type': f"{self.get_type_display()} interfaces cannot have a cable attached." 'type': _("{display_type} interfaces cannot have a cable attached.").format(
display_type=self.get_type_display()
)
}) })
# Virtual Interfaces cannot be marked as connected # Virtual Interfaces cannot be marked as connected
if self.is_virtual and self.mark_connected: if self.is_virtual and self.mark_connected:
raise ValidationError({ raise ValidationError({
'mark_connected': f"{self.get_type_display()} interfaces cannot be marked as connected." 'mark_connected': _("{display_type} interfaces cannot be marked as connected.".format(
display_type=self.get_type_display())
)
}) })
# Parent validation # Parent validation
# An interface cannot be its own parent # An interface cannot be its own parent
if self.pk and self.parent_id == self.pk: if self.pk and self.parent_id == self.pk:
raise ValidationError({'parent': "An interface cannot be its own parent."}) raise ValidationError({'parent': _("An interface cannot be its own parent.")})
# A physical interface cannot have a parent interface # A physical interface cannot have a parent interface
if self.type != InterfaceTypeChoices.TYPE_VIRTUAL and self.parent is not None: if self.type != InterfaceTypeChoices.TYPE_VIRTUAL and self.parent is not None:
raise ValidationError({'parent': "Only virtual interfaces may be assigned to a parent interface."}) raise ValidationError({'parent': _("Only virtual interfaces may be assigned to a parent interface.")})
# An interface's parent must belong to the same device or virtual chassis # An interface's parent must belong to the same device or virtual chassis
if self.parent and self.parent.device != self.device: if self.parent and self.parent.device != self.device:
if self.device.virtual_chassis is None: if self.device.virtual_chassis is None:
raise ValidationError({ raise ValidationError({
'parent': f"The selected parent interface ({self.parent}) belongs to a different device " 'parent': _(
f"({self.parent.device})." "The selected parent interface ({interface}) belongs to a different device ({device})"
).format(interface=self.parent, device=self.parent.device)
}) })
elif self.parent.device.virtual_chassis != self.parent.virtual_chassis: elif self.parent.device.virtual_chassis != self.parent.virtual_chassis:
raise ValidationError({ raise ValidationError({
'parent': f"The selected parent interface ({self.parent}) belongs to {self.parent.device}, which " 'parent': _(
f"is not part of virtual chassis {self.device.virtual_chassis}." "The selected parent interface ({interface}) belongs to {device}, which is not part of "
"virtual chassis {virtual_chassis}."
).format(
interface=self.parent,
device=self.parent_device,
virtual_chassis=self.device.virtual_chassis
)
}) })
# Bridge validation # Bridge validation
# An interface cannot be bridged to itself # An interface cannot be bridged to itself
if self.pk and self.bridge_id == self.pk: if self.pk and self.bridge_id == self.pk:
raise ValidationError({'bridge': "An interface cannot be bridged to itself."}) raise ValidationError({'bridge': _("An interface cannot be bridged to itself.")})
# A bridged interface belong to the same device or virtual chassis # A bridged interface belong to the same device or virtual chassis
if self.bridge and self.bridge.device != self.device: if self.bridge and self.bridge.device != self.device:
if self.device.virtual_chassis is None: if self.device.virtual_chassis is None:
raise ValidationError({ raise ValidationError({
'bridge': f"The selected bridge interface ({self.bridge}) belongs to a different device " 'bridge': _("""
f"({self.bridge.device})." The selected bridge interface ({bridge}) belongs to a different device
({device}).""").format(bridge=self.bridge, device=self.bridge.device)
}) })
elif self.bridge.device.virtual_chassis != self.device.virtual_chassis: elif self.bridge.device.virtual_chassis != self.device.virtual_chassis:
raise ValidationError({ raise ValidationError({
'bridge': f"The selected bridge interface ({self.bridge}) belongs to {self.bridge.device}, which " 'bridge': _(
f"is not part of virtual chassis {self.device.virtual_chassis}." "The selected bridge interface ({interface}) belongs to {device}, which is not part of virtual "
"chassis {virtual_chassis}."
).format(
interface=self.bridge, device=self.bridge.device, virtual_chassis=self.device.virtual_chassis
)
}) })
# LAG validation # LAG validation
# A virtual interface cannot have a parent LAG # A virtual interface cannot have a parent LAG
if self.type == InterfaceTypeChoices.TYPE_VIRTUAL and self.lag is not None: if self.type == InterfaceTypeChoices.TYPE_VIRTUAL and self.lag is not None:
raise ValidationError({'lag': "Virtual interfaces cannot have a parent LAG interface."}) raise ValidationError({'lag': _("Virtual interfaces cannot have a parent LAG interface.")})
# A LAG interface cannot be its own parent # A LAG interface cannot be its own parent
if self.pk and self.lag_id == self.pk: if self.pk and self.lag_id == self.pk:
raise ValidationError({'lag': "A LAG interface cannot be its own parent."}) raise ValidationError({'lag': _("A LAG interface cannot be its own parent.")})
# An interface's LAG must belong to the same device or virtual chassis # An interface's LAG must belong to the same device or virtual chassis
if self.lag and self.lag.device != self.device: if self.lag and self.lag.device != self.device:
if self.device.virtual_chassis is None: if self.device.virtual_chassis is None:
raise ValidationError({ raise ValidationError({
'lag': f"The selected LAG interface ({self.lag}) belongs to a different device ({self.lag.device})." 'lag': _(
"The selected LAG interface ({lag}) belongs to a different device ({device})."
).format(lag=self.lag, device=self.lag.device)
}) })
elif self.lag.device.virtual_chassis != self.device.virtual_chassis: elif self.lag.device.virtual_chassis != self.device.virtual_chassis:
raise ValidationError({ raise ValidationError({
'lag': f"The selected LAG interface ({self.lag}) belongs to {self.lag.device}, which is not part " 'lag': _(
f"of virtual chassis {self.device.virtual_chassis}." "The selected LAG interface ({lag}) belongs to {device}, which is not part of virtual chassis "
"{virtual_chassis}.".format(
lag=self.lag, device=self.lag.device, virtual_chassis=self.device.virtual_chassis)
)
}) })
# PoE validation # PoE validation
@ -782,52 +827,54 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
# Only physical interfaces may have a PoE mode/type assigned # Only physical interfaces may have a PoE mode/type assigned
if self.poe_mode and self.is_virtual: if self.poe_mode and self.is_virtual:
raise ValidationError({ raise ValidationError({
'poe_mode': "Virtual interfaces cannot have a PoE mode." 'poe_mode': _("Virtual interfaces cannot have a PoE mode.")
}) })
if self.poe_type and self.is_virtual: if self.poe_type and self.is_virtual:
raise ValidationError({ raise ValidationError({
'poe_type': "Virtual interfaces cannot have a PoE type." 'poe_type': _("Virtual interfaces cannot have a PoE type.")
}) })
# An interface with a PoE type set must also specify a mode # An interface with a PoE type set must also specify a mode
if self.poe_type and not self.poe_mode: if self.poe_type and not self.poe_mode:
raise ValidationError({ raise ValidationError({
'poe_type': "Must specify PoE mode when designating a PoE type." 'poe_type': _("Must specify PoE mode when designating a PoE type.")
}) })
# Wireless validation # Wireless validation
# RF role & channel may only be set for wireless interfaces # RF role & channel may only be set for wireless interfaces
if self.rf_role and not self.is_wireless: if self.rf_role and not self.is_wireless:
raise ValidationError({'rf_role': "Wireless role may be set only on wireless interfaces."}) raise ValidationError({'rf_role': _("Wireless role may be set only on wireless interfaces.")})
if self.rf_channel and not self.is_wireless: if self.rf_channel and not self.is_wireless:
raise ValidationError({'rf_channel': "Channel may be set only on wireless interfaces."}) raise ValidationError({'rf_channel': _("Channel may be set only on wireless interfaces.")})
# Validate channel frequency against interface type and selected channel (if any) # Validate channel frequency against interface type and selected channel (if any)
if self.rf_channel_frequency: if self.rf_channel_frequency:
if not self.is_wireless: if not self.is_wireless:
raise ValidationError({ raise ValidationError({
'rf_channel_frequency': "Channel frequency may be set only on wireless interfaces.", 'rf_channel_frequency': _("Channel frequency may be set only on wireless interfaces."),
}) })
if self.rf_channel and self.rf_channel_frequency != get_channel_attr(self.rf_channel, 'frequency'): if self.rf_channel and self.rf_channel_frequency != get_channel_attr(self.rf_channel, 'frequency'):
raise ValidationError({ raise ValidationError({
'rf_channel_frequency': "Cannot specify custom frequency with channel selected.", 'rf_channel_frequency': _("Cannot specify custom frequency with channel selected."),
}) })
# Validate channel width against interface type and selected channel (if any) # Validate channel width against interface type and selected channel (if any)
if self.rf_channel_width: if self.rf_channel_width:
if not self.is_wireless: if not self.is_wireless:
raise ValidationError({'rf_channel_width': "Channel width may be set only on wireless interfaces."}) raise ValidationError({'rf_channel_width': _("Channel width may be set only on wireless interfaces.")})
if self.rf_channel and self.rf_channel_width != get_channel_attr(self.rf_channel, 'width'): if self.rf_channel and self.rf_channel_width != get_channel_attr(self.rf_channel, 'width'):
raise ValidationError({'rf_channel_width': "Cannot specify custom width with channel selected."}) raise ValidationError({'rf_channel_width': _("Cannot specify custom width with channel selected.")})
# VLAN validation # VLAN validation
# Validate untagged VLAN # Validate untagged VLAN
if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]: if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:
raise ValidationError({ raise ValidationError({
'untagged_vlan': f"The untagged VLAN ({self.untagged_vlan}) must belong to the same site as the " 'untagged_vlan': _("""
f"interface's parent device, or it must be global." The untagged VLAN ({untagged_vlan}) must belong to the same site as the
interface's parent device, or it must be global.
""").format(untagged_vlan=self.untagged_vlan)
}) })
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -894,10 +941,12 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
A pass-through port on the front of a Device. A pass-through port on the front of a Device.
""" """
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=PortTypeChoices choices=PortTypeChoices
) )
color = ColorField( color = ColorField(
verbose_name=_('color'),
blank=True blank=True
) )
rear_port = models.ForeignKey( rear_port = models.ForeignKey(
@ -906,6 +955,7 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
related_name='frontports' related_name='frontports'
) )
rear_port_position = models.PositiveSmallIntegerField( rear_port_position = models.PositiveSmallIntegerField(
verbose_name=_('rear port position'),
default=1, default=1,
validators=[ validators=[
MinValueValidator(REARPORT_POSITIONS_MIN), MinValueValidator(REARPORT_POSITIONS_MIN),
@ -939,14 +989,22 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
# Validate rear port assignment # Validate rear port assignment
if self.rear_port.device != self.device: if self.rear_port.device != self.device:
raise ValidationError({ raise ValidationError({
"rear_port": f"Rear port ({self.rear_port}) must belong to the same device" "rear_port": _(
"Rear port ({rear_port}) must belong to the same device"
).format(rear_port=self.rear_port)
}) })
# Validate rear port position assignment # Validate rear port position assignment
if self.rear_port_position > self.rear_port.positions: if self.rear_port_position > self.rear_port.positions:
raise ValidationError({ raise ValidationError({
"rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port " "rear_port_position": _(
f"{self.rear_port.name} has only {self.rear_port.positions} positions" "Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
"positions."
).format(
rear_port_position=self.rear_port_position,
name=self.rear_port.name,
positions=self.rear_port.positions
)
}) })
@ -955,13 +1013,16 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
A pass-through port on the rear of a Device. A pass-through port on the rear of a Device.
""" """
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=PortTypeChoices choices=PortTypeChoices
) )
color = ColorField( color = ColorField(
verbose_name=_('color'),
blank=True blank=True
) )
positions = models.PositiveSmallIntegerField( positions = models.PositiveSmallIntegerField(
verbose_name=_('positions'),
default=1, default=1,
validators=[ validators=[
MinValueValidator(REARPORT_POSITIONS_MIN), MinValueValidator(REARPORT_POSITIONS_MIN),
@ -982,8 +1043,9 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
frontport_count = self.frontports.count() frontport_count = self.frontports.count()
if self.positions < frontport_count: if self.positions < frontport_count:
raise ValidationError({ raise ValidationError({
"positions": f"The number of positions cannot be less than the number of mapped front ports " "positions": _("""
f"({frontport_count})" The number of positions cannot be less than the number of mapped front ports
({frontport_count})""").format(frontport_count=frontport_count)
}) })
@ -996,6 +1058,7 @@ class ModuleBay(ComponentModel, TrackingModelMixin):
An empty space within a Device which can house a child device An empty space within a Device which can house a child device
""" """
position = models.CharField( position = models.CharField(
verbose_name=_('position'),
max_length=30, max_length=30,
blank=True, blank=True,
help_text=_('Identifier to reference when renaming installed components') help_text=_('Identifier to reference when renaming installed components')
@ -1014,7 +1077,7 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
installed_device = models.OneToOneField( installed_device = models.OneToOneField(
to='dcim.Device', to='dcim.Device',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name='parent_bay', related_name=_('parent_bay'),
blank=True, blank=True,
null=True null=True
) )
@ -1029,22 +1092,22 @@ class DeviceBay(ComponentModel, TrackingModelMixin):
# Validate that the parent Device can have DeviceBays # Validate that the parent Device can have DeviceBays
if not self.device.device_type.is_parent_device: if not self.device.device_type.is_parent_device:
raise ValidationError("This type of device ({}) does not support device bays.".format( raise ValidationError(_("This type of device ({device_type}) does not support device bays.").format(
self.device.device_type device_type=self.device.device_type
)) ))
# Cannot install a device into itself, obviously # Cannot install a device into itself, obviously
if self.device == self.installed_device: if self.device == self.installed_device:
raise ValidationError("Cannot install a device into itself.") raise ValidationError(_("Cannot install a device into itself."))
# Check that the installed device is not already installed elsewhere # Check that the installed device is not already installed elsewhere
if self.installed_device: if self.installed_device:
current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first() current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first()
if current_bay and current_bay != self: if current_bay and current_bay != self:
raise ValidationError({ raise ValidationError({
'installed_device': "Cannot install the specified device; device is already installed in {}".format( 'installed_device': _(
current_bay "Cannot install the specified device; device is already installed in {bay}."
) ).format(bay=current_bay)
}) })
@ -1058,6 +1121,7 @@ class InventoryItemRole(OrganizationalModel):
Inventory items may optionally be assigned a functional role. Inventory items may optionally be assigned a functional role.
""" """
color = ColorField( color = ColorField(
verbose_name=_('color'),
default=ColorChoices.COLOR_GREY default=ColorChoices.COLOR_GREY
) )
@ -1110,13 +1174,13 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
) )
part_id = models.CharField( part_id = models.CharField(
max_length=50, max_length=50,
verbose_name='Part ID', verbose_name=_('part ID'),
blank=True, blank=True,
help_text=_('Manufacturer-assigned part identifier') help_text=_('Manufacturer-assigned part identifier')
) )
serial = models.CharField( serial = models.CharField(
max_length=50, max_length=50,
verbose_name='Serial number', verbose_name=_('serial number'),
blank=True blank=True
) )
asset_tag = models.CharField( asset_tag = models.CharField(
@ -1124,10 +1188,11 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
unique=True, unique=True,
blank=True, blank=True,
null=True, null=True,
verbose_name='Asset tag', verbose_name=_('asset tag'),
help_text=_('A unique tag used to identify this item') help_text=_('A unique tag used to identify this item')
) )
discovered = models.BooleanField( discovered = models.BooleanField(
verbose_name=_('discovered'),
default=False, default=False,
help_text=_('This item was automatically discovered') help_text=_('This item was automatically discovered')
) )
@ -1154,7 +1219,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
# An InventoryItem cannot be its own parent # An InventoryItem cannot be its own parent
if self.pk and self.parent_id == self.pk: if self.pk and self.parent_id == self.pk:
raise ValidationError({ raise ValidationError({
"parent": "Cannot assign self as parent." "parent": _("Cannot assign self as parent.")
}) })
# Validation for moving InventoryItems # Validation for moving InventoryItems
@ -1162,13 +1227,13 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
# Cannot move an InventoryItem to another device if it has a parent # Cannot move an InventoryItem to another device if it has a parent
if self.parent and self.parent.device != self.device: if self.parent and self.parent.device != self.device:
raise ValidationError({ raise ValidationError({
"parent": "Parent inventory item does not belong to the same device." "parent": _("Parent inventory item does not belong to the same device.")
}) })
# Prevent moving InventoryItems with children # Prevent moving InventoryItems with children
first_child = self.get_children().first() first_child = self.get_children().first()
if first_child and first_child.device != self.device: if first_child and first_child.device != self.device:
raise ValidationError("Cannot move an inventory item with dependent children") raise ValidationError(_("Cannot move an inventory item with dependent children"))
# When moving an InventoryItem to another device, remove any associated component # When moving an InventoryItem to another device, remove any associated component
if self.component and self.component.device != self.device: if self.component and self.component.device != self.device:
@ -1176,5 +1241,5 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
else: else:
if self.component and self.component.device != self.device: if self.component and self.component.device != self.device:
raise ValidationError({ raise ValidationError({
"device": "Cannot assign inventory item to component on another device" "device": _("Cannot assign inventory item to component on another device")
}) })

View File

@ -12,7 +12,7 @@ from django.db.models.functions import Lower
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
@ -78,9 +78,11 @@ class DeviceType(PrimaryModel, WeightMixin):
related_name='device_types' related_name='device_types'
) )
model = models.CharField( model = models.CharField(
verbose_name=_('model'),
max_length=100 max_length=100
) )
slug = models.SlugField( slug = models.SlugField(
verbose_name=_('slug'),
max_length=100 max_length=100
) )
default_platform = models.ForeignKey( default_platform = models.ForeignKey(
@ -89,9 +91,10 @@ class DeviceType(PrimaryModel, WeightMixin):
related_name='+', related_name='+',
blank=True, blank=True,
null=True, null=True,
verbose_name='Default platform' verbose_name=_('default platform')
) )
part_number = models.CharField( part_number = models.CharField(
verbose_name=_('part number'),
max_length=50, max_length=50,
blank=True, blank=True,
help_text=_('Discrete part number (optional)') help_text=_('Discrete part number (optional)')
@ -100,22 +103,23 @@ class DeviceType(PrimaryModel, WeightMixin):
max_digits=4, max_digits=4,
decimal_places=1, decimal_places=1,
default=1.0, default=1.0,
verbose_name='Height (U)' verbose_name=_('height (U)')
) )
is_full_depth = models.BooleanField( is_full_depth = models.BooleanField(
default=True, default=True,
verbose_name='Is full depth', verbose_name=_('is full depth'),
help_text=_('Device consumes both front and rear rack faces') help_text=_('Device consumes both front and rear rack faces')
) )
subdevice_role = models.CharField( subdevice_role = models.CharField(
max_length=50, max_length=50,
choices=SubdeviceRoleChoices, choices=SubdeviceRoleChoices,
blank=True, blank=True,
verbose_name='Parent/child status', verbose_name=_('parent/child status'),
help_text=_('Parent devices house child devices in device bays. Leave blank ' help_text=_('Parent devices house child devices in device bays. Leave blank '
'if this device type is neither a parent nor a child.') 'if this device type is neither a parent nor a child.')
) )
airflow = models.CharField( airflow = models.CharField(
verbose_name=_('airflow'),
max_length=50, max_length=50,
choices=DeviceAirflowChoices, choices=DeviceAirflowChoices,
blank=True blank=True
@ -176,7 +180,8 @@ class DeviceType(PrimaryModel, WeightMixin):
) )
clone_fields = ( clone_fields = (
'manufacturer', 'default_platform', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit' 'manufacturer', 'default_platform', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight',
'weight_unit',
) )
prerequisite_models = ( prerequisite_models = (
'dcim.Manufacturer', 'dcim.Manufacturer',
@ -275,9 +280,9 @@ class DeviceType(PrimaryModel, WeightMixin):
super().clean() super().clean()
# U height must be divisible by 0.5 # U height must be divisible by 0.5
if self.u_height % decimal.Decimal(0.5): if decimal.Decimal(self.u_height) % decimal.Decimal(0.5):
raise ValidationError({ raise ValidationError({
'u_height': "U height must be in increments of 0.5 rack units." 'u_height': _("U height must be in increments of 0.5 rack units.")
}) })
# If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have # If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have
@ -293,8 +298,8 @@ class DeviceType(PrimaryModel, WeightMixin):
) )
if d.position not in u_available: if d.position not in u_available:
raise ValidationError({ raise ValidationError({
'u_height': "Device {} in rack {} does not have sufficient space to accommodate a height of " 'u_height': _("Device {} in rack {} does not have sufficient space to accommodate a height of "
"{}U".format(d, d.rack, self.u_height) "{}U").format(d, d.rack, self.u_height)
}) })
# If modifying the height of an existing DeviceType to 0U, check for any instances assigned to a rack position. # If modifying the height of an existing DeviceType to 0U, check for any instances assigned to a rack position.
@ -306,23 +311,23 @@ class DeviceType(PrimaryModel, WeightMixin):
if racked_instance_count: if racked_instance_count:
url = f"{reverse('dcim:device_list')}?manufactuer_id={self.manufacturer_id}&device_type_id={self.pk}" url = f"{reverse('dcim:device_list')}?manufactuer_id={self.manufacturer_id}&device_type_id={self.pk}"
raise ValidationError({ raise ValidationError({
'u_height': mark_safe( 'u_height': mark_safe(_(
f'Unable to set 0U height: Found <a href="{url}">{racked_instance_count} instances</a> already ' 'Unable to set 0U height: Found <a href="{url}">{racked_instance_count} instances</a> already '
f'mounted within racks.' 'mounted within racks.'
) ).format(url=url, racked_instance_count=racked_instance_count))
}) })
if ( if (
self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT
) and self.pk and self.devicebaytemplates.count(): ) and self.pk and self.devicebaytemplates.count():
raise ValidationError({ raise ValidationError({
'subdevice_role': "Must delete all device bay templates associated with this device before " 'subdevice_role': _("Must delete all device bay templates associated with this device before "
"declassifying it as a parent device." "declassifying it as a parent device.")
}) })
if self.u_height and self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD: if self.u_height and self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD:
raise ValidationError({ raise ValidationError({
'u_height': "Child device types must be 0U." 'u_height': _("Child device types must be 0U.")
}) })
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -367,9 +372,11 @@ class ModuleType(PrimaryModel, WeightMixin):
related_name='module_types' related_name='module_types'
) )
model = models.CharField( model = models.CharField(
verbose_name=_('model'),
max_length=100 max_length=100
) )
part_number = models.CharField( part_number = models.CharField(
verbose_name=_('part number'),
max_length=50, max_length=50,
blank=True, blank=True,
help_text=_('Discrete part number (optional)') help_text=_('Discrete part number (optional)')
@ -454,11 +461,12 @@ class DeviceRole(OrganizationalModel):
virtual machines as well. virtual machines as well.
""" """
color = ColorField( color = ColorField(
verbose_name=_('color'),
default=ColorChoices.COLOR_GREY default=ColorChoices.COLOR_GREY
) )
vm_role = models.BooleanField( vm_role = models.BooleanField(
default=True, default=True,
verbose_name='VM Role', verbose_name=_('VM role'),
help_text=_('Virtual machines may be assigned to this role') help_text=_('Virtual machines may be assigned to this role')
) )
config_template = models.ForeignKey( config_template = models.ForeignKey(
@ -550,6 +558,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
null=True null=True
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=64, max_length=64,
blank=True, blank=True,
null=True null=True
@ -563,7 +572,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
serial = models.CharField( serial = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
verbose_name='Serial number', verbose_name=_('serial number'),
help_text=_("Chassis serial number, assigned by the manufacturer") help_text=_("Chassis serial number, assigned by the manufacturer")
) )
asset_tag = models.CharField( asset_tag = models.CharField(
@ -571,7 +580,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
blank=True, blank=True,
null=True, null=True,
unique=True, unique=True,
verbose_name='Asset tag', verbose_name=_('asset tag'),
help_text=_('A unique tag used to identify this device') help_text=_('A unique tag used to identify this device')
) )
site = models.ForeignKey( site = models.ForeignKey(
@ -599,21 +608,23 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
blank=True, blank=True,
null=True, null=True,
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX + 0.5)], validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX + 0.5)],
verbose_name='Position (U)', verbose_name=_('position (U)'),
help_text=_('The lowest-numbered unit occupied by the device') help_text=_('The lowest-numbered unit occupied by the device')
) )
face = models.CharField( face = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
choices=DeviceFaceChoices, choices=DeviceFaceChoices,
verbose_name='Rack face' verbose_name=_('rack face')
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=DeviceStatusChoices, choices=DeviceStatusChoices,
default=DeviceStatusChoices.STATUS_ACTIVE default=DeviceStatusChoices.STATUS_ACTIVE
) )
airflow = models.CharField( airflow = models.CharField(
verbose_name=_('airflow'),
max_length=50, max_length=50,
choices=DeviceAirflowChoices, choices=DeviceAirflowChoices,
blank=True blank=True
@ -624,7 +635,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
related_name='+', related_name='+',
blank=True, blank=True,
null=True, null=True,
verbose_name='Primary IPv4' verbose_name=_('primary IPv4')
) )
primary_ip6 = models.OneToOneField( primary_ip6 = models.OneToOneField(
to='ipam.IPAddress', to='ipam.IPAddress',
@ -632,7 +643,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
related_name='+', related_name='+',
blank=True, blank=True,
null=True, null=True,
verbose_name='Primary IPv6' verbose_name=_('primary IPv6')
) )
oob_ip = models.OneToOneField( oob_ip = models.OneToOneField(
to='ipam.IPAddress', to='ipam.IPAddress',
@ -640,7 +651,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
related_name='+', related_name='+',
blank=True, blank=True,
null=True, null=True,
verbose_name='Out-of-band IP' verbose_name=_('out-of-band IP')
) )
cluster = models.ForeignKey( cluster = models.ForeignKey(
to='virtualization.Cluster', to='virtualization.Cluster',
@ -657,12 +668,14 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
null=True null=True
) )
vc_position = models.PositiveSmallIntegerField( vc_position = models.PositiveSmallIntegerField(
verbose_name=_('VC position'),
blank=True, blank=True,
null=True, null=True,
validators=[MaxValueValidator(255)], validators=[MaxValueValidator(255)],
help_text=_('Virtual chassis position') help_text=_('Virtual chassis position')
) )
vc_priority = models.PositiveSmallIntegerField( vc_priority = models.PositiveSmallIntegerField(
verbose_name=_('VC priority'),
blank=True, blank=True,
null=True, null=True,
validators=[MaxValueValidator(255)], validators=[MaxValueValidator(255)],
@ -676,6 +689,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
null=True null=True
) )
latitude = models.DecimalField( latitude = models.DecimalField(
verbose_name=_('latitude'),
max_digits=8, max_digits=8,
decimal_places=6, decimal_places=6,
blank=True, blank=True,
@ -683,6 +697,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)") help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
) )
longitude = models.DecimalField( longitude = models.DecimalField(
verbose_name=_('longitude'),
max_digits=9, max_digits=9,
decimal_places=6, decimal_places=6,
blank=True, blank=True,
@ -763,7 +778,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
Lower('name'), 'site', Lower('name'), 'site',
name='%(app_label)s_%(class)s_unique_name_site', name='%(app_label)s_%(class)s_unique_name_site',
condition=Q(tenant__isnull=True), condition=Q(tenant__isnull=True),
violation_error_message="Device name must be unique per site." violation_error_message=_("Device name must be unique per site.")
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=('rack', 'position', 'face'), fields=('rack', 'position', 'face'),
@ -799,42 +814,48 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
# Validate site/location/rack combination # Validate site/location/rack combination
if self.rack and self.site != self.rack.site: if self.rack and self.site != self.rack.site:
raise ValidationError({ raise ValidationError({
'rack': f"Rack {self.rack} does not belong to site {self.site}.", 'rack': _("Rack {rack} does not belong to site {site}.").format(rack=self.rack, site=self.site),
}) })
if self.location and self.site != self.location.site: if self.location and self.site != self.location.site:
raise ValidationError({ raise ValidationError({
'location': f"Location {self.location} does not belong to site {self.site}.", 'location': _(
"Location {location} does not belong to site {site}."
).format(location=self.location, site=self.site)
}) })
if self.rack and self.location and self.rack.location != self.location: if self.rack and self.location and self.rack.location != self.location:
raise ValidationError({ raise ValidationError({
'rack': f"Rack {self.rack} does not belong to location {self.location}.", 'rack': _(
"Rack {rack} does not belong to location {location}."
).format(rack=self.rack, location=self.location)
}) })
if self.rack is None: if self.rack is None:
if self.face: if self.face:
raise ValidationError({ raise ValidationError({
'face': "Cannot select a rack face without assigning a rack.", 'face': _("Cannot select a rack face without assigning a rack."),
}) })
if self.position: if self.position:
raise ValidationError({ raise ValidationError({
'position': "Cannot select a rack position without assigning a rack.", 'position': _("Cannot select a rack position without assigning a rack."),
}) })
# Validate rack position and face # Validate rack position and face
if self.position and self.position % decimal.Decimal(0.5): if self.position and self.position % decimal.Decimal(0.5):
raise ValidationError({ raise ValidationError({
'position': "Position must be in increments of 0.5 rack units." 'position': _("Position must be in increments of 0.5 rack units.")
}) })
if self.position and not self.face: if self.position and not self.face:
raise ValidationError({ raise ValidationError({
'face': "Must specify rack face when defining rack position.", 'face': _("Must specify rack face when defining rack position."),
}) })
# Prevent 0U devices from being assigned to a specific position # Prevent 0U devices from being assigned to a specific position
if hasattr(self, 'device_type'): if hasattr(self, 'device_type'):
if self.position and self.device_type.u_height == 0: if self.position and self.device_type.u_height == 0:
raise ValidationError({ raise ValidationError({
'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position." 'position': _(
"A U0 device type ({device_type}) cannot be assigned to a rack position."
).format(device_type=self.device_type)
}) })
if self.rack: if self.rack:
@ -843,13 +864,17 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
# Child devices cannot be assigned to a rack face/unit # Child devices cannot be assigned to a rack face/unit
if self.device_type.is_child_device and self.face: if self.device_type.is_child_device and self.face:
raise ValidationError({ raise ValidationError({
'face': "Child device types cannot be assigned to a rack face. This is an attribute of the " 'face': _(
"parent device." "Child device types cannot be assigned to a rack face. This is an attribute of the parent "
"device."
)
}) })
if self.device_type.is_child_device and self.position: if self.device_type.is_child_device and self.position:
raise ValidationError({ raise ValidationError({
'position': "Child device types cannot be assigned to a rack position. This is an attribute of " 'position': _(
"the parent device." "Child device types cannot be assigned to a rack position. This is an attribute of the "
"parent device."
)
}) })
# Validate rack space # Validate rack space
@ -860,8 +885,12 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
) )
if self.position and self.position not in available_units: if self.position and self.position not in available_units:
raise ValidationError({ raise ValidationError({
'position': f"U{self.position} is already occupied or does not have sufficient space to " 'position': _(
f"accommodate this device type: {self.device_type} ({self.device_type.u_height}U)" "U{position} is already occupied or does not have sufficient space to accommodate this "
"device type: {device_type} ({u_height}U)"
).format(
position=self.position, device_type=self.device_type, u_height=self.device_type.u_height
)
}) })
except DeviceType.DoesNotExist: except DeviceType.DoesNotExist:
@ -872,7 +901,7 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
if self.primary_ip4: if self.primary_ip4:
if self.primary_ip4.family != 4: if self.primary_ip4.family != 4:
raise ValidationError({ raise ValidationError({
'primary_ip4': f"{self.primary_ip4} is not an IPv4 address." 'primary_ip4': _("{primary_ip4} is not an IPv4 address.").format(primary_ip4=self.primary_ip4)
}) })
if self.primary_ip4.assigned_object in vc_interfaces: if self.primary_ip4.assigned_object in vc_interfaces:
pass pass
@ -880,12 +909,14 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
pass pass
else: else:
raise ValidationError({ raise ValidationError({
'primary_ip4': f"The specified IP address ({self.primary_ip4}) is not assigned to this device." 'primary_ip4': _(
"The specified IP address ({primary_ip4}) is not assigned to this device."
).format(primary_ip4=self.primary_ip4)
}) })
if self.primary_ip6: if self.primary_ip6:
if self.primary_ip6.family != 6: if self.primary_ip6.family != 6:
raise ValidationError({ raise ValidationError({
'primary_ip6': f"{self.primary_ip6} is not an IPv6 address." 'primary_ip6': _("{primary_ip6} is not an IPv6 address.").format(primary_ip6=self.primary_ip6m)
}) })
if self.primary_ip6.assigned_object in vc_interfaces: if self.primary_ip6.assigned_object in vc_interfaces:
pass pass
@ -893,7 +924,9 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
pass pass
else: else:
raise ValidationError({ raise ValidationError({
'primary_ip6': f"The specified IP address ({self.primary_ip6}) is not assigned to this device." 'primary_ip6': _(
"The specified IP address ({primary_ip6}) is not assigned to this device."
).format(primary_ip6=self.primary_ip6)
}) })
if self.oob_ip: if self.oob_ip:
if self.oob_ip.assigned_object in vc_interfaces: if self.oob_ip.assigned_object in vc_interfaces:
@ -909,20 +942,25 @@ class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
if hasattr(self, 'device_type') and self.platform: if hasattr(self, 'device_type') and self.platform:
if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer: if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer:
raise ValidationError({ raise ValidationError({
'platform': f"The assigned platform is limited to {self.platform.manufacturer} device types, but " 'platform': _(
f"this device's type belongs to {self.device_type.manufacturer}." "The assigned platform is limited to {platform_manufacturer} device types, but this device's "
"type belongs to {device_type_manufacturer}."
).format(
platform_manufacturer=self.platform.manufacturer,
device_type_manufacturer=self.device_type.manufacturer
)
}) })
# A Device can only be assigned to a Cluster in the same Site (or no Site) # A Device can only be assigned to a Cluster in the same Site (or no Site)
if self.cluster and self.cluster.site is not None and self.cluster.site != self.site: if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
raise ValidationError({ raise ValidationError({
'cluster': "The assigned cluster belongs to a different site ({})".format(self.cluster.site) 'cluster': _("The assigned cluster belongs to a different site ({})").format(self.cluster.site)
}) })
# Validate virtual chassis assignment # Validate virtual chassis assignment
if self.virtual_chassis and self.vc_position is None: if self.virtual_chassis and self.vc_position is None:
raise ValidationError({ raise ValidationError({
'vc_position': "A device assigned to a virtual chassis must have its position defined." 'vc_position': _("A device assigned to a virtual chassis must have its position defined.")
}) })
def _instantiate_components(self, queryset, bulk_create=True): def _instantiate_components(self, queryset, bulk_create=True):
@ -1107,6 +1145,7 @@ class Module(PrimaryModel, ConfigContextModel):
related_name='instances' related_name='instances'
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=ModuleStatusChoices, choices=ModuleStatusChoices,
default=ModuleStatusChoices.STATUS_ACTIVE default=ModuleStatusChoices.STATUS_ACTIVE
@ -1114,14 +1153,14 @@ class Module(PrimaryModel, ConfigContextModel):
serial = models.CharField( serial = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
verbose_name='Serial number' verbose_name=_('serial number')
) )
asset_tag = models.CharField( asset_tag = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
null=True, null=True,
unique=True, unique=True,
verbose_name='Asset tag', verbose_name=_('asset tag'),
help_text=_('A unique tag used to identify this device') help_text=_('A unique tag used to identify this device')
) )
@ -1144,7 +1183,9 @@ class Module(PrimaryModel, ConfigContextModel):
if hasattr(self, "module_bay") and (self.module_bay.device != self.device): if hasattr(self, "module_bay") and (self.module_bay.device != self.device):
raise ValidationError( raise ValidationError(
f"Module must be installed within a module bay belonging to the assigned device ({self.device})." _("Module must be installed within a module bay belonging to the assigned device ({device}).").format(
device=self.device
)
) )
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -1242,9 +1283,11 @@ class VirtualChassis(PrimaryModel):
null=True null=True
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=64 max_length=64
) )
domain = models.CharField( domain = models.CharField(
verbose_name=_('domain'),
max_length=30, max_length=30,
blank=True blank=True
) )
@ -1272,7 +1315,9 @@ class VirtualChassis(PrimaryModel):
# VirtualChassis.) # VirtualChassis.)
if self.pk and self.master and self.master not in self.members.all(): if self.pk and self.master and self.master not in self.members.all():
raise ValidationError({ raise ValidationError({
'master': f"The selected master ({self.master}) is not assigned to this virtual chassis." 'master': _("The selected master ({master}) is not assigned to this virtual chassis.").format(
master=self.master
)
}) })
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
@ -1285,10 +1330,10 @@ class VirtualChassis(PrimaryModel):
lag__device=F('device') lag__device=F('device')
) )
if interfaces: if interfaces:
raise ProtectedError( raise ProtectedError(_(
f"Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG", "Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG "
interfaces "interfaces."
) ).format(self=self, interfaces=InterfaceSpeedChoices))
return super().delete(*args, **kwargs) return super().delete(*args, **kwargs)
@ -1302,14 +1347,17 @@ class VirtualDeviceContext(PrimaryModel):
null=True null=True
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=64 max_length=64
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=VirtualDeviceContextStatusChoices, choices=VirtualDeviceContextStatusChoices,
) )
identifier = models.PositiveSmallIntegerField( identifier = models.PositiveSmallIntegerField(
help_text='Numeric identifier unique to the parent device', verbose_name=_('identifier'),
help_text=_('Numeric identifier unique to the parent device'),
blank=True, blank=True,
null=True, null=True,
) )
@ -1319,7 +1367,7 @@ class VirtualDeviceContext(PrimaryModel):
related_name='+', related_name='+',
blank=True, blank=True,
null=True, null=True,
verbose_name='Primary IPv4' verbose_name=_('primary IPv4')
) )
primary_ip6 = models.OneToOneField( primary_ip6 = models.OneToOneField(
to='ipam.IPAddress', to='ipam.IPAddress',
@ -1327,7 +1375,7 @@ class VirtualDeviceContext(PrimaryModel):
related_name='+', related_name='+',
blank=True, blank=True,
null=True, null=True,
verbose_name='Primary IPv6' verbose_name=_('primary IPv6')
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
@ -1337,6 +1385,7 @@ class VirtualDeviceContext(PrimaryModel):
null=True null=True
) )
comments = models.TextField( comments = models.TextField(
verbose_name=_('comments'),
blank=True blank=True
) )
@ -1382,7 +1431,9 @@ class VirtualDeviceContext(PrimaryModel):
continue continue
if primary_ip.family != family: if primary_ip.family != family:
raise ValidationError({ raise ValidationError({
f'primary_ip{family}': f"{primary_ip} is not an IPv{family} address." f'primary_ip{family}': _(
"{primary_ip} is not an IPv{family} address."
).format(family=family, primary_ip=primary_ip)
}) })
device_interfaces = self.device.vc_interfaces(if_master=False) device_interfaces = self.device.vc_interfaces(if_master=False)
if primary_ip.assigned_object not in device_interfaces: if primary_ip.assigned_object not in device_interfaces:

View File

@ -1,17 +1,20 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _
from dcim.choices import * from dcim.choices import *
from utilities.utils import to_grams from utilities.utils import to_grams
class WeightMixin(models.Model): class WeightMixin(models.Model):
weight = models.DecimalField( weight = models.DecimalField(
verbose_name=_('weight'),
max_digits=8, max_digits=8,
decimal_places=2, decimal_places=2,
blank=True, blank=True,
null=True null=True
) )
weight_unit = models.CharField( weight_unit = models.CharField(
verbose_name=_('weight unit'),
max_length=50, max_length=50,
choices=WeightUnitChoices, choices=WeightUnitChoices,
blank=True, blank=True,
@ -40,4 +43,4 @@ class WeightMixin(models.Model):
# Validate weight and weight_unit # Validate weight and weight_unit
if self.weight and not self.weight_unit: if self.weight and not self.weight_unit:
raise ValidationError("Must specify a unit when setting a weight") raise ValidationError(_("Must specify a unit when setting a weight"))

View File

@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from dcim.choices import * from dcim.choices import *
from netbox.config import ConfigItem from netbox.config import ConfigItem
@ -36,6 +36,7 @@ class PowerPanel(PrimaryModel):
null=True null=True
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100 max_length=100
) )
@ -72,7 +73,8 @@ class PowerPanel(PrimaryModel):
# Location must belong to assigned Site # Location must belong to assigned Site
if self.location and self.location.site != self.site: if self.location and self.location.site != self.site:
raise ValidationError( raise ValidationError(
f"Location {self.location} ({self.location.site}) is in a different site than {self.site}" _("Location {location} ({location_site}) is in a different site than {site}").format(
location=self.location, location_site=self.location.site, site=self.site)
) )
@ -92,49 +94,65 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
null=True null=True
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100 max_length=100
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=PowerFeedStatusChoices, choices=PowerFeedStatusChoices,
default=PowerFeedStatusChoices.STATUS_ACTIVE default=PowerFeedStatusChoices.STATUS_ACTIVE
) )
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=PowerFeedTypeChoices, choices=PowerFeedTypeChoices,
default=PowerFeedTypeChoices.TYPE_PRIMARY default=PowerFeedTypeChoices.TYPE_PRIMARY
) )
supply = models.CharField( supply = models.CharField(
verbose_name=_('supply'),
max_length=50, max_length=50,
choices=PowerFeedSupplyChoices, choices=PowerFeedSupplyChoices,
default=PowerFeedSupplyChoices.SUPPLY_AC default=PowerFeedSupplyChoices.SUPPLY_AC
) )
phase = models.CharField( phase = models.CharField(
verbose_name=_('phase'),
max_length=50, max_length=50,
choices=PowerFeedPhaseChoices, choices=PowerFeedPhaseChoices,
default=PowerFeedPhaseChoices.PHASE_SINGLE default=PowerFeedPhaseChoices.PHASE_SINGLE
) )
voltage = models.SmallIntegerField( voltage = models.SmallIntegerField(
verbose_name=_('voltage'),
default=ConfigItem('POWERFEED_DEFAULT_VOLTAGE'), default=ConfigItem('POWERFEED_DEFAULT_VOLTAGE'),
validators=[ExclusionValidator([0])] validators=[ExclusionValidator([0])]
) )
amperage = models.PositiveSmallIntegerField( amperage = models.PositiveSmallIntegerField(
verbose_name=_('amperage'),
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
default=ConfigItem('POWERFEED_DEFAULT_AMPERAGE') default=ConfigItem('POWERFEED_DEFAULT_AMPERAGE')
) )
max_utilization = models.PositiveSmallIntegerField( max_utilization = models.PositiveSmallIntegerField(
verbose_name=_('max utilization'),
validators=[MinValueValidator(1), MaxValueValidator(100)], validators=[MinValueValidator(1), MaxValueValidator(100)],
default=ConfigItem('POWERFEED_DEFAULT_MAX_UTILIZATION'), default=ConfigItem('POWERFEED_DEFAULT_MAX_UTILIZATION'),
help_text=_("Maximum permissible draw (percentage)") help_text=_("Maximum permissible draw (percentage)")
) )
available_power = models.PositiveIntegerField( available_power = models.PositiveIntegerField(
verbose_name=_('available power'),
default=0, default=0,
editable=False editable=False
) )
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='power_feeds',
blank=True,
null=True
)
clone_fields = ( clone_fields = (
'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
'max_utilization', 'max_utilization', 'tenant',
) )
prerequisite_models = ( prerequisite_models = (
'dcim.PowerPanel', 'dcim.PowerPanel',
@ -160,14 +178,14 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
# Rack must belong to same Site as PowerPanel # Rack must belong to same Site as PowerPanel
if self.rack and self.rack.site != self.power_panel.site: if self.rack and self.rack.site != self.power_panel.site:
raise ValidationError("Rack {} ({}) and power panel {} ({}) are in different sites".format( raise ValidationError(_("Rack {} ({}) and power panel {} ({}) are in different sites").format(
self.rack, self.rack.site, self.power_panel, self.power_panel.site self.rack, self.rack.site, self.power_panel, self.power_panel.site
)) ))
# AC voltage cannot be negative # AC voltage cannot be negative
if self.voltage < 0 and self.supply == PowerFeedSupplyChoices.SUPPLY_AC: if self.voltage < 0 and self.supply == PowerFeedSupplyChoices.SUPPLY_AC:
raise ValidationError({ raise ValidationError({
"voltage": "Voltage cannot be negative for AC supply" "voltage": _("Voltage cannot be negative for AC supply")
}) })
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

View File

@ -9,7 +9,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Count from django.db.models import Count
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
@ -39,6 +39,7 @@ class RackRole(OrganizationalModel):
Racks can be organized by functional role, similar to Devices. Racks can be organized by functional role, similar to Devices.
""" """
color = ColorField( color = ColorField(
verbose_name=_('color'),
default=ColorChoices.COLOR_GREY default=ColorChoices.COLOR_GREY
) )
@ -52,6 +53,7 @@ class Rack(PrimaryModel, WeightMixin):
Each Rack is assigned to a Site and (optionally) a Location. Each Rack is assigned to a Site and (optionally) a Location.
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100 max_length=100
) )
_name = NaturalOrderingField( _name = NaturalOrderingField(
@ -63,7 +65,7 @@ class Rack(PrimaryModel, WeightMixin):
max_length=50, max_length=50,
blank=True, blank=True,
null=True, null=True,
verbose_name='Facility ID', verbose_name=_('facility ID'),
help_text=_("Locally-assigned identifier") help_text=_("Locally-assigned identifier")
) )
site = models.ForeignKey( site = models.ForeignKey(
@ -86,6 +88,7 @@ class Rack(PrimaryModel, WeightMixin):
null=True null=True
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=RackStatusChoices, choices=RackStatusChoices,
default=RackStatusChoices.STATUS_ACTIVE default=RackStatusChoices.STATUS_ACTIVE
@ -101,60 +104,64 @@ class Rack(PrimaryModel, WeightMixin):
serial = models.CharField( serial = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
verbose_name='Serial number' verbose_name=_('serial number')
) )
asset_tag = models.CharField( asset_tag = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
null=True, null=True,
unique=True, unique=True,
verbose_name='Asset tag', verbose_name=_('asset tag'),
help_text=_('A unique tag used to identify this rack') help_text=_('A unique tag used to identify this rack')
) )
type = models.CharField( type = models.CharField(
choices=RackTypeChoices, choices=RackTypeChoices,
max_length=50, max_length=50,
blank=True, blank=True,
verbose_name='Type' verbose_name=_('type')
) )
width = models.PositiveSmallIntegerField( width = models.PositiveSmallIntegerField(
choices=RackWidthChoices, choices=RackWidthChoices,
default=RackWidthChoices.WIDTH_19IN, default=RackWidthChoices.WIDTH_19IN,
verbose_name='Width', verbose_name=_('width'),
help_text=_('Rail-to-rail width') help_text=_('Rail-to-rail width')
) )
u_height = models.PositiveSmallIntegerField( u_height = models.PositiveSmallIntegerField(
default=RACK_U_HEIGHT_DEFAULT, default=RACK_U_HEIGHT_DEFAULT,
verbose_name='Height (U)', verbose_name=_('height (U)'),
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX)], validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX)],
help_text=_('Height in rack units') help_text=_('Height in rack units')
) )
starting_unit = models.PositiveSmallIntegerField( starting_unit = models.PositiveSmallIntegerField(
default=RACK_STARTING_UNIT_DEFAULT, default=RACK_STARTING_UNIT_DEFAULT,
verbose_name='Starting unit', verbose_name=_('starting unit'),
help_text=_('Starting unit for rack') help_text=_('Starting unit for rack')
) )
desc_units = models.BooleanField( desc_units = models.BooleanField(
default=False, default=False,
verbose_name='Descending units', verbose_name=_('descending units'),
help_text=_('Units are numbered top-to-bottom') help_text=_('Units are numbered top-to-bottom')
) )
outer_width = models.PositiveSmallIntegerField( outer_width = models.PositiveSmallIntegerField(
verbose_name=_('outer width'),
blank=True, blank=True,
null=True, null=True,
help_text=_('Outer dimension of rack (width)') help_text=_('Outer dimension of rack (width)')
) )
outer_depth = models.PositiveSmallIntegerField( outer_depth = models.PositiveSmallIntegerField(
verbose_name=_('outer depth'),
blank=True, blank=True,
null=True, null=True,
help_text=_('Outer dimension of rack (depth)') help_text=_('Outer dimension of rack (depth)')
) )
outer_unit = models.CharField( outer_unit = models.CharField(
verbose_name=_('outer unit'),
max_length=50, max_length=50,
choices=RackDimensionUnitChoices, choices=RackDimensionUnitChoices,
blank=True, blank=True,
) )
max_weight = models.PositiveIntegerField( max_weight = models.PositiveIntegerField(
verbose_name=_('max weight'),
blank=True, blank=True,
null=True, null=True,
help_text=_('Maximum load capacity for the rack') help_text=_('Maximum load capacity for the rack')
@ -165,6 +172,7 @@ class Rack(PrimaryModel, WeightMixin):
null=True null=True
) )
mounting_depth = models.PositiveSmallIntegerField( mounting_depth = models.PositiveSmallIntegerField(
verbose_name=_('mounting depth'),
blank=True, blank=True,
null=True, null=True,
help_text=( help_text=(
@ -222,15 +230,15 @@ class Rack(PrimaryModel, WeightMixin):
# Validate location/site assignment # Validate location/site assignment
if self.site and self.location and self.location.site != self.site: if self.site and self.location and self.location.site != self.site:
raise ValidationError(f"Assigned location must belong to parent site ({self.site}).") raise ValidationError(_("Assigned location must belong to parent site ({site}).").format(site=self.site))
# Validate outer dimensions and unit # Validate outer dimensions and unit
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit: if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
raise ValidationError("Must specify a unit when setting an outer width/depth") raise ValidationError(_("Must specify a unit when setting an outer width/depth"))
# Validate max_weight and weight_unit # Validate max_weight and weight_unit
if self.max_weight and not self.weight_unit: if self.max_weight and not self.weight_unit:
raise ValidationError("Must specify a unit when setting a maximum weight") raise ValidationError(_("Must specify a unit when setting a maximum weight"))
if self.pk: if self.pk:
mounted_devices = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('position') mounted_devices = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('position')
@ -240,22 +248,22 @@ class Rack(PrimaryModel, WeightMixin):
min_height = top_device.position + top_device.device_type.u_height - self.starting_unit min_height = top_device.position + top_device.device_type.u_height - self.starting_unit
if self.u_height < min_height: if self.u_height < min_height:
raise ValidationError({ raise ValidationError({
'u_height': f"Rack must be at least {min_height}U tall to house currently installed devices." 'u_height': _("Rack must be at least {min_height}U tall to house currently installed devices.").format(min_height=min_height)
}) })
# Validate that the Rack's starting unit is less than or equal to the position of the lowest mounted Device # Validate that the Rack's starting unit is less than or equal to the position of the lowest mounted Device
if last_device := mounted_devices.first(): if last_device := mounted_devices.first():
if self.starting_unit > last_device.position: if self.starting_unit > last_device.position:
raise ValidationError({ raise ValidationError({
'starting_unit': f"Rack unit numbering must begin at {last_device.position} or less to house " 'starting_unit': _("Rack unit numbering must begin at {position} or less to house "
f"currently installed devices." "currently installed devices.").format(position=last_device.position)
}) })
# Validate that Rack was assigned a Location of its same site, if applicable # Validate that Rack was assigned a Location of its same site, if applicable
if self.location: if self.location:
if self.location.site != self.site: if self.location.site != self.site:
raise ValidationError({ raise ValidationError({
'location': f"Location must be from the same site, {self.site}." 'location': _("Location must be from the same site, {site}.").format(site=self.site)
}) })
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -504,6 +512,7 @@ class RackReservation(PrimaryModel):
related_name='reservations' related_name='reservations'
) )
units = ArrayField( units = ArrayField(
verbose_name=_('units'),
base_field=models.PositiveSmallIntegerField() base_field=models.PositiveSmallIntegerField()
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(
@ -518,6 +527,7 @@ class RackReservation(PrimaryModel):
on_delete=models.PROTECT on_delete=models.PROTECT
) )
description = models.CharField( description = models.CharField(
verbose_name=_('description'),
max_length=200 max_length=200
) )
@ -544,7 +554,7 @@ class RackReservation(PrimaryModel):
invalid_units = [u for u in self.units if u not in self.rack.units] invalid_units = [u for u in self.units if u not in self.rack.units]
if invalid_units: if invalid_units:
raise ValidationError({ raise ValidationError({
'units': "Invalid unit(s) for {}U rack: {}".format( 'units': _("Invalid unit(s) for {}U rack: {}").format(
self.rack.u_height, self.rack.u_height,
', '.join([str(u) for u in invalid_units]), ', '.join([str(u) for u in invalid_units]),
), ),
@ -557,7 +567,7 @@ class RackReservation(PrimaryModel):
conflicting_units = [u for u in self.units if u in reserved_units] conflicting_units = [u for u in self.units if u in reserved_units]
if conflicting_units: if conflicting_units:
raise ValidationError({ raise ValidationError({
'units': 'The following units have already been reserved: {}'.format( 'units': _('The following units have already been reserved: {}').format(
', '.join([str(u) for u in conflicting_units]), ', '.join([str(u) for u in conflicting_units]),
) )
}) })

View File

@ -2,7 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from timezone_field import TimeZoneField from timezone_field import TimeZoneField
from dcim.choices import * from dcim.choices import *
@ -49,7 +49,7 @@ class Region(NestedGroupModel):
fields=('name',), fields=('name',),
name='%(app_label)s_%(class)s_name', name='%(app_label)s_%(class)s_name',
condition=Q(parent__isnull=True), condition=Q(parent__isnull=True),
violation_error_message="A top-level region with this name already exists." violation_error_message=_("A top-level region with this name already exists.")
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=('parent', 'slug'), fields=('parent', 'slug'),
@ -59,7 +59,7 @@ class Region(NestedGroupModel):
fields=('slug',), fields=('slug',),
name='%(app_label)s_%(class)s_slug', name='%(app_label)s_%(class)s_slug',
condition=Q(parent__isnull=True), condition=Q(parent__isnull=True),
violation_error_message="A top-level region with this slug already exists." violation_error_message=_("A top-level region with this slug already exists.")
), ),
) )
@ -104,7 +104,7 @@ class SiteGroup(NestedGroupModel):
fields=('name',), fields=('name',),
name='%(app_label)s_%(class)s_name', name='%(app_label)s_%(class)s_name',
condition=Q(parent__isnull=True), condition=Q(parent__isnull=True),
violation_error_message="A top-level site group with this name already exists." violation_error_message=_("A top-level site group with this name already exists.")
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=('parent', 'slug'), fields=('parent', 'slug'),
@ -114,7 +114,7 @@ class SiteGroup(NestedGroupModel):
fields=('slug',), fields=('slug',),
name='%(app_label)s_%(class)s_slug', name='%(app_label)s_%(class)s_slug',
condition=Q(parent__isnull=True), condition=Q(parent__isnull=True),
violation_error_message="A top-level site group with this slug already exists." violation_error_message=_("A top-level site group with this slug already exists.")
), ),
) )
@ -138,6 +138,7 @@ class Site(PrimaryModel):
field can be used to include an external designation, such as a data center name (e.g. Equinix SV6). field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100, max_length=100,
unique=True, unique=True,
help_text=_("Full name of the site") help_text=_("Full name of the site")
@ -148,10 +149,12 @@ class Site(PrimaryModel):
blank=True blank=True
) )
slug = models.SlugField( slug = models.SlugField(
verbose_name=_('slug'),
max_length=100, max_length=100,
unique=True unique=True
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=SiteStatusChoices, choices=SiteStatusChoices,
default=SiteStatusChoices.STATUS_ACTIVE default=SiteStatusChoices.STATUS_ACTIVE
@ -178,9 +181,10 @@ class Site(PrimaryModel):
null=True null=True
) )
facility = models.CharField( facility = models.CharField(
verbose_name=_('facility'),
max_length=50, max_length=50,
blank=True, blank=True,
help_text=_("Local facility ID or description") help_text=_('Local facility ID or description')
) )
asns = models.ManyToManyField( asns = models.ManyToManyField(
to='ipam.ASN', to='ipam.ASN',
@ -191,28 +195,32 @@ class Site(PrimaryModel):
blank=True blank=True
) )
physical_address = models.CharField( physical_address = models.CharField(
verbose_name=_('physical address'),
max_length=200, max_length=200,
blank=True, blank=True,
help_text=_("Physical location of the building") help_text=_('Physical location of the building')
) )
shipping_address = models.CharField( shipping_address = models.CharField(
verbose_name=_('shipping address'),
max_length=200, max_length=200,
blank=True, blank=True,
help_text=_("If different from the physical address") help_text=_('If different from the physical address')
) )
latitude = models.DecimalField( latitude = models.DecimalField(
verbose_name=_('latitude'),
max_digits=8, max_digits=8,
decimal_places=6, decimal_places=6,
blank=True, blank=True,
null=True, null=True,
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)") help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
) )
longitude = models.DecimalField( longitude = models.DecimalField(
verbose_name=_('longitude'),
max_digits=9, max_digits=9,
decimal_places=6, decimal_places=6,
blank=True, blank=True,
null=True, null=True,
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)") help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
) )
# Generic relations # Generic relations
@ -262,6 +270,7 @@ class Location(NestedGroupModel):
related_name='locations' related_name='locations'
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=LocationStatusChoices, choices=LocationStatusChoices,
default=LocationStatusChoices.STATUS_ACTIVE default=LocationStatusChoices.STATUS_ACTIVE
@ -304,7 +313,7 @@ class Location(NestedGroupModel):
fields=('site', 'name'), fields=('site', 'name'),
name='%(app_label)s_%(class)s_name', name='%(app_label)s_%(class)s_name',
condition=Q(parent__isnull=True), condition=Q(parent__isnull=True),
violation_error_message="A location with this name already exists within the specified site." violation_error_message=_("A location with this name already exists within the specified site.")
), ),
models.UniqueConstraint( models.UniqueConstraint(
fields=('site', 'parent', 'slug'), fields=('site', 'parent', 'slug'),
@ -314,7 +323,7 @@ class Location(NestedGroupModel):
fields=('site', 'slug'), fields=('site', 'slug'),
name='%(app_label)s_%(class)s_slug', name='%(app_label)s_%(class)s_slug',
condition=Q(parent__isnull=True), condition=Q(parent__isnull=True),
violation_error_message="A location with this slug already exists within the specified site." violation_error_message=_("A location with this slug already exists within the specified site.")
), ),
) )
@ -329,4 +338,6 @@ class Location(NestedGroupModel):
# Parent Location (if any) must belong to the same Site # Parent Location (if any) must belong to the same Site
if self.parent and self.parent.site != self.site: if self.parent and self.parent.site != self.site:
raise ValidationError(f"Parent location ({self.parent}) must belong to the same site ({self.site})") raise ValidationError(_(
"Parent location ({parent}) must belong to the same site ({site})."
).format(parent=self.parent, site=self.site))

View File

@ -1,7 +1,7 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import django_tables2 as tables import django_tables2 as tables
from dcim.models import PowerFeed, PowerPanel from dcim.models import PowerFeed, PowerPanel
from tenancy.tables import ContactsColumnMixin from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
@ -57,7 +57,7 @@ class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
# We're not using PathEndpointTable for PowerFeed because power connections # We're not using PathEndpointTable for PowerFeed because power connections
# cannot traverse pass-through ports. # cannot traverse pass-through ports.
class PowerFeedTable(CableTerminationTable): class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
name = tables.Column( name = tables.Column(
verbose_name=_('Name'), verbose_name=_('Name'),
linkify=True linkify=True
@ -83,6 +83,10 @@ class PowerFeedTable(CableTerminationTable):
available_power = tables.Column( available_power = tables.Column(
verbose_name=_('Available Power (VA)') verbose_name=_('Available Power (VA)')
) )
tenant = tables.Column(
linkify=True,
verbose_name=_('Tenant')
)
comments = columns.MarkdownColumn( comments = columns.MarkdownColumn(
verbose_name=_('Comments'), verbose_name=_('Comments'),
) )
@ -94,8 +98,8 @@ class PowerFeedTable(CableTerminationTable):
model = PowerFeed model = PowerFeed
fields = ( fields = (
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power', 'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power', 'tenant',
'description', 'comments', 'tags', 'created', 'last_updated', 'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable', 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',

View File

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

View File

@ -681,13 +681,6 @@ class RackView(generic.ObjectView):
(PowerFeed.objects.restrict(request.user).filter(rack=instance), 'rack_id'), (PowerFeed.objects.restrict(request.user).filter(rack=instance), 'rack_id'),
) )
# Get 0U devices located within the rack
nonracked_devices = Device.objects.filter(
rack=instance,
position__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site) peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
if instance.location: if instance.location:
@ -704,7 +697,6 @@ class RackView(generic.ObjectView):
return { return {
'related_models': related_models, 'related_models': related_models,
'nonracked_devices': nonracked_devices,
'next_rack': next_rack, 'next_rack': next_rack,
'prev_rack': prev_rack, 'prev_rack': prev_rack,
'svg_extra': svg_extra, 'svg_extra': svg_extra,
@ -731,6 +723,26 @@ class RackRackReservationsView(generic.ObjectChildrenView):
return parent.reservations.restrict(request.user, 'view') return parent.reservations.restrict(request.user, 'view')
@register_model_view(Rack, 'nonracked_devices', 'nonracked-devices')
class RackNonRackedView(generic.ObjectChildrenView):
queryset = Rack.objects.all()
child_model = Device
table = tables.DeviceTable
filterset = filtersets.DeviceFilterSet
template_name = 'dcim/rack/non_racked_devices.html'
tab = ViewTab(
label=_('Non-Racked Devices'),
badge=lambda obj: obj.devices.filter(rack=obj, position__isnull=True, parent_bay__isnull=True).count(),
weight=500,
permission='dcim.view_device',
)
def get_children(self, request, parent):
return parent.devices.restrict(request.user, 'view').filter(
rack=parent, position__isnull=True, parent_bay__isnull=True
)
@register_model_view(Rack, 'edit') @register_model_view(Rack, 'edit')
class RackEditView(generic.ObjectEditView): class RackEditView(generic.ObjectEditView):
queryset = Rack.objects.all() queryset = Rack.objects.all()

View File

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

View File

@ -1,11 +1,11 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404
from django_rq.queues import get_connection from django_rq.queues import get_connection
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.generics import RetrieveUpdateDestroyAPIView from rest_framework.generics import RetrieveUpdateDestroyAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.routers import APIRootView from rest_framework.routers import APIRootView
@ -64,6 +64,26 @@ class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
serializer_class = serializers.CustomFieldChoiceSetSerializer serializer_class = serializers.CustomFieldChoiceSetSerializer
filterset_class = filtersets.CustomFieldChoiceSetFilterSet filterset_class = filtersets.CustomFieldChoiceSetFilterSet
@action(detail=True)
def choices(self, request, pk):
"""
Provides an endpoint to iterate through each choice in a set.
"""
choiceset = get_object_or_404(self.queryset, pk=pk)
choices = choiceset.choices
# Enable filtering
if q := request.GET.get('q'):
q = q.lower()
choices = [c for c in choices if q in c[0].lower() or q in c[1].lower()]
# Paginate data
if page := self.paginate_queryset(choices):
data = [
{'value': c[0], 'label': c[1]} for c in page
]
return self.get_paginated_response(data)
# #
# Custom links # Custom links
@ -319,7 +339,7 @@ class ScriptViewSet(ViewSet):
# Attach Job objects to each script (if any) # Attach Job objects to each script (if any)
for script in script_list: for script in script_list:
script.result = results.get(script.name, None) script.result = results.get(script.class_name, None)
serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request}) serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request})
@ -330,7 +350,7 @@ class ScriptViewSet(ViewSet):
object_type = ContentType.objects.get(app_label='extras', model='scriptmodule') object_type = ContentType.objects.get(app_label='extras', model='scriptmodule')
script.result = Job.objects.filter( script.result = Job.objects.filter(
object_type=object_type, object_type=object_type,
name=script.name, name=script.class_name,
status__in=JobStatusChoices.TERMINAL_STATE_CHOICES status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
).first() ).first()
serializer = serializers.ScriptDetailSerializer(script, context={'request': request}) serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
@ -397,7 +417,7 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
""" """
Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects. Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects.
""" """
permission_classes = (IsAuthenticated,) permission_classes = [IsAuthenticatedOrLoginNotRequired]
queryset = ContentType.objects.order_by('app_label', 'model') queryset = ContentType.objects.order_by('app_label', 'model')
serializer_class = serializers.ContentTypeSerializer serializer_class = serializers.ContentTypeSerializer
filterset_class = filtersets.ContentTypeFilterSet filterset_class = filtersets.ContentTypeFilterSet

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
@ -27,16 +27,20 @@ class CustomFieldBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
) )
group_name = forms.CharField( group_name = forms.CharField(
label=_('Group name'),
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
required=False required=False
) )
required = forms.NullBooleanField( required = forms.NullBooleanField(
label=_('Required'),
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
weight = forms.IntegerField( weight = forms.IntegerField(
label=_('Weight'),
required=False required=False
) )
choice_set = DynamicModelChoiceField( choice_set = DynamicModelChoiceField(
@ -50,6 +54,7 @@ class CustomFieldBulkEditForm(BulkEditForm):
initial='' initial=''
) )
is_cloneable = forms.NullBooleanField( is_cloneable = forms.NullBooleanField(
label=_('Is cloneable'),
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
@ -62,6 +67,10 @@ class CustomFieldChoiceSetBulkEditForm(BulkEditForm):
queryset=CustomFieldChoiceSet.objects.all(), queryset=CustomFieldChoiceSet.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
) )
base_choices = forms.ChoiceField(
choices=add_blank_choice(CustomFieldChoiceSetBaseChoices),
required=False
)
description = forms.CharField( description = forms.CharField(
required=False required=False
) )
@ -70,7 +79,7 @@ class CustomFieldChoiceSetBulkEditForm(BulkEditForm):
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
nullable_fields = ('description',) nullable_fields = ('base_choices', 'description')
class CustomLinkBulkEditForm(BulkEditForm): class CustomLinkBulkEditForm(BulkEditForm):
@ -79,17 +88,21 @@ class CustomLinkBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
) )
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
new_window = forms.NullBooleanField( new_window = forms.NullBooleanField(
label=_('New window'),
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
weight = forms.IntegerField( weight = forms.IntegerField(
label=_('Weight'),
required=False required=False
) )
button_class = forms.ChoiceField( button_class = forms.ChoiceField(
label=_('Button class'),
choices=add_blank_choice(CustomLinkButtonClassChoices), choices=add_blank_choice(CustomLinkButtonClassChoices),
required=False required=False
) )
@ -101,18 +114,22 @@ class ExportTemplateBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
mime_type = forms.CharField( mime_type = forms.CharField(
label=_('MIME type'),
max_length=50, max_length=50,
required=False required=False
) )
file_extension = forms.CharField( file_extension = forms.CharField(
label=_('File extension'),
max_length=15, max_length=15,
required=False required=False
) )
as_attachment = forms.NullBooleanField( as_attachment = forms.NullBooleanField(
label=_('As attachment'),
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
@ -126,17 +143,21 @@ class SavedFilterBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
weight = forms.IntegerField( weight = forms.IntegerField(
label=_('Weight'),
required=False required=False
) )
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
shared = forms.NullBooleanField( shared = forms.NullBooleanField(
label=_('Shared'),
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
@ -150,26 +171,32 @@ class WebhookBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
) )
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
type_create = forms.NullBooleanField( type_create = forms.NullBooleanField(
label=_('On create'),
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
type_update = forms.NullBooleanField( type_update = forms.NullBooleanField(
label=_('On update'),
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
type_delete = forms.NullBooleanField( type_delete = forms.NullBooleanField(
label=_('On delete'),
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
type_job_start = forms.NullBooleanField( type_job_start = forms.NullBooleanField(
label=_('On job start'),
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
type_job_end = forms.NullBooleanField( type_job_end = forms.NullBooleanField(
label=_('On job end'),
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
@ -188,6 +215,7 @@ class WebhookBulkEditForm(BulkEditForm):
label=_('SSL verification') label=_('SSL verification')
) )
secret = forms.CharField( secret = forms.CharField(
label=_('Secret'),
required=False required=False
) )
ca_file_path = forms.CharField( ca_file_path = forms.CharField(
@ -204,9 +232,11 @@ class TagBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
) )
color = ColorField( color = ColorField(
label=_('Color'),
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
@ -220,14 +250,17 @@ class ConfigContextBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
) )
weight = forms.IntegerField( weight = forms.IntegerField(
label=_('Weight'),
required=False, required=False,
min_value=0 min_value=0
) )
is_active = forms.NullBooleanField( is_active = forms.NullBooleanField(
label=_('Is active'),
required=False, required=False,
widget=BulkEditNullBooleanSelect() widget=BulkEditNullBooleanSelect()
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
required=False, required=False,
max_length=100 max_length=100
) )
@ -241,6 +274,7 @@ class ConfigTemplateBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
@ -254,10 +288,12 @@ class JournalEntryBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
) )
kind = forms.ChoiceField( kind = forms.ChoiceField(
label=_('Kind'),
choices=add_blank_choice(JournalEntryKindChoices), choices=add_blank_choice(JournalEntryKindChoices),
required=False required=False
) )
comments = forms.CharField( comments = forms.CharField(
label=_('Comments'),
required=False, required=False,
widget=forms.Textarea() widget=forms.Textarea()
) )

View File

@ -2,9 +2,9 @@ from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.forms import SimpleArrayField from django.contrib.postgres.forms import SimpleArrayField
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from extras.choices import CustomFieldVisibilityChoices, CustomFieldTypeChoices, JournalEntryKindChoices from extras.choices import *
from extras.models import * from extras.models import *
from extras.utils import FeatureQuery from extras.utils import FeatureQuery
from netbox.forms import NetBoxModelImportForm from netbox.forms import NetBoxModelImportForm
@ -28,27 +28,32 @@ __all__ = (
class CustomFieldImportForm(CSVModelForm): class CustomFieldImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField( content_types = CSVMultipleContentTypeField(
label=_('Content types'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'), limit_choices_to=FeatureQuery('custom_fields'),
help_text=_("One or more assigned object types") help_text=_("One or more assigned object types")
) )
type = CSVChoiceField( type = CSVChoiceField(
label=_('Type'),
choices=CustomFieldTypeChoices, choices=CustomFieldTypeChoices,
help_text=_('Field data type (e.g. text, integer, etc.)') help_text=_('Field data type (e.g. text, integer, etc.)')
) )
object_type = CSVContentTypeField( object_type = CSVContentTypeField(
label=_('Object type'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'), limit_choices_to=FeatureQuery('custom_fields'),
required=False, required=False,
help_text=_("Object type (for object or multi-object fields)") help_text=_("Object type (for object or multi-object fields)")
) )
choice_set = CSVModelChoiceField( choice_set = CSVModelChoiceField(
label=_('Choice set'),
queryset=CustomFieldChoiceSet.objects.all(), queryset=CustomFieldChoiceSet.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_('Choice set (for selection fields)') help_text=_('Choice set (for selection fields)')
) )
ui_visibility = CSVChoiceField( ui_visibility = CSVChoiceField(
label=_('UI visibility'),
choices=CustomFieldVisibilityChoices, choices=CustomFieldVisibilityChoices,
help_text=_('How the custom field is displayed in the user interface') help_text=_('How the custom field is displayed in the user interface')
) )
@ -63,6 +68,11 @@ class CustomFieldImportForm(CSVModelForm):
class CustomFieldChoiceSetImportForm(CSVModelForm): class CustomFieldChoiceSetImportForm(CSVModelForm):
base_choices = CSVChoiceField(
choices=CustomFieldChoiceSetBaseChoices,
required=False,
help_text=_('The base set of predefined choices to use (if any)')
)
extra_choices = SimpleArrayField( extra_choices = SimpleArrayField(
base_field=forms.CharField(), base_field=forms.CharField(),
required=False, required=False,
@ -78,6 +88,7 @@ class CustomFieldChoiceSetImportForm(CSVModelForm):
class CustomLinkImportForm(CSVModelForm): class CustomLinkImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField( content_types = CSVMultipleContentTypeField(
label=_('Content types'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_links'), limit_choices_to=FeatureQuery('custom_links'),
help_text=_("One or more assigned object types") help_text=_("One or more assigned object types")
@ -93,6 +104,7 @@ class CustomLinkImportForm(CSVModelForm):
class ExportTemplateImportForm(CSVModelForm): class ExportTemplateImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField( content_types = CSVMultipleContentTypeField(
label=_('Content types'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('export_templates'), limit_choices_to=FeatureQuery('export_templates'),
help_text=_("One or more assigned object types") help_text=_("One or more assigned object types")
@ -116,6 +128,7 @@ class ConfigTemplateImportForm(CSVModelForm):
class SavedFilterImportForm(CSVModelForm): class SavedFilterImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField( content_types = CSVMultipleContentTypeField(
label=_('Content types'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
help_text=_("One or more assigned object types") help_text=_("One or more assigned object types")
) )
@ -129,6 +142,7 @@ class SavedFilterImportForm(CSVModelForm):
class WebhookImportForm(CSVModelForm): class WebhookImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField( content_types = CSVMultipleContentTypeField(
label=_('Content types'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('webhooks'), limit_choices_to=FeatureQuery('webhooks'),
help_text=_("One or more assigned object types") help_text=_("One or more assigned object types")
@ -160,6 +174,7 @@ class JournalEntryImportForm(NetBoxModelImportForm):
label=_('Assigned object type'), label=_('Assigned object type'),
) )
kind = CSVChoiceField( kind = CSVChoiceField(
label=_('Kind'),
choices=JournalEntryKindChoices, choices=JournalEntryKindChoices,
help_text=_('The classification of entry') help_text=_('The classification of entry')
) )

View File

@ -1,7 +1,7 @@
from django import forms from django import forms
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from core.models import DataFile, DataSource from core.models import DataFile, DataSource
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
@ -11,7 +11,9 @@ from extras.utils import FeatureQuery
from netbox.forms.base import NetBoxModelFilterSetForm from netbox.forms.base import NetBoxModelFilterSetForm
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField from utilities.forms.fields import (
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
)
from utilities.forms.widgets import APISelectMultiple, DateTimePicker from utilities.forms.widgets import APISelectMultiple, DateTimePicker
from virtualization.models import Cluster, ClusterGroup, ClusterType from virtualization.models import Cluster, ClusterGroup, ClusterType
from .mixins import SavedFiltersMixin from .mixins import SavedFiltersMixin
@ -37,7 +39,7 @@ __all__ = (
class CustomFieldFilterForm(SavedFiltersMixin, FilterForm): class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
('Attributes', ( (_('Attributes'), (
'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visibility', 'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visibility',
'is_cloneable', 'is_cloneable',
)), )),
@ -53,12 +55,15 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
label=_('Field type') label=_('Field type')
) )
group_name = forms.CharField( group_name = forms.CharField(
label=_('Group name'),
required=False required=False
) )
weight = forms.IntegerField( weight = forms.IntegerField(
label=_('Weight'),
required=False required=False
) )
required = forms.NullBooleanField( required = forms.NullBooleanField(
label=_('Required'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
@ -75,6 +80,7 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
label=_('UI visibility') label=_('UI visibility')
) )
is_cloneable = forms.NullBooleanField( is_cloneable = forms.NullBooleanField(
label=_('Is cloneable'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
@ -84,7 +90,12 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm): class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'choice')), (None, ('q', 'filter_id')),
(_('Choices'), ('base_choices', 'choice')),
)
base_choices = forms.MultipleChoiceField(
choices=CustomFieldChoiceSetBaseChoices,
required=False
) )
choice = forms.CharField( choice = forms.CharField(
required=False required=False
@ -97,22 +108,26 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
(_('Attributes'), ('content_types', 'enabled', 'new_window', 'weight')), (_('Attributes'), ('content_types', 'enabled', 'new_window', 'weight')),
) )
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query()), queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query()),
required=False required=False
) )
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
new_window = forms.NullBooleanField( new_window = forms.NullBooleanField(
label=_('New window'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
weight = forms.IntegerField( weight = forms.IntegerField(
label=_('Weight'),
required=False required=False
) )
@ -120,8 +135,8 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm): class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
('Data', ('data_source_id', 'data_file_id')), (_('Data'), ('data_source_id', 'data_file_id')),
('Attributes', ('content_types', 'mime_type', 'file_extension', 'as_attachment')), (_('Attributes'), ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
) )
data_source_id = DynamicModelMultipleChoiceField( data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(), queryset=DataSource.objects.all(),
@ -137,6 +152,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
} }
) )
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()), queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
required=False required=False
) )
@ -145,9 +161,11 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
label=_('MIME type') label=_('MIME type')
) )
file_extension = forms.CharField( file_extension = forms.CharField(
label=_('File extension'),
required=False required=False
) )
as_attachment = forms.NullBooleanField( as_attachment = forms.NullBooleanField(
label=_('As attachment'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
@ -158,13 +176,15 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
('Attributes', ('content_type_id', 'name',)), (_('Attributes'), ('content_type_id', 'name',)),
) )
content_type_id = ContentTypeChoiceField( content_type_id = ContentTypeChoiceField(
label=_('Content type'),
queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()), queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
required=False required=False
) )
name = forms.CharField( name = forms.CharField(
label=_('Name'),
required=False required=False
) )
@ -172,25 +192,29 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
('Attributes', ('content_types', 'enabled', 'shared', 'weight')), (_('Attributes'), ('content_types', 'enabled', 'shared', 'weight')),
) )
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()), queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
required=False required=False
) )
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
shared = forms.NullBooleanField( shared = forms.NullBooleanField(
label=_('Shared'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
weight = forms.IntegerField( weight = forms.IntegerField(
label=_('Weight'),
required=False required=False
) )
@ -198,8 +222,8 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
class WebhookFilterForm(SavedFiltersMixin, FilterForm): class WebhookFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
('Attributes', ('content_type_id', 'http_method', 'enabled')), (_('Attributes'), ('content_type_id', 'http_method', 'enabled')),
('Events', ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
) )
content_type_id = ContentTypeMultipleChoiceField( content_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()), queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()),
@ -212,6 +236,7 @@ class WebhookFilterForm(SavedFiltersMixin, FilterForm):
label=_('HTTP method') label=_('HTTP method')
) )
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False, required=False,
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
@ -271,11 +296,11 @@ class TagFilterForm(SavedFiltersMixin, FilterForm):
class ConfigContextFilterForm(SavedFiltersMixin, FilterForm): class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag_id')), (None, ('q', 'filter_id', 'tag_id')),
('Data', ('data_source_id', 'data_file_id')), (_('Data'), ('data_source_id', 'data_file_id')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Device', ('device_type_id', 'platform_id', 'role_id')), (_('Device'), ('device_type_id', 'platform_id', 'role_id')),
('Cluster', ('cluster_type_id', 'cluster_group_id', 'cluster_id')), (_('Cluster'), ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
('Tenant', ('tenant_group_id', 'tenant_id')) (_('Tenant'), ('tenant_group_id', 'tenant_id'))
) )
data_source_id = DynamicModelMultipleChoiceField( data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(), queryset=DataSource.objects.all(),
@ -361,7 +386,7 @@ class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm): class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Data', ('data_source_id', 'data_file_id')), (_('Data'), ('data_source_id', 'data_file_id')),
) )
data_source_id = DynamicModelMultipleChoiceField( data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(), queryset=DataSource.objects.all(),
@ -393,8 +418,8 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
model = JournalEntry model = JournalEntry
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Creation', ('created_before', 'created_after', 'created_by_id')), (_('Creation'), ('created_before', 'created_after', 'created_by_id')),
('Attributes', ('assigned_object_type_id', 'kind')) (_('Attributes'), ('assigned_object_type_id', 'kind'))
) )
created_after = forms.DateTimeField( created_after = forms.DateTimeField(
required=False, required=False,
@ -423,6 +448,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
) )
) )
kind = forms.ChoiceField( kind = forms.ChoiceField(
label=_('Kind'),
choices=add_blank_choice(JournalEntryKindChoices), choices=add_blank_choice(JournalEntryKindChoices),
required=False required=False
) )
@ -433,8 +459,8 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
model = ObjectChange model = ObjectChange
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
('Time', ('time_before', 'time_after')), (_('Time'), ('time_before', 'time_after')),
('Attributes', ('action', 'user_id', 'changed_object_type_id')), (_('Attributes'), ('action', 'user_id', 'changed_object_type_id')),
) )
time_after = forms.DateTimeField( time_after = forms.DateTimeField(
required=False, required=False,
@ -447,6 +473,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
widget=DateTimePicker() widget=DateTimePicker()
) )
action = forms.ChoiceField( action = forms.ChoiceField(
label=_('Action'),
choices=add_blank_choice(ObjectChangeActionChoices), choices=add_blank_choice(ObjectChangeActionChoices),
required=False required=False
) )

View File

@ -1,4 +1,5 @@
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _
__all__ = ( __all__ = (
'RenderMarkdownForm', 'RenderMarkdownForm',
@ -10,5 +11,6 @@ class RenderMarkdownForm(forms.Form):
Provides basic validation for markup to be rendered. Provides basic validation for markup to be rendered.
""" """
text = forms.CharField( text = forms.CharField(
label=_('Text'),
required=False required=False
) )

View File

@ -4,7 +4,7 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.db.models import Q from django.db.models import Q
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from core.forms.mixins import SyncedDataMixin from core.forms.mixins import SyncedDataMixin
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
@ -19,7 +19,7 @@ from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, JSONField, SlugField, DynamicModelMultipleChoiceField, JSONField, SlugField,
) )
from utilities.forms.widgets import ArrayWidget from utilities.forms.widgets import ChoicesWidget
from virtualization.models import Cluster, ClusterGroup, ClusterType from virtualization.models import Cluster, ClusterGroup, ClusterType
@ -42,10 +42,12 @@ __all__ = (
class CustomFieldForm(BootstrapMixin, forms.ModelForm): class CustomFieldForm(BootstrapMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'), limit_choices_to=FeatureQuery('custom_fields'),
) )
object_type = ContentTypeChoiceField( object_type = ContentTypeChoiceField(
label=_('Object type'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
# TODO: Come up with a canonical way to register suitable models # TODO: Come up with a canonical way to register suitable models
limit_choices_to=FeatureQuery('webhooks').get_query() | Q(app_label='auth', model__in=['user', 'group']), limit_choices_to=FeatureQuery('webhooks').get_query() | Q(app_label='auth', model__in=['user', 'group']),
@ -58,12 +60,12 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
) )
fieldsets = ( fieldsets = (
('Custom Field', ( (_('Custom Field'), (
'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description', 'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description',
)), )),
('Behavior', ('search_weight', 'filter_logic', 'ui_visibility', 'weight', 'is_cloneable')), (_('Behavior'), ('search_weight', 'filter_logic', 'ui_visibility', 'weight', 'is_cloneable')),
('Values', ('default', 'choice_set')), (_('Values'), ('default', 'choice_set')),
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')), (_('Validation'), ('validation_minimum', 'validation_maximum', 'validation_regex')),
) )
class Meta: class Meta:
@ -86,27 +88,34 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm): class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
extra_choices = forms.CharField( extra_choices = forms.CharField(
widget=ArrayWidget(), widget=ChoicesWidget(),
help_text=_('Enter one choice per line.')
) )
class Meta: class Meta:
model = CustomFieldChoiceSet model = CustomFieldChoiceSet
fields = ('name', 'description', 'extra_choices', 'order_alphabetically') fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically')
def clean_extra_choices(self): def clean_extra_choices(self):
return self.cleaned_data['extra_choices'].splitlines() data = []
for line in self.cleaned_data['extra_choices'].splitlines():
try:
value, label = line.split(',', maxsplit=1)
except ValueError:
value, label = line, line
data.append((value, label))
return data
class CustomLinkForm(BootstrapMixin, forms.ModelForm): class CustomLinkForm(BootstrapMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_links') limit_choices_to=FeatureQuery('custom_links')
) )
fieldsets = ( fieldsets = (
('Custom Link', ('name', 'content_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')), (_('Custom Link'), ('name', 'content_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')),
('Templates', ('link_text', 'link_url')), (_('Templates'), ('link_text', 'link_url')),
) )
class Meta: class Meta:
@ -127,18 +136,20 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm): class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('export_templates') limit_choices_to=FeatureQuery('export_templates')
) )
template_code = forms.CharField( template_code = forms.CharField(
label=_('Template code'),
required=False, required=False,
widget=forms.Textarea(attrs={'class': 'font-monospace'}) widget=forms.Textarea(attrs={'class': 'font-monospace'})
) )
fieldsets = ( fieldsets = (
('Export Template', ('name', 'content_types', 'description', 'template_code')), (_('Export Template'), ('name', 'content_types', 'description', 'template_code')),
('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')), (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
('Rendering', ('mime_type', 'file_extension', 'as_attachment')), (_('Rendering'), ('mime_type', 'file_extension', 'as_attachment')),
) )
class Meta: class Meta:
@ -159,7 +170,7 @@ class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
super().clean() super().clean()
if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'): if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
raise forms.ValidationError("Must specify either local content or a data file") raise forms.ValidationError(_("Must specify either local content or a data file"))
return self.cleaned_data return self.cleaned_data
@ -167,13 +178,14 @@ class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
class SavedFilterForm(BootstrapMixin, forms.ModelForm): class SavedFilterForm(BootstrapMixin, forms.ModelForm):
slug = SlugField() slug = SlugField()
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.all() queryset=ContentType.objects.all()
) )
parameters = JSONField() parameters = JSONField()
fieldsets = ( fieldsets = (
('Saved Filter', ('name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared')), (_('Saved Filter'), ('name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared')),
('Parameters', ('parameters',)), (_('Parameters'), ('parameters',)),
) )
class Meta: class Meta:
@ -192,6 +204,7 @@ class SavedFilterForm(BootstrapMixin, forms.ModelForm):
class BookmarkForm(BootstrapMixin, forms.ModelForm): class BookmarkForm(BootstrapMixin, forms.ModelForm):
object_type = ContentTypeChoiceField( object_type = ContentTypeChoiceField(
label=_('Object type'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('bookmarks').get_query() limit_choices_to=FeatureQuery('bookmarks').get_query()
) )
@ -203,29 +216,30 @@ class BookmarkForm(BootstrapMixin, forms.ModelForm):
class WebhookForm(BootstrapMixin, forms.ModelForm): class WebhookForm(BootstrapMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('webhooks') limit_choices_to=FeatureQuery('webhooks')
) )
fieldsets = ( fieldsets = (
('Webhook', ('name', 'content_types', 'enabled')), (_('Webhook'), ('name', 'content_types', 'enabled')),
('Events', ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
('HTTP Request', ( (_('HTTP Request'), (
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
)), )),
('Conditions', ('conditions',)), (_('Conditions'), ('conditions',)),
('SSL', ('ssl_verification', 'ca_file_path')), (_('SSL'), ('ssl_verification', 'ca_file_path')),
) )
class Meta: class Meta:
model = Webhook model = Webhook
fields = '__all__' fields = '__all__'
labels = { labels = {
'type_create': 'Creations', 'type_create': _('Creations'),
'type_update': 'Updates', 'type_update': _('Updates'),
'type_delete': 'Deletions', 'type_delete': _('Deletions'),
'type_job_start': 'Job executions', 'type_job_start': _('Job executions'),
'type_job_end': 'Job terminations', 'type_job_end': _('Job terminations'),
} }
widgets = { widgets = {
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}), 'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
@ -237,6 +251,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
class TagForm(BootstrapMixin, forms.ModelForm): class TagForm(BootstrapMixin, forms.ModelForm):
slug = SlugField() slug = SlugField()
object_types = ContentTypeMultipleChoiceField( object_types = ContentTypeMultipleChoiceField(
label=_('Object types'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('tags'), limit_choices_to=FeatureQuery('tags'),
required=False required=False
@ -255,65 +270,79 @@ class TagForm(BootstrapMixin, forms.ModelForm):
class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm): class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
regions = DynamicModelMultipleChoiceField( regions = DynamicModelMultipleChoiceField(
label=_('Regions'),
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False required=False
) )
site_groups = DynamicModelMultipleChoiceField( site_groups = DynamicModelMultipleChoiceField(
label=_('Site groups'),
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
required=False required=False
) )
sites = DynamicModelMultipleChoiceField( sites = DynamicModelMultipleChoiceField(
label=_('Sites'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False required=False
) )
locations = DynamicModelMultipleChoiceField( locations = DynamicModelMultipleChoiceField(
label=_('Locations'),
queryset=Location.objects.all(), queryset=Location.objects.all(),
required=False required=False
) )
device_types = DynamicModelMultipleChoiceField( device_types = DynamicModelMultipleChoiceField(
label=_('Device types'),
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
required=False required=False
) )
roles = DynamicModelMultipleChoiceField( roles = DynamicModelMultipleChoiceField(
label=_('Roles'),
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
required=False required=False
) )
platforms = DynamicModelMultipleChoiceField( platforms = DynamicModelMultipleChoiceField(
label=_('Platforms'),
queryset=Platform.objects.all(), queryset=Platform.objects.all(),
required=False required=False
) )
cluster_types = DynamicModelMultipleChoiceField( cluster_types = DynamicModelMultipleChoiceField(
label=_('Cluster types'),
queryset=ClusterType.objects.all(), queryset=ClusterType.objects.all(),
required=False required=False
) )
cluster_groups = DynamicModelMultipleChoiceField( cluster_groups = DynamicModelMultipleChoiceField(
label=_('Cluster groups'),
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
required=False required=False
) )
clusters = DynamicModelMultipleChoiceField( clusters = DynamicModelMultipleChoiceField(
label=_('Clusters'),
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
required=False required=False
) )
tenant_groups = DynamicModelMultipleChoiceField( tenant_groups = DynamicModelMultipleChoiceField(
label=_('Tenat groups'),
queryset=TenantGroup.objects.all(), queryset=TenantGroup.objects.all(),
required=False required=False
) )
tenants = DynamicModelMultipleChoiceField( tenants = DynamicModelMultipleChoiceField(
label=_('Tenants'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False
) )
tags = DynamicModelMultipleChoiceField( tags = DynamicModelMultipleChoiceField(
label=_('Tags'),
queryset=Tag.objects.all(), queryset=Tag.objects.all(),
required=False required=False
) )
data = JSONField( data = JSONField(
label=_('Data'),
required=False required=False
) )
fieldsets = ( fieldsets = (
('Config Context', ('name', 'weight', 'description', 'data', 'is_active')), (_('Config Context'), ('name', 'weight', 'description', 'data', 'is_active')),
('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')), (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
('Assignment', ( (_('Assignment'), (
'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
)), )),
@ -345,25 +374,27 @@ class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
super().clean() super().clean()
if not self.cleaned_data.get('data') and not self.cleaned_data.get('data_file'): if not self.cleaned_data.get('data') and not self.cleaned_data.get('data_file'):
raise forms.ValidationError("Must specify either local data or a data file") raise forms.ValidationError(_("Must specify either local data or a data file"))
return self.cleaned_data return self.cleaned_data
class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm): class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
tags = DynamicModelMultipleChoiceField( tags = DynamicModelMultipleChoiceField(
label=_('Tags'),
queryset=Tag.objects.all(), queryset=Tag.objects.all(),
required=False required=False
) )
template_code = forms.CharField( template_code = forms.CharField(
label=_('Template code'),
required=False, required=False,
widget=forms.Textarea(attrs={'class': 'font-monospace'}) widget=forms.Textarea(attrs={'class': 'font-monospace'})
) )
fieldsets = ( fieldsets = (
('Config Template', ('name', 'description', 'environment_params', 'tags')), (_('Config Template'), ('name', 'description', 'environment_params', 'tags')),
('Content', ('template_code',)), (_('Content'), ('template_code',)),
('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')), (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
) )
class Meta: class Meta:
@ -387,7 +418,7 @@ class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
super().clean() super().clean()
if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'): if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
raise forms.ValidationError("Must specify either local content or a data file") raise forms.ValidationError(_("Must specify either local content or a data file"))
return self.cleaned_data return self.cleaned_data
@ -403,6 +434,7 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
class JournalEntryForm(NetBoxModelForm): class JournalEntryForm(NetBoxModelForm):
kind = forms.ChoiceField( kind = forms.ChoiceField(
label=_('Kind'),
choices=add_blank_choice(JournalEntryKindChoices), choices=add_blank_choice(JournalEntryKindChoices),
required=False required=False
) )
@ -445,16 +477,16 @@ class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMe
""" """
fieldsets = ( fieldsets = (
('Rack Elevations', ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')), (_('Rack Elevations'), ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')),
('Power', ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')), (_('Power'), ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')),
('IPAM', ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')), (_('IPAM'), ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')),
('Security', ('ALLOWED_URL_SCHEMES',)), (_('Security'), ('ALLOWED_URL_SCHEMES',)),
('Banners', ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')), (_('Banners'), ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')),
('Pagination', ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')), (_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
('Validation', ('CUSTOM_VALIDATORS',)), (_('Validation'), ('CUSTOM_VALIDATORS',)),
('User Preferences', ('DEFAULT_USER_PREFERENCES',)), (_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)),
('Miscellaneous', ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL')), (_('Miscellaneous'), ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL')),
('Config Revision', ('comment',)) (_('Config Revision'), ('comment',))
) )
class Meta: class Meta:
@ -481,11 +513,11 @@ class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMe
help_text = self.fields[param.name].help_text help_text = self.fields[param.name].help_text
if help_text: if help_text:
help_text += '<br />' # Line break help_text += '<br />' # Line break
help_text += f'Current value: <strong>{value}</strong>' help_text += _('Current value: <strong>{value}</strong>').format(value=value)
if is_static: if is_static:
help_text += ' (defined statically)' help_text += _(' (defined statically)')
elif value == param.default: elif value == param.default:
help_text += ' (default)' help_text += _(' (default)')
self.fields[param.name].help_text = help_text self.fields[param.name].help_text = help_text
self.fields[param.name].initial = value self.fields[param.name].initial = value
if is_static: if is_static:

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from extras.choices import DurationChoices from extras.choices import DurationChoices
from utilities.forms import BootstrapMixin from utilities.forms import BootstrapMixin
@ -33,7 +33,7 @@ class ReportForm(BootstrapMixin, forms.Form):
# Annotate the current system time for reference # Annotate the current system time for reference
now = local_now().strftime('%Y-%m-%d %H:%M:%S') now = local_now().strftime('%Y-%m-%d %H:%M:%S')
self.fields['schedule_at'].help_text += f' (current time: <strong>{now}</strong>)' self.fields['schedule_at'].help_text += _(' (current time: <strong>{now}</strong>)').format(now=now)
# Remove scheduling fields if scheduling is disabled # Remove scheduling fields if scheduling is disabled
if not scheduling_enabled: if not scheduling_enabled:

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from extras.choices import DurationChoices from extras.choices import DurationChoices
from utilities.forms import BootstrapMixin from utilities.forms import BootstrapMixin
@ -39,7 +39,7 @@ class ScriptForm(BootstrapMixin, forms.Form):
# Annotate the current system time for reference # Annotate the current system time for reference
now = local_now().strftime('%Y-%m-%d %H:%M:%S') now = local_now().strftime('%Y-%m-%d %H:%M:%S')
self.fields['_schedule_at'].help_text += f' (current time: <strong>{now}</strong>)' self.fields['_schedule_at'].help_text += _(' (current time: <strong>{now}</strong>)').format(now=now)
# Remove scheduling fields if scheduling is disabled # Remove scheduling fields if scheduling is disabled
if not scheduling_enabled: if not scheduling_enabled:

View File

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

View File

@ -3,6 +3,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from extras.choices import * from extras.choices import *
from ..querysets import ObjectChangeQuerySet from ..querysets import ObjectChangeQuerySet
@ -19,6 +20,7 @@ class ObjectChange(models.Model):
parent device. This will ensure changes made to component models appear in the parent model's changelog. parent device. This will ensure changes made to component models appear in the parent model's changelog.
""" """
time = models.DateTimeField( time = models.DateTimeField(
verbose_name=_('time'),
auto_now_add=True, auto_now_add=True,
editable=False, editable=False,
db_index=True db_index=True
@ -31,14 +33,17 @@ class ObjectChange(models.Model):
null=True null=True
) )
user_name = models.CharField( user_name = models.CharField(
verbose_name=_('user name'),
max_length=150, max_length=150,
editable=False editable=False
) )
request_id = models.UUIDField( request_id = models.UUIDField(
verbose_name=_('request ID'),
editable=False, editable=False,
db_index=True db_index=True
) )
action = models.CharField( action = models.CharField(
verbose_name=_('action'),
max_length=50, max_length=50,
choices=ObjectChangeActionChoices choices=ObjectChangeActionChoices
) )
@ -72,11 +77,13 @@ class ObjectChange(models.Model):
editable=False editable=False
) )
prechange_data = models.JSONField( prechange_data = models.JSONField(
verbose_name=_('pre-change data'),
editable=False, editable=False,
blank=True, blank=True,
null=True null=True
) )
postchange_data = models.JSONField( postchange_data = models.JSONField(
verbose_name=_('post-change data'),
editable=False, editable=False,
blank=True, blank=True,
null=True null=True

View File

@ -2,7 +2,7 @@ from django.conf import settings
from django.core.validators import ValidationError from django.core.validators import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from jinja2.loaders import BaseLoader from jinja2.loaders import BaseLoader
from jinja2.sandbox import SandboxedEnvironment from jinja2.sandbox import SandboxedEnvironment
@ -31,17 +31,21 @@ class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel):
will be available to a Device in site A assigned to tenant B. Data is stored in JSON format. will be available to a Device in site A assigned to tenant B. Data is stored in JSON format.
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100, max_length=100,
unique=True unique=True
) )
weight = models.PositiveSmallIntegerField( weight = models.PositiveSmallIntegerField(
verbose_name=_('weight'),
default=1000 default=1000
) )
description = models.CharField( description = models.CharField(
verbose_name=_('description'),
max_length=200, max_length=200,
blank=True blank=True
) )
is_active = models.BooleanField( is_active = models.BooleanField(
verbose_name=_('is active'),
default=True, default=True,
) )
regions = models.ManyToManyField( regions = models.ManyToManyField(
@ -138,7 +142,7 @@ class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel):
# Verify that JSON data is provided as an object # Verify that JSON data is provided as an object
if type(self.data) is not dict: if type(self.data) is not dict:
raise ValidationError( raise ValidationError(
{'data': 'JSON data must be in object form. Example: {"foo": 123}'} {'data': _('JSON data must be in object form. Example: {"foo": 123}')}
) )
def sync_data(self): def sync_data(self):
@ -194,7 +198,7 @@ class ConfigContextModel(models.Model):
# Verify that JSON data is provided as an object # Verify that JSON data is provided as an object
if self.local_context_data and type(self.local_context_data) is not dict: if self.local_context_data and type(self.local_context_data) is not dict:
raise ValidationError( raise ValidationError(
{'local_context_data': 'JSON data must be in object form. Example: {"foo": 123}'} {'local_context_data': _('JSON data must be in object form. Example: {"foo": 123}')}
) )
@ -204,16 +208,20 @@ class ConfigContextModel(models.Model):
class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100 max_length=100
) )
description = models.CharField( description = models.CharField(
verbose_name=_('description'),
max_length=200, max_length=200,
blank=True blank=True
) )
template_code = models.TextField( template_code = models.TextField(
verbose_name=_('template code'),
help_text=_('Jinja2 template code.') help_text=_('Jinja2 template code.')
) )
environment_params = models.JSONField( environment_params = models.JSONField(
verbose_name=_('environment parameters'),
blank=True, blank=True,
null=True, null=True,
default=dict, default=dict,

View File

@ -12,20 +12,21 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from extras.choices import * from extras.choices import *
from extras.data import CHOICE_SETS
from extras.utils import FeatureQuery from extras.utils import FeatureQuery
from netbox.models import ChangeLoggedModel from netbox.models import ChangeLoggedModel
from netbox.models.features import CloningMixin, ExportTemplatesMixin from netbox.models.features import CloningMixin, ExportTemplatesMixin
from netbox.search import FieldTypes from netbox.search import FieldTypes
from utilities import filters from utilities import filters
from utilities.forms.fields import ( from utilities.forms.fields import (
CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DynamicModelChoiceField, CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DynamicChoiceField,
DynamicModelMultipleChoiceField, JSONField, LaxURLField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, DynamicMultipleChoiceField, JSONField, LaxURLField,
) )
from utilities.forms.utils import add_blank_choice from utilities.forms.utils import add_blank_choice
from utilities.forms.widgets import DatePicker, DateTimePicker from utilities.forms.widgets import APISelect, APISelectMultiple, DatePicker, DateTimePicker
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.validators import validate_regex from utilities.validators import validate_regex
@ -64,6 +65,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
help_text=_('The object(s) to which this field applies.') help_text=_('The object(s) to which this field applies.')
) )
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=CustomFieldTypeChoices, choices=CustomFieldTypeChoices,
default=CustomFieldTypeChoices.TYPE_TEXT, default=CustomFieldTypeChoices.TYPE_TEXT,
@ -77,83 +79,93 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
help_text=_('The type of NetBox object this field maps to (for object fields)') help_text=_('The type of NetBox object this field maps to (for object fields)')
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=50, max_length=50,
unique=True, unique=True,
help_text=_('Internal field name'), help_text=_('Internal field name'),
validators=( validators=(
RegexValidator( RegexValidator(
regex=r'^[a-z0-9_]+$', regex=r'^[a-z0-9_]+$',
message="Only alphanumeric characters and underscores are allowed.", message=_("Only alphanumeric characters and underscores are allowed."),
flags=re.IGNORECASE flags=re.IGNORECASE
), ),
RegexValidator( RegexValidator(
regex=r'__', regex=r'__',
message="Double underscores are not permitted in custom field names.", message=_("Double underscores are not permitted in custom field names."),
flags=re.IGNORECASE, flags=re.IGNORECASE,
inverse_match=True inverse_match=True
), ),
) )
) )
label = models.CharField( label = models.CharField(
verbose_name=_('label'),
max_length=50, max_length=50,
blank=True, blank=True,
help_text=_('Name of the field as displayed to users (if not provided, ' help_text=_(
'the field\'s name will be used)') "Name of the field as displayed to users (if not provided, 'the field's name will be used)"
)
) )
group_name = models.CharField( group_name = models.CharField(
verbose_name=_('group name'),
max_length=50, max_length=50,
blank=True, blank=True,
help_text=_("Custom fields within the same group will be displayed together") help_text=_("Custom fields within the same group will be displayed together")
) )
description = models.CharField( description = models.CharField(
verbose_name=_('description'),
max_length=200, max_length=200,
blank=True blank=True
) )
required = models.BooleanField( required = models.BooleanField(
verbose_name=_('required'),
default=False, default=False,
help_text=_('If true, this field is required when creating new objects ' help_text=_("If true, this field is required when creating new objects or editing an existing object.")
'or editing an existing object.')
) )
search_weight = models.PositiveSmallIntegerField( search_weight = models.PositiveSmallIntegerField(
verbose_name=_('search weight'),
default=1000, default=1000,
help_text=_('Weighting for search. Lower values are considered more important. ' help_text=_(
'Fields with a search weight of zero will be ignored.') "Weighting for search. Lower values are considered more important. Fields with a search weight of zero "
"will be ignored."
)
) )
filter_logic = models.CharField( filter_logic = models.CharField(
verbose_name=_('filter logic'),
max_length=50, max_length=50,
choices=CustomFieldFilterLogicChoices, choices=CustomFieldFilterLogicChoices,
default=CustomFieldFilterLogicChoices.FILTER_LOOSE, default=CustomFieldFilterLogicChoices.FILTER_LOOSE,
help_text=_('Loose matches any instance of a given string; exact ' help_text=_("Loose matches any instance of a given string; exact matches the entire field.")
'matches the entire field.')
) )
default = models.JSONField( default = models.JSONField(
verbose_name=_('default'),
blank=True, blank=True,
null=True, null=True,
help_text=_('Default value for the field (must be a JSON value). Encapsulate ' help_text=_(
'strings with double quotes (e.g. "Foo").') 'Default value for the field (must be a JSON value). Encapsulate strings with double quotes (e.g. "Foo").'
)
) )
weight = models.PositiveSmallIntegerField( weight = models.PositiveSmallIntegerField(
default=100, default=100,
verbose_name='Display weight', verbose_name=_('display weight'),
help_text=_('Fields with higher weights appear lower in a form.') help_text=_('Fields with higher weights appear lower in a form.')
) )
validation_minimum = models.IntegerField( validation_minimum = models.IntegerField(
blank=True, blank=True,
null=True, null=True,
verbose_name='Minimum value', verbose_name=_('minimum value'),
help_text=_('Minimum allowed value (for numeric fields)') help_text=_('Minimum allowed value (for numeric fields)')
) )
validation_maximum = models.IntegerField( validation_maximum = models.IntegerField(
blank=True, blank=True,
null=True, null=True,
verbose_name='Maximum value', verbose_name=_('maximum value'),
help_text=_('Maximum allowed value (for numeric fields)') help_text=_('Maximum allowed value (for numeric fields)')
) )
validation_regex = models.CharField( validation_regex = models.CharField(
blank=True, blank=True,
validators=[validate_regex], validators=[validate_regex],
max_length=500, max_length=500,
verbose_name='Validation regex', verbose_name=_('validation regex'),
help_text=_( help_text=_(
'Regular expression to enforce on text field values. Use ^ and $ to force matching of entire string. For ' 'Regular expression to enforce on text field values. Use ^ and $ to force matching of entire string. For '
'example, <code>^[A-Z]{3}$</code> will limit values to exactly three uppercase letters.' 'example, <code>^[A-Z]{3}$</code> will limit values to exactly three uppercase letters.'
@ -163,6 +175,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
to='CustomFieldChoiceSet', to='CustomFieldChoiceSet',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='choices_for', related_name='choices_for',
verbose_name=_('choice set'),
blank=True, blank=True,
null=True null=True
) )
@ -170,12 +183,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
max_length=50, max_length=50,
choices=CustomFieldVisibilityChoices, choices=CustomFieldVisibilityChoices,
default=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE, default=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
verbose_name='UI visibility', verbose_name=_('UI visibility'),
help_text=_('Specifies the visibility of custom field in the UI') help_text=_('Specifies the visibility of custom field in the UI')
) )
is_cloneable = models.BooleanField( is_cloneable = models.BooleanField(
default=False, default=False,
verbose_name='Cloneable', verbose_name=_('is cloneable'),
help_text=_('Replicate this value when cloning objects') help_text=_('Replicate this value when cloning objects')
) )
@ -265,15 +278,17 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
self.validate(default_value) self.validate(default_value)
except ValidationError as err: except ValidationError as err:
raise ValidationError({ raise ValidationError({
'default': f'Invalid default value "{self.default}": {err.message}' 'default': _(
'Invalid default value "{default}": {message}'
).format(default=self.default, message=self.message)
}) })
# Minimum/maximum values can be set only for numeric fields # Minimum/maximum values can be set only for numeric fields
if self.type not in (CustomFieldTypeChoices.TYPE_INTEGER, CustomFieldTypeChoices.TYPE_DECIMAL): if self.type not in (CustomFieldTypeChoices.TYPE_INTEGER, CustomFieldTypeChoices.TYPE_DECIMAL):
if self.validation_minimum: if self.validation_minimum:
raise ValidationError({'validation_minimum': "A minimum value may be set only for numeric fields"}) raise ValidationError({'validation_minimum': _("A minimum value may be set only for numeric fields")})
if self.validation_maximum: if self.validation_maximum:
raise ValidationError({'validation_maximum': "A maximum value may be set only for numeric fields"}) raise ValidationError({'validation_maximum': _("A maximum value may be set only for numeric fields")})
# Regex validation can be set only for text fields # Regex validation can be set only for text fields
regex_types = ( regex_types = (
@ -283,7 +298,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
) )
if self.validation_regex and self.type not in regex_types: if self.validation_regex and self.type not in regex_types:
raise ValidationError({ raise ValidationError({
'validation_regex': "Regular expression validation is supported only for text and URL fields" 'validation_regex': _("Regular expression validation is supported only for text and URL fields")
}) })
# Choice set must be set on selection fields, and *only* on selection fields # Choice set must be set on selection fields, and *only* on selection fields
@ -293,28 +308,32 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
): ):
if not self.choice_set: if not self.choice_set:
raise ValidationError({ raise ValidationError({
'choice_set': "Selection fields must specify a set of choices." 'choice_set': _("Selection fields must specify a set of choices.")
}) })
elif self.choice_set: elif self.choice_set:
raise ValidationError({ raise ValidationError({
'choice_set': "Choices may be set only on selection fields." 'choice_set': _("Choices may be set only on selection fields.")
}) })
# A selection field's default (if any) must be present in its available choices # A selection field's default (if any) must be present in its available choices
if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.default and self.default not in self.choices: if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.default and self.default not in self.choices:
raise ValidationError({ raise ValidationError({
'default': f"The specified default value ({self.default}) is not listed as an available choice." 'default': _(
"The specified default value ({default}) is not listed as an available choice."
).format(default=self.default)
}) })
# Object fields must define an object_type; other fields must not # Object fields must define an object_type; other fields must not
if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT): if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
if not self.object_type: if not self.object_type:
raise ValidationError({ raise ValidationError({
'object_type': "Object fields must define an object type." 'object_type': _("Object fields must define an object type.")
}) })
elif self.object_type: elif self.object_type:
raise ValidationError({ raise ValidationError({
'object_type': f"{self.get_type_display()} fields may not define an object type." 'object_type': _(
"{type_display} fields may not define an object type.")
.format(type_display=self.get_type_display())
}) })
def serialize(self, value): def serialize(self, value):
@ -393,8 +412,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN: elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
choices = ( choices = (
(None, '---------'), (None, '---------'),
(True, 'True'), (True, _('True')),
(False, 'False'), (False, _('False')),
) )
field = forms.NullBooleanField( field = forms.NullBooleanField(
required=required, initial=initial, widget=forms.Select(choices=choices) required=required, initial=initial, widget=forms.Select(choices=choices)
@ -410,7 +429,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Select # Select
elif self.type in (CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT): elif self.type in (CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT):
choices = [(c, c) for c in self.choices] choices = self.choice_set.choices
default_choice = self.default if self.default in self.choices else None default_choice = self.default if self.default in self.choices else None
if not required or default_choice is None: if not required or default_choice is None:
@ -421,11 +440,17 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
initial = default_choice initial = default_choice
if self.type == CustomFieldTypeChoices.TYPE_SELECT: if self.type == CustomFieldTypeChoices.TYPE_SELECT:
field_class = CSVChoiceField if for_csv_import else forms.ChoiceField field_class = CSVChoiceField if for_csv_import else DynamicChoiceField
field = field_class(choices=choices, required=required, initial=initial) widget_class = APISelect
else: else:
field_class = CSVMultipleChoiceField if for_csv_import else forms.MultipleChoiceField field_class = CSVMultipleChoiceField if for_csv_import else DynamicMultipleChoiceField
field = field_class(choices=choices, required=required, initial=initial) widget_class = APISelectMultiple
field = field_class(
choices=choices,
required=required,
initial=initial,
widget=widget_class(api_url=f'/api/extras/custom-field-choices/{self.choice_set.pk}/choices/')
)
# URL # URL
elif self.type == CustomFieldTypeChoices.TYPE_URL: elif self.type == CustomFieldTypeChoices.TYPE_URL:
@ -463,7 +488,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
field.validators = [ field.validators = [
RegexValidator( RegexValidator(
regex=self.validation_regex, regex=self.validation_regex,
message=mark_safe(f"Values must match this regex: <code>{self.validation_regex}</code>") message=mark_safe(_("Values must match this regex: <code>{regex}</code>").format(
regex=self.validation_regex
))
) )
] ]
@ -476,7 +503,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
if enforce_visibility and self.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY: if enforce_visibility and self.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
field.disabled = True field.disabled = True
prepend = '<br />' if field.help_text else '' prepend = '<br />' if field.help_text else ''
field.help_text += f'{prepend}<i class="mdi mdi-alert-circle-outline"></i> Field is set to read-only.' field.help_text += f'{prepend}<i class="mdi mdi-alert-circle-outline"></i> ' + _('Field is set to read-only.')
return field return field
@ -558,33 +585,41 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Validate text field # Validate text field
if self.type in (CustomFieldTypeChoices.TYPE_TEXT, CustomFieldTypeChoices.TYPE_LONGTEXT): if self.type in (CustomFieldTypeChoices.TYPE_TEXT, CustomFieldTypeChoices.TYPE_LONGTEXT):
if type(value) is not str: if type(value) is not str:
raise ValidationError(f"Value must be a string.") raise ValidationError(_("Value must be a string."))
if self.validation_regex and not re.match(self.validation_regex, value): if self.validation_regex and not re.match(self.validation_regex, value):
raise ValidationError(f"Value must match regex '{self.validation_regex}'") raise ValidationError(_("Value must match regex '{regex}'").format(regex=self.validation_regex))
# Validate integer # Validate integer
elif self.type == CustomFieldTypeChoices.TYPE_INTEGER: elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
if type(value) is not int: if type(value) is not int:
raise ValidationError("Value must be an integer.") raise ValidationError(_("Value must be an integer."))
if self.validation_minimum is not None and value < self.validation_minimum: if self.validation_minimum is not None and value < self.validation_minimum:
raise ValidationError(f"Value must be at least {self.validation_minimum}") raise ValidationError(
_("Value must be at least {minimum}").format(minimum=self.validation_maximum)
)
if self.validation_maximum is not None and value > self.validation_maximum: if self.validation_maximum is not None and value > self.validation_maximum:
raise ValidationError(f"Value must not exceed {self.validation_maximum}") raise ValidationError(
_("Value must not exceed {maximum}").format(maximum=self.validation_maximum)
)
# Validate decimal # Validate decimal
elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL: elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
try: try:
decimal.Decimal(value) decimal.Decimal(value)
except decimal.InvalidOperation: except decimal.InvalidOperation:
raise ValidationError("Value must be a decimal.") raise ValidationError(_("Value must be a decimal."))
if self.validation_minimum is not None and value < self.validation_minimum: if self.validation_minimum is not None and value < self.validation_minimum:
raise ValidationError(f"Value must be at least {self.validation_minimum}") raise ValidationError(
_("Value must be at least {minimum}").format(minimum=self.validation_minimum)
)
if self.validation_maximum is not None and value > self.validation_maximum: if self.validation_maximum is not None and value > self.validation_maximum:
raise ValidationError(f"Value must not exceed {self.validation_maximum}") raise ValidationError(
_("Value must not exceed {maximum}").format(maximum=self.validation_maximum)
)
# Validate boolean # Validate boolean
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]: elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
raise ValidationError("Value must be true or false.") raise ValidationError(_("Value must be true or false."))
# Validate date # Validate date
elif self.type == CustomFieldTypeChoices.TYPE_DATE: elif self.type == CustomFieldTypeChoices.TYPE_DATE:
@ -592,7 +627,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
try: try:
date.fromisoformat(value) date.fromisoformat(value)
except ValueError: except ValueError:
raise ValidationError("Date values must be in ISO 8601 format (YYYY-MM-DD).") raise ValidationError(_("Date values must be in ISO 8601 format (YYYY-MM-DD)."))
# Validate date & time # Validate date & time
elif self.type == CustomFieldTypeChoices.TYPE_DATETIME: elif self.type == CustomFieldTypeChoices.TYPE_DATETIME:
@ -600,37 +635,44 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
try: try:
datetime.fromisoformat(value) datetime.fromisoformat(value)
except ValueError: except ValueError:
raise ValidationError("Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS).") raise ValidationError(
_("Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS).")
)
# Validate selected choice # Validate selected choice
elif self.type == CustomFieldTypeChoices.TYPE_SELECT: elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
if value not in self.choices: if value not in [c[0] for c in self.choices]:
raise ValidationError( raise ValidationError(
f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}" _("Invalid choice ({value}). Available choices are: {choices}").format(
value=value, choices=', '.join(self.choices)
)
) )
# Validate all selected choices # Validate all selected choices
elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT: elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
if not set(value).issubset(self.choices): if not set(value).issubset([c[0] for c in self.choices]):
raise ValidationError( raise ValidationError(
f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}" _("Invalid choice(s) ({invalid_choices}). Available choices are: {available_choices}").format(
invalid_choices=', '.join(value), available_choices=', '.join(self.choices))
) )
# Validate selected object # Validate selected object
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT: elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
if type(value) is not int: if type(value) is not int:
raise ValidationError(f"Value must be an object ID, not {type(value).__name__}") raise ValidationError(_("Value must be an object ID, not {type}").format(type=type(value).__name__))
# Validate selected objects # Validate selected objects
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
if type(value) is not list: if type(value) is not list:
raise ValidationError(f"Value must be a list of object IDs, not {type(value).__name__}") raise ValidationError(
_("Value must be a list of object IDs, not {type}").format(type=type(value).__name__)
)
for id in value: for id in value:
if type(id) is not int: if type(id) is not int:
raise ValidationError(f"Found invalid object ID: {id}") raise ValidationError(_("Found invalid object ID: {id}").format(id=id))
elif self.required: elif self.required:
raise ValidationError("Required field cannot be empty.") raise ValidationError(_("Required field cannot be empty."))
class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
@ -645,13 +687,23 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
max_length=200, max_length=200,
blank=True blank=True
) )
base_choices = models.CharField(
max_length=50,
choices=CustomFieldChoiceSetBaseChoices,
blank=True,
help_text=_('Base set of predefined choices (optional)')
)
extra_choices = ArrayField( extra_choices = ArrayField(
ArrayField(
base_field=models.CharField(max_length=100), base_field=models.CharField(max_length=100),
help_text=_('List of field choices') size=2
),
blank=True,
null=True
) )
order_alphabetically = models.BooleanField( order_alphabetically = models.BooleanField(
default=False, default=False,
help_text=_('Choices are automatically ordered alphabetically on save') help_text=_('Choices are automatically ordered alphabetically')
) )
clone_fields = ('extra_choices', 'order_alphabetically') clone_fields = ('extra_choices', 'order_alphabetically')
@ -667,16 +719,31 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
@property @property
def choices(self): def choices(self):
return self.extra_choices """
Returns a concatenation of the base and extra choices.
"""
if not hasattr(self, '_choices'):
self._choices = []
if self.base_choices:
self._choices.extend(CHOICE_SETS.get(self.base_choices))
if self.extra_choices:
self._choices.extend(self.extra_choices)
if self.order_alphabetically:
self._choices = sorted(self._choices, key=lambda x: x[0])
return self._choices
@property @property
def choices_count(self): def choices_count(self):
return len(self.choices) return len(self.choices)
def clean(self):
if not self.base_choices and not self.extra_choices:
raise ValidationError(_("Must define base or extra choices."))
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Sort choices if alphabetical ordering is enforced # Sort choices if alphabetical ordering is enforced
if self.order_alphabetically: if self.order_alphabetically:
self.extra_choices = sorted(self.choices) self.extra_choices = sorted(self.extra_choices, key=lambda x: x[0])
return super().save(*args, **kwargs) return super().save(*args, **kwargs)

View File

@ -1,5 +1,6 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _
from extras.dashboard.utils import get_widget_class from extras.dashboard.utils import get_widget_class
@ -15,9 +16,11 @@ class Dashboard(models.Model):
related_name='dashboard' related_name='dashboard'
) )
layout = models.JSONField( layout = models.JSONField(
verbose_name=_('layout'),
default=list default=list
) )
config = models.JSONField( config = models.JSONField(
verbose_name=_('config'),
default=dict default=dict
) )

View File

@ -12,7 +12,7 @@ from django.http import HttpResponse
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from rest_framework.utils.encoders import JSONEncoder from rest_framework.utils.encoders import JSONEncoder
from extras.choices import * from extras.choices import *
@ -48,93 +48,113 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel):
content_types = models.ManyToManyField( content_types = models.ManyToManyField(
to=ContentType, to=ContentType,
related_name='webhooks', related_name='webhooks',
verbose_name='Object types', verbose_name=_('object types'),
limit_choices_to=FeatureQuery('webhooks'), limit_choices_to=FeatureQuery('webhooks'),
help_text=_("The object(s) to which this Webhook applies.") help_text=_("The object(s) to which this Webhook applies.")
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=150, max_length=150,
unique=True unique=True
) )
type_create = models.BooleanField( type_create = models.BooleanField(
verbose_name=_('on create'),
default=False, default=False,
help_text=_("Triggers when a matching object is created.") help_text=_("Triggers when a matching object is created.")
) )
type_update = models.BooleanField( type_update = models.BooleanField(
verbose_name=_('on update'),
default=False, default=False,
help_text=_("Triggers when a matching object is updated.") help_text=_("Triggers when a matching object is updated.")
) )
type_delete = models.BooleanField( type_delete = models.BooleanField(
verbose_name=_('on delete'),
default=False, default=False,
help_text=_("Triggers when a matching object is deleted.") help_text=_("Triggers when a matching object is deleted.")
) )
type_job_start = models.BooleanField( type_job_start = models.BooleanField(
verbose_name=_('on job start'),
default=False, default=False,
help_text=_("Triggers when a job for a matching object is started.") help_text=_("Triggers when a job for a matching object is started.")
) )
type_job_end = models.BooleanField( type_job_end = models.BooleanField(
verbose_name=_('on job end'),
default=False, default=False,
help_text=_("Triggers when a job for a matching object terminates.") help_text=_("Triggers when a job for a matching object terminates.")
) )
payload_url = models.CharField( payload_url = models.CharField(
max_length=500, max_length=500,
verbose_name='URL', verbose_name=_('URL'),
help_text=_('This URL will be called using the HTTP method defined when the webhook is called. ' help_text=_(
'Jinja2 template processing is supported with the same context as the request body.') "This URL will be called using the HTTP method defined when the webhook is called. Jinja2 template "
"processing is supported with the same context as the request body."
)
) )
enabled = models.BooleanField( enabled = models.BooleanField(
verbose_name=_('enabled'),
default=True default=True
) )
http_method = models.CharField( http_method = models.CharField(
max_length=30, max_length=30,
choices=WebhookHttpMethodChoices, choices=WebhookHttpMethodChoices,
default=WebhookHttpMethodChoices.METHOD_POST, default=WebhookHttpMethodChoices.METHOD_POST,
verbose_name='HTTP method' verbose_name=_('HTTP method')
) )
http_content_type = models.CharField( http_content_type = models.CharField(
max_length=100, max_length=100,
default=HTTP_CONTENT_TYPE_JSON, default=HTTP_CONTENT_TYPE_JSON,
verbose_name='HTTP content type', verbose_name=_('HTTP content type'),
help_text=_('The complete list of official content types is available ' help_text=_(
'<a href="https://www.iana.org/assignments/media-types/media-types.xhtml">here</a>.') 'The complete list of official content types is available '
'<a href="https://www.iana.org/assignments/media-types/media-types.xhtml">here</a>.'
)
) )
additional_headers = models.TextField( additional_headers = models.TextField(
verbose_name=_('additional headers'),
blank=True, blank=True,
help_text=_("User-supplied HTTP headers to be sent with the request in addition to the HTTP content type. " help_text=_(
"Headers should be defined in the format <code>Name: Value</code>. Jinja2 template processing is " "User-supplied HTTP headers to be sent with the request in addition to the HTTP content type. Headers "
"supported with the same context as the request body (below).") "should be defined in the format <code>Name: Value</code>. Jinja2 template processing is supported with "
"the same context as the request body (below)."
)
) )
body_template = models.TextField( body_template = models.TextField(
verbose_name=_('body template'),
blank=True, blank=True,
help_text=_('Jinja2 template for a custom request body. If blank, a JSON object representing the change will be ' help_text=_(
'included. Available context data includes: <code>event</code>, <code>model</code>, ' "Jinja2 template for a custom request body. If blank, a JSON object representing the change will be "
'<code>timestamp</code>, <code>username</code>, <code>request_id</code>, and <code>data</code>.') "included. Available context data includes: <code>event</code>, <code>model</code>, "
"<code>timestamp</code>, <code>username</code>, <code>request_id</code>, and <code>data</code>."
)
) )
secret = models.CharField( secret = models.CharField(
verbose_name=_('secret'),
max_length=255, max_length=255,
blank=True, blank=True,
help_text=_("When provided, the request will include a 'X-Hook-Signature' " help_text=_(
"header containing a HMAC hex digest of the payload body using " "When provided, the request will include a <code>X-Hook-Signature</code> header containing a HMAC hex "
"the secret as the key. The secret is not transmitted in " "digest of the payload body using the secret as the key. The secret is not transmitted in the request."
"the request.") )
) )
conditions = models.JSONField( conditions = models.JSONField(
verbose_name=_('conditions'),
blank=True, blank=True,
null=True, null=True,
help_text=_("A set of conditions which determine whether the webhook will be generated.") help_text=_("A set of conditions which determine whether the webhook will be generated.")
) )
ssl_verification = models.BooleanField( ssl_verification = models.BooleanField(
default=True, default=True,
verbose_name='SSL verification', verbose_name=_('SSL verification'),
help_text=_("Enable SSL certificate verification. Disable with caution!") help_text=_("Enable SSL certificate verification. Disable with caution!")
) )
ca_file_path = models.CharField( ca_file_path = models.CharField(
max_length=4096, max_length=4096,
null=True, null=True,
blank=True, blank=True,
verbose_name='CA File Path', verbose_name=_('CA File Path'),
help_text=_('The specific CA certificate file to use for SSL verification. ' help_text=_(
'Leave blank to use the system defaults.') "The specific CA certificate file to use for SSL verification. Leave blank to use the system defaults."
)
) )
class Meta: class Meta:
@ -164,7 +184,7 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel):
self.type_create, self.type_update, self.type_delete, self.type_job_start, self.type_job_end self.type_create, self.type_update, self.type_delete, self.type_job_start, self.type_job_end
]): ]):
raise ValidationError( raise ValidationError(
"At least one event type must be selected: create, update, delete, job_start, and/or job_end." _("At least one event type must be selected: create, update, delete, job_start, and/or job_end.")
) )
if self.conditions: if self.conditions:
@ -176,7 +196,7 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel):
# CA file path requires SSL verification enabled # CA file path requires SSL verification enabled
if not self.ssl_verification and self.ca_file_path: if not self.ssl_verification and self.ca_file_path:
raise ValidationError({ raise ValidationError({
'ca_file_path': 'Do not specify a CA certificate file if SSL verification is disabled.' 'ca_file_path': _('Do not specify a CA certificate file if SSL verification is disabled.')
}) })
def render_headers(self, context): def render_headers(self, context):
@ -219,34 +239,41 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
help_text=_('The object type(s) to which this link applies.') help_text=_('The object type(s) to which this link applies.')
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100, max_length=100,
unique=True unique=True
) )
enabled = models.BooleanField( enabled = models.BooleanField(
verbose_name=_('enabled'),
default=True default=True
) )
link_text = models.TextField( link_text = models.TextField(
verbose_name=_('link text'),
help_text=_("Jinja2 template code for link text") help_text=_("Jinja2 template code for link text")
) )
link_url = models.TextField( link_url = models.TextField(
verbose_name='Link URL', verbose_name=_('link URL'),
help_text=_("Jinja2 template code for link URL") help_text=_("Jinja2 template code for link URL")
) )
weight = models.PositiveSmallIntegerField( weight = models.PositiveSmallIntegerField(
verbose_name=_('weight'),
default=100 default=100
) )
group_name = models.CharField( group_name = models.CharField(
verbose_name=_('group name'),
max_length=50, max_length=50,
blank=True, blank=True,
help_text=_("Links with the same group will appear as a dropdown menu") help_text=_("Links with the same group will appear as a dropdown menu")
) )
button_class = models.CharField( button_class = models.CharField(
verbose_name=_('button class'),
max_length=30, max_length=30,
choices=CustomLinkButtonClassChoices, choices=CustomLinkButtonClassChoices,
default=CustomLinkButtonClassChoices.DEFAULT, default=CustomLinkButtonClassChoices.DEFAULT,
help_text=_("The class of the first link in a group will be used for the dropdown button") help_text=_("The class of the first link in a group will be used for the dropdown button")
) )
new_window = models.BooleanField( new_window = models.BooleanField(
verbose_name=_('new window'),
default=False, default=False,
help_text=_("Force link to open in a new window") help_text=_("Force link to open in a new window")
) )
@ -306,28 +333,34 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
help_text=_('The object type(s) to which this template applies.') help_text=_('The object type(s) to which this template applies.')
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100 max_length=100
) )
description = models.CharField( description = models.CharField(
verbose_name=_('description'),
max_length=200, max_length=200,
blank=True blank=True
) )
template_code = models.TextField( template_code = models.TextField(
help_text=_('Jinja2 template code. The list of objects being exported is passed as a context variable named ' help_text=_(
'<code>queryset</code>.') "Jinja2 template code. The list of objects being exported is passed as a context variable named "
"<code>queryset</code>."
)
) )
mime_type = models.CharField( mime_type = models.CharField(
max_length=50, max_length=50,
blank=True, blank=True,
verbose_name='MIME type', verbose_name=_('MIME type'),
help_text=_('Defaults to <code>text/plain; charset=utf-8</code>') help_text=_('Defaults to <code>text/plain; charset=utf-8</code>')
) )
file_extension = models.CharField( file_extension = models.CharField(
verbose_name=_('file extension'),
max_length=15, max_length=15,
blank=True, blank=True,
help_text=_('Extension to append to the rendered filename') help_text=_('Extension to append to the rendered filename')
) )
as_attachment = models.BooleanField( as_attachment = models.BooleanField(
verbose_name=_('as attachment'),
default=True, default=True,
help_text=_("Download file as attachment") help_text=_("Download file as attachment")
) )
@ -354,7 +387,7 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
if self.name.lower() == 'table': if self.name.lower() == 'table':
raise ValidationError({ raise ValidationError({
'name': f'"{self.name}" is a reserved name. Please choose a different name.' 'name': _('"{name}" is a reserved name. Please choose a different name.').format(name=self.name)
}) })
def sync_data(self): def sync_data(self):
@ -407,14 +440,17 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
help_text=_('The object type(s) to which this filter applies.') help_text=_('The object type(s) to which this filter applies.')
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100, max_length=100,
unique=True unique=True
) )
slug = models.SlugField( slug = models.SlugField(
verbose_name=_('slug'),
max_length=100, max_length=100,
unique=True unique=True
) )
description = models.CharField( description = models.CharField(
verbose_name=_('description'),
max_length=200, max_length=200,
blank=True blank=True
) )
@ -425,15 +461,20 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
null=True null=True
) )
weight = models.PositiveSmallIntegerField( weight = models.PositiveSmallIntegerField(
verbose_name=_('weight'),
default=100 default=100
) )
enabled = models.BooleanField( enabled = models.BooleanField(
verbose_name=_('enabled'),
default=True default=True
) )
shared = models.BooleanField( shared = models.BooleanField(
verbose_name=_('shared'),
default=True default=True
) )
parameters = models.JSONField() parameters = models.JSONField(
verbose_name=_('parameters')
)
clone_fields = ( clone_fields = (
'content_types', 'weight', 'enabled', 'parameters', 'content_types', 'weight', 'enabled', 'parameters',
@ -458,7 +499,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Verify that `parameters` is a JSON object # Verify that `parameters` is a JSON object
if type(self.parameters) is not dict: if type(self.parameters) is not dict:
raise ValidationError( raise ValidationError(
{'parameters': 'Filter parameters must be stored as a dictionary of keyword arguments.'} {'parameters': _('Filter parameters must be stored as a dictionary of keyword arguments.')}
) )
@property @property
@ -485,9 +526,14 @@ class ImageAttachment(ChangeLoggedModel):
height_field='image_height', height_field='image_height',
width_field='image_width' width_field='image_width'
) )
image_height = models.PositiveSmallIntegerField() image_height = models.PositiveSmallIntegerField(
image_width = models.PositiveSmallIntegerField() verbose_name=_('image height'),
)
image_width = models.PositiveSmallIntegerField(
verbose_name=_('image width'),
)
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=50, max_length=50,
blank=True blank=True
) )
@ -565,11 +611,14 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
null=True null=True
) )
kind = models.CharField( kind = models.CharField(
verbose_name=_('kind'),
max_length=30, max_length=30,
choices=JournalEntryKindChoices, choices=JournalEntryKindChoices,
default=JournalEntryKindChoices.KIND_INFO default=JournalEntryKindChoices.KIND_INFO
) )
comments = models.TextField() comments = models.TextField(
verbose_name=_('comments'),
)
class Meta: class Meta:
ordering = ('-created',) ordering = ('-created',)
@ -588,7 +637,9 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
# Prevent the creation of journal entries on unsupported models # Prevent the creation of journal entries on unsupported models
permitted_types = ContentType.objects.filter(FeatureQuery('journaling').get_query()) permitted_types = ContentType.objects.filter(FeatureQuery('journaling').get_query())
if self.assigned_object_type not in permitted_types: if self.assigned_object_type not in permitted_types:
raise ValidationError(f"Journaling is not supported for this object type ({self.assigned_object_type}).") raise ValidationError(
_("Journaling is not supported for this object type ({type}).").format(type=self.assigned_object_type)
)
def get_kind_color(self): def get_kind_color(self):
return JournalEntryKindChoices.colors.get(self.kind) return JournalEntryKindChoices.colors.get(self.kind)
@ -599,6 +650,7 @@ class Bookmark(models.Model):
An object bookmarked by a User. An object bookmarked by a User.
""" """
created = models.DateTimeField( created = models.DateTimeField(
verbose_name=_('created'),
auto_now_add=True auto_now_add=True
) )
object_type = models.ForeignKey( object_type = models.ForeignKey(
@ -637,16 +689,18 @@ class ConfigRevision(models.Model):
An atomic revision of NetBox's configuration. An atomic revision of NetBox's configuration.
""" """
created = models.DateTimeField( created = models.DateTimeField(
verbose_name=_('created'),
auto_now_add=True auto_now_add=True
) )
comment = models.CharField( comment = models.CharField(
verbose_name=_('comment'),
max_length=200, max_length=200,
blank=True blank=True
) )
data = models.JSONField( data = models.JSONField(
blank=True, blank=True,
null=True, null=True,
verbose_name='Configuration data' verbose_name=_('configuration data')
) )
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()

View File

@ -2,6 +2,7 @@ import uuid
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _
from utilities.fields import RestrictedGenericForeignKey from utilities.fields import RestrictedGenericForeignKey
from ..fields import CachedValueField from ..fields import CachedValueField
@ -18,6 +19,7 @@ class CachedValue(models.Model):
editable=False editable=False
) )
timestamp = models.DateTimeField( timestamp = models.DateTimeField(
verbose_name=_('timestamp'),
auto_now_add=True, auto_now_add=True,
editable=False editable=False
) )
@ -32,13 +34,18 @@ class CachedValue(models.Model):
fk_field='object_id' fk_field='object_id'
) )
field = models.CharField( field = models.CharField(
verbose_name=_('field'),
max_length=200 max_length=200
) )
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=30 max_length=30
) )
value = CachedValueField() value = CachedValueField(
verbose_name=_('value'),
)
weight = models.PositiveSmallIntegerField( weight = models.PositiveSmallIntegerField(
verbose_name=_('weight'),
default=1000 default=1000
) )

View File

@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models, transaction from django.db import models, transaction
from django.utils.translation import gettext_lazy as _
from extras.choices import ChangeActionChoices from extras.choices import ChangeActionChoices
from netbox.models import ChangeLoggedModel from netbox.models import ChangeLoggedModel
@ -22,10 +23,12 @@ class Branch(ChangeLoggedModel):
A collection of related StagedChanges. A collection of related StagedChanges.
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100, max_length=100,
unique=True unique=True
) )
description = models.CharField( description = models.CharField(
verbose_name=_('description'),
max_length=200, max_length=200,
blank=True blank=True
) )
@ -61,6 +64,7 @@ class StagedChange(ChangeLoggedModel):
related_name='staged_changes' related_name='staged_changes'
) )
action = models.CharField( action = models.CharField(
verbose_name=_('action'),
max_length=20, max_length=20,
choices=ChangeActionChoices choices=ChangeActionChoices
) )
@ -78,6 +82,7 @@ class StagedChange(ChangeLoggedModel):
fk_field='object_id' fk_field='object_id'
) )
data = models.JSONField( data = models.JSONField(
verbose_name=_('data'),
blank=True, blank=True,
null=True null=True
) )

View File

@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from taggit.models import TagBase, GenericTaggedItemBase from taggit.models import TagBase, GenericTaggedItemBase
from extras.utils import FeatureQuery from extras.utils import FeatureQuery
@ -28,9 +28,11 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
primary_key=True primary_key=True
) )
color = ColorField( color = ColorField(
verbose_name=_('color'),
default=ColorChoices.COLOR_GREY default=ColorChoices.COLOR_GREY
) )
description = models.CharField( description = models.CharField(
verbose_name=_('description'),
max_length=200, max_length=200,
blank=True, blank=True,
) )

View File

@ -66,18 +66,22 @@ class CustomFieldTable(NetBoxTable):
linkify=True linkify=True
) )
content_types = columns.ContentTypesColumn( content_types = columns.ContentTypesColumn(
verbose_name=_('Content Types'), verbose_name=_('Content Types')
) )
required = columns.BooleanColumn( required = columns.BooleanColumn(
verbose_name=_('Required'), verbose_name=_('Required')
) )
ui_visibility = columns.ChoiceFieldColumn( ui_visibility = columns.ChoiceFieldColumn(
verbose_name=_("UI Visibility") verbose_name=_('UI Visibility')
) )
description = columns.MarkdownColumn( description = columns.MarkdownColumn(
verbose_name=_('Description'), verbose_name=_('Description')
) )
choices = columns.ArrayColumn( choice_set = tables.Column(
linkify=True,
verbose_name=_('Choice set')
)
choices = columns.ChoicesColumn(
max_items=10, max_items=10,
orderable=False, orderable=False,
verbose_name=_('Choices') verbose_name=_('Choices')
@ -90,8 +94,8 @@ class CustomFieldTable(NetBoxTable):
model = CustomField model = CustomField
fields = ( fields = (
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description', 'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description',
'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'weight', 'choices', 'created', 'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'weight', 'choice_set', 'choices',
'last_updated', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description') default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
@ -101,11 +105,13 @@ class CustomFieldChoiceSetTable(NetBoxTable):
verbose_name=_('Name'), verbose_name=_('Name'),
linkify=True linkify=True
) )
choices = columns.ArrayColumn( base_choices = columns.ChoiceFieldColumn()
extra_choices = tables.TemplateColumn(
template_code="""{% for k, v in value.items %}{{ v }}{% if not forloop.last %}, {% endif %}{% endfor %}"""
)
choices = columns.ChoicesColumn(
max_items=10, max_items=10,
accessor=tables.A('extra_choices'), orderable=False
orderable=False,
verbose_name=_('Choices')
) )
choice_count = tables.TemplateColumn( choice_count = tables.TemplateColumn(
accessor=tables.A('extra_choices'), accessor=tables.A('extra_choices'),
@ -120,10 +126,10 @@ class CustomFieldChoiceSetTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = CustomFieldChoiceSet model = CustomFieldChoiceSet
fields = ( fields = (
'pk', 'id', 'name', 'description', 'choice_count', 'choices', 'order_alphabetically', 'created', 'pk', 'id', 'name', 'description', 'base_choices', 'extra_choices', 'choice_count', 'choices',
'last_updated', 'order_alphabetically', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'choice_count', 'description') default_columns = ('pk', 'name', 'base_choices', 'choice_count', 'description')
class CustomLinkTable(NetBoxTable): class CustomLinkTable(NetBoxTable):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from utilities.forms import BootstrapMixin from utilities.forms import BootstrapMixin
from utilities.forms.fields import ExpandableIPAddressField from utilities.forms.fields import ExpandableIPAddressField

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from dcim.models import Region, Site, SiteGroup from dcim.models import Region, Site, SiteGroup
from ipam.choices import * from ipam.choices import *
@ -37,6 +37,7 @@ __all__ = (
class VRFBulkEditForm(NetBoxModelBulkEditForm): class VRFBulkEditForm(NetBoxModelBulkEditForm):
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False
) )
@ -46,12 +47,11 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
label=_('Enforce unique space') label=_('Enforce unique space')
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label='Comments'
)
model = VRF model = VRF
fieldsets = ( fieldsets = (
@ -62,16 +62,16 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm):
class RouteTargetBulkEditForm(NetBoxModelBulkEditForm): class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label='Comments'
)
model = RouteTarget model = RouteTarget
fieldsets = ( fieldsets = (
@ -82,10 +82,12 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm):
class RIRBulkEditForm(NetBoxModelBulkEditForm): class RIRBulkEditForm(NetBoxModelBulkEditForm):
is_private = forms.NullBooleanField( is_private = forms.NullBooleanField(
label=_('Is private'),
required=False, required=False,
widget=BulkEditNullBooleanSelect widget=BulkEditNullBooleanSelect
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
@ -104,10 +106,12 @@ class ASNRangeBulkEditForm(NetBoxModelBulkEditForm):
label=_('RIR') label=_('RIR')
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
@ -121,6 +125,7 @@ class ASNRangeBulkEditForm(NetBoxModelBulkEditForm):
class ASNBulkEditForm(NetBoxModelBulkEditForm): class ASNBulkEditForm(NetBoxModelBulkEditForm):
sites = DynamicModelMultipleChoiceField( sites = DynamicModelMultipleChoiceField(
label=_('Sites'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False required=False
) )
@ -130,16 +135,16 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm):
label=_('RIR') label=_('RIR')
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label='Comments'
)
model = ASN model = ASN
fieldsets = ( fieldsets = (
@ -155,19 +160,20 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
label=_('RIR') label=_('RIR')
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False
) )
date_added = forms.DateField( date_added = forms.DateField(
label=_('Date added'),
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label='Comments'
)
model = Aggregate model = Aggregate
fieldsets = ( fieldsets = (
@ -178,9 +184,11 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm):
class RoleBulkEditForm(NetBoxModelBulkEditForm): class RoleBulkEditForm(NetBoxModelBulkEditForm):
weight = forms.IntegerField( weight = forms.IntegerField(
label=_('Weight'),
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
@ -194,14 +202,17 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm):
class PrefixBulkEditForm(NetBoxModelBulkEditForm): class PrefixBulkEditForm(NetBoxModelBulkEditForm):
region = DynamicModelChoiceField( region = DynamicModelChoiceField(
label=_('Region'),
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False required=False
) )
site_group = DynamicModelChoiceField( site_group = DynamicModelChoiceField(
label=_('Site group'),
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
required=False required=False
) )
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -215,19 +226,23 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
label=_('VRF') label=_('VRF')
) )
prefix_length = forms.IntegerField( prefix_length = forms.IntegerField(
label=_('Prefix length'),
min_value=PREFIX_LENGTH_MIN, min_value=PREFIX_LENGTH_MIN,
max_value=PREFIX_LENGTH_MAX, max_value=PREFIX_LENGTH_MAX,
required=False required=False
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False
) )
status = forms.ChoiceField( status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(PrefixStatusChoices), choices=add_blank_choice(PrefixStatusChoices),
required=False required=False
) )
role = DynamicModelChoiceField( role = DynamicModelChoiceField(
label=_('Role'),
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False required=False
) )
@ -242,18 +257,17 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
label=_('Treat as 100% utilized') label=_('Treat as 100% utilized')
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label='Comments'
)
model = Prefix model = Prefix
fieldsets = ( fieldsets = (
(None, ('tenant', 'status', 'role', 'description')), (None, ('tenant', 'status', 'role', 'description')),
('Site', ('region', 'site_group', 'site')), (_('Site'), ('region', 'site_group', 'site')),
('Addressing', ('vrf', 'prefix_length', 'is_pool', 'mark_utilized')), (_('Addressing'), ('vrf', 'prefix_length', 'is_pool', 'mark_utilized')),
) )
nullable_fields = ( nullable_fields = (
'site', 'vrf', 'tenant', 'role', 'description', 'comments', 'site', 'vrf', 'tenant', 'role', 'description', 'comments',
@ -267,14 +281,17 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
label=_('VRF') label=_('VRF')
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False
) )
status = forms.ChoiceField( status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(IPRangeStatusChoices), choices=add_blank_choice(IPRangeStatusChoices),
required=False required=False
) )
role = DynamicModelChoiceField( role = DynamicModelChoiceField(
label=_('Role'),
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False required=False
) )
@ -284,12 +301,11 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
label=_('Treat as 100% utilized') label=_('Treat as 100% utilized')
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label='Comments'
)
model = IPRange model = IPRange
fieldsets = ( fieldsets = (
@ -307,19 +323,23 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
label=_('VRF') label=_('VRF')
) )
mask_length = forms.IntegerField( mask_length = forms.IntegerField(
label=_('Mask length'),
min_value=IPADDRESS_MASK_LENGTH_MIN, min_value=IPADDRESS_MASK_LENGTH_MIN,
max_value=IPADDRESS_MASK_LENGTH_MAX, max_value=IPADDRESS_MASK_LENGTH_MAX,
required=False required=False
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False
) )
status = forms.ChoiceField( status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(IPAddressStatusChoices), choices=add_blank_choice(IPAddressStatusChoices),
required=False required=False
) )
role = forms.ChoiceField( role = forms.ChoiceField(
label=_('Role'),
choices=add_blank_choice(IPAddressRoleChoices), choices=add_blank_choice(IPAddressRoleChoices),
required=False required=False
) )
@ -329,17 +349,16 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
label=_('DNS name') label=_('DNS name')
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label='Comments'
)
model = IPAddress model = IPAddress
fieldsets = ( fieldsets = (
(None, ('status', 'role', 'tenant', 'description')), (None, ('status', 'role', 'tenant', 'description')),
('Addressing', ('vrf', 'mask_length', 'dns_name')), (_('Addressing'), ('vrf', 'mask_length', 'dns_name')),
) )
nullable_fields = ( nullable_fields = (
'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments', 'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
@ -348,6 +367,7 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
protocol = forms.ChoiceField( protocol = forms.ChoiceField(
label=_('Protocol'),
choices=add_blank_choice(FHRPGroupProtocolChoices), choices=add_blank_choice(FHRPGroupProtocolChoices),
required=False required=False
) )
@ -367,27 +387,28 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm):
label=_('Authentication key') label=_('Authentication key')
) )
name = forms.CharField( name = forms.CharField(
label=_('Name'),
max_length=100, max_length=100,
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label='Comments'
)
model = FHRPGroup model = FHRPGroup
fieldsets = ( fieldsets = (
(None, ('protocol', 'group_id', 'name', 'description')), (None, ('protocol', 'group_id', 'name', 'description')),
('Authentication', ('auth_type', 'auth_key')), (_('Authentication'), ('auth_type', 'auth_key')),
) )
nullable_fields = ('auth_type', 'auth_key', 'name', 'description', 'comments') nullable_fields = ('auth_type', 'auth_key', 'name', 'description', 'comments')
class VLANGroupBulkEditForm(NetBoxModelBulkEditForm): class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False required=False
) )
@ -404,6 +425,7 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
label=_('Maximum child VLAN VID') label=_('Maximum child VLAN VID')
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
@ -417,14 +439,17 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm):
class VLANBulkEditForm(NetBoxModelBulkEditForm): class VLANBulkEditForm(NetBoxModelBulkEditForm):
region = DynamicModelChoiceField( region = DynamicModelChoiceField(
label=_('Region'),
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False required=False
) )
site_group = DynamicModelChoiceField( site_group = DynamicModelChoiceField(
label=_('Site group'),
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
required=False required=False
) )
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -433,6 +458,7 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
} }
) )
group = DynamicModelChoiceField( group = DynamicModelChoiceField(
label=_('Group'),
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -440,29 +466,31 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
} }
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False
) )
status = forms.ChoiceField( status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(VLANStatusChoices), choices=add_blank_choice(VLANStatusChoices),
required=False required=False
) )
role = DynamicModelChoiceField( role = DynamicModelChoiceField(
label=_('Role'),
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label='Comments'
)
model = VLAN model = VLAN
fieldsets = ( fieldsets = (
(None, ('status', 'role', 'tenant', 'description')), (None, ('status', 'role', 'tenant', 'description')),
('Site & Group', ('region', 'site_group', 'site', 'group')), (_('Site & Group'), ('region', 'site_group', 'site', 'group')),
) )
nullable_fields = ( nullable_fields = (
'site', 'group', 'tenant', 'role', 'description', 'comments', 'site', 'group', 'tenant', 'role', 'description', 'comments',
@ -471,10 +499,12 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm):
class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm): class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
protocol = forms.ChoiceField( protocol = forms.ChoiceField(
label=_('Protocol'),
choices=add_blank_choice(ServiceProtocolChoices), choices=add_blank_choice(ServiceProtocolChoices),
required=False required=False
) )
ports = NumericArrayField( ports = NumericArrayField(
label=_('Ports'),
base_field=forms.IntegerField( base_field=forms.IntegerField(
min_value=SERVICE_PORT_MIN, min_value=SERVICE_PORT_MIN,
max_value=SERVICE_PORT_MAX max_value=SERVICE_PORT_MAX
@ -482,12 +512,11 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label='Comments'
)
model = ServiceTemplate model = ServiceTemplate
fieldsets = ( fieldsets = (
@ -502,20 +531,21 @@ class ServiceBulkEditForm(ServiceTemplateBulkEditForm):
class L2VPNBulkEditForm(NetBoxModelBulkEditForm): class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
type = forms.ChoiceField( type = forms.ChoiceField(
label=_('Type'),
choices=add_blank_choice(L2VPNTypeChoices), choices=add_blank_choice(L2VPNTypeChoices),
required=False required=False
) )
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False
) )
description = forms.CharField( description = forms.CharField(
label=_('Description'),
max_length=200, max_length=200,
required=False required=False
) )
comments = CommentField( comments = CommentField()
label='Comments'
)
model = L2VPN model = L2VPN
fieldsets = ( fieldsets = (

View File

@ -2,7 +2,7 @@ from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface, Site from dcim.models import Device, Interface, Site
from ipam.choices import * from ipam.choices import *
@ -36,6 +36,7 @@ __all__ = (
class VRFImportForm(NetBoxModelImportForm): class VRFImportForm(NetBoxModelImportForm):
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -49,6 +50,7 @@ class VRFImportForm(NetBoxModelImportForm):
class RouteTargetImportForm(NetBoxModelImportForm): class RouteTargetImportForm(NetBoxModelImportForm):
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -70,11 +72,13 @@ class RIRImportForm(NetBoxModelImportForm):
class AggregateImportForm(NetBoxModelImportForm): class AggregateImportForm(NetBoxModelImportForm):
rir = CSVModelChoiceField( rir = CSVModelChoiceField(
label=_('RIR'),
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Assigned RIR') help_text=_('Assigned RIR')
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -88,11 +92,13 @@ class AggregateImportForm(NetBoxModelImportForm):
class ASNRangeImportForm(NetBoxModelImportForm): class ASNRangeImportForm(NetBoxModelImportForm):
rir = CSVModelChoiceField( rir = CSVModelChoiceField(
label=_('RIR'),
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Assigned RIR') help_text=_('Assigned RIR')
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -106,11 +112,13 @@ class ASNRangeImportForm(NetBoxModelImportForm):
class ASNImportForm(NetBoxModelImportForm): class ASNImportForm(NetBoxModelImportForm):
rir = CSVModelChoiceField( rir = CSVModelChoiceField(
label=_('RIR'),
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
to_field_name='name', to_field_name='name',
help_text=_('Assigned RIR') help_text=_('Assigned RIR')
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -132,40 +140,47 @@ class RoleImportForm(NetBoxModelImportForm):
class PrefixImportForm(NetBoxModelImportForm): class PrefixImportForm(NetBoxModelImportForm):
vrf = CSVModelChoiceField( vrf = CSVModelChoiceField(
label=_('VRF'),
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_('Assigned VRF') help_text=_('Assigned VRF')
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Assigned tenant') help_text=_('Assigned tenant')
) )
site = CSVModelChoiceField( site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Assigned site') help_text=_('Assigned site')
) )
vlan_group = CSVModelChoiceField( vlan_group = CSVModelChoiceField(
label=_('VLAN group'),
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_("VLAN's group (if any)") help_text=_("VLAN's group (if any)")
) )
vlan = CSVModelChoiceField( vlan = CSVModelChoiceField(
label=_('VLAN'),
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
required=False, required=False,
to_field_name='vid', to_field_name='vid',
help_text=_("Assigned VLAN") help_text=_("Assigned VLAN")
) )
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'),
choices=PrefixStatusChoices, choices=PrefixStatusChoices,
help_text=_('Operational status') help_text=_('Operational status')
) )
role = CSVModelChoiceField( role = CSVModelChoiceField(
label=_('Role'),
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -211,22 +226,26 @@ class PrefixImportForm(NetBoxModelImportForm):
class IPRangeImportForm(NetBoxModelImportForm): class IPRangeImportForm(NetBoxModelImportForm):
vrf = CSVModelChoiceField( vrf = CSVModelChoiceField(
label=_('VRF'),
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_('Assigned VRF') help_text=_('Assigned VRF')
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Assigned tenant') help_text=_('Assigned tenant')
) )
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'),
choices=IPRangeStatusChoices, choices=IPRangeStatusChoices,
help_text=_('Operational status') help_text=_('Operational status')
) )
role = CSVModelChoiceField( role = CSVModelChoiceField(
label=_('Role'),
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -243,45 +262,53 @@ class IPRangeImportForm(NetBoxModelImportForm):
class IPAddressImportForm(NetBoxModelImportForm): class IPAddressImportForm(NetBoxModelImportForm):
vrf = CSVModelChoiceField( vrf = CSVModelChoiceField(
label=_('VRF'),
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_('Assigned VRF') help_text=_('Assigned VRF')
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_('Assigned tenant') help_text=_('Assigned tenant')
) )
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'),
choices=IPAddressStatusChoices, choices=IPAddressStatusChoices,
help_text=_('Operational status') help_text=_('Operational status')
) )
role = CSVChoiceField( role = CSVChoiceField(
label=_('Role'),
choices=IPAddressRoleChoices, choices=IPAddressRoleChoices,
required=False, required=False,
help_text=_('Functional role') help_text=_('Functional role')
) )
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Parent device of assigned interface (if any)') help_text=_('Parent device of assigned interface (if any)')
) )
virtual_machine = CSVModelChoiceField( virtual_machine = CSVModelChoiceField(
label=_('Virtual machine'),
queryset=VirtualMachine.objects.all(), queryset=VirtualMachine.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Parent VM of assigned interface (if any)') help_text=_('Parent VM of assigned interface (if any)')
) )
interface = CSVModelChoiceField( interface = CSVModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.none(), # Can also refer to VMInterface queryset=Interface.objects.none(), # Can also refer to VMInterface
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Assigned interface') help_text=_('Assigned interface')
) )
is_primary = forms.BooleanField( is_primary = forms.BooleanField(
label=_('Is primary'),
help_text=_('Make this the primary IP for the assigned device'), help_text=_('Make this the primary IP for the assigned device'),
required=False required=False
) )
@ -321,11 +348,11 @@ class IPAddressImportForm(NetBoxModelImportForm):
# Validate is_primary # Validate is_primary
if is_primary and not device and not virtual_machine: if is_primary and not device and not virtual_machine:
raise forms.ValidationError({ raise forms.ValidationError({
"is_primary": "No device or virtual machine specified; cannot set as primary IP" "is_primary": _("No device or virtual machine specified; cannot set as primary IP")
}) })
if is_primary and not interface: if is_primary and not interface:
raise forms.ValidationError({ raise forms.ValidationError({
"is_primary": "No interface specified; cannot set as primary IP" "is_primary": _("No interface specified; cannot set as primary IP")
}) })
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -350,9 +377,11 @@ class IPAddressImportForm(NetBoxModelImportForm):
class FHRPGroupImportForm(NetBoxModelImportForm): class FHRPGroupImportForm(NetBoxModelImportForm):
protocol = CSVChoiceField( protocol = CSVChoiceField(
label=_('Protocol'),
choices=FHRPGroupProtocolChoices choices=FHRPGroupProtocolChoices
) )
auth_type = CSVChoiceField( auth_type = CSVChoiceField(
label=_('Auth type'),
choices=FHRPGroupAuthTypeChoices, choices=FHRPGroupAuthTypeChoices,
required=False required=False
) )
@ -373,13 +402,13 @@ class VLANGroupImportForm(NetBoxModelImportForm):
min_value=VLAN_VID_MIN, min_value=VLAN_VID_MIN,
max_value=VLAN_VID_MAX, max_value=VLAN_VID_MAX,
required=False, required=False,
label=f'Minimum child VLAN VID (default: {VLAN_VID_MIN})' label=_('Minimum child VLAN VID (default: {minimum})').format(minimum=VLAN_VID_MIN)
) )
max_vid = forms.IntegerField( max_vid = forms.IntegerField(
min_value=VLAN_VID_MIN, min_value=VLAN_VID_MIN,
max_value=VLAN_VID_MAX, max_value=VLAN_VID_MAX,
required=False, required=False,
label=f'Maximum child VLAN VID (default: {VLAN_VID_MIN})' label=_('Maximum child VLAN VID (default: {maximum})').format(maximum=VLAN_VID_MIN)
) )
class Meta: class Meta:
@ -392,28 +421,33 @@ class VLANGroupImportForm(NetBoxModelImportForm):
class VLANImportForm(NetBoxModelImportForm): class VLANImportForm(NetBoxModelImportForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Assigned site') help_text=_('Assigned site')
) )
group = CSVModelChoiceField( group = CSVModelChoiceField(
label=_('Group'),
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Assigned VLAN group') help_text=_('Assigned VLAN group')
) )
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='name', to_field_name='name',
required=False, required=False,
help_text=_('Assigned tenant') help_text=_('Assigned tenant')
) )
status = CSVChoiceField( status = CSVChoiceField(
label=_('Status'),
choices=VLANStatusChoices, choices=VLANStatusChoices,
help_text=_('Operational status') help_text=_('Operational status')
) )
role = CSVModelChoiceField( role = CSVModelChoiceField(
label=_('Role'),
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -427,6 +461,7 @@ class VLANImportForm(NetBoxModelImportForm):
class ServiceTemplateImportForm(NetBoxModelImportForm): class ServiceTemplateImportForm(NetBoxModelImportForm):
protocol = CSVChoiceField( protocol = CSVChoiceField(
label=_('Protocol'),
choices=ServiceProtocolChoices, choices=ServiceProtocolChoices,
help_text=_('IP protocol') help_text=_('IP protocol')
) )
@ -438,18 +473,21 @@ class ServiceTemplateImportForm(NetBoxModelImportForm):
class ServiceImportForm(NetBoxModelImportForm): class ServiceImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Required if not assigned to a VM') help_text=_('Required if not assigned to a VM')
) )
virtual_machine = CSVModelChoiceField( virtual_machine = CSVModelChoiceField(
label=_('Virtual machine'),
queryset=VirtualMachine.objects.all(), queryset=VirtualMachine.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Required if not assigned to a device') help_text=_('Required if not assigned to a device')
) )
protocol = CSVChoiceField( protocol = CSVChoiceField(
label=_('Protocol'),
choices=ServiceProtocolChoices, choices=ServiceProtocolChoices,
help_text=_('IP protocol') help_text=_('IP protocol')
) )
@ -461,11 +499,13 @@ class ServiceImportForm(NetBoxModelImportForm):
class L2VPNImportForm(NetBoxModelImportForm): class L2VPNImportForm(NetBoxModelImportForm):
tenant = CSVModelChoiceField( tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
) )
type = CSVChoiceField( type = CSVChoiceField(
label=_('Type'),
choices=L2VPNTypeChoices, choices=L2VPNTypeChoices,
help_text=_('L2VPN type') help_text=_('L2VPN type')
) )
@ -484,24 +524,28 @@ class L2VPNTerminationImportForm(NetBoxModelImportForm):
label=_('L2VPN'), label=_('L2VPN'),
) )
device = CSVModelChoiceField( device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Parent device (for interface)') help_text=_('Parent device (for interface)')
) )
virtual_machine = CSVModelChoiceField( virtual_machine = CSVModelChoiceField(
label=_('Virtual machine'),
queryset=VirtualMachine.objects.all(), queryset=VirtualMachine.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Parent virtual machine (for interface)') help_text=_('Parent virtual machine (for interface)')
) )
interface = CSVModelChoiceField( interface = CSVModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.none(), # Can also refer to VMInterface queryset=Interface.objects.none(), # Can also refer to VMInterface
required=False, required=False,
to_field_name='name', to_field_name='name',
help_text=_('Assigned interface (device or VM)') help_text=_('Assigned interface (device or VM)')
) )
vlan = CSVModelChoiceField( vlan = CSVModelChoiceField(
label=_('VLAN'),
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
required=False, required=False,
to_field_name='name', to_field_name='name',
@ -531,10 +575,10 @@ class L2VPNTerminationImportForm(NetBoxModelImportForm):
super().clean() super().clean()
if self.cleaned_data.get('device') and self.cleaned_data.get('virtual_machine'): if self.cleaned_data.get('device') and self.cleaned_data.get('virtual_machine'):
raise ValidationError('Cannot import device and VM interface terminations simultaneously.') raise ValidationError(_('Cannot import device and VM interface terminations simultaneously.'))
if not (self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')): if not (self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')):
raise ValidationError('Each termination must specify either an interface or a VLAN.') raise ValidationError(_('Each termination must specify either an interface or a VLAN.'))
if self.cleaned_data.get('interface') and self.cleaned_data.get('vlan'): if self.cleaned_data.get('interface') and self.cleaned_data.get('vlan'):
raise ValidationError('Cannot assign both an interface and a VLAN.') raise ValidationError(_('Cannot assign both an interface and a VLAN.'))
self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vlan') self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')

View File

@ -1,6 +1,6 @@
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from dcim.models import Location, Rack, Region, Site, SiteGroup, Device from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
from ipam.choices import * from ipam.choices import *
@ -47,8 +47,8 @@ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VRF model = VRF
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Route Targets', ('import_target_id', 'export_target_id')), (_('Route Targets'), ('import_target_id', 'export_target_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
) )
import_target_id = DynamicModelMultipleChoiceField( import_target_id = DynamicModelMultipleChoiceField(
queryset=RouteTarget.objects.all(), queryset=RouteTarget.objects.all(),
@ -67,8 +67,8 @@ class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = RouteTarget model = RouteTarget
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('VRF', ('importing_vrf_id', 'exporting_vrf_id')), (_('VRF'), ('importing_vrf_id', 'exporting_vrf_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
) )
importing_vrf_id = DynamicModelMultipleChoiceField( importing_vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
@ -99,8 +99,8 @@ class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Aggregate model = Aggregate
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('family', 'rir_id')), (_('Attributes'), ('family', 'rir_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
) )
family = forms.ChoiceField( family = forms.ChoiceField(
required=False, required=False,
@ -119,8 +119,8 @@ class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = ASNRange model = ASNRange
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Range', ('rir_id', 'start', 'end')), (_('Range'), ('rir_id', 'start', 'end')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
) )
rir_id = DynamicModelMultipleChoiceField( rir_id = DynamicModelMultipleChoiceField(
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
@ -128,9 +128,11 @@ class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
label=_('RIR') label=_('RIR')
) )
start = forms.IntegerField( start = forms.IntegerField(
label=_('Start'),
required=False required=False
) )
end = forms.IntegerField( end = forms.IntegerField(
label=_('End'),
required=False required=False
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -140,8 +142,8 @@ class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = ASN model = ASN
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Assignment', ('rir_id', 'site_id')), (_('Assignment'), ('rir_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
) )
rir_id = DynamicModelMultipleChoiceField( rir_id = DynamicModelMultipleChoiceField(
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
@ -165,10 +167,10 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Prefix model = Prefix
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Addressing', ('within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized')), (_('Addressing'), ('within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized')),
('VRF', ('vrf_id', 'present_in_vrf_id')), (_('VRF'), ('vrf_id', 'present_in_vrf_id')),
('Location', ('region_id', 'site_group_id', 'site_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
) )
mask_length__lte = forms.IntegerField( mask_length__lte = forms.IntegerField(
widget=forms.HiddenInput() widget=forms.HiddenInput()
@ -204,6 +206,7 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
label=_('Present in VRF') label=_('Present in VRF')
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
choices=PrefixStatusChoices, choices=PrefixStatusChoices,
required=False required=False
) )
@ -253,8 +256,8 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = IPRange model = IPRange
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attriubtes', ('family', 'vrf_id', 'status', 'role_id', 'mark_utilized')), (_('Attriubtes'), ('family', 'vrf_id', 'status', 'role_id', 'mark_utilized')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
) )
family = forms.ChoiceField( family = forms.ChoiceField(
required=False, required=False,
@ -268,6 +271,7 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
null_option='Global' null_option='Global'
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
choices=IPRangeStatusChoices, choices=IPRangeStatusChoices,
required=False required=False
) )
@ -291,10 +295,10 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = IPAddress model = IPAddress
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')), (_('Attributes'), ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')),
('VRF', ('vrf_id', 'present_in_vrf_id')), (_('VRF'), ('vrf_id', 'present_in_vrf_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
('Device/VM', ('device_id', 'virtual_machine_id')), (_('Device/VM'), ('device_id', 'virtual_machine_id')),
) )
parent = forms.CharField( parent = forms.CharField(
required=False, required=False,
@ -337,10 +341,12 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
label=_('Assigned VM'), label=_('Assigned VM'),
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
choices=IPAddressStatusChoices, choices=IPAddressStatusChoices,
required=False required=False
) )
role = forms.MultipleChoiceField( role = forms.MultipleChoiceField(
label=_('Role'),
choices=IPAddressRoleChoices, choices=IPAddressRoleChoices,
required=False required=False
) )
@ -358,29 +364,31 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
model = FHRPGroup model = FHRPGroup
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'protocol', 'group_id')), (_('Attributes'), ('name', 'protocol', 'group_id')),
('Authentication', ('auth_type', 'auth_key')), (_('Authentication'), ('auth_type', 'auth_key')),
) )
name = forms.CharField( name = forms.CharField(
label=_('Name'),
required=False required=False
) )
protocol = forms.MultipleChoiceField( protocol = forms.MultipleChoiceField(
label=_('Protocol'),
choices=FHRPGroupProtocolChoices, choices=FHRPGroupProtocolChoices,
required=False required=False
) )
group_id = forms.IntegerField( group_id = forms.IntegerField(
min_value=0, min_value=0,
required=False, required=False,
label='Group ID' label=_('Group ID')
) )
auth_type = forms.MultipleChoiceField( auth_type = forms.MultipleChoiceField(
choices=FHRPGroupAuthTypeChoices, choices=FHRPGroupAuthTypeChoices,
required=False, required=False,
label='Authentication type' label=_('Authentication type')
) )
auth_key = forms.CharField( auth_key = forms.CharField(
required=False, required=False,
label='Authentication key' label=_('Authentication key')
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -388,8 +396,8 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm):
class VLANGroupFilterForm(NetBoxModelFilterSetForm): class VLANGroupFilterForm(NetBoxModelFilterSetForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Location', ('region', 'sitegroup', 'site', 'location', 'rack')), (_('Location'), ('region', 'sitegroup', 'site', 'location', 'rack')),
('VLAN ID', ('min_vid', 'max_vid')), (_('VLAN ID'), ('min_vid', 'max_vid')),
) )
model = VLANGroup model = VLANGroup
region = DynamicModelMultipleChoiceField( region = DynamicModelMultipleChoiceField(
@ -436,9 +444,9 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VLAN model = VLAN
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')), (_('Location'), ('region_id', 'site_group_id', 'site_id')),
('Attributes', ('group_id', 'status', 'role_id', 'vid', 'l2vpn_id')), (_('Attributes'), ('group_id', 'status', 'role_id', 'vid', 'l2vpn_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
) )
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -469,6 +477,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
label=_('VLAN group') label=_('VLAN group')
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(
label=_('Status'),
choices=VLANStatusChoices, choices=VLANStatusChoices,
required=False required=False
) )
@ -480,7 +489,7 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
) )
vid = forms.IntegerField( vid = forms.IntegerField(
required=False, required=False,
label='VLAN ID' label=_('VLAN ID')
) )
l2vpn_id = DynamicModelMultipleChoiceField( l2vpn_id = DynamicModelMultipleChoiceField(
queryset=L2VPN.objects.all(), queryset=L2VPN.objects.all(),
@ -494,13 +503,15 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
model = ServiceTemplate model = ServiceTemplate
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('protocol', 'port')), (_('Attributes'), ('protocol', 'port')),
) )
protocol = forms.ChoiceField( protocol = forms.ChoiceField(
label=_('Protocol'),
choices=add_blank_choice(ServiceProtocolChoices), choices=add_blank_choice(ServiceProtocolChoices),
required=False required=False
) )
port = forms.IntegerField( port = forms.IntegerField(
label=_('Port'),
required=False, required=False,
) )
tag = TagFilterField(model) tag = TagFilterField(model)
@ -515,10 +526,11 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = L2VPN model = L2VPN
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
('Attributes', ('type', 'import_target_id', 'export_target_id')), (_('Attributes'), ('type', 'import_target_id', 'export_target_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), (_('Tenant'), ('tenant_group_id', 'tenant_id')),
) )
type = forms.ChoiceField( type = forms.ChoiceField(
label=_('Type'),
choices=add_blank_choice(L2VPNTypeChoices), choices=add_blank_choice(L2VPNTypeChoices),
required=False required=False
) )
@ -539,14 +551,14 @@ class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
model = L2VPNTermination model = L2VPNTermination
fieldsets = ( fieldsets = (
(None, ('filter_id', 'l2vpn_id',)), (None, ('filter_id', 'l2vpn_id',)),
('Assigned Object', ( (_('Assigned Object'), (
'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id', 'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
)), )),
) )
l2vpn_id = DynamicModelChoiceField( l2vpn_id = DynamicModelChoiceField(
queryset=L2VPN.objects.all(), queryset=L2VPN.objects.all(),
required=False, required=False,
label='L2VPN' label=_('L2VPN')
) )
assigned_object_type_id = ContentTypeMultipleChoiceField( assigned_object_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.filter(L2VPN_ASSIGNMENT_MODELS), queryset=ContentType.objects.filter(L2VPN_ASSIGNMENT_MODELS),

View File

@ -1,7 +1,7 @@
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
from ipam.choices import * from ipam.choices import *
@ -46,19 +46,21 @@ __all__ = (
class VRFForm(TenancyForm, NetBoxModelForm): class VRFForm(TenancyForm, NetBoxModelForm):
import_targets = DynamicModelMultipleChoiceField( import_targets = DynamicModelMultipleChoiceField(
label=_('Import targets'),
queryset=RouteTarget.objects.all(), queryset=RouteTarget.objects.all(),
required=False required=False
) )
export_targets = DynamicModelMultipleChoiceField( export_targets = DynamicModelMultipleChoiceField(
label=_('Export targets'),
queryset=RouteTarget.objects.all(), queryset=RouteTarget.objects.all(),
required=False required=False
) )
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('VRF', ('name', 'rd', 'enforce_unique', 'description', 'tags')), (_('VRF'), ('name', 'rd', 'enforce_unique', 'description', 'tags')),
('Route Targets', ('import_targets', 'export_targets')), (_('Route Targets'), ('import_targets', 'export_targets')),
('Tenancy', ('tenant_group', 'tenant')), (_('Tenancy'), ('tenant_group', 'tenant')),
) )
class Meta: class Meta:
@ -90,7 +92,7 @@ class RIRForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
('RIR', ( (_('RIR'), (
'name', 'slug', 'is_private', 'description', 'tags', 'name', 'slug', 'is_private', 'description', 'tags',
)), )),
) )
@ -110,8 +112,8 @@ class AggregateForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Aggregate', ('prefix', 'rir', 'date_added', 'description', 'tags')), (_('Aggregate'), ('prefix', 'rir', 'date_added', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')), (_('Tenancy'), ('tenant_group', 'tenant')),
) )
class Meta: class Meta:
@ -131,8 +133,8 @@ class ASNRangeForm(TenancyForm, NetBoxModelForm):
) )
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
('ASN Range', ('name', 'slug', 'rir', 'start', 'end', 'description', 'tags')), (_('ASN Range'), ('name', 'slug', 'rir', 'start', 'end', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')), (_('Tenancy'), ('tenant_group', 'tenant')),
) )
class Meta: class Meta:
@ -155,8 +157,8 @@ class ASNForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('ASN', ('asn', 'rir', 'sites', 'description', 'tags')), (_('ASN'), ('asn', 'rir', 'sites', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')), (_('Tenancy'), ('tenant_group', 'tenant')),
) )
class Meta: class Meta:
@ -184,7 +186,7 @@ class RoleForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
('Role', ( (_('Role'), (
'name', 'slug', 'weight', 'description', 'tags', 'name', 'slug', 'weight', 'description', 'tags',
)), )),
) )
@ -203,6 +205,7 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
label=_('VRF') label=_('VRF')
) )
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
selector=True, selector=True,
@ -215,15 +218,16 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
label=_('VLAN'), label=_('VLAN'),
) )
role = DynamicModelChoiceField( role = DynamicModelChoiceField(
label=_('Role'),
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False required=False
) )
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')), (_('Prefix'), ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
('Site/VLAN Assignment', ('site', 'vlan')), (_('Site/VLAN Assignment'), ('site', 'vlan')),
('Tenancy', ('tenant_group', 'tenant')), (_('Tenancy'), ('tenant_group', 'tenant')),
) )
class Meta: class Meta:
@ -241,14 +245,15 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
label=_('VRF') label=_('VRF')
) )
role = DynamicModelChoiceField( role = DynamicModelChoiceField(
label=_('Role'),
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False required=False
) )
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('IP Range', ('vrf', 'start_address', 'end_address', 'role', 'status', 'mark_utilized', 'description', 'tags')), (_('IP Range'), ('vrf', 'start_address', 'end_address', 'role', 'status', 'mark_utilized', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')), (_('Tenancy'), ('tenant_group', 'tenant')),
) )
class Meta: class Meta:
@ -261,6 +266,7 @@ class IPRangeForm(TenancyForm, NetBoxModelForm):
class IPAddressForm(TenancyForm, NetBoxModelForm): class IPAddressForm(TenancyForm, NetBoxModelForm):
interface = DynamicModelChoiceField( interface = DynamicModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False, required=False,
selector=True, selector=True,
@ -341,13 +347,13 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
] ]
if len(selected_objects) > 1: if len(selected_objects) > 1:
raise forms.ValidationError({ raise forms.ValidationError({
selected_objects[1]: "An IP address can only be assigned to a single object." selected_objects[1]: _("An IP address can only be assigned to a single object.")
}) })
elif selected_objects: elif selected_objects:
assigned_object = self.cleaned_data[selected_objects[0]] assigned_object = self.cleaned_data[selected_objects[0]]
if self.instance.pk and self.cleaned_data['primary_for_parent'] and assigned_object != self.instance.assigned_object: if self.instance.pk and self.cleaned_data['primary_for_parent'] and assigned_object != self.instance.assigned_object:
raise ValidationError( raise ValidationError(
"Cannot reassign IP address while it is designated as the primary IP for the parent object" _("Cannot reassign IP address while it is designated as the primary IP for the parent object")
) )
self.instance.assigned_object = assigned_object self.instance.assigned_object = assigned_object
else: else:
@ -357,19 +363,21 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface') interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
if self.cleaned_data.get('primary_for_parent') and not interface: if self.cleaned_data.get('primary_for_parent') and not interface:
self.add_error( self.add_error(
'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs." 'primary_for_parent', _("Only IP addresses assigned to an interface can be designated as primary IPs.")
) )
# Do not allow assigning a network ID or broadcast address to an interface. # Do not allow assigning a network ID or broadcast address to an interface.
if interface and (address := self.cleaned_data.get('address')): if interface and (address := self.cleaned_data.get('address')):
if address.ip == address.network: if address.ip == address.network:
msg = f"{address} is a network ID, which may not be assigned to an interface." msg = _("{address} is a network ID, which may not be assigned to an interface.").format(address=address)
if address.version == 4 and address.prefixlen not in (31, 32): if address.version == 4 and address.prefixlen not in (31, 32):
raise ValidationError(msg) raise ValidationError(msg)
if address.version == 6 and address.prefixlen not in (127, 128): if address.version == 6 and address.prefixlen not in (127, 128):
raise ValidationError(msg) raise ValidationError(msg)
if address.version == 4 and address.ip == address.broadcast and address.prefixlen not in (31, 32): if address.version == 4 and address.ip == address.broadcast and address.prefixlen not in (31, 32):
msg = f"{address} is a broadcast address, which may not be assigned to an interface." msg = _("{address} is a broadcast address, which may not be assigned to an interface.").format(
address=address
)
raise ValidationError(msg) raise ValidationError(msg)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -442,9 +450,9 @@ class FHRPGroupForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('FHRP Group', ('protocol', 'group_id', 'name', 'description', 'tags')), (_('FHRP Group'), ('protocol', 'group_id', 'name', 'description', 'tags')),
('Authentication', ('auth_type', 'auth_key')), (_('Authentication'), ('auth_type', 'auth_key')),
('Virtual IP Address', ('ip_vrf', 'ip_address', 'ip_status')) (_('Virtual IP Address'), ('ip_vrf', 'ip_address', 'ip_status'))
) )
class Meta: class Meta:
@ -497,6 +505,7 @@ class FHRPGroupForm(NetBoxModelForm):
class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm): class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm):
group = DynamicModelChoiceField( group = DynamicModelChoiceField(
label=_('Group'),
queryset=FHRPGroup.objects.all() queryset=FHRPGroup.objects.all()
) )
@ -514,10 +523,12 @@ class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm):
class VLANGroupForm(NetBoxModelForm): class VLANGroupForm(NetBoxModelForm):
scope_type = ContentTypeChoiceField( scope_type = ContentTypeChoiceField(
label=_('Scope type'),
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
required=False required=False
) )
region = DynamicModelChoiceField( region = DynamicModelChoiceField(
label=_('Region'),
queryset=Region.objects.all(), queryset=Region.objects.all(),
required=False, required=False,
initial_params={ initial_params={
@ -533,6 +544,7 @@ class VLANGroupForm(NetBoxModelForm):
label=_('Site group') label=_('Site group')
) )
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
initial_params={ initial_params={
@ -544,6 +556,7 @@ class VLANGroupForm(NetBoxModelForm):
} }
) )
location = DynamicModelChoiceField( location = DynamicModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(), queryset=Location.objects.all(),
required=False, required=False,
initial_params={ initial_params={
@ -554,6 +567,7 @@ class VLANGroupForm(NetBoxModelForm):
} }
) )
rack = DynamicModelChoiceField( rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -570,6 +584,7 @@ class VLANGroupForm(NetBoxModelForm):
label=_('Cluster group') label=_('Cluster group')
) )
cluster = DynamicModelChoiceField( cluster = DynamicModelChoiceField(
label=_('Cluster'),
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
required=False, required=False,
query_params={ query_params={
@ -579,9 +594,9 @@ class VLANGroupForm(NetBoxModelForm):
slug = SlugField() slug = SlugField()
fieldsets = ( fieldsets = (
('VLAN Group', ('name', 'slug', 'description', 'tags')), (_('VLAN Group'), ('name', 'slug', 'description', 'tags')),
('Child VLANs', ('min_vid', 'max_vid')), (_('Child VLANs'), ('min_vid', 'max_vid')),
('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')), (_('Scope'), ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')),
) )
class Meta: class Meta:
@ -621,12 +636,14 @@ class VLANForm(TenancyForm, NetBoxModelForm):
label=_('VLAN Group') label=_('VLAN Group')
) )
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False, required=False,
null_option='None', null_option='None',
selector=True selector=True
) )
role = DynamicModelChoiceField( role = DynamicModelChoiceField(
label=_('Role'),
queryset=Role.objects.all(), queryset=Role.objects.all(),
required=False required=False
) )
@ -642,6 +659,7 @@ class VLANForm(TenancyForm, NetBoxModelForm):
class ServiceTemplateForm(NetBoxModelForm): class ServiceTemplateForm(NetBoxModelForm):
ports = NumericArrayField( ports = NumericArrayField(
label=_('Ports'),
base_field=forms.IntegerField( base_field=forms.IntegerField(
min_value=SERVICE_PORT_MIN, min_value=SERVICE_PORT_MIN,
max_value=SERVICE_PORT_MAX max_value=SERVICE_PORT_MAX
@ -651,7 +669,7 @@ class ServiceTemplateForm(NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Service Template', ( (_('Service Template'), (
'name', 'protocol', 'ports', 'description', 'tags', 'name', 'protocol', 'ports', 'description', 'tags',
)), )),
) )
@ -663,16 +681,19 @@ class ServiceTemplateForm(NetBoxModelForm):
class ServiceForm(NetBoxModelForm): class ServiceForm(NetBoxModelForm):
device = DynamicModelChoiceField( device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
selector=True selector=True
) )
virtual_machine = DynamicModelChoiceField( virtual_machine = DynamicModelChoiceField(
label=_('Virtual machine'),
queryset=VirtualMachine.objects.all(), queryset=VirtualMachine.objects.all(),
required=False, required=False,
selector=True selector=True
) )
ports = NumericArrayField( ports = NumericArrayField(
label=_('Ports'),
base_field=forms.IntegerField( base_field=forms.IntegerField(
min_value=SERVICE_PORT_MIN, min_value=SERVICE_PORT_MIN,
max_value=SERVICE_PORT_MAX max_value=SERVICE_PORT_MAX
@ -699,6 +720,7 @@ class ServiceForm(NetBoxModelForm):
class ServiceCreateForm(ServiceForm): class ServiceCreateForm(ServiceForm):
service_template = DynamicModelChoiceField( service_template = DynamicModelChoiceField(
label=_('Service template'),
queryset=ServiceTemplate.objects.all(), queryset=ServiceTemplate.objects.all(),
required=False required=False
) )
@ -739,19 +761,21 @@ class ServiceCreateForm(ServiceForm):
class L2VPNForm(TenancyForm, NetBoxModelForm): class L2VPNForm(TenancyForm, NetBoxModelForm):
slug = SlugField() slug = SlugField()
import_targets = DynamicModelMultipleChoiceField( import_targets = DynamicModelMultipleChoiceField(
label=_('Import targets'),
queryset=RouteTarget.objects.all(), queryset=RouteTarget.objects.all(),
required=False required=False
) )
export_targets = DynamicModelMultipleChoiceField( export_targets = DynamicModelMultipleChoiceField(
label=_('Export targets'),
queryset=RouteTarget.objects.all(), queryset=RouteTarget.objects.all(),
required=False required=False
) )
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('L2VPN', ('name', 'slug', 'type', 'identifier', 'description', 'tags')), (_('L2VPN'), ('name', 'slug', 'type', 'identifier', 'description', 'tags')),
('Route Targets', ('import_targets', 'export_targets')), (_('Route Targets'), ('import_targets', 'export_targets')),
('Tenancy', ('tenant_group', 'tenant')), (_('Tenancy'), ('tenant_group', 'tenant')),
) )
class Meta: class Meta:
@ -777,6 +801,7 @@ class L2VPNTerminationForm(NetBoxModelForm):
label=_('VLAN') label=_('VLAN')
) )
interface = DynamicModelChoiceField( interface = DynamicModelChoiceField(
label=_('Interface'),
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False, required=False,
selector=True selector=True
@ -815,8 +840,8 @@ class L2VPNTerminationForm(NetBoxModelForm):
vlan = self.cleaned_data.get('vlan') vlan = self.cleaned_data.get('vlan')
if not (interface or vminterface or vlan): if not (interface or vminterface or vlan):
raise ValidationError('A termination must specify an interface or VLAN.') raise ValidationError(_('A termination must specify an interface or VLAN.'))
if len([x for x in (interface, vminterface, vlan) if x]) > 1: if len([x for x in (interface, vminterface, vlan) if x]) > 1:
raise ValidationError('A termination can only have one terminating object (an interface or VLAN).') raise ValidationError(_('A termination can only have one terminating object (an interface or VLAN).'))
self.instance.assigned_object = interface or vminterface or vlan self.instance.assigned_object = interface or vminterface or vlan

View File

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

View File

@ -1,7 +1,7 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from ipam.fields import ASNField from ipam.fields import ASNField
from ipam.querysets import ASNRangeQuerySet from ipam.querysets import ASNRangeQuerySet
@ -15,10 +15,12 @@ __all__ = (
class ASNRange(OrganizationalModel): class ASNRange(OrganizationalModel):
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100, max_length=100,
unique=True unique=True
) )
slug = models.SlugField( slug = models.SlugField(
verbose_name=_('slug'),
max_length=100, max_length=100,
unique=True unique=True
) )
@ -26,10 +28,14 @@ class ASNRange(OrganizationalModel):
to='ipam.RIR', to='ipam.RIR',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='asn_ranges', related_name='asn_ranges',
verbose_name='RIR' verbose_name=_('RIR')
)
start = ASNField(
verbose_name=_('start'),
)
end = ASNField(
verbose_name=_('end'),
) )
start = ASNField()
end = ASNField()
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -62,7 +68,11 @@ class ASNRange(OrganizationalModel):
super().clean() super().clean()
if self.end <= self.start: if self.end <= self.start:
raise ValidationError(f"Starting ASN ({self.start}) must be lower than ending ASN ({self.end}).") raise ValidationError(
_("Starting ASN ({start}) must be lower than ending ASN ({end}).").format(
start=self.start, end=self.end
)
)
def get_child_asns(self): def get_child_asns(self):
return ASN.objects.filter( return ASN.objects.filter(
@ -90,12 +100,12 @@ class ASN(PrimaryModel):
to='ipam.RIR', to='ipam.RIR',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='asns', related_name='asns',
verbose_name='RIR', verbose_name=_('RIR'),
help_text=_("Regional Internet Registry responsible for this AS number space") help_text=_("Regional Internet Registry responsible for this AS number space")
) )
asn = ASNField( asn = ASNField(
unique=True, unique=True,
verbose_name='ASN', verbose_name=_('ASN'),
help_text=_('16- or 32-bit autonomous system number') help_text=_('16- or 32-bit autonomous system number')
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(

View File

@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from netbox.models import ChangeLoggedModel, PrimaryModel from netbox.models import ChangeLoggedModel, PrimaryModel
from ipam.choices import * from ipam.choices import *
@ -19,13 +20,15 @@ class FHRPGroup(PrimaryModel):
A grouping of next hope resolution protocol (FHRP) peers. (For instance, VRRP or HSRP.) A grouping of next hope resolution protocol (FHRP) peers. (For instance, VRRP or HSRP.)
""" """
group_id = models.PositiveSmallIntegerField( group_id = models.PositiveSmallIntegerField(
verbose_name='Group ID' verbose_name=_('group ID')
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100, max_length=100,
blank=True blank=True
) )
protocol = models.CharField( protocol = models.CharField(
verbose_name=_('protocol'),
max_length=50, max_length=50,
choices=FHRPGroupProtocolChoices choices=FHRPGroupProtocolChoices
) )
@ -33,12 +36,12 @@ class FHRPGroup(PrimaryModel):
max_length=50, max_length=50,
choices=FHRPGroupAuthTypeChoices, choices=FHRPGroupAuthTypeChoices,
blank=True, blank=True,
verbose_name='Authentication type' verbose_name=_('authentication type')
) )
auth_key = models.CharField( auth_key = models.CharField(
max_length=255, max_length=255,
blank=True, blank=True,
verbose_name='Authentication key' verbose_name=_('authentication key')
) )
ip_addresses = GenericRelation( ip_addresses = GenericRelation(
to='ipam.IPAddress', to='ipam.IPAddress',
@ -87,6 +90,7 @@ class FHRPGroupAssignment(ChangeLoggedModel):
on_delete=models.CASCADE on_delete=models.CASCADE
) )
priority = models.PositiveSmallIntegerField( priority = models.PositiveSmallIntegerField(
verbose_name=_('priority'),
validators=( validators=(
MinValueValidator(FHRPGROUPASSIGNMENT_PRIORITY_MIN), MinValueValidator(FHRPGROUPASSIGNMENT_PRIORITY_MIN),
MaxValueValidator(FHRPGROUPASSIGNMENT_PRIORITY_MAX) MaxValueValidator(FHRPGROUPASSIGNMENT_PRIORITY_MAX)
@ -103,7 +107,7 @@ class FHRPGroupAssignment(ChangeLoggedModel):
name='%(app_label)s_%(class)s_unique_interface_group' name='%(app_label)s_%(class)s_unique_interface_group'
), ),
) )
verbose_name = 'FHRP group assignment' verbose_name = _('FHRP group assignment')
def __str__(self): def __str__(self):
return f'{self.interface}: {self.group} ({self.priority})' return f'{self.interface}: {self.group} ({self.priority})'

View File

@ -6,7 +6,7 @@ from django.db import models
from django.db.models import F from django.db.models import F
from django.urls import reverse from django.urls import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from ipam.choices import * from ipam.choices import *
from ipam.constants import * from ipam.constants import *
@ -59,14 +59,14 @@ class RIR(OrganizationalModel):
""" """
is_private = models.BooleanField( is_private = models.BooleanField(
default=False, default=False,
verbose_name='Private', verbose_name=_('private'),
help_text=_('IP space managed by this RIR is considered private') help_text=_('IP space managed by this RIR is considered private')
) )
class Meta: class Meta:
ordering = ('name',) ordering = ('name',)
verbose_name = 'RIR' verbose_name = _('RIR')
verbose_name_plural = 'RIRs' verbose_name_plural = _('RIRs')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('ipam:rir', args=[self.pk]) return reverse('ipam:rir', args=[self.pk])
@ -84,7 +84,7 @@ class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
to='ipam.RIR', to='ipam.RIR',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='aggregates', related_name='aggregates',
verbose_name='RIR', verbose_name=_('RIR'),
help_text=_("Regional Internet Registry responsible for this IP space") help_text=_("Regional Internet Registry responsible for this IP space")
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(
@ -95,6 +95,7 @@ class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
null=True null=True
) )
date_added = models.DateField( date_added = models.DateField(
verbose_name=_('date added'),
blank=True, blank=True,
null=True null=True
) )
@ -123,7 +124,7 @@ class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
# /0 masks are not acceptable # /0 masks are not acceptable
if self.prefix.prefixlen == 0: if self.prefix.prefixlen == 0:
raise ValidationError({ raise ValidationError({
'prefix': "Cannot create aggregate with /0 mask." 'prefix': _("Cannot create aggregate with /0 mask.")
}) })
# Ensure that the aggregate being added is not covered by an existing aggregate # Ensure that the aggregate being added is not covered by an existing aggregate
@ -134,9 +135,9 @@ class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
covering_aggregates = covering_aggregates.exclude(pk=self.pk) covering_aggregates = covering_aggregates.exclude(pk=self.pk)
if covering_aggregates: if covering_aggregates:
raise ValidationError({ raise ValidationError({
'prefix': "Aggregates cannot overlap. {} is already covered by an existing aggregate ({}).".format( 'prefix': _(
self.prefix, covering_aggregates[0] "Aggregates cannot overlap. {} is already covered by an existing aggregate ({})."
) ).format(self.prefix, covering_aggregates[0])
}) })
# Ensure that the aggregate being added does not cover an existing aggregate # Ensure that the aggregate being added does not cover an existing aggregate
@ -145,7 +146,7 @@ class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
covered_aggregates = covered_aggregates.exclude(pk=self.pk) covered_aggregates = covered_aggregates.exclude(pk=self.pk)
if covered_aggregates: if covered_aggregates:
raise ValidationError({ raise ValidationError({
'prefix': "Aggregates cannot overlap. {} covers an existing aggregate ({}).".format( 'prefix': _("Aggregates cannot overlap. {} covers an existing aggregate ({}).").format(
self.prefix, covered_aggregates[0] self.prefix, covered_aggregates[0]
) )
}) })
@ -179,6 +180,7 @@ class Role(OrganizationalModel):
"Management." "Management."
""" """
weight = models.PositiveSmallIntegerField( weight = models.PositiveSmallIntegerField(
verbose_name=_('weight'),
default=1000 default=1000
) )
@ -199,6 +201,7 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
assigned to a VLAN where appropriate. assigned to a VLAN where appropriate.
""" """
prefix = IPNetworkField( prefix = IPNetworkField(
verbose_name=_('prefix'),
help_text=_('IPv4 or IPv6 network with mask') help_text=_('IPv4 or IPv6 network with mask')
) )
site = models.ForeignKey( site = models.ForeignKey(
@ -214,7 +217,7 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
related_name='prefixes', related_name='prefixes',
blank=True, blank=True,
null=True, null=True,
verbose_name='VRF' verbose_name=_('VRF')
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
@ -228,14 +231,13 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='prefixes', related_name='prefixes',
blank=True, blank=True,
null=True, null=True
verbose_name='VLAN'
) )
status = models.CharField( status = models.CharField(
max_length=50, max_length=50,
choices=PrefixStatusChoices, choices=PrefixStatusChoices,
default=PrefixStatusChoices.STATUS_ACTIVE, default=PrefixStatusChoices.STATUS_ACTIVE,
verbose_name='Status', verbose_name=_('status'),
help_text=_('Operational status of this prefix') help_text=_('Operational status of this prefix')
) )
role = models.ForeignKey( role = models.ForeignKey(
@ -247,11 +249,12 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
help_text=_('The primary function of this prefix') help_text=_('The primary function of this prefix')
) )
is_pool = models.BooleanField( is_pool = models.BooleanField(
verbose_name='Is a pool', verbose_name=_('is a pool'),
default=False, default=False,
help_text=_('All IP addresses within this prefix are considered usable') help_text=_('All IP addresses within this prefix are considered usable')
) )
mark_utilized = models.BooleanField( mark_utilized = models.BooleanField(
verbose_name=_('mark utilized'),
default=False, default=False,
help_text=_("Treat as 100% utilized") help_text=_("Treat as 100% utilized")
) )
@ -297,7 +300,7 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
# /0 masks are not acceptable # /0 masks are not acceptable
if self.prefix.prefixlen == 0: if self.prefix.prefixlen == 0:
raise ValidationError({ raise ValidationError({
'prefix': "Cannot create prefix with /0 mask." 'prefix': _("Cannot create prefix with /0 mask.")
}) })
# Enforce unique IP space (if applicable) # Enforce unique IP space (if applicable)
@ -305,8 +308,8 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
duplicate_prefixes = self.get_duplicates() duplicate_prefixes = self.get_duplicates()
if duplicate_prefixes: if duplicate_prefixes:
raise ValidationError({ raise ValidationError({
'prefix': "Duplicate prefix found in {}: {}".format( 'prefix': _("Duplicate prefix found in {}: {}").format(
"VRF {}".format(self.vrf) if self.vrf else "global table", _("VRF {}").format(self.vrf) if self.vrf else _("global table"),
duplicate_prefixes.first(), duplicate_prefixes.first(),
) )
}) })
@ -474,12 +477,15 @@ class IPRange(PrimaryModel):
A range of IP addresses, defined by start and end addresses. A range of IP addresses, defined by start and end addresses.
""" """
start_address = IPAddressField( start_address = IPAddressField(
verbose_name=_('start address'),
help_text=_('IPv4 or IPv6 address (with mask)') help_text=_('IPv4 or IPv6 address (with mask)')
) )
end_address = IPAddressField( end_address = IPAddressField(
verbose_name=_('end address'),
help_text=_('IPv4 or IPv6 address (with mask)') help_text=_('IPv4 or IPv6 address (with mask)')
) )
size = models.PositiveIntegerField( size = models.PositiveIntegerField(
verbose_name=_('size'),
editable=False editable=False
) )
vrf = models.ForeignKey( vrf = models.ForeignKey(
@ -488,7 +494,7 @@ class IPRange(PrimaryModel):
related_name='ip_ranges', related_name='ip_ranges',
blank=True, blank=True,
null=True, null=True,
verbose_name='VRF' verbose_name=_('VRF')
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
@ -498,6 +504,7 @@ class IPRange(PrimaryModel):
null=True null=True
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=IPRangeStatusChoices, choices=IPRangeStatusChoices,
default=IPRangeStatusChoices.STATUS_ACTIVE, default=IPRangeStatusChoices.STATUS_ACTIVE,
@ -512,6 +519,7 @@ class IPRange(PrimaryModel):
help_text=_('The primary function of this range') help_text=_('The primary function of this range')
) )
mark_utilized = models.BooleanField( mark_utilized = models.BooleanField(
verbose_name=_('mark utilized'),
default=False, default=False,
help_text=_("Treat as 100% utilized") help_text=_("Treat as 100% utilized")
) )
@ -539,21 +547,33 @@ class IPRange(PrimaryModel):
# Check that start & end IP versions match # Check that start & end IP versions match
if self.start_address.version != self.end_address.version: if self.start_address.version != self.end_address.version:
raise ValidationError({ raise ValidationError({
'end_address': f"Ending address version (IPv{self.end_address.version}) does not match starting " 'end_address': _(
f"address (IPv{self.start_address.version})" "Ending address version (IPv{end_address_version}) does not match starting address "
"(IPv{start_address_version})"
).format(
end_address_version=self.end_address.version,
start_address_version=self.start_address.version
)
}) })
# Check that the start & end IP prefix lengths match # Check that the start & end IP prefix lengths match
if self.start_address.prefixlen != self.end_address.prefixlen: if self.start_address.prefixlen != self.end_address.prefixlen:
raise ValidationError({ raise ValidationError({
'end_address': f"Ending address mask (/{self.end_address.prefixlen}) does not match starting " 'end_address': _(
f"address mask (/{self.start_address.prefixlen})" "Ending address mask (/{end_address_prefixlen}) does not match starting address mask "
"(/{start_address_prefixlen})"
).format(
end_address_prefixlen=self.end_address.prefixlen,
start_address_prefixlen=self.start_address.prefixlen
)
}) })
# Check that the ending address is greater than the starting address # Check that the ending address is greater than the starting address
if not self.end_address > self.start_address: if not self.end_address > self.start_address:
raise ValidationError({ raise ValidationError({
'end_address': f"Ending address must be lower than the starting address ({self.start_address})" 'end_address': _(
"Ending address must be lower than the starting address ({start_address})"
).format(start_address=self.start_address)
}) })
# Check for overlapping ranges # Check for overlapping ranges
@ -563,12 +583,18 @@ class IPRange(PrimaryModel):
Q(start_address__lte=self.start_address, end_address__gte=self.end_address) # Starts & ends outside Q(start_address__lte=self.start_address, end_address__gte=self.end_address) # Starts & ends outside
).first() ).first()
if overlapping_range: if overlapping_range:
raise ValidationError(f"Defined addresses overlap with range {overlapping_range} in VRF {self.vrf}") raise ValidationError(
_("Defined addresses overlap with range {overlapping_range} in VRF {vrf}").format(
overlapping_range=overlapping_range,
vrf=self.vrf
))
# Validate maximum size # Validate maximum size
MAX_SIZE = 2 ** 32 - 1 MAX_SIZE = 2 ** 32 - 1
if int(self.end_address.ip - self.start_address.ip) + 1 > MAX_SIZE: if int(self.end_address.ip - self.start_address.ip) + 1 > MAX_SIZE:
raise ValidationError(f"Defined range exceeds maximum supported size ({MAX_SIZE})") raise ValidationError(
_("Defined range exceeds maximum supported size ({max_size})").format(max_size=MAX_SIZE)
)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -679,6 +705,7 @@ class IPAddress(PrimaryModel):
which has a NAT outside IP, that Interface's Device can use either the inside or outside IP as its primary IP. which has a NAT outside IP, that Interface's Device can use either the inside or outside IP as its primary IP.
""" """
address = IPAddressField( address = IPAddressField(
verbose_name=_('address'),
help_text=_('IPv4 or IPv6 address (with mask)') help_text=_('IPv4 or IPv6 address (with mask)')
) )
vrf = models.ForeignKey( vrf = models.ForeignKey(
@ -687,7 +714,7 @@ class IPAddress(PrimaryModel):
related_name='ip_addresses', related_name='ip_addresses',
blank=True, blank=True,
null=True, null=True,
verbose_name='VRF' verbose_name=_('VRF')
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
@ -697,12 +724,14 @@ class IPAddress(PrimaryModel):
null=True null=True
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=IPAddressStatusChoices, choices=IPAddressStatusChoices,
default=IPAddressStatusChoices.STATUS_ACTIVE, default=IPAddressStatusChoices.STATUS_ACTIVE,
help_text=_('The operational status of this IP') help_text=_('The operational status of this IP')
) )
role = models.CharField( role = models.CharField(
verbose_name=_('role'),
max_length=50, max_length=50,
choices=IPAddressRoleChoices, choices=IPAddressRoleChoices,
blank=True, blank=True,
@ -730,14 +759,14 @@ class IPAddress(PrimaryModel):
related_name='nat_outside', related_name='nat_outside',
blank=True, blank=True,
null=True, null=True,
verbose_name='NAT (Inside)', verbose_name=_('NAT (inside)'),
help_text=_('The IP for which this address is the "outside" IP') help_text=_('The IP for which this address is the "outside" IP')
) )
dns_name = models.CharField( dns_name = models.CharField(
max_length=255, max_length=255,
blank=True, blank=True,
validators=[DNSValidator], validators=[DNSValidator],
verbose_name='DNS Name', verbose_name=_('DNS name'),
help_text=_('Hostname or FQDN (not case-sensitive)') help_text=_('Hostname or FQDN (not case-sensitive)')
) )
@ -799,7 +828,7 @@ class IPAddress(PrimaryModel):
# /0 masks are not acceptable # /0 masks are not acceptable
if self.address.prefixlen == 0: if self.address.prefixlen == 0:
raise ValidationError({ raise ValidationError({
'address': "Cannot create IP address with /0 mask." 'address': _("Cannot create IP address with /0 mask.")
}) })
# Enforce unique IP space (if applicable) # Enforce unique IP space (if applicable)
@ -810,8 +839,8 @@ class IPAddress(PrimaryModel):
any(dip.role not in IPADDRESS_ROLES_NONUNIQUE for dip in duplicate_ips) any(dip.role not in IPADDRESS_ROLES_NONUNIQUE for dip in duplicate_ips)
): ):
raise ValidationError({ raise ValidationError({
'address': "Duplicate IP address found in {}: {}".format( 'address': _("Duplicate IP address found in {}: {}").format(
"VRF {}".format(self.vrf) if self.vrf else "global table", _("VRF {}").format(self.vrf) if self.vrf else _("global table"),
duplicate_ips.first(), duplicate_ips.first(),
) )
}) })
@ -819,7 +848,7 @@ class IPAddress(PrimaryModel):
# Validate IP status selection # Validate IP status selection
if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6: if self.status == IPAddressStatusChoices.STATUS_SLAAC and self.family != 6:
raise ValidationError({ raise ValidationError({
'status': "Only IPv6 addresses can be assigned SLAAC status" 'status': _("Only IPv6 addresses can be assigned SLAAC status")
}) })
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

View File

@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from ipam.choices import L2VPNTypeChoices from ipam.choices import L2VPNTypeChoices
from ipam.constants import L2VPN_ASSIGNMENT_MODELS from ipam.constants import L2VPN_ASSIGNMENT_MODELS
@ -17,18 +18,22 @@ __all__ = (
class L2VPN(PrimaryModel): class L2VPN(PrimaryModel):
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100, max_length=100,
unique=True unique=True
) )
slug = models.SlugField( slug = models.SlugField(
verbose_name=_('slug'),
max_length=100, max_length=100,
unique=True unique=True
) )
type = models.CharField( type = models.CharField(
verbose_name=_('type'),
max_length=50, max_length=50,
choices=L2VPNTypeChoices choices=L2VPNTypeChoices
) )
identifier = models.BigIntegerField( identifier = models.BigIntegerField(
verbose_name=_('identifier'),
null=True, null=True,
blank=True blank=True
) )
@ -123,7 +128,11 @@ class L2VPNTermination(NetBoxModel):
obj_type = ContentType.objects.get_for_model(self.assigned_object) obj_type = ContentType.objects.get_for_model(self.assigned_object)
if L2VPNTermination.objects.filter(assigned_object_id=obj_id, assigned_object_type=obj_type).\ if L2VPNTermination.objects.filter(assigned_object_id=obj_id, assigned_object_type=obj_type).\
exclude(pk=self.pk).count() > 0: exclude(pk=self.pk).count() > 0:
raise ValidationError(f'L2VPN Termination already assigned ({self.assigned_object})') raise ValidationError(
_('L2VPN Termination already assigned ({assigned_object})').format(
assigned_object=self.assigned_object
)
)
# Only check if L2VPN is set and is of type P2P # Only check if L2VPN is set and is of type P2P
if hasattr(self, 'l2vpn') and self.l2vpn.type in L2VPNTypeChoices.P2P: if hasattr(self, 'l2vpn') and self.l2vpn.type in L2VPNTypeChoices.P2P:
@ -131,9 +140,10 @@ class L2VPNTermination(NetBoxModel):
if terminations_count >= 2: if terminations_count >= 2:
l2vpn_type = self.l2vpn.get_type_display() l2vpn_type = self.l2vpn.get_type_display()
raise ValidationError( raise ValidationError(
f'{l2vpn_type} L2VPNs cannot have more than two terminations; found {terminations_count} already ' _(
f'defined.' '{l2vpn_type} L2VPNs cannot have more than two terminations; found {terminations_count} '
) 'already defined.'
).format(l2vpn_type=l2vpn_type, terminations_count=terminations_count))
@property @property
def assigned_object_parent(self): def assigned_object_parent(self):

View File

@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from ipam.choices import * from ipam.choices import *
from ipam.constants import * from ipam.constants import *
@ -19,6 +19,7 @@ __all__ = (
class ServiceBase(models.Model): class ServiceBase(models.Model):
protocol = models.CharField( protocol = models.CharField(
verbose_name=_('protocol'),
max_length=50, max_length=50,
choices=ServiceProtocolChoices choices=ServiceProtocolChoices
) )
@ -29,7 +30,7 @@ class ServiceBase(models.Model):
MaxValueValidator(SERVICE_PORT_MAX) MaxValueValidator(SERVICE_PORT_MAX)
] ]
), ),
verbose_name='Port numbers' verbose_name=_('port numbers')
) )
class Meta: class Meta:
@ -48,6 +49,7 @@ class ServiceTemplate(ServiceBase, PrimaryModel):
A template for a Service to be applied to a device or virtual machine. A template for a Service to be applied to a device or virtual machine.
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100, max_length=100,
unique=True unique=True
) )
@ -68,7 +70,7 @@ class Service(ServiceBase, PrimaryModel):
to='dcim.Device', to='dcim.Device',
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='services', related_name='services',
verbose_name='device', verbose_name=_('device'),
null=True, null=True,
blank=True blank=True
) )
@ -80,13 +82,14 @@ class Service(ServiceBase, PrimaryModel):
blank=True blank=True
) )
name = models.CharField( name = models.CharField(
max_length=100 max_length=100,
verbose_name=_('name')
) )
ipaddresses = models.ManyToManyField( ipaddresses = models.ManyToManyField(
to='ipam.IPAddress', to='ipam.IPAddress',
related_name='services', related_name='services',
blank=True, blank=True,
verbose_name='IP addresses', verbose_name=_('IP addresses'),
help_text=_("The specific IP addresses (if any) to which this service is bound") help_text=_("The specific IP addresses (if any) to which this service is bound")
) )
@ -107,6 +110,6 @@ class Service(ServiceBase, PrimaryModel):
# A Service must belong to a Device *or* to a VirtualMachine # A Service must belong to a Device *or* to a VirtualMachine
if self.device and self.virtual_machine: if self.device and self.virtual_machine:
raise ValidationError("A service cannot be associated with both a device and a virtual machine.") raise ValidationError(_("A service cannot be associated with both a device and a virtual machine."))
if not self.device and not self.virtual_machine: if not self.device and not self.virtual_machine:
raise ValidationError("A service must be associated with either a device or a virtual machine.") raise ValidationError(_("A service must be associated with either a device or a virtual machine."))

View File

@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy as _
from dcim.models import Interface from dcim.models import Interface
from ipam.choices import * from ipam.choices import *
@ -24,9 +24,11 @@ class VLANGroup(OrganizationalModel):
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique. A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=100 max_length=100
) )
slug = models.SlugField( slug = models.SlugField(
verbose_name=_('slug'),
max_length=100 max_length=100
) )
scope_type = models.ForeignKey( scope_type = models.ForeignKey(
@ -45,7 +47,7 @@ class VLANGroup(OrganizationalModel):
fk_field='scope_id' fk_field='scope_id'
) )
min_vid = models.PositiveSmallIntegerField( min_vid = models.PositiveSmallIntegerField(
verbose_name='Minimum VLAN ID', verbose_name=_('minimum VLAN ID'),
default=VLAN_VID_MIN, default=VLAN_VID_MIN,
validators=( validators=(
MinValueValidator(VLAN_VID_MIN), MinValueValidator(VLAN_VID_MIN),
@ -54,7 +56,7 @@ class VLANGroup(OrganizationalModel):
help_text=_('Lowest permissible ID of a child VLAN') help_text=_('Lowest permissible ID of a child VLAN')
) )
max_vid = models.PositiveSmallIntegerField( max_vid = models.PositiveSmallIntegerField(
verbose_name='Maximum VLAN ID', verbose_name=_('maximum VLAN ID'),
default=VLAN_VID_MAX, default=VLAN_VID_MAX,
validators=( validators=(
MinValueValidator(VLAN_VID_MIN), MinValueValidator(VLAN_VID_MIN),
@ -88,14 +90,14 @@ class VLANGroup(OrganizationalModel):
# Validate scope assignment # Validate scope assignment
if self.scope_type and not self.scope_id: if self.scope_type and not self.scope_id:
raise ValidationError("Cannot set scope_type without scope_id.") raise ValidationError(_("Cannot set scope_type without scope_id."))
if self.scope_id and not self.scope_type: if self.scope_id and not self.scope_type:
raise ValidationError("Cannot set scope_id without scope_type.") raise ValidationError(_("Cannot set scope_id without scope_type."))
# Validate min/max child VID limits # Validate min/max child VID limits
if self.max_vid < self.min_vid: if self.max_vid < self.min_vid:
raise ValidationError({ raise ValidationError({
'max_vid': "Maximum child VID must be greater than or equal to minimum child VID" 'max_vid': _("Maximum child VID must be greater than or equal to minimum child VID")
}) })
def get_available_vids(self): def get_available_vids(self):
@ -143,7 +145,7 @@ class VLAN(PrimaryModel):
help_text=_("VLAN group (optional)") help_text=_("VLAN group (optional)")
) )
vid = models.PositiveSmallIntegerField( vid = models.PositiveSmallIntegerField(
verbose_name='ID', verbose_name=_('VLAN ID'),
validators=( validators=(
MinValueValidator(VLAN_VID_MIN), MinValueValidator(VLAN_VID_MIN),
MaxValueValidator(VLAN_VID_MAX) MaxValueValidator(VLAN_VID_MAX)
@ -151,6 +153,7 @@ class VLAN(PrimaryModel):
help_text=_("Numeric VLAN ID (1-4094)") help_text=_("Numeric VLAN ID (1-4094)")
) )
name = models.CharField( name = models.CharField(
verbose_name=_('name'),
max_length=64 max_length=64
) )
tenant = models.ForeignKey( tenant = models.ForeignKey(
@ -161,6 +164,7 @@ class VLAN(PrimaryModel):
null=True null=True
) )
status = models.CharField( status = models.CharField(
verbose_name=_('status'),
max_length=50, max_length=50,
choices=VLANStatusChoices, choices=VLANStatusChoices,
default=VLANStatusChoices.STATUS_ACTIVE, default=VLANStatusChoices.STATUS_ACTIVE,
@ -215,15 +219,17 @@ class VLAN(PrimaryModel):
# Validate VLAN group (if assigned) # Validate VLAN group (if assigned)
if self.group and self.site and self.group.scope != self.site: if self.group and self.site and self.group.scope != self.site:
raise ValidationError({ raise ValidationError({
'group': f"VLAN is assigned to group {self.group} (scope: {self.group.scope}); cannot also assign to " 'group': _(
f"site {self.site}." "VLAN is assigned to group {group} (scope: {scope}); cannot also assign to site {site}."
).format(group=self.group, scope=self.group.scope, site=self.site)
}) })
# Validate group min/max VIDs # Validate group min/max VIDs
if self.group and not self.group.min_vid <= self.vid <= self.group.max_vid: if self.group and not self.group.min_vid <= self.vid <= self.group.max_vid:
raise ValidationError({ raise ValidationError({
'vid': f"VID must be between {self.group.min_vid} and {self.group.max_vid} for VLANs in group " 'vid': _(
f"{self.group}" "VID must be between {min_vid} and {max_vid} for VLANs in group {group}"
).format(min_vid=self.group.min_vid, max_vid=self.group.max_vid, group=self.group)
}) })
def get_status_color(self): def get_status_color(self):

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