fields.py 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. from django.db import models
  2. from django.forms.widgets import Textarea
  3. from wagtail.fields import StreamField
  4. from wagtailcrx.widgets import ColorPickerWidget
  5. class CoderedStreamField(StreamField):
  6. """
  7. An exact copy of the Wagtail StreamField, modified to NOT PRESERVE HISTORY
  8. in Django migrations.
  9. Since our StreamFields are generally huge, and we also let sites override
  10. the blocks in our concrete models dynamically, this creates a slew of
  11. migration problems (most commonly: a client overrides CODERED_FRONTEND_*,
  12. which changes a string used in a concrete model, which triggers a migration
  13. back in wagtailcrx). Eliminiating the blocks from the deconstructed
  14. StreamField allows us to have dynamic streamfields without breaking
  15. migrations or having to refactor the core concepts of this package.
  16. Internally, we should ALWAYS use CoderedStreamField on CONCRETE models,
  17. meaning models which are part of our package and saved to the database. For
  18. ABSTRACT models - meaning they are made concrete in the client site - we
  19. should continue to use Wagtail StreamField to keep things Wagtail-ish by
  20. default. The client may then decide to use CoderedStreamField if they are
  21. annoyed by the big migrations.
  22. CAVEAT EMPTOR:
  23. Client sites built with CRX may use this in place of the Wagtail
  24. StreamField, in order to avoid huge migration files. However, note that it
  25. will not be possible to mine data out of a CoderedStreamField during a
  26. migration (e.g. RunPython).
  27. Inspired by:
  28. https://cynthiakiser.com/blog/2022/01/06/trimming-wagtail-migration-cruft.html
  29. """
  30. def __init__(self, *args, **kwargs):
  31. """
  32. Patch init to work around django reconstruct not sending empty args.
  33. """
  34. # If we did not get an arg, pass an empty list through to the parent.
  35. if not args:
  36. args = [[]]
  37. return super().__init__(*args, **kwargs)
  38. def deconstruct(self):
  39. """
  40. Override to ignore any blocks within the StreamField when
  41. decustructing into a migration.
  42. The output should look something like this, regardless of how many
  43. blocks are nested within the StreamField::
  44. ("body", wagtailcrx.fields.CoderedStreamField([]))
  45. """
  46. name, path, block_types, kwargs = super().deconstruct()
  47. block_types = []
  48. return name, path, block_types, kwargs
  49. class ColorField(models.CharField):
  50. """
  51. A CharField which uses the HTML5 color picker widget.
  52. """
  53. def __init__(self, *args, **kwargs):
  54. kwargs['max_length'] = 255
  55. super().__init__(*args, **kwargs)
  56. def formfield(self, **kwargs):
  57. kwargs['widget'] = ColorPickerWidget
  58. return super().formfield(**kwargs)
  59. class MonospaceField(models.TextField):
  60. """
  61. A TextField which renders as a large HTML textarea with monospace font.
  62. """
  63. def formfield(self, **kwargs):
  64. kwargs["widget"] = Textarea(attrs={
  65. "rows": 12,
  66. "class": "monospace",
  67. "spellcheck": "false",
  68. })
  69. return super().formfield(**kwargs)