forms.py 7.5 KB

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