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.
|
||||
|
||||
### 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
|
||||
|
||||
The list of valid choices, entered as a comma-separated list.
|
||||
A list of additional choices, one per line.
|
||||
|
||||
### Order Alphabetically
|
||||
|
||||
|
@ -135,8 +135,8 @@ class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
|
||||
class Meta:
|
||||
model = CustomFieldChoiceSet
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'description', 'extra_choices', 'order_alphabetically', 'choices_count',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
|
||||
'choices_count', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_rq.queues import get_connection
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
@ -64,6 +65,29 @@ class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
|
||||
serializer_class = serializers.CustomFieldChoiceSetSerializer
|
||||
filterset_class = filtersets.CustomFieldChoiceSetFilterSet
|
||||
|
||||
@action(detail=True)
|
||||
def choices(self, request, pk):
|
||||
"""
|
||||
Provides an endpoint to iterate through each choice in a set.
|
||||
"""
|
||||
choiceset = get_object_or_404(self.queryset, pk=pk)
|
||||
choices = choiceset.choices
|
||||
|
||||
# Enable filtering
|
||||
if q := request.GET.get('q'):
|
||||
q = q.lower()
|
||||
choices = [c for c in choices if q in c[0].lower() or q in c[1].lower()]
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
#
|
||||
|
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:
|
||||
model = CustomFieldChoiceSet
|
||||
fields = [
|
||||
'id', 'name', 'description', 'order_alphabetically',
|
||||
'id', 'name', 'description', 'base_choices', 'order_alphabetically',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
|
@ -62,6 +62,10 @@ class CustomFieldChoiceSetBulkEditForm(BulkEditForm):
|
||||
queryset=CustomFieldChoiceSet.objects.all(),
|
||||
widget=forms.MultipleHiddenInput
|
||||
)
|
||||
base_choices = forms.ChoiceField(
|
||||
choices=add_blank_choice(CustomFieldChoiceSetBaseChoices),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
@ -70,7 +74,7 @@ class CustomFieldChoiceSetBulkEditForm(BulkEditForm):
|
||||
widget=BulkEditNullBooleanSelect()
|
||||
)
|
||||
|
||||
nullable_fields = ('description',)
|
||||
nullable_fields = ('base_choices', 'description')
|
||||
|
||||
|
||||
class CustomLinkBulkEditForm(BulkEditForm):
|
||||
|
@ -4,7 +4,7 @@ from django.contrib.postgres.forms import SimpleArrayField
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from extras.choices import CustomFieldVisibilityChoices, CustomFieldTypeChoices, JournalEntryKindChoices
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
@ -63,6 +63,11 @@ class CustomFieldImportForm(CSVModelForm):
|
||||
|
||||
|
||||
class CustomFieldChoiceSetImportForm(CSVModelForm):
|
||||
base_choices = CSVChoiceField(
|
||||
choices=CustomFieldChoiceSetBaseChoices,
|
||||
required=False,
|
||||
help_text=_('The classification of entry')
|
||||
)
|
||||
extra_choices = SimpleArrayField(
|
||||
base_field=forms.CharField(),
|
||||
required=False,
|
||||
|
@ -84,7 +84,12 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
||||
|
||||
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'choice')),
|
||||
(None, ('q', 'filter_id')),
|
||||
('Choices', ('base_choices', 'choice')),
|
||||
)
|
||||
base_choices = forms.MultipleChoiceField(
|
||||
choices=CustomFieldChoiceSetBaseChoices,
|
||||
required=False
|
||||
)
|
||||
choice = forms.CharField(
|
||||
required=False
|
||||
|
@ -87,12 +87,13 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
||||
class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
|
||||
extra_choices = forms.CharField(
|
||||
widget=ArrayWidget(),
|
||||
required=False,
|
||||
help_text=_('Enter one choice per line.')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomFieldChoiceSet
|
||||
fields = ('name', 'description', 'extra_choices', 'order_alphabetically')
|
||||
fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically')
|
||||
|
||||
def clean_extra_choices(self):
|
||||
return self.cleaned_data['extra_choices'].splitlines()
|
||||
|
@ -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 extras.choices import *
|
||||
from extras.data import CHOICE_SETS
|
||||
from extras.utils import FeatureQuery
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from netbox.models.features import CloningMixin, ExportTemplatesMixin
|
||||
from netbox.search import FieldTypes
|
||||
from utilities import filters
|
||||
from utilities.forms.fields import (
|
||||
CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DynamicModelChoiceField,
|
||||
DynamicModelMultipleChoiceField, JSONField, LaxURLField,
|
||||
CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVMultipleChoiceField, DynamicChoiceField,
|
||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, DynamicMultipleChoiceField, JSONField, LaxURLField,
|
||||
)
|
||||
from utilities.forms.utils import add_blank_choice
|
||||
from utilities.forms.widgets import DatePicker, DateTimePicker
|
||||
from utilities.forms.widgets import APISelect, APISelectMultiple, DatePicker, DateTimePicker
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.validators import validate_regex
|
||||
|
||||
@ -410,7 +411,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
|
||||
# Select
|
||||
elif self.type in (CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT):
|
||||
choices = [(c, c) for c in self.choices]
|
||||
choices = self.choices
|
||||
default_choice = self.default if self.default in self.choices else None
|
||||
|
||||
if not required or default_choice is None:
|
||||
@ -421,11 +422,17 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
initial = default_choice
|
||||
|
||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
field_class = CSVChoiceField if for_csv_import else forms.ChoiceField
|
||||
field = field_class(choices=choices, required=required, initial=initial)
|
||||
field_class = CSVChoiceField if for_csv_import else DynamicChoiceField
|
||||
widget_class = APISelect
|
||||
else:
|
||||
field_class = CSVMultipleChoiceField if for_csv_import else forms.MultipleChoiceField
|
||||
field = field_class(choices=choices, required=required, initial=initial)
|
||||
field_class = CSVMultipleChoiceField if for_csv_import else DynamicMultipleChoiceField
|
||||
widget_class = APISelectMultiple
|
||||
field = field_class(
|
||||
choices=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
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_URL:
|
||||
@ -604,14 +611,14 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
|
||||
# Validate selected choice
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
if value not in self.choices:
|
||||
if value not in [c[0] for c in self.choices]:
|
||||
raise ValidationError(
|
||||
f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}"
|
||||
)
|
||||
|
||||
# Validate all selected choices
|
||||
elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
||||
if not set(value).issubset(self.choices):
|
||||
if not set(value).issubset([c[0] for c in self.choices]):
|
||||
raise ValidationError(
|
||||
f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}"
|
||||
)
|
||||
@ -645,9 +652,17 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
|
||||
max_length=200,
|
||||
blank=True
|
||||
)
|
||||
base_choices = models.CharField(
|
||||
max_length=50,
|
||||
choices=CustomFieldChoiceSetBaseChoices,
|
||||
blank=True,
|
||||
help_text=_('Base set of predefined choices (optional)')
|
||||
)
|
||||
extra_choices = ArrayField(
|
||||
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(
|
||||
default=False,
|
||||
@ -667,7 +682,20 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
return self.extra_choices
|
||||
"""
|
||||
Returns a concatenation of the base and extra choices.
|
||||
"""
|
||||
if not hasattr(self, '_choices'):
|
||||
self._choices = []
|
||||
if self.base_choices:
|
||||
self._choices.extend(CHOICE_SETS.get(self.base_choices))
|
||||
if self.extra_choices:
|
||||
self._choices.extend([(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
|
||||
def choices_count(self):
|
||||
|
@ -87,11 +87,12 @@ class CustomFieldChoiceSetTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
base_choices = columns.ChoiceFieldColumn()
|
||||
choices = columns.ArrayColumn(
|
||||
max_items=10,
|
||||
accessor=tables.A('extra_choices'),
|
||||
orderable=False,
|
||||
verbose_name=_('Choices')
|
||||
verbose_name=_('Count')
|
||||
)
|
||||
choice_count = tables.TemplateColumn(
|
||||
accessor=tables.A('extra_choices'),
|
||||
@ -104,10 +105,10 @@ class CustomFieldChoiceSetTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CustomFieldChoiceSet
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'description', 'choice_count', 'choices', 'order_alphabetically', 'created',
|
||||
'last_updated',
|
||||
'pk', 'id', 'name', 'description', 'base_choices', 'choice_count', 'choices', 'order_alphabetically',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'choice_count', 'description')
|
||||
default_columns = ('pk', 'name', 'base_choices', 'choice_count', 'description')
|
||||
|
||||
|
||||
class CustomLinkTable(NetBoxTable):
|
||||
|
@ -17,6 +17,10 @@
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|markdown|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Base Choices</th>
|
||||
<td>{{ object.get_base_choices_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Choices</th>
|
||||
<td>{{ object.choices|length }}</td>
|
||||
|
@ -8,11 +8,49 @@ from utilities.forms import widgets
|
||||
from utilities.utils import get_viewname
|
||||
|
||||
__all__ = (
|
||||
'DynamicChoiceField',
|
||||
'DynamicModelChoiceField',
|
||||
'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:
|
||||
"""
|
||||
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