forms.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.management.forms
  4. ~~~~~~~~~~~~~~~~~~~~~~~~
  5. It provides the forms that are needed for the management views.
  6. :copyright: (c) 2014 by the FlaskBB Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. import logging
  10. from flask_wtf import FlaskForm
  11. from wtforms import (BooleanField, HiddenField, IntegerField, PasswordField,
  12. SelectField, StringField, SubmitField, TextAreaField)
  13. from wtforms.validators import (DataRequired, Optional, Email, regexp, Length,
  14. URL, ValidationError)
  15. from wtforms.ext.sqlalchemy.fields import (QuerySelectField,
  16. QuerySelectMultipleField)
  17. from sqlalchemy.orm.session import make_transient, make_transient_to_detached
  18. from flask_babelplus import lazy_gettext as _
  19. from flaskbb.utils.fields import BirthdayField
  20. from flaskbb.extensions import db
  21. from flaskbb.forum.models import Forum, Category
  22. from flaskbb.user.models import User, Group
  23. from flaskbb.utils.requirements import IsAtleastModerator
  24. from flask_allows import Permission
  25. logger = logging.getLogger(__name__)
  26. USERNAME_RE = r'^[\w.+-]+$'
  27. is_username = regexp(USERNAME_RE,
  28. message=_("You can only use letters, numbers or dashes."))
  29. def selectable_forums():
  30. return Forum.query.order_by(Forum.position)
  31. def selectable_categories():
  32. return Category.query.order_by(Category.position)
  33. def selectable_groups():
  34. return Group.query.order_by(Group.id.asc()).all()
  35. def select_primary_group():
  36. return Group.query.filter(Group.guest != True).order_by(Group.id)
  37. class UserForm(FlaskForm):
  38. username = StringField(_("Username"), validators=[
  39. DataRequired(message=_("A valid username is required.")),
  40. is_username])
  41. email = StringField(_("Email address"), validators=[
  42. DataRequired(message=_("A valid email address is required.")),
  43. Email(message=_("Invalid email address."))])
  44. password = PasswordField("Password", validators=[
  45. DataRequired()])
  46. birthday = BirthdayField(_("Birthday"), format="%d %m %Y", validators=[
  47. Optional()])
  48. gender = SelectField(_("Gender"), default="None", choices=[
  49. ("None", ""),
  50. ("Male", _("Male")),
  51. ("Female", _("Female"))])
  52. location = StringField(_("Location"), validators=[
  53. Optional()])
  54. website = StringField(_("Website"), validators=[
  55. Optional(), URL()])
  56. avatar = StringField(_("Avatar"), validators=[
  57. Optional(), URL()])
  58. signature = TextAreaField(_("Forum signature"), validators=[
  59. Optional(), Length(min=0, max=250)])
  60. notes = TextAreaField(_("Notes"), validators=[
  61. Optional(), Length(min=0, max=5000)])
  62. activated = BooleanField(_("Is active?"), validators=[
  63. Optional()])
  64. primary_group = QuerySelectField(
  65. _("Primary group"),
  66. query_factory=select_primary_group,
  67. get_label="name")
  68. secondary_groups = QuerySelectMultipleField(
  69. _("Secondary groups"),
  70. # TODO: Template rendering errors "NoneType is not callable"
  71. # without this, figure out why.
  72. query_factory=select_primary_group,
  73. get_label="name")
  74. submit = SubmitField(_("Save"))
  75. def validate_username(self, field):
  76. if hasattr(self, "user"):
  77. user = User.query.filter(
  78. db.and_(
  79. User.username.like(field.data.lower()),
  80. db.not_(User.id == self.user.id)
  81. )
  82. ).first()
  83. else:
  84. user = User.query.filter(
  85. User.username.like(field.data.lower())
  86. ).first()
  87. if user:
  88. raise ValidationError(_("This username is already taken."))
  89. def validate_email(self, field):
  90. if hasattr(self, "user"):
  91. user = User.query.filter(
  92. db.and_(
  93. User.email.like(field.data.lower()),
  94. db.not_(User.id == self.user.id)
  95. )
  96. ).first()
  97. else:
  98. user = User.query.filter(
  99. User.email.like(field.data.lower())
  100. ).first()
  101. if user:
  102. raise ValidationError(_("This email address is already taken."))
  103. def save(self):
  104. data = self.data
  105. data.pop('submit', None)
  106. data.pop('csrf_token', None)
  107. user = User(**data)
  108. return user.save()
  109. class AddUserForm(UserForm):
  110. pass
  111. class EditUserForm(UserForm):
  112. password = PasswordField("Password", validators=[Optional()])
  113. def __init__(self, user, *args, **kwargs):
  114. self.user = user
  115. kwargs['obj'] = self.user
  116. UserForm.__init__(self, *args, **kwargs)
  117. class GroupForm(FlaskForm):
  118. name = StringField(_("Group name"), validators=[
  119. DataRequired(message=_("Please enter a name for the group."))])
  120. description = TextAreaField(_("Description"), validators=[
  121. Optional()])
  122. admin = BooleanField(
  123. _("Is 'Admin' group?"),
  124. description=_("With this option the group has access to "
  125. "the admin panel.")
  126. )
  127. super_mod = BooleanField(
  128. _("Is 'Super Moderator' group?"),
  129. description=_("Check this, if the users in this group are allowed to "
  130. "moderate every forum.")
  131. )
  132. mod = BooleanField(
  133. _("Is 'Moderator' group?"),
  134. description=_("Check this, if the users in this group are allowed to "
  135. "moderate specified forums.")
  136. )
  137. banned = BooleanField(
  138. _("Is 'Banned' group?"),
  139. description=_("Only one group of type 'Banned' is allowed.")
  140. )
  141. guest = BooleanField(
  142. _("Is 'Guest' group?"),
  143. description=_("Only one group of type 'Guest' is allowed.")
  144. )
  145. editpost = BooleanField(
  146. _("Can edit posts"),
  147. description=_("Check this, if the users in this group can edit posts.")
  148. )
  149. deletepost = BooleanField(
  150. _("Can delete posts"),
  151. description=_("Check this, if the users in this group can delete "
  152. "posts.")
  153. )
  154. deletetopic = BooleanField(
  155. _("Can delete topics"),
  156. description=_("Check this, if the users in this group can delete "
  157. "topics.")
  158. )
  159. posttopic = BooleanField(
  160. _("Can create topics"),
  161. description=_("Check this, if the users in this group can create "
  162. "topics.")
  163. )
  164. postreply = BooleanField(
  165. _("Can post replies"),
  166. description=_("Check this, if the users in this group can post "
  167. "replies.")
  168. )
  169. mod_edituser = BooleanField(
  170. _("Moderators can edit user profiles"),
  171. description=_("Allow moderators to edit another user's profile "
  172. "including password and email changes.")
  173. )
  174. mod_banuser = BooleanField(
  175. _("Moderators can ban users"),
  176. description=_("Allow moderators to ban other users.")
  177. )
  178. viewhidden = BooleanField(
  179. _("Can view hidden posts and topics"),
  180. description=_("Allows a user to view hidden posts and topics"),
  181. )
  182. makehidden = BooleanField(
  183. _("Can hide posts and topics"),
  184. description=_("Allows a user to hide posts and topics"),
  185. )
  186. submit = SubmitField(_("Save"))
  187. def validate_name(self, field):
  188. if hasattr(self, "group"):
  189. group = Group.query.filter(
  190. db.and_(
  191. Group.name.like(field.data.lower()),
  192. db.not_(Group.id == self.group.id)
  193. )
  194. ).first()
  195. else:
  196. group = Group.query.filter(
  197. Group.name.like(field.data.lower())
  198. ).first()
  199. if group:
  200. raise ValidationError(_("This group name is already taken."))
  201. def validate_banned(self, field):
  202. if hasattr(self, "group"):
  203. group = Group.query.filter(
  204. db.and_(
  205. Group.banned,
  206. db.not_(Group.id == self.group.id)
  207. )
  208. ).count()
  209. else:
  210. group = Group.query.filter_by(banned=True).count()
  211. if field.data and group > 0:
  212. raise ValidationError(_("There is already a group of type "
  213. "'Banned'."))
  214. def validate_guest(self, field):
  215. if hasattr(self, "group"):
  216. group = Group.query.filter(
  217. db.and_(
  218. Group.guest,
  219. db.not_(Group.id == self.group.id)
  220. )
  221. ).count()
  222. else:
  223. group = Group.query.filter_by(guest=True).count()
  224. if field.data and group > 0:
  225. raise ValidationError(_("There is already a group of type "
  226. "'Guest'."))
  227. def validate(self):
  228. if not super(GroupForm, self).validate():
  229. return False
  230. result = True
  231. permission_fields = (
  232. self.editpost, self.deletepost, self.deletetopic,
  233. self.posttopic, self.postreply, self.mod_edituser,
  234. self.mod_banuser, self.viewhidden, self.makehidden
  235. )
  236. group_fields = [
  237. self.admin, self.super_mod, self.mod, self.banned, self.guest
  238. ]
  239. # we do not allow to modify any guest permissions
  240. if self.guest.data:
  241. for field in permission_fields:
  242. if field.data:
  243. # if done in 'validate_guest' it would display this
  244. # warning on the fields
  245. field.errors.append(
  246. _("Can't assign any permissions to this group.")
  247. )
  248. result = False
  249. checked = []
  250. for field in group_fields:
  251. if field.data and field.data in checked:
  252. if len(checked) > 1:
  253. field.errors.append(
  254. "A group can't have multiple group types."
  255. )
  256. result = False
  257. else:
  258. checked.append(field.data)
  259. return result
  260. def save(self):
  261. data = self.data
  262. data.pop('submit', None)
  263. data.pop('csrf_token', None)
  264. group = Group(**data)
  265. return group.save()
  266. class EditGroupForm(GroupForm):
  267. def __init__(self, group, *args, **kwargs):
  268. self.group = group
  269. kwargs['obj'] = self.group
  270. GroupForm.__init__(self, *args, **kwargs)
  271. class AddGroupForm(GroupForm):
  272. pass
  273. class ForumForm(FlaskForm):
  274. title = StringField(
  275. _("Forum title"),
  276. validators=[DataRequired(message=_("Please enter a forum title."))]
  277. )
  278. description = TextAreaField(
  279. _("Description"),
  280. validators=[Optional()],
  281. description=_("You can format your description with Markdown.")
  282. )
  283. position = IntegerField(
  284. _("Position"),
  285. default=1,
  286. validators=[DataRequired(message=_("Please enter a position for the"
  287. "forum."))]
  288. )
  289. category = QuerySelectField(
  290. _("Category"),
  291. query_factory=selectable_categories,
  292. allow_blank=False,
  293. get_label="title",
  294. description=_("The category that contains this forum.")
  295. )
  296. external = StringField(
  297. _("External link"),
  298. validators=[Optional(), URL()],
  299. description=_("A link to a website i.e. 'http://flaskbb.org'.")
  300. )
  301. moderators = StringField(
  302. _("Moderators"),
  303. description=_("Comma separated usernames. Leave it blank if you do "
  304. "not want to set any moderators.")
  305. )
  306. show_moderators = BooleanField(
  307. _("Show moderators"),
  308. description=_("Do you want to show the moderators on the index page?")
  309. )
  310. locked = BooleanField(
  311. _("Locked?"),
  312. description=_("Disable new posts and topics in this forum.")
  313. )
  314. groups = QuerySelectMultipleField(
  315. _("Group access"),
  316. query_factory=selectable_groups,
  317. get_label="name",
  318. description=_("Select the groups that can access this forum.")
  319. )
  320. submit = SubmitField(_("Save"))
  321. def validate_external(self, field):
  322. if hasattr(self, "forum"):
  323. if self.forum.topics.count() > 0:
  324. raise ValidationError(_("You cannot convert a forum that "
  325. "contains topics into an "
  326. "external link."))
  327. def validate_show_moderators(self, field):
  328. if field.data and not self.moderators.data:
  329. raise ValidationError(_("You also need to specify some "
  330. "moderators."))
  331. def validate_moderators(self, field):
  332. approved_moderators = []
  333. if field.data:
  334. moderators = [mod.strip() for mod in field.data.split(',')]
  335. users = User.query.filter(User.username.in_(moderators))
  336. for user in users:
  337. if not Permission(IsAtleastModerator, identity=user):
  338. raise ValidationError(
  339. _("%(user)s is not in a moderators group.",
  340. user=user.username)
  341. )
  342. else:
  343. approved_moderators.append(user)
  344. field.data = approved_moderators
  345. def save(self):
  346. data = self.data
  347. # delete submit and csrf_token from data
  348. data.pop('submit', None)
  349. data.pop('csrf_token', None)
  350. forum = Forum(**data)
  351. return forum.save()
  352. class EditForumForm(ForumForm):
  353. id = HiddenField()
  354. def __init__(self, forum, *args, **kwargs):
  355. self.forum = forum
  356. kwargs['obj'] = self.forum
  357. ForumForm.__init__(self, *args, **kwargs)
  358. def save(self):
  359. data = self.data
  360. # delete submit and csrf_token from data
  361. data.pop('submit', None)
  362. data.pop('csrf_token', None)
  363. forum = Forum(**data)
  364. # flush SQLA info from created instance so that it can be merged
  365. make_transient(forum)
  366. make_transient_to_detached(forum)
  367. return forum.save()
  368. class AddForumForm(ForumForm):
  369. pass
  370. class CategoryForm(FlaskForm):
  371. title = StringField(_("Category title"), validators=[
  372. DataRequired(message=_("Please enter a category title."))])
  373. description = TextAreaField(
  374. _("Description"),
  375. validators=[Optional()],
  376. description=_("You can format your description with Markdown.")
  377. )
  378. position = IntegerField(
  379. _("Position"),
  380. default=1,
  381. validators=[DataRequired(message=_("Please enter a position for the "
  382. "category."))]
  383. )
  384. submit = SubmitField(_("Save"))
  385. def save(self):
  386. data = self.data
  387. # delete submit and csrf_token from data
  388. data.pop('submit', None)
  389. data.pop('csrf_token', None)
  390. category = Category(**data)
  391. return category.save()