forms.py 9.0 KB

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