forms.py 9.5 KB

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