forms.py 8.5 KB

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