admin.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. from django.contrib.auth import get_user_model
  2. from django.utils.translation import ugettext_lazy as _, ungettext
  3. from misago.conf import settings
  4. from misago.core import forms, threadstore
  5. from misago.core.validators import validate_sluggable
  6. from misago.acl.models import Role
  7. from misago.users.models import (
  8. AUTO_SUBSCRIBE_CHOICES, PRIVATE_THREAD_INVITES_LIMITS_CHOICES,
  9. BANS_CHOICES, RESTRICTIONS_CHOICES, Ban, Rank, WarningLevel)
  10. from misago.users.validators import (
  11. validate_username, validate_email, validate_password)
  12. """
  13. Users
  14. """
  15. class UserBaseForm(forms.ModelForm):
  16. username = forms.CharField(label=_("Username"))
  17. title = forms.CharField(label=_("Custom title"), required=False)
  18. email = forms.EmailField(label=_("E-mail address"))
  19. class Meta:
  20. model = get_user_model()
  21. fields = ['username', 'email', 'title']
  22. def clean_username(self):
  23. data = self.cleaned_data['username']
  24. validate_username(data, exclude=self.instance)
  25. return data
  26. def clean_email(self):
  27. data = self.cleaned_data['email']
  28. validate_email(data, exclude=self.instance)
  29. return data
  30. def clean_new_password(self):
  31. data = self.cleaned_data['new_password']
  32. if data:
  33. validate_password(data)
  34. return data
  35. def clean_roles(self):
  36. data = self.cleaned_data['roles']
  37. for role in data:
  38. if role.special_role == 'authenticated':
  39. break
  40. else:
  41. message = _('All registered members must have "Member" role.')
  42. raise forms.ValidationError(message)
  43. return data
  44. class NewUserForm(UserBaseForm):
  45. new_password = forms.CharField(
  46. label=_("Password"),
  47. widget=forms.PasswordInput
  48. )
  49. class Meta:
  50. model = get_user_model()
  51. fields = ['username', 'email', 'title']
  52. class EditUserForm(UserBaseForm):
  53. new_password = forms.CharField(
  54. label=_("Change password to"),
  55. widget=forms.PasswordInput,
  56. required=False
  57. )
  58. is_avatar_locked = forms.YesNoSwitch(
  59. label=_("Lock avatar"),
  60. help_text=_("Setting this to yes will stop user from changing "
  61. "his/her avatar, and will reset his/her avatar to "
  62. "procedurally generated one.")
  63. )
  64. avatar_lock_user_message = forms.CharField(
  65. label=_("User message"),
  66. help_text=_("Optional message for user explaining "
  67. "why he/she is banned form changing avatar."),
  68. widget=forms.Textarea(attrs={'rows': 3}),
  69. required=False
  70. )
  71. avatar_lock_staff_message = forms.CharField(
  72. label=_("Staff message"),
  73. help_text=_("Optional message for forum team members explaining "
  74. "why user is banned form changing avatar."),
  75. widget=forms.Textarea(attrs={'rows': 3}),
  76. required=False
  77. )
  78. signature = forms.CharField(
  79. label=_("Signature contents"),
  80. widget=forms.Textarea(attrs={'rows': 3}),
  81. required=False
  82. )
  83. is_signature_locked = forms.YesNoSwitch(
  84. label=_("Lock signature"),
  85. help_text=_("Setting this to yes will stop user from "
  86. "making changes to his/her signature.")
  87. )
  88. signature_lock_user_message = forms.CharField(
  89. label=_("User message"),
  90. help_text=_("Optional message to user explaining "
  91. "why his/hers signature is locked."),
  92. widget=forms.Textarea(attrs={'rows': 3}),
  93. required=False
  94. )
  95. signature_lock_staff_message = forms.CharField(
  96. label=_("Staff message"),
  97. help_text=_("Optional message to team members explaining "
  98. "why user signature is locked."),
  99. widget=forms.Textarea(attrs={'rows': 3}),
  100. required=False
  101. )
  102. is_hiding_presence = forms.YesNoSwitch(label=_("Hides presence"))
  103. limits_private_thread_invites_to = forms.TypedChoiceField(
  104. label=_("Who can add user to private threads"),
  105. coerce=int,
  106. choices=PRIVATE_THREAD_INVITES_LIMITS_CHOICES
  107. )
  108. subscribe_to_started_threads = forms.TypedChoiceField(
  109. label=_("Started threads"),
  110. coerce=int,
  111. choices=AUTO_SUBSCRIBE_CHOICES
  112. )
  113. subscribe_to_replied_threads = forms.TypedChoiceField(
  114. label=_("Replid threads"),
  115. coerce=int,
  116. choices=AUTO_SUBSCRIBE_CHOICES
  117. )
  118. class Meta:
  119. model = get_user_model()
  120. fields = [
  121. 'username',
  122. 'email',
  123. 'title',
  124. 'is_avatar_locked',
  125. 'avatar_lock_user_message',
  126. 'avatar_lock_staff_message',
  127. 'signature',
  128. 'is_signature_locked',
  129. 'is_hiding_presence',
  130. 'limits_private_thread_invites_to',
  131. 'signature_lock_user_message',
  132. 'signature_lock_staff_message',
  133. 'subscribe_to_started_threads',
  134. 'subscribe_to_replied_threads',
  135. ]
  136. def clean_signature(self):
  137. data = self.cleaned_data['signature']
  138. length_limit = settings.signature_length_max
  139. if len(data) > length_limit:
  140. raise forms.ValidationError(ungettext(
  141. "Signature can't be longer than %(limit)s character.",
  142. "Signature can't be longer than %(limit)s characters.",
  143. length_limit) % {'limit': length_limit})
  144. return data
  145. def UserFormFactory(FormType, instance):
  146. extra_fields = {}
  147. extra_fields['rank'] = forms.ModelChoiceField(
  148. label=_("Rank"),
  149. help_text=_("Ranks are used to group and distinguish users. They are "
  150. "also used to add permissions to groups of users."),
  151. queryset=Rank.objects.order_by('name'),
  152. initial=instance.rank
  153. )
  154. roles = Role.objects.order_by('name')
  155. extra_fields['roles'] = forms.ModelMultipleChoiceField(
  156. label=_("Roles"),
  157. help_text=_('Individual roles of this user. '
  158. 'All users must have "member" role.'),
  159. queryset=roles,
  160. initial=instance.roles.all() if instance.pk else None,
  161. widget=forms.CheckboxSelectMultiple
  162. )
  163. return type('UserFormFinal', (FormType,), extra_fields)
  164. def StaffFlagUserFormFactory(FormType, instance, add_staff_field):
  165. FormType = UserFormFactory(FormType, instance)
  166. if add_staff_field:
  167. staff_levels = (
  168. (0, _("No access")),
  169. (1, _("Administrator")),
  170. (2, _("Superadmin")),
  171. )
  172. staff_fields = {
  173. 'staff_level': forms.TypedChoiceField(
  174. label=_("Admin level"),
  175. help_text=_("Only administrators can access admin sites. "
  176. "In addition to admin site access, superadmins "
  177. "can also change other members admin levels."),
  178. coerce=int,
  179. choices=staff_levels,
  180. initial=instance.staff_level
  181. ),
  182. }
  183. return type('StaffUserForm', (FormType,), staff_fields)
  184. else:
  185. return FormType
  186. class SearchUsersFormBase(forms.Form):
  187. username = forms.CharField(label=_("Username starts with"), required=False)
  188. email = forms.CharField(label=_("E-mail starts with"), required=False)
  189. inactive = forms.YesNoSwitch(label=_("Inactive only"))
  190. is_staff = forms.YesNoSwitch(label=_("Admins only"))
  191. def filter_queryset(self, search_criteria, queryset):
  192. criteria = search_criteria
  193. if criteria.get('username'):
  194. queryset = queryset.filter(
  195. slug__startswith=criteria.get('username').lower())
  196. if criteria.get('email'):
  197. queryset = queryset.filter(
  198. email__istartswith=criteria.get('email'))
  199. if criteria.get('rank'):
  200. queryset = queryset.filter(
  201. rank_id=criteria.get('rank'))
  202. if criteria.get('role'):
  203. queryset = queryset.filter(
  204. roles__id=criteria.get('role'))
  205. if criteria.get('inactive'):
  206. queryset = queryset.filter(requires_activation__gt=0)
  207. if criteria.get('is_staff'):
  208. queryset = queryset.filter(is_staff=True)
  209. return queryset
  210. def SearchUsersForm(*args, **kwargs):
  211. """
  212. Factory that uses cache for ranks and roles,
  213. and makes those ranks and roles typed choice fields that play nice
  214. with passing values via GET
  215. """
  216. ranks_choices = threadstore.get('misago_admin_ranks_choices', 'nada')
  217. if ranks_choices == 'nada':
  218. ranks_choices = [('', _("All ranks"))]
  219. for rank in Rank.objects.order_by('name').iterator():
  220. ranks_choices.append((rank.pk, rank.name))
  221. threadstore.set('misago_admin_ranks_choices', ranks_choices)
  222. roles_choices = threadstore.get('misago_admin_roles_choices', 'nada')
  223. if roles_choices == 'nada':
  224. roles_choices = [('', _("All roles"))]
  225. for role in Role.objects.order_by('name').iterator():
  226. roles_choices.append((role.pk, role.name))
  227. threadstore.set('misago_admin_roles_choices', roles_choices)
  228. extra_fields = {
  229. 'rank': forms.TypedChoiceField(
  230. label=_("Has rank"),
  231. coerce=int,
  232. required=False,
  233. choices=ranks_choices
  234. ),
  235. 'role': forms.TypedChoiceField(
  236. label=_("Has role"),
  237. coerce=int,
  238. required=False,
  239. choices=roles_choices
  240. )
  241. }
  242. FinalForm = type(
  243. 'SearchUsersFormFinal', (SearchUsersFormBase,), extra_fields)
  244. return FinalForm(*args, **kwargs)
  245. """
  246. Ranks
  247. """
  248. class RankForm(forms.ModelForm):
  249. name = forms.CharField(
  250. label=_("Name"),
  251. validators=[validate_sluggable()],
  252. help_text=_('Short and descriptive name of all users with this rank. '
  253. '"The Team" or "Game Masters" are good examples.')
  254. )
  255. title = forms.CharField(
  256. label=_("User title"),
  257. required=False,
  258. help_text=_('Optional, singular version of rank name displayed by '
  259. 'user names. For example "GM" or "Dev".')
  260. )
  261. description = forms.CharField(
  262. label=_("Description"),
  263. max_length=2048,
  264. required=False,
  265. widget=forms.Textarea(attrs={'rows': 3}),
  266. help_text=_("Optional description explaining function or status of "
  267. "members distincted with this rank.")
  268. )
  269. roles = forms.ModelMultipleChoiceField(
  270. label=_("User roles"),
  271. widget=forms.CheckboxSelectMultiple,
  272. queryset=Role.objects.order_by('name'),
  273. required=False,
  274. help_text=_('Rank can give additional roles to users with it.')
  275. )
  276. css_class = forms.CharField(
  277. label=_("CSS class"),
  278. required=False,
  279. help_text=_("Optional css class added to content belonging to this "
  280. "rank owner.")
  281. )
  282. is_tab = forms.BooleanField(
  283. label=_("Give rank dedicated tab on users list"),
  284. required=False,
  285. help_text=_("Selecting this option will make users with this rank "
  286. "easily discoverable by others trough dedicated page on "
  287. "forum users list.")
  288. )
  289. class Meta:
  290. model = Rank
  291. fields = [
  292. 'name',
  293. 'description',
  294. 'css_class',
  295. 'title',
  296. 'roles',
  297. 'is_tab',
  298. ]
  299. def clean_name(self):
  300. data = self.cleaned_data['name']
  301. self.instance.set_name(data)
  302. unique_qs = Rank.objects.filter(slug=self.instance.slug)
  303. if self.instance.pk:
  304. unique_qs = unique_qs.exclude(pk=self.instance.pk)
  305. if unique_qs.exists():
  306. raise forms.ValidationError(
  307. _("This name collides with other rank."))
  308. return data
  309. """
  310. Bans
  311. """
  312. class BanUsersForm(forms.Form):
  313. ban_type = forms.MultipleChoiceField(
  314. label=_("Values to ban"),
  315. widget=forms.CheckboxSelectMultiple,
  316. choices=(
  317. ('usernames', _('Usernames')),
  318. ('emails', _('E-mails')),
  319. ('domains', _('E-mail domains')),
  320. ('ip', _('IP addresses')),
  321. ('ip_first', _('First segment of IP addresses')),
  322. ('ip_two', _('First two segments of IP addresses'))
  323. )
  324. )
  325. user_message = forms.CharField(
  326. label=_("User message"),
  327. required=False,
  328. max_length=1000,
  329. help_text=_("Optional message displayed to users "
  330. "instead of default one."),
  331. widget=forms.Textarea(attrs={'rows': 3}),
  332. error_messages={
  333. 'max_length': _("Message can't be longer than 1000 characters.")
  334. }
  335. )
  336. staff_message = forms.CharField(
  337. label=_("Team message"),
  338. required=False,
  339. max_length=1000,
  340. help_text=_("Optional ban message for moderators and administrators."),
  341. widget=forms.Textarea(attrs={'rows': 3}),
  342. error_messages={
  343. 'max_length': _("Message can't be longer than 1000 characters.")
  344. }
  345. )
  346. expires_on = forms.IsoDateTimeField(
  347. label=_("Expires on"),
  348. required=False,
  349. help_text=_('Leave this field empty for set bans to never expire.')
  350. )
  351. class BanForm(forms.ModelForm):
  352. check_type = forms.TypedChoiceField(
  353. label=_("Check type"),
  354. coerce=int,
  355. choices=BANS_CHOICES
  356. )
  357. banned_value = forms.CharField(
  358. label=_("Banned value"),
  359. max_length=250,
  360. help_text=_('This value is case-insensitive and accepts asterisk (*) '
  361. 'for rought matches. For example, making IP ban for value '
  362. '"83.*" will ban all IP addresses beginning with "83.".'),
  363. error_messages={
  364. 'max_length': _("Banned value can't be longer "
  365. "than 250 characters.")
  366. }
  367. )
  368. user_message = forms.CharField(
  369. label=_("User message"),
  370. required=False,
  371. max_length=1000,
  372. help_text=_("Optional message displayed to user "
  373. "instead of default one."),
  374. widget=forms.Textarea(attrs={'rows': 3}),
  375. error_messages={
  376. 'max_length': _("Message can't be longer than 1000 characters.")
  377. }
  378. )
  379. staff_message = forms.CharField(
  380. label=_("Team message"),
  381. required=False,
  382. max_length=1000,
  383. help_text=_("Optional ban message for moderators and administrators."),
  384. widget=forms.Textarea(attrs={'rows': 3}),
  385. error_messages={
  386. 'max_length': _("Message can't be longer than 1000 characters.")
  387. }
  388. )
  389. expires_on = forms.IsoDateTimeField(
  390. label=_("Expires on"),
  391. required=False,
  392. help_text=_('Leave this field empty for this ban to never expire.')
  393. )
  394. class Meta:
  395. model = Ban
  396. fields = [
  397. 'check_type',
  398. 'banned_value',
  399. 'user_message',
  400. 'staff_message',
  401. 'expires_on',
  402. ]
  403. def clean_banned_value(self):
  404. data = self.cleaned_data['banned_value']
  405. while '**' in data:
  406. data = data.replace('**', '*')
  407. if data == '*':
  408. raise forms.ValidationError(_("Banned value is too vague."))
  409. return data
  410. SARCH_BANS_CHOICES = (
  411. ('', _('All bans')),
  412. ('names', _('Usernames')),
  413. ('emails', _('E-mails')),
  414. ('ips', _('IPs')),
  415. )
  416. class SearchBansForm(forms.Form):
  417. check_type = forms.ChoiceField(
  418. label=_("Type"),
  419. required=False,
  420. choices=SARCH_BANS_CHOICES
  421. )
  422. value = forms.CharField(
  423. label=_("Banned value begins with"),
  424. required=False
  425. )
  426. state = forms.ChoiceField(
  427. label=_("State"),
  428. required=False,
  429. choices=(
  430. ('', _('Any')),
  431. ('used', _('Active')),
  432. ('unused', _('Expired')),
  433. )
  434. )
  435. def filter_queryset(self, search_criteria, queryset):
  436. criteria = search_criteria
  437. if criteria.get('check_type') == 'names':
  438. queryset = queryset.filter(check_type=0)
  439. if criteria.get('check_type') == 'emails':
  440. queryset = queryset.filter(check_type=1)
  441. if criteria.get('check_type') == 'ips':
  442. queryset = queryset.filter(check_type=2)
  443. if criteria.get('value'):
  444. queryset = queryset.filter(
  445. banned_value__startswith=criteria.get('value').lower())
  446. if criteria.get('state') == 'used':
  447. queryset = queryset.filter(is_checked=True)
  448. if criteria.get('state') == 'unused':
  449. queryset = queryset.filter(is_checked=False)
  450. return queryset
  451. """
  452. Warning levels
  453. """
  454. class WarningLevelForm(forms.ModelForm):
  455. name = forms.CharField(label=_("Level name"), max_length=255)
  456. length_in_minutes = forms.IntegerField(
  457. label=_("Length in minutes"),
  458. min_value=0,
  459. help_text=_("Enter number of minutes since this warning level was "
  460. "imposed on member until it's reduced, or 0 to make "
  461. "this warning level permanent.")
  462. )
  463. restricts_posting_replies = forms.TypedChoiceField(
  464. label=_("Posting replies"),
  465. coerce=int,
  466. choices=RESTRICTIONS_CHOICES
  467. )
  468. restricts_posting_threads = forms.TypedChoiceField(
  469. label=_("Posting threads"),
  470. coerce=int,
  471. choices=RESTRICTIONS_CHOICES
  472. )
  473. class Meta:
  474. model = WarningLevel
  475. fields = [
  476. 'name',
  477. 'length_in_minutes',
  478. 'restricts_posting_replies',
  479. 'restricts_posting_threads',
  480. ]