choices.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. from django.conf import settings
  2. from django.utils.translation import gettext_lazy as _
  3. __all__ = (
  4. 'ChoiceSet',
  5. 'unpack_grouped_choices',
  6. )
  7. class ChoiceSetMeta(type):
  8. """
  9. Metaclass for ChoiceSet
  10. """
  11. def __new__(mcs, name, bases, attrs):
  12. # Extend static choices with any configured choices
  13. if key := attrs.get('key'):
  14. assert type(attrs['CHOICES']) is list, _(
  15. "{name} has a key defined but CHOICES is not a list"
  16. ).format(name=name)
  17. app = attrs['__module__'].split('.', 1)[0]
  18. replace_key = f'{app}.{key}'
  19. extend_key = f'{replace_key}+' if replace_key else None
  20. if replace_key and replace_key in settings.FIELD_CHOICES:
  21. # Replace the stock choices
  22. attrs['CHOICES'] = settings.FIELD_CHOICES[replace_key]
  23. elif extend_key and extend_key in settings.FIELD_CHOICES:
  24. # Extend the stock choices
  25. attrs['CHOICES'].extend(settings.FIELD_CHOICES[extend_key])
  26. # Define choice tuples and color maps
  27. attrs['_choices'] = []
  28. attrs['colors'] = {}
  29. for choice in attrs['CHOICES']:
  30. if isinstance(choice[1], (list, tuple)):
  31. grouped_choices = []
  32. for c in choice[1]:
  33. grouped_choices.append((c[0], c[1]))
  34. if len(c) == 3:
  35. attrs['colors'][c[0]] = c[2]
  36. attrs['_choices'].append((choice[0], grouped_choices))
  37. else:
  38. attrs['_choices'].append((choice[0], choice[1]))
  39. if len(choice) == 3:
  40. attrs['colors'][choice[0]] = choice[2]
  41. return super().__new__(mcs, name, bases, attrs)
  42. def __call__(cls, *args, **kwargs):
  43. # django-filters will check if a 'choices' value is callable, and if so assume that it returns an iterable
  44. return getattr(cls, '_choices', ())
  45. def __iter__(cls):
  46. return iter(getattr(cls, '_choices', ()))
  47. class ChoiceSet(metaclass=ChoiceSetMeta):
  48. """
  49. Holds an iterable of choice tuples suitable for passing to a Django model or form field. Choices can be defined
  50. statically within the class as CHOICES and/or gleaned from the FIELD_CHOICES configuration parameter.
  51. """
  52. CHOICES = list()
  53. @classmethod
  54. def values(cls):
  55. return [c[0] for c in unpack_grouped_choices(cls._choices)]
  56. def unpack_grouped_choices(choices):
  57. """
  58. Unpack a grouped choices hierarchy into a flat list of two-tuples. For example:
  59. choices = (
  60. ('Foo', (
  61. (1, 'A'),
  62. (2, 'B')
  63. )),
  64. ('Bar', (
  65. (3, 'C'),
  66. (4, 'D')
  67. ))
  68. )
  69. becomes:
  70. choices = (
  71. (1, 'A'),
  72. (2, 'B'),
  73. (3, 'C'),
  74. (4, 'D')
  75. )
  76. """
  77. unpacked_choices = []
  78. for key, value in choices:
  79. if isinstance(value, (list, tuple)):
  80. # Entered an optgroup
  81. for optgroup_key, optgroup_value in value:
  82. unpacked_choices.append((optgroup_key, optgroup_value))
  83. else:
  84. unpacked_choices.append((key, value))
  85. return unpacked_choices