forms.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. from django import forms
  2. from django.db import models
  3. from django.utils.html import conditional_escape, mark_safe
  4. from django.utils.translation import gettext_lazy as _
  5. from mptt.forms import TreeNodeChoiceField, TreeNodeMultipleChoiceField
  6. from . import THREADS_ROOT_NAME
  7. from ..admin.forms import YesNoSwitch
  8. from ..core.validators import validate_sluggable
  9. from ..threads.threadtypes import trees_map
  10. from .models import Category, CategoryRole
  11. class AdminCategoryFieldMixin:
  12. def __init__(self, *args, **kwargs):
  13. self.base_level = kwargs.pop("base_level", 1)
  14. kwargs["level_indicator"] = kwargs.get("level_indicator", "- - ")
  15. threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
  16. queryset = Category.objects.filter(tree_id=threads_tree_id)
  17. if not kwargs.pop("include_root", False):
  18. queryset = queryset.exclude(special_role="root_category")
  19. kwargs.setdefault("queryset", queryset)
  20. super().__init__(*args, **kwargs)
  21. def _get_level_indicator(self, obj):
  22. level = getattr(obj, obj._mptt_meta.level_attr) - self.base_level
  23. if level > 0:
  24. return mark_safe(conditional_escape(self.level_indicator) * level)
  25. return ""
  26. class AdminCategoryChoiceField(AdminCategoryFieldMixin, TreeNodeChoiceField):
  27. pass
  28. class AdminCategoryMultipleChoiceField(
  29. AdminCategoryFieldMixin, TreeNodeMultipleChoiceField
  30. ):
  31. pass
  32. class CategoryFormBase(forms.ModelForm):
  33. name = forms.CharField(label=_("Name"), validators=[validate_sluggable()])
  34. description = forms.CharField(
  35. label=_("Description"),
  36. max_length=2048,
  37. required=False,
  38. widget=forms.Textarea(attrs={"rows": 3}),
  39. help_text=_("Optional description explaining category intented purpose."),
  40. )
  41. css_class = forms.CharField(
  42. label=_("CSS class"),
  43. required=False,
  44. help_text=_(
  45. "Optional CSS class used to customize this category appearance from templates."
  46. ),
  47. )
  48. is_closed = YesNoSwitch(
  49. label=_("Closed category"),
  50. required=False,
  51. help_text=_(
  52. "Only members with valid permissions can post in closed categories."
  53. ),
  54. )
  55. css_class = forms.CharField(
  56. label=_("CSS class"),
  57. required=False,
  58. help_text=_(
  59. "Optional CSS class used to customize this category appearance from templates."
  60. ),
  61. )
  62. require_threads_approval = YesNoSwitch(
  63. label=_("Threads"),
  64. required=False,
  65. help_text=_(
  66. "All threads started in this category will require moderator approval."
  67. ),
  68. )
  69. require_replies_approval = YesNoSwitch(
  70. label=_("Replies"),
  71. required=False,
  72. help_text=_(
  73. "All replies posted in this category will require moderator approval."
  74. ),
  75. )
  76. require_edits_approval = YesNoSwitch(
  77. label=_("Edits"),
  78. required=False,
  79. help_text=_(
  80. "Will make all edited replies return to unapproved state for moderator to review."
  81. ),
  82. )
  83. prune_started_after = forms.IntegerField(
  84. label=_("Thread age"),
  85. min_value=0,
  86. help_text=_(
  87. "Prune thread if number of days since its creation is greater than specified. "
  88. "Enter 0 to disable this pruning criteria."
  89. ),
  90. )
  91. prune_replied_after = forms.IntegerField(
  92. label=_("Last reply"),
  93. min_value=0,
  94. help_text=_(
  95. "Prune thread if number of days since last reply is greater than specified. "
  96. "Enter 0 to disable this pruning criteria."
  97. ),
  98. )
  99. class Meta:
  100. model = Category
  101. fields = [
  102. "name",
  103. "description",
  104. "css_class",
  105. "is_closed",
  106. "require_threads_approval",
  107. "require_replies_approval",
  108. "require_edits_approval",
  109. "prune_started_after",
  110. "prune_replied_after",
  111. "archive_pruned_in",
  112. ]
  113. def clean_copy_permissions(self):
  114. data = self.cleaned_data["copy_permissions"]
  115. if data and data.pk == self.instance.pk:
  116. message = _("Permissions cannot be copied from category into itself.")
  117. raise forms.ValidationError(message)
  118. return data
  119. def clean_archive_pruned_in(self):
  120. data = self.cleaned_data["archive_pruned_in"]
  121. if data and data.pk == self.instance.pk:
  122. message = _("Category cannot act as archive for itself.")
  123. raise forms.ValidationError(message)
  124. return data
  125. def clean(self):
  126. data = super().clean()
  127. self.instance.set_name(data.get("name"))
  128. return data
  129. def CategoryFormFactory(instance):
  130. parent_queryset = Category.objects.all_categories(True).order_by("lft")
  131. if instance.pk:
  132. not_siblings = models.Q(lft__lt=instance.lft)
  133. not_siblings = not_siblings | models.Q(rght__gt=instance.rght)
  134. parent_queryset = parent_queryset.filter(not_siblings)
  135. return type(
  136. "CategoryFormFinal",
  137. (CategoryFormBase,),
  138. {
  139. "new_parent": AdminCategoryChoiceField(
  140. label=_("Parent category"),
  141. queryset=parent_queryset,
  142. initial=instance.parent,
  143. empty_label=None,
  144. ),
  145. "copy_permissions": AdminCategoryChoiceField(
  146. label=_("Copy permissions"),
  147. help_text=_(
  148. "You can replace this category permissions with "
  149. "permissions copied from category selected here."
  150. ),
  151. queryset=Category.objects.all_categories(),
  152. empty_label=_("Don't copy permissions"),
  153. required=False,
  154. ),
  155. "archive_pruned_in": AdminCategoryChoiceField(
  156. label=_("Archive"),
  157. help_text=_(
  158. "Instead of being deleted, pruned threads can be "
  159. "moved to designated category."
  160. ),
  161. queryset=Category.objects.all_categories(),
  162. empty_label=_("Don't archive pruned threads"),
  163. required=False,
  164. ),
  165. },
  166. )
  167. class DeleteCategoryFormBase(forms.ModelForm):
  168. class Meta:
  169. model = Category
  170. fields = []
  171. def clean(self):
  172. data = super().clean()
  173. if data.get("move_threads_to"):
  174. if data["move_threads_to"].pk == self.instance.pk:
  175. message = _("You are trying to move this category threads to itself.")
  176. raise forms.ValidationError(message)
  177. moving_to_child = self.instance.has_child(data["move_threads_to"])
  178. if moving_to_child and not data.get("move_children_to"):
  179. message = _(
  180. "You are trying to move this category threads to a "
  181. "child category that will be deleted together with "
  182. "this category."
  183. )
  184. raise forms.ValidationError(message)
  185. return data
  186. def DeleteFormFactory(instance):
  187. content_queryset = Category.objects.all_categories().order_by("lft")
  188. fields = {
  189. "move_threads_to": AdminCategoryChoiceField(
  190. label=_("Move category threads to"),
  191. queryset=content_queryset,
  192. initial=instance.parent,
  193. empty_label=_("Delete with category"),
  194. required=False,
  195. )
  196. }
  197. not_siblings = models.Q(lft__lt=instance.lft)
  198. not_siblings = not_siblings | models.Q(rght__gt=instance.rght)
  199. children_queryset = Category.objects.all_categories(True)
  200. children_queryset = children_queryset.filter(not_siblings).order_by("lft")
  201. if children_queryset.exists():
  202. fields["move_children_to"] = AdminCategoryChoiceField(
  203. label=_("Move child categories to"),
  204. queryset=children_queryset,
  205. empty_label=_("Delete with category"),
  206. required=False,
  207. )
  208. return type("DeleteCategoryFormFinal", (DeleteCategoryFormBase,), fields)
  209. class CategoryRoleForm(forms.ModelForm):
  210. name = forms.CharField(label=_("Role name"))
  211. class Meta:
  212. model = CategoryRole
  213. fields = ["name"]
  214. def RoleCategoryACLFormFactory(category, category_roles, selected_role):
  215. attrs = {
  216. "category": category,
  217. "role": forms.ModelChoiceField(
  218. label=_("Role"),
  219. required=False,
  220. queryset=category_roles,
  221. initial=selected_role,
  222. empty_label=_("No access"),
  223. ),
  224. }
  225. return type("RoleCategoryACLForm", (forms.Form,), attrs)
  226. def CategoryRolesACLFormFactory(role, category_roles, selected_role):
  227. attrs = {
  228. "role": role,
  229. "category_role": forms.ModelChoiceField(
  230. label=_("Role"),
  231. required=False,
  232. queryset=category_roles,
  233. initial=selected_role,
  234. empty_label=_("No access"),
  235. ),
  236. }
  237. return type("CategoryRolesACLForm", (forms.Form,), attrs)