forms.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. from django import forms
  2. from django.core.files.base import ContentFile
  3. from django.utils.translation import gettext, gettext_lazy as _
  4. from mptt.forms import TreeNodeChoiceField
  5. from ..models import Theme, Css
  6. from .css import css_needs_rebuilding, create_css, get_next_css_order
  7. from .media import create_media
  8. from .utils import get_file_hash
  9. from .validators import validate_css_name, validate_css_name_is_available
  10. class ThemeChoiceField(TreeNodeChoiceField):
  11. level_indicator = "- - "
  12. def __init__(self, *args, **kwargs):
  13. kwargs.setdefault("queryset", Theme.objects.all())
  14. kwargs.setdefault("empty_label", _("No parent"))
  15. kwargs.setdefault("level_indicator", self.level_indicator)
  16. super().__init__(*args, **kwargs)
  17. class ThemeForm(forms.ModelForm):
  18. name = forms.CharField(label=_("Name"))
  19. parent = ThemeChoiceField(label=_("Parent"), required=False)
  20. version = forms.CharField(label=_("Version"), required=False)
  21. author = forms.CharField(label=_("Author(s)"), required=False)
  22. url = forms.URLField(label=_("Url"), required=False)
  23. class Meta:
  24. model = Theme
  25. fields = ["name", "parent", "version", "author", "url"]
  26. def __init__(self, *args, **kwargs):
  27. super().__init__(*args, **kwargs)
  28. self.limit_parent_choices()
  29. def limit_parent_choices(self):
  30. if not self.instance or not self.instance.pk:
  31. return
  32. self.fields["parent"].queryset = Theme.objects.exclude(
  33. tree_id=self.instance.tree_id,
  34. lft__gte=self.instance.lft,
  35. rght__lte=self.instance.rght,
  36. )
  37. class ImportForm(forms.Form):
  38. name = forms.CharField(
  39. label=_("Name"),
  40. help_text=_("Leave this field empty to use theme name from imported file."),
  41. max_length=255,
  42. required=False,
  43. )
  44. parent = ThemeChoiceField(label=_("Parent"), required=False)
  45. upload = forms.FileField(
  46. label=_("Theme file"),
  47. help_text=_("Theme file should be a ZIP file."),
  48. )
  49. def clean_upload(self):
  50. data = self.cleaned_data["upload"]
  51. error_message = gettext("Uploaded file is not a valid ZIP file.")
  52. if not data.name.lower().endswith(".zip"):
  53. raise forms.ValidationError(error_message)
  54. if data.content_type not in ("application/zip", "application/octet-stream"):
  55. raise forms.ValidationError(error_message)
  56. return data
  57. class UploadAssetsForm(forms.Form):
  58. allowed_content_types = []
  59. allowed_extensions = []
  60. assets = forms.FileField(
  61. widget=forms.ClearableFileInput(attrs={"multiple": True}),
  62. error_messages={"required": _("No files have been uploaded.")},
  63. )
  64. def __init__(self, *args, instance=None):
  65. self.instance = instance
  66. super().__init__(*args)
  67. def clean_assets(self):
  68. assets = []
  69. for asset in self.files.getlist("assets"):
  70. try:
  71. if self.allowed_content_types:
  72. self.validate_asset_content_type(asset)
  73. if self.allowed_extensions:
  74. self.validate_asset_extension(asset)
  75. except forms.ValidationError as e:
  76. self.add_error("assets", e)
  77. else:
  78. assets.append(asset)
  79. return assets
  80. def validate_asset_content_type(self, asset):
  81. if asset.content_type in self.allowed_content_types:
  82. return
  83. message = gettext(
  84. 'File "%(file)s" content type "%(content_type)s" is not allowed.'
  85. )
  86. details = {"file": asset.name, "content_type": asset.content_type}
  87. raise forms.ValidationError(message % details)
  88. def validate_asset_extension(self, asset):
  89. filename = asset.name.lower()
  90. for extension in self.allowed_extensions:
  91. if filename.endswith(".%s" % extension):
  92. return
  93. message = gettext('File "%(file)s" extension is invalid.')
  94. details = {"file": asset.name}
  95. raise forms.ValidationError(message % details)
  96. def save(self):
  97. for asset in self.cleaned_data["assets"]:
  98. self.save_asset(asset)
  99. class UploadCssForm(UploadAssetsForm):
  100. allowed_content_types = ["text/css"]
  101. allowed_extensions = ["css"]
  102. def save_asset(self, asset):
  103. create_css(self.instance, asset)
  104. class UploadMediaForm(UploadAssetsForm):
  105. def save_asset(self, asset):
  106. create_media(self.instance, asset)
  107. class CssEditorForm(forms.ModelForm):
  108. name = forms.CharField(
  109. label=_("Name"),
  110. help_text=_(
  111. "Should be an correct filename and include the .css extension. It will be lowercased."
  112. ),
  113. validators=[validate_css_name],
  114. )
  115. source = forms.CharField(widget=forms.Textarea(), required=False)
  116. class Meta:
  117. model = Css
  118. fields = ["name"]
  119. def clean_name(self):
  120. data = self.cleaned_data["name"]
  121. validate_css_name_is_available(self.instance, data)
  122. return data
  123. def clean(self):
  124. cleaned_data = super().clean()
  125. if not cleaned_data.get("source"):
  126. raise forms.ValidationError(gettext("You need to enter CSS for this file."))
  127. return cleaned_data
  128. def save(self):
  129. name = self.cleaned_data["name"]
  130. source = self.cleaned_data["source"].encode("utf-8")
  131. source_file = ContentFile(source, name)
  132. self.instance.name = name
  133. if self.instance.source_file:
  134. self.instance.source_file.delete(save=False)
  135. self.instance.source_file = source_file
  136. self.instance.source_hash = get_file_hash(source_file)
  137. self.instance.source_needs_building = css_needs_rebuilding(source_file)
  138. self.instance.size = len(source)
  139. if not self.instance.pk:
  140. self.instance.order = get_next_css_order(self.instance.theme)
  141. self.instance.save()
  142. return self.instance
  143. class CssLinkForm(forms.ModelForm):
  144. name = forms.CharField(
  145. label=_("Link name"),
  146. help_text=_('Can be descriptive (e.g. "roboto from fonts.google.com").'),
  147. )
  148. url = forms.URLField(label=_("Remote CSS URL"))
  149. class Meta:
  150. model = Css
  151. fields = ["name", "url"]
  152. def clean_name(self):
  153. data = self.cleaned_data["name"]
  154. validate_css_name_is_available(self.instance, data)
  155. return data
  156. def save(self):
  157. if not self.instance.pk:
  158. self.instance.order = get_next_css_order(self.instance.theme)
  159. self.instance.save()
  160. else:
  161. self.instance.save(update_fields=["name", "url", "modified_on"])
  162. return self.instance