forms.py 9.0 KB

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