admin.py 19 KB

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