forms.py 22 KB

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