fields.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import json
  2. from django import forms
  3. from django.db.models import Count
  4. from django.forms.fields import JSONField as _JSONField, InvalidJSONInput
  5. from django.templatetags.static import static
  6. from django.utils.translation import gettext_lazy as _
  7. from netaddr import AddrFormatError, EUI
  8. from utilities.forms import widgets
  9. from utilities.validators import EnhancedURLValidator
  10. __all__ = (
  11. 'ColorField',
  12. 'CommentField',
  13. 'JSONField',
  14. 'LaxURLField',
  15. 'MACAddressField',
  16. 'SlugField',
  17. 'TagFilterField',
  18. )
  19. class CommentField(forms.CharField):
  20. """
  21. A textarea with support for Markdown rendering. Exists mostly just to add a standard `help_text`.
  22. """
  23. widget = widgets.MarkdownWidget
  24. label = _('Comments')
  25. help_text = _(
  26. '<i class="mdi mdi-information-outline"></i> '
  27. '<a href="{url}" target="_blank" tabindex="-1">Markdown</a> syntax is supported'
  28. ).format(url=static('docs/reference/markdown/'))
  29. def __init__(self, *, label=label, help_text=help_text, required=False, **kwargs):
  30. super().__init__(label=label, help_text=help_text, required=required, **kwargs)
  31. class SlugField(forms.SlugField):
  32. """
  33. Extend Django's built-in SlugField to automatically populate from a field called `name` unless otherwise specified.
  34. Parameters:
  35. slug_source: Name of the form field from which the slug value will be derived
  36. """
  37. widget = widgets.SlugWidget
  38. label = _('Slug')
  39. help_text = _("URL-friendly unique shorthand")
  40. def __init__(self, *, slug_source='name', label=label, help_text=help_text, **kwargs):
  41. super().__init__(label=label, help_text=help_text, **kwargs)
  42. self.widget.attrs['slug-source'] = slug_source
  43. class ColorField(forms.CharField):
  44. """
  45. A field which represents a color value in hexadecimal `RRGGBB` format. Utilizes NetBox's `ColorSelect` widget to
  46. render choices.
  47. """
  48. widget = widgets.ColorSelect
  49. class TagFilterField(forms.MultipleChoiceField):
  50. """
  51. A filter field for the tags of a model. Only the tags used by a model are displayed.
  52. :param model: The model of the filter
  53. """
  54. def __init__(self, model, *args, **kwargs):
  55. def get_choices():
  56. tags = model.tags.annotate(
  57. count=Count('extras_taggeditem_items')
  58. ).order_by('name')
  59. return [
  60. (str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags
  61. ]
  62. # Choices are fetched each time the form is initialized
  63. super().__init__(label=_('Tags'), choices=get_choices, required=False, *args, **kwargs)
  64. class LaxURLField(forms.URLField):
  65. """
  66. Modifies Django's built-in URLField to remove the requirement for fully-qualified domain names
  67. (e.g. http://myserver/ is valid)
  68. """
  69. default_validators = [EnhancedURLValidator()]
  70. class JSONField(_JSONField):
  71. """
  72. Custom wrapper around Django's built-in JSONField to avoid presenting "null" as the default text.
  73. """
  74. empty_values = [None, '', ()]
  75. def __init__(self, *args, **kwargs):
  76. super().__init__(*args, **kwargs)
  77. if not self.help_text:
  78. self.help_text = _('Enter context data in <a href="https://json.org/">JSON</a> format.')
  79. self.widget.attrs['placeholder'] = ''
  80. self.widget.attrs['class'] = 'font-monospace'
  81. def prepare_value(self, value):
  82. if isinstance(value, InvalidJSONInput):
  83. return value
  84. if value in ('', None):
  85. return ''
  86. if type(value) is str:
  87. try:
  88. value = json.loads(value, cls=self.decoder)
  89. except json.decoder.JSONDecodeError:
  90. return value
  91. return json.dumps(value, sort_keys=True, indent=4, ensure_ascii=False, cls=self.encoder)
  92. class MACAddressField(forms.Field):
  93. """
  94. Validates a 48-bit MAC address.
  95. """
  96. widget = forms.CharField
  97. default_error_messages = {
  98. 'invalid': _('MAC address must be in EUI-48 format'),
  99. }
  100. def to_python(self, value):
  101. value = super().to_python(value)
  102. # Validate MAC address format
  103. try:
  104. value = EUI(value.strip())
  105. except AddrFormatError:
  106. raise forms.ValidationError(self.error_messages['invalid'], code='invalid')
  107. return value