mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 08:46:10 -06:00
Initial work on predefined choices for custom fields
This commit is contained in:
parent
306cfeeebb
commit
b8cf15ac97
@ -8,9 +8,16 @@ 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.
|
||||||
|
|
||||||
|
* 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 list of additional choices, one per line.
|
||||||
|
|
||||||
### Order Alphabetically
|
### Order Alphabetically
|
||||||
|
|
||||||
|
@ -135,8 +135,8 @@ class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
|
|||||||
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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django_rq.queues import get_connection
|
from django_rq.queues import get_connection
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
@ -64,6 +65,29 @@ 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()]
|
||||||
|
|
||||||
|
page = self.paginate_queryset(choices)
|
||||||
|
if page is not None:
|
||||||
|
data = [
|
||||||
|
{
|
||||||
|
'id': c[0],
|
||||||
|
'display': c[1],
|
||||||
|
} for c in page
|
||||||
|
]
|
||||||
|
return self.get_paginated_response(data)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Custom links
|
# Custom links
|
||||||
|
@ -66,6 +66,17 @@ class CustomFieldVisibilityChoices(ChoiceSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFieldChoiceSetBaseChoices(ChoiceSet):
|
||||||
|
|
||||||
|
ISO_3166 = 'ISO_3166'
|
||||||
|
UN_LOCODE = 'UN_LOCODE'
|
||||||
|
|
||||||
|
CHOICES = (
|
||||||
|
(ISO_3166, 'ISO 3166'),
|
||||||
|
(UN_LOCODE, 'UN/LOCODE'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# CustomLinks
|
# CustomLinks
|
||||||
#
|
#
|
||||||
|
7
netbox/extras/data/__init__.py
Normal file
7
netbox/extras/data/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from .iso_3166 import ISO_3166
|
||||||
|
from .un_locode import UN_LOCODE
|
||||||
|
|
||||||
|
CHOICE_SETS = {
|
||||||
|
'ISO_3166': ISO_3166,
|
||||||
|
'UN_LOCODE': UN_LOCODE,
|
||||||
|
}
|
253
netbox/extras/data/iso_3166.py
Normal file
253
netbox/extras/data/iso_3166.py
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
# Two-letter country codes defined by ISO 3166
|
||||||
|
# Source: https://datahub.io/core/country-list
|
||||||
|
ISO_3166 = [
|
||||||
|
('AD', 'AD (Andorra)'),
|
||||||
|
('AE', 'AE (United Arab Emirates)'),
|
||||||
|
('AF', 'AF (Afghanistan)'),
|
||||||
|
('AG', 'AG (Antigua and Barbuda)'),
|
||||||
|
('AI', 'AI (Anguilla)'),
|
||||||
|
('AL', 'AL (Albania)'),
|
||||||
|
('AM', 'AM (Armenia)'),
|
||||||
|
('AO', 'AO (Angola)'),
|
||||||
|
('AQ', 'AQ (Antarctica)'),
|
||||||
|
('AR', 'AR (Argentina)'),
|
||||||
|
('AS', 'AS (American Samoa)'),
|
||||||
|
('AT', 'AT (Austria)'),
|
||||||
|
('AU', 'AU (Australia)'),
|
||||||
|
('AW', 'AW (Aruba)'),
|
||||||
|
('AX', 'AX (Åland Islands)'),
|
||||||
|
('AZ', 'AZ (Azerbaijan)'),
|
||||||
|
('BA', 'BA (Bosnia and Herzegovina)'),
|
||||||
|
('BB', 'BB (Barbados)'),
|
||||||
|
('BD', 'BD (Bangladesh)'),
|
||||||
|
('BE', 'BE (Belgium)'),
|
||||||
|
('BF', 'BF (Burkina Faso)'),
|
||||||
|
('BG', 'BG (Bulgaria)'),
|
||||||
|
('BH', 'BH (Bahrain)'),
|
||||||
|
('BI', 'BI (Burundi)'),
|
||||||
|
('BJ', 'BJ (Benin)'),
|
||||||
|
('BL', 'BL (Saint Barthélemy)'),
|
||||||
|
('BM', 'BM (Bermuda)'),
|
||||||
|
('BN', 'BN (Brunei Darussalam)'),
|
||||||
|
('BO', 'BO (Bolivia, Plurinational State of)'),
|
||||||
|
('BQ', 'BQ (Bonaire, Sint Eustatius and Saba)'),
|
||||||
|
('BR', 'BR (Brazil)'),
|
||||||
|
('BS', 'BS (Bahamas)'),
|
||||||
|
('BT', 'BT (Bhutan)'),
|
||||||
|
('BV', 'BV (Bouvet Island)'),
|
||||||
|
('BW', 'BW (Botswana)'),
|
||||||
|
('BY', 'BY (Belarus)'),
|
||||||
|
('BZ', 'BZ (Belize)'),
|
||||||
|
('CA', 'CA (Canada)'),
|
||||||
|
('CC', 'CC (Cocos (Keeling) Islands)'),
|
||||||
|
('CD', 'CD (Congo, the Democratic Republic of the)'),
|
||||||
|
('CF', 'CF (Central African Republic)'),
|
||||||
|
('CG', 'CG (Congo)'),
|
||||||
|
('CH', 'CH (Switzerland)'),
|
||||||
|
('CI', "CI (Côte d'Ivoire)"),
|
||||||
|
('CK', 'CK (Cook Islands)'),
|
||||||
|
('CL', 'CL (Chile)'),
|
||||||
|
('CM', 'CM (Cameroon)'),
|
||||||
|
('CN', 'CN (China)'),
|
||||||
|
('CO', 'CO (Colombia)'),
|
||||||
|
('CR', 'CR (Costa Rica)'),
|
||||||
|
('CU', 'CU (Cuba)'),
|
||||||
|
('CV', 'CV (Cape Verde)'),
|
||||||
|
('CW', 'CW (Curaçao)'),
|
||||||
|
('CX', 'CX (Christmas Island)'),
|
||||||
|
('CY', 'CY (Cyprus)'),
|
||||||
|
('CZ', 'CZ (Czech Republic)'),
|
||||||
|
('DE', 'DE (Germany)'),
|
||||||
|
('DJ', 'DJ (Djibouti)'),
|
||||||
|
('DK', 'DK (Denmark)'),
|
||||||
|
('DM', 'DM (Dominica)'),
|
||||||
|
('DO', 'DO (Dominican Republic)'),
|
||||||
|
('DZ', 'DZ (Algeria)'),
|
||||||
|
('EC', 'EC (Ecuador)'),
|
||||||
|
('EE', 'EE (Estonia)'),
|
||||||
|
('EG', 'EG (Egypt)'),
|
||||||
|
('EH', 'EH (Western Sahara)'),
|
||||||
|
('ER', 'ER (Eritrea)'),
|
||||||
|
('ES', 'ES (Spain)'),
|
||||||
|
('ET', 'ET (Ethiopia)'),
|
||||||
|
('FI', 'FI (Finland)'),
|
||||||
|
('FJ', 'FJ (Fiji)'),
|
||||||
|
('FK', 'FK (Falkland Islands (Malvinas))'),
|
||||||
|
('FM', 'FM (Micronesia, Federated States of)'),
|
||||||
|
('FO', 'FO (Faroe Islands)'),
|
||||||
|
('FR', 'FR (France)'),
|
||||||
|
('GA', 'GA (Gabon)'),
|
||||||
|
('GB', 'GB (United Kingdom)'),
|
||||||
|
('GD', 'GD (Grenada)'),
|
||||||
|
('GE', 'GE (Georgia)'),
|
||||||
|
('GF', 'GF (French Guiana)'),
|
||||||
|
('GG', 'GG (Guernsey)'),
|
||||||
|
('GH', 'GH (Ghana)'),
|
||||||
|
('GI', 'GI (Gibraltar)'),
|
||||||
|
('GL', 'GL (Greenland)'),
|
||||||
|
('GM', 'GM (Gambia)'),
|
||||||
|
('GN', 'GN (Guinea)'),
|
||||||
|
('GP', 'GP (Guadeloupe)'),
|
||||||
|
('GQ', 'GQ (Equatorial Guinea)'),
|
||||||
|
('GR', 'GR (Greece)'),
|
||||||
|
('GS', 'GS (South Georgia and the South Sandwich Islands)'),
|
||||||
|
('GT', 'GT (Guatemala)'),
|
||||||
|
('GU', 'GU (Guam)'),
|
||||||
|
('GW', 'GW (Guinea-Bissau)'),
|
||||||
|
('GY', 'GY (Guyana)'),
|
||||||
|
('HK', 'HK (Hong Kong)'),
|
||||||
|
('HM', 'HM (Heard Island and McDonald Islands)'),
|
||||||
|
('HN', 'HN (Honduras)'),
|
||||||
|
('HR', 'HR (Croatia)'),
|
||||||
|
('HT', 'HT (Haiti)'),
|
||||||
|
('HU', 'HU (Hungary)'),
|
||||||
|
('ID', 'ID (Indonesia)'),
|
||||||
|
('IE', 'IE (Ireland)'),
|
||||||
|
('IL', 'IL (Israel)'),
|
||||||
|
('IM', 'IM (Isle of Man)'),
|
||||||
|
('IN', 'IN (India)'),
|
||||||
|
('IO', 'IO (British Indian Ocean Territory)'),
|
||||||
|
('IQ', 'IQ (Iraq)'),
|
||||||
|
('IR', 'IR (Iran, Islamic Republic of)'),
|
||||||
|
('IS', 'IS (Iceland)'),
|
||||||
|
('IT', 'IT (Italy)'),
|
||||||
|
('JE', 'JE (Jersey)'),
|
||||||
|
('JM', 'JM (Jamaica)'),
|
||||||
|
('JO', 'JO (Jordan)'),
|
||||||
|
('JP', 'JP (Japan)'),
|
||||||
|
('KE', 'KE (Kenya)'),
|
||||||
|
('KG', 'KG (Kyrgyzstan)'),
|
||||||
|
('KH', 'KH (Cambodia)'),
|
||||||
|
('KI', 'KI (Kiribati)'),
|
||||||
|
('KM', 'KM (Comoros)'),
|
||||||
|
('KN', 'KN (Saint Kitts and Nevis)'),
|
||||||
|
('KP', "KP (Korea, Democratic People's Republic of)"),
|
||||||
|
('KR', 'KR (Korea, Republic of)'),
|
||||||
|
('KW', 'KW (Kuwait)'),
|
||||||
|
('KY', 'KY (Cayman Islands)'),
|
||||||
|
('KZ', 'KZ (Kazakhstan)'),
|
||||||
|
('LA', "LA (Lao People's Democratic Republic)"),
|
||||||
|
('LB', 'LB (Lebanon)'),
|
||||||
|
('LC', 'LC (Saint Lucia)'),
|
||||||
|
('LI', 'LI (Liechtenstein)'),
|
||||||
|
('LK', 'LK (Sri Lanka)'),
|
||||||
|
('LR', 'LR (Liberia)'),
|
||||||
|
('LS', 'LS (Lesotho)'),
|
||||||
|
('LT', 'LT (Lithuania)'),
|
||||||
|
('LU', 'LU (Luxembourg)'),
|
||||||
|
('LV', 'LV (Latvia)'),
|
||||||
|
('LY', 'LY (Libya)'),
|
||||||
|
('MA', 'MA (Morocco)'),
|
||||||
|
('MC', 'MC (Monaco)'),
|
||||||
|
('MD', 'MD (Moldova, Republic of)'),
|
||||||
|
('ME', 'ME (Montenegro)'),
|
||||||
|
('MF', 'MF (Saint Martin (French part))'),
|
||||||
|
('MG', 'MG (Madagascar)'),
|
||||||
|
('MH', 'MH (Marshall Islands)'),
|
||||||
|
('MK', 'MK (Macedonia, the Former Yugoslav Republic of)'),
|
||||||
|
('ML', 'ML (Mali)'),
|
||||||
|
('MM', 'MM (Myanmar)'),
|
||||||
|
('MN', 'MN (Mongolia)'),
|
||||||
|
('MO', 'MO (Macao)'),
|
||||||
|
('MP', 'MP (Northern Mariana Islands)'),
|
||||||
|
('MQ', 'MQ (Martinique)'),
|
||||||
|
('MR', 'MR (Mauritania)'),
|
||||||
|
('MS', 'MS (Montserrat)'),
|
||||||
|
('MT', 'MT (Malta)'),
|
||||||
|
('MU', 'MU (Mauritius)'),
|
||||||
|
('MV', 'MV (Maldives)'),
|
||||||
|
('MW', 'MW (Malawi)'),
|
||||||
|
('MX', 'MX (Mexico)'),
|
||||||
|
('MY', 'MY (Malaysia)'),
|
||||||
|
('MZ', 'MZ (Mozambique)'),
|
||||||
|
('NA', 'NA (Namibia)'),
|
||||||
|
('NC', 'NC (New Caledonia)'),
|
||||||
|
('NE', 'NE (Niger)'),
|
||||||
|
('NF', 'NF (Norfolk Island)'),
|
||||||
|
('NG', 'NG (Nigeria)'),
|
||||||
|
('NI', 'NI (Nicaragua)'),
|
||||||
|
('NL', 'NL (Netherlands)'),
|
||||||
|
('NO', 'NO (Norway)'),
|
||||||
|
('NP', 'NP (Nepal)'),
|
||||||
|
('NR', 'NR (Nauru)'),
|
||||||
|
('NU', 'NU (Niue)'),
|
||||||
|
('NZ', 'NZ (New Zealand)'),
|
||||||
|
('OM', 'OM (Oman)'),
|
||||||
|
('PA', 'PA (Panama)'),
|
||||||
|
('PE', 'PE (Peru)'),
|
||||||
|
('PF', 'PF (French Polynesia)'),
|
||||||
|
('PG', 'PG (Papua New Guinea)'),
|
||||||
|
('PH', 'PH (Philippines)'),
|
||||||
|
('PK', 'PK (Pakistan)'),
|
||||||
|
('PL', 'PL (Poland)'),
|
||||||
|
('PM', 'PM (Saint Pierre and Miquelon)'),
|
||||||
|
('PN', 'PN (Pitcairn)'),
|
||||||
|
('PR', 'PR (Puerto Rico)'),
|
||||||
|
('PS', 'PS (Palestine, State of)'),
|
||||||
|
('PT', 'PT (Portugal)'),
|
||||||
|
('PW', 'PW (Palau)'),
|
||||||
|
('PY', 'PY (Paraguay)'),
|
||||||
|
('QA', 'QA (Qatar)'),
|
||||||
|
('RE', 'RE (Réunion)'),
|
||||||
|
('RO', 'RO (Romania)'),
|
||||||
|
('RS', 'RS (Serbia)'),
|
||||||
|
('RU', 'RU (Russian Federation)'),
|
||||||
|
('RW', 'RW (Rwanda)'),
|
||||||
|
('SA', 'SA (Saudi Arabia)'),
|
||||||
|
('SB', 'SB (Solomon Islands)'),
|
||||||
|
('SC', 'SC (Seychelles)'),
|
||||||
|
('SD', 'SD (Sudan)'),
|
||||||
|
('SE', 'SE (Sweden)'),
|
||||||
|
('SG', 'SG (Singapore)'),
|
||||||
|
('SH', 'SH (Saint Helena, Ascension and Tristan da Cunha)'),
|
||||||
|
('SI', 'SI (Slovenia)'),
|
||||||
|
('SJ', 'SJ (Svalbard and Jan Mayen)'),
|
||||||
|
('SK', 'SK (Slovakia)'),
|
||||||
|
('SL', 'SL (Sierra Leone)'),
|
||||||
|
('SM', 'SM (San Marino)'),
|
||||||
|
('SN', 'SN (Senegal)'),
|
||||||
|
('SO', 'SO (Somalia)'),
|
||||||
|
('SR', 'SR (Suriname)'),
|
||||||
|
('SS', 'SS (South Sudan)'),
|
||||||
|
('ST', 'ST (Sao Tome and Principe)'),
|
||||||
|
('SV', 'SV (El Salvador)'),
|
||||||
|
('SX', 'SX (Sint Maarten (Dutch part))'),
|
||||||
|
('SY', 'SY (Syrian Arab Republic)'),
|
||||||
|
('SZ', 'SZ (Swaziland)'),
|
||||||
|
('TC', 'TC (Turks and Caicos Islands)'),
|
||||||
|
('TD', 'TD (Chad)'),
|
||||||
|
('TF', 'TF (French Southern Territories)'),
|
||||||
|
('TG', 'TG (Togo)'),
|
||||||
|
('TH', 'TH (Thailand)'),
|
||||||
|
('TJ', 'TJ (Tajikistan)'),
|
||||||
|
('TK', 'TK (Tokelau)'),
|
||||||
|
('TL', 'TL (Timor-Leste)'),
|
||||||
|
('TM', 'TM (Turkmenistan)'),
|
||||||
|
('TN', 'TN (Tunisia)'),
|
||||||
|
('TO', 'TO (Tonga)'),
|
||||||
|
('TR', 'TR (Turkey)'),
|
||||||
|
('TT', 'TT (Trinidad and Tobago)'),
|
||||||
|
('TV', 'TV (Tuvalu)'),
|
||||||
|
('TW', 'TW (Taiwan, Province of China)'),
|
||||||
|
('TZ', 'TZ (Tanzania, United Republic of)'),
|
||||||
|
('UA', 'UA (Ukraine)'),
|
||||||
|
('UG', 'UG (Uganda)'),
|
||||||
|
('UM', 'UM (United States Minor Outlying Islands)'),
|
||||||
|
('US', 'US (United States)'),
|
||||||
|
('UY', 'UY (Uruguay)'),
|
||||||
|
('UZ', 'UZ (Uzbekistan)'),
|
||||||
|
('VA', 'VA (Holy See (Vatican City State))'),
|
||||||
|
('VC', 'VC (Saint Vincent and the Grenadines)'),
|
||||||
|
('VE', 'VE (Venezuela, Bolivarian Republic of)'),
|
||||||
|
('VG', 'VG (Virgin Islands, British)'),
|
||||||
|
('VI', 'VI (Virgin Islands, U.S.)'),
|
||||||
|
('VN', 'VN (Viet Nam)'),
|
||||||
|
('VU', 'VU (Vanuatu)'),
|
||||||
|
('WF', 'WF (Wallis and Futuna)'),
|
||||||
|
('WS', 'WS (Samoa)'),
|
||||||
|
('YE', 'YE (Yemen)'),
|
||||||
|
('YT', 'YT (Mayotte)'),
|
||||||
|
('ZA', 'ZA (South Africa)'),
|
||||||
|
('ZM', 'ZM (Zambia)'),
|
||||||
|
('ZW', 'ZW (Zimbabwe)')
|
||||||
|
]
|
111557
netbox/extras/data/un_locode.py
Normal file
111557
netbox/extras/data/un_locode.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -114,7 +114,7 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
|
|||||||
class Meta:
|
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):
|
||||||
|
@ -62,6 +62,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 +74,7 @@ class CustomFieldChoiceSetBulkEditForm(BulkEditForm):
|
|||||||
widget=BulkEditNullBooleanSelect()
|
widget=BulkEditNullBooleanSelect()
|
||||||
)
|
)
|
||||||
|
|
||||||
nullable_fields = ('description',)
|
nullable_fields = ('base_choices', 'description')
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkBulkEditForm(BulkEditForm):
|
class CustomLinkBulkEditForm(BulkEditForm):
|
||||||
|
@ -4,7 +4,7 @@ from django.contrib.postgres.forms import SimpleArrayField
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext 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
|
||||||
@ -63,6 +63,11 @@ class CustomFieldImportForm(CSVModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class CustomFieldChoiceSetImportForm(CSVModelForm):
|
class CustomFieldChoiceSetImportForm(CSVModelForm):
|
||||||
|
base_choices = CSVChoiceField(
|
||||||
|
choices=CustomFieldChoiceSetBaseChoices,
|
||||||
|
required=False,
|
||||||
|
help_text=_('The classification of entry')
|
||||||
|
)
|
||||||
extra_choices = SimpleArrayField(
|
extra_choices = SimpleArrayField(
|
||||||
base_field=forms.CharField(),
|
base_field=forms.CharField(),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -84,7 +84,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
|
||||||
|
@ -87,12 +87,13 @@ 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=ArrayWidget(),
|
||||||
|
required=False,
|
||||||
help_text=_('Enter one choice per line.')
|
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()
|
return self.cleaned_data['extra_choices'].splitlines()
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 4.1.10 on 2023-07-18 13:56
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0097_customfield_remove_choices'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customfieldchoiceset',
|
||||||
|
name='base_choices',
|
||||||
|
field=models.CharField(blank=True, max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customfieldchoiceset',
|
||||||
|
name='extra_choices',
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, null=True, size=None),
|
||||||
|
),
|
||||||
|
]
|
@ -15,17 +15,18 @@ from django.utils.safestring import mark_safe
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
|
from extras.data import CHOICE_SETS
|
||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
from netbox.models import ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
from netbox.models.features import CloningMixin, ExportTemplatesMixin
|
from netbox.models.features import CloningMixin, ExportTemplatesMixin
|
||||||
from netbox.search import FieldTypes
|
from netbox.search import FieldTypes
|
||||||
from utilities import filters
|
from utilities import filters
|
||||||
from utilities.forms.fields import (
|
from utilities.forms.fields import (
|
||||||
CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DynamicModelChoiceField,
|
CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DynamicChoiceField,
|
||||||
DynamicModelMultipleChoiceField, JSONField, LaxURLField,
|
DynamicModelChoiceField, DynamicModelMultipleChoiceField, DynamicMultipleChoiceField, JSONField, LaxURLField,
|
||||||
)
|
)
|
||||||
from utilities.forms.utils import add_blank_choice
|
from utilities.forms.utils import add_blank_choice
|
||||||
from utilities.forms.widgets import DatePicker, DateTimePicker
|
from utilities.forms.widgets import APISelect, APISelectMultiple, DatePicker, DateTimePicker
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.validators import validate_regex
|
from utilities.validators import validate_regex
|
||||||
|
|
||||||
@ -410,7 +411,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
|
|
||||||
# Select
|
# Select
|
||||||
elif self.type in (CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT):
|
elif self.type in (CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT):
|
||||||
choices = [(c, c) for c in self.choices]
|
choices = self.choices
|
||||||
default_choice = self.default if self.default in self.choices else None
|
default_choice = self.default if self.default in self.choices else None
|
||||||
|
|
||||||
if not required or default_choice is None:
|
if not required or default_choice is None:
|
||||||
@ -421,11 +422,17 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
initial = default_choice
|
initial = default_choice
|
||||||
|
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||||
field_class = CSVChoiceField if for_csv_import else forms.ChoiceField
|
field_class = CSVChoiceField if for_csv_import else DynamicChoiceField
|
||||||
field = field_class(choices=choices, required=required, initial=initial)
|
widget_class = APISelect
|
||||||
else:
|
else:
|
||||||
field_class = CSVMultipleChoiceField if for_csv_import else forms.MultipleChoiceField
|
field_class = CSVMultipleChoiceField if for_csv_import else DynamicMultipleChoiceField
|
||||||
field = field_class(choices=choices, required=required, initial=initial)
|
widget_class = APISelectMultiple
|
||||||
|
field = field_class(
|
||||||
|
choices=self.choice_set.choices,
|
||||||
|
required=required,
|
||||||
|
initial=initial,
|
||||||
|
widget=widget_class(api_url=f'/api/extras/custom-field-choices/{self.choice_set.pk}/choices/')
|
||||||
|
)
|
||||||
|
|
||||||
# URL
|
# URL
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_URL:
|
elif self.type == CustomFieldTypeChoices.TYPE_URL:
|
||||||
@ -604,14 +611,14 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
|
|
||||||
# Validate selected choice
|
# Validate selected choice
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||||
if value not in self.choices:
|
if value not in [c[0] for c in self.choices]:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}"
|
f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate all selected choices
|
# Validate all selected choices
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
||||||
if not set(value).issubset(self.choices):
|
if not set(value).issubset([c[0] for c in self.choices]):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}"
|
f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}"
|
||||||
)
|
)
|
||||||
@ -645,9 +652,17 @@ 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(
|
||||||
base_field=models.CharField(max_length=100),
|
base_field=models.CharField(max_length=100),
|
||||||
help_text=_('List of field choices')
|
help_text=_('List of field choices'),
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
)
|
)
|
||||||
order_alphabetically = models.BooleanField(
|
order_alphabetically = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
@ -667,7 +682,20 @@ 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([(k, k) for k in self.extra_choices])
|
||||||
|
return self._choices
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if not self.base_choices and not self.extra_choices:
|
||||||
|
raise ValidationError(_("Must define base or extra choices."))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def choices_count(self):
|
def choices_count(self):
|
||||||
|
@ -87,11 +87,12 @@ class CustomFieldChoiceSetTable(NetBoxTable):
|
|||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
base_choices = columns.ChoiceFieldColumn()
|
||||||
choices = columns.ArrayColumn(
|
choices = columns.ArrayColumn(
|
||||||
max_items=10,
|
max_items=10,
|
||||||
accessor=tables.A('extra_choices'),
|
accessor=tables.A('extra_choices'),
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name=_('Choices')
|
verbose_name=_('Count')
|
||||||
)
|
)
|
||||||
choice_count = tables.TemplateColumn(
|
choice_count = tables.TemplateColumn(
|
||||||
accessor=tables.A('extra_choices'),
|
accessor=tables.A('extra_choices'),
|
||||||
@ -104,10 +105,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', 'choice_count', 'choices', 'order_alphabetically',
|
||||||
'last_updated',
|
'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):
|
||||||
|
@ -17,6 +17,10 @@
|
|||||||
<th scope="row">Description</th>
|
<th scope="row">Description</th>
|
||||||
<td>{{ object.description|markdown|placeholder }}</td>
|
<td>{{ object.description|markdown|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Base Choices</th>
|
||||||
|
<td>{{ object.get_base_choices_display }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Choices</th>
|
<th scope="row">Choices</th>
|
||||||
<td>{{ object.choices|length }}</td>
|
<td>{{ object.choices|length }}</td>
|
||||||
|
@ -8,11 +8,49 @@ from utilities.forms import widgets
|
|||||||
from utilities.utils import get_viewname
|
from utilities.utils import get_viewname
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'DynamicChoiceField',
|
||||||
'DynamicModelChoiceField',
|
'DynamicModelChoiceField',
|
||||||
'DynamicModelMultipleChoiceField',
|
'DynamicModelMultipleChoiceField',
|
||||||
|
'DynamicMultipleChoiceField',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Choice fields
|
||||||
|
#
|
||||||
|
|
||||||
|
class DynamicChoiceField(forms.ChoiceField):
|
||||||
|
|
||||||
|
def get_bound_field(self, form, field_name):
|
||||||
|
bound_field = BoundField(form, self, field_name)
|
||||||
|
data = bound_field.value()
|
||||||
|
|
||||||
|
if data is not None:
|
||||||
|
self.choices = [
|
||||||
|
choice for choice in self.choices if choice[0] == data
|
||||||
|
]
|
||||||
|
|
||||||
|
return bound_field
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicMultipleChoiceField(forms.MultipleChoiceField):
|
||||||
|
|
||||||
|
def get_bound_field(self, form, field_name):
|
||||||
|
bound_field = BoundField(form, self, field_name)
|
||||||
|
data = bound_field.value()
|
||||||
|
|
||||||
|
if data is not None:
|
||||||
|
self.choices = [
|
||||||
|
choice for choice in self.choices if choice[0] in data
|
||||||
|
]
|
||||||
|
|
||||||
|
return bound_field
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Model choice fields
|
||||||
|
#
|
||||||
|
|
||||||
class DynamicModelChoiceMixin:
|
class DynamicModelChoiceMixin:
|
||||||
"""
|
"""
|
||||||
Override `get_bound_field()` to avoid pre-populating field choices with a SQL query. The field will be
|
Override `get_bound_field()` to avoid pre-populating field choices with a SQL query. The field will be
|
||||||
|
Loading…
Reference in New Issue
Block a user