Initial work on predefined choices for custom fields

This commit is contained in:
Jeremy Stretch 2023-07-18 16:42:47 -04:00
parent 306cfeeebb
commit b8cf15ac97
17 changed files with 111993 additions and 24 deletions

View File

@ -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

View File

@ -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',
]

View File

@ -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

View File

@ -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
#

View 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,
}

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:
model = CustomFieldChoiceSet
fields = [
'id', 'name', 'description', 'order_alphabetically',
'id', 'name', 'description', 'base_choices', 'order_alphabetically',
]
def search(self, queryset, name, value):

View File

@ -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):

View File

@ -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,

View File

@ -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

View File

@ -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()

View File

@ -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),
),
]

View File

@ -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):

View File

@ -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):

View File

@ -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>

View File

@ -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