forms.py 15 KB

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