forms.py 9.4 KB

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