forms.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2014-2016 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. """
  7. # pylint: disable=too-few-public-methods
  8. # pylint: disable=no-init
  9. # pylint: disable=super-on-old-class
  10. import re
  11. import tempfile
  12. import flask
  13. import flask_wtf as wtf
  14. try:
  15. from flask_wtf import FlaskForm
  16. except ImportError:
  17. from flask_wtf import Form as FlaskForm
  18. import six
  19. import wtforms
  20. import pagure
  21. import pagure.lib
  22. STRICT_REGEX = '^[a-zA-Z0-9-_]+$'
  23. TAGS_REGEX = '^[a-zA-Z0-9-_, .]+$'
  24. PROJECT_NAME_REGEX = \
  25. '^[a-zA-z0-9_][a-zA-Z0-9-_]*$'
  26. def convert_value(val):
  27. """ Convert the provided values to strings when possible. """
  28. if val:
  29. if not isinstance(val, (list, tuple, six.string_types)):
  30. return val.decode('utf-8')
  31. elif isinstance(val, six.string_types):
  32. return val
  33. class MultipleEmail(wtforms.validators.Email):
  34. """ Split the value by comma and run them through the email validator
  35. of wtforms.
  36. """
  37. def __call__(self, form, field):
  38. regex = re.compile(r'^.+@[^.].*\.[a-z]{2,10}$', re.IGNORECASE)
  39. message = field.gettext('One or more invalid email address.')
  40. for data in field.data.split(','):
  41. data = data.strip()
  42. if not self.regex.match(data or ''):
  43. raise wtforms.validators.ValidationError(message)
  44. def file_virus_validator(form, field):
  45. if not pagure.APP.config['VIRUS_SCAN_ATTACHMENTS']:
  46. return
  47. from pyclamd import ClamdUnixSocket
  48. if field.name not in flask.request.files or \
  49. flask.request.files[field.name].filename == '':
  50. # If no file was uploaded, this field is correct
  51. return
  52. uploaded = flask.request.files[field.name]
  53. clam = ClamdUnixSocket()
  54. if not clam.ping():
  55. raise wtforms.ValidationError(
  56. 'Unable to communicate with virus scanner')
  57. results = clam.scan_stream(uploaded.stream.read())
  58. if results is None:
  59. uploaded.stream.seek(0)
  60. return
  61. else:
  62. result = results.values()
  63. res_type, res_msg = result
  64. if res_type == 'FOUND':
  65. raise wtforms.ValidationError('Virus found: %s' % res_msg)
  66. else:
  67. raise wtforms.ValidationError('Error scanning uploaded file')
  68. def ssh_key_validator(form, field):
  69. if not pagure.lib.are_valid_ssh_keys(field.data):
  70. raise wtforms.ValidationError('Invalid SSH keys')
  71. class ProjectFormSimplified(FlaskForm):
  72. ''' Form to edit the description of a project. '''
  73. description = wtforms.TextField(
  74. 'Description <span class="error">*</span>',
  75. [wtforms.validators.Required()]
  76. )
  77. url = wtforms.TextField(
  78. 'URL',
  79. [wtforms.validators.optional()]
  80. )
  81. avatar_email = wtforms.TextField(
  82. 'Avatar email',
  83. [wtforms.validators.optional()]
  84. )
  85. tags = wtforms.TextField(
  86. 'Project tags',
  87. [
  88. wtforms.validators.optional(),
  89. wtforms.validators.Length(max=255),
  90. ]
  91. )
  92. class ProjectForm(ProjectFormSimplified):
  93. ''' Form to create or edit project. '''
  94. name = wtforms.TextField(
  95. 'Project name <span class="error">*</span>',
  96. [
  97. wtforms.validators.Required(),
  98. wtforms.validators.Regexp(PROJECT_NAME_REGEX, flags=re.IGNORECASE)
  99. ]
  100. )
  101. create_readme = wtforms.BooleanField(
  102. 'Create README',
  103. [wtforms.validators.optional()],
  104. )
  105. namespace = wtforms.SelectField(
  106. 'Project Namespace',
  107. [wtforms.validators.optional()],
  108. choices=[],
  109. coerce=convert_value
  110. )
  111. def __init__(self, *args, **kwargs):
  112. """ Calls the default constructor with the normal argument but
  113. uses the list of collection provided to fill the choices of the
  114. drop-down list.
  115. """
  116. super(ProjectForm, self).__init__(*args, **kwargs)
  117. if 'namespaces' in kwargs:
  118. self.namespace.choices = [
  119. (namespace, namespace) for namespace in kwargs['namespaces']
  120. ]
  121. self.namespace.choices.insert(0, ('', ''))
  122. class IssueFormSimplied(FlaskForm):
  123. ''' Form to create or edit an issue. '''
  124. title = wtforms.TextField(
  125. 'Title<span class="error">*</span>',
  126. [wtforms.validators.Required()]
  127. )
  128. issue_content = wtforms.TextAreaField(
  129. 'Content<span class="error">*</span>',
  130. [wtforms.validators.Required()]
  131. )
  132. private = wtforms.BooleanField(
  133. 'Private',
  134. [wtforms.validators.optional()],
  135. )
  136. class IssueForm(IssueFormSimplied):
  137. ''' Form to create or edit an issue. '''
  138. status = wtforms.SelectField(
  139. 'Status',
  140. [wtforms.validators.Required()],
  141. choices=[]
  142. )
  143. def __init__(self, *args, **kwargs):
  144. """ Calls the default constructor with the normal argument but
  145. uses the list of collection provided to fill the choices of the
  146. drop-down list.
  147. """
  148. super(IssueForm, self).__init__(*args, **kwargs)
  149. if 'status' in kwargs:
  150. self.status.choices = [
  151. (status, status) for status in kwargs['status']
  152. ]
  153. class RequestPullForm(FlaskForm):
  154. ''' Form to create a request pull. '''
  155. title = wtforms.TextField(
  156. 'Title<span class="error">*</span>',
  157. [wtforms.validators.Required()]
  158. )
  159. initial_comment = wtforms.TextAreaField(
  160. 'Initial Comment', [wtforms.validators.Optional()])
  161. class RemoteRequestPullForm(RequestPullForm):
  162. ''' Form to create a remote request pull. '''
  163. git_repo = wtforms.TextField(
  164. 'Git repo address<span class="error">*</span>',
  165. [wtforms.validators.Required()]
  166. )
  167. branch_from = wtforms.TextField(
  168. 'Git branch<span class="error">*</span>',
  169. [wtforms.validators.Required()]
  170. )
  171. branch_to = wtforms.TextField(
  172. 'Git branch to merge in<span class="error">*</span>',
  173. [wtforms.validators.Required()]
  174. )
  175. class AddIssueTagForm(FlaskForm):
  176. ''' Form to add a comment to an issue. '''
  177. tag = wtforms.TextField(
  178. 'tag',
  179. [
  180. wtforms.validators.Optional(),
  181. wtforms.validators.Regexp(TAGS_REGEX, flags=re.IGNORECASE),
  182. wtforms.validators.Length(max=255),
  183. ]
  184. )
  185. class StatusForm(FlaskForm):
  186. ''' Form to add/change the status of an issue. '''
  187. status = wtforms.SelectField(
  188. 'Status',
  189. [wtforms.validators.Required()],
  190. choices=[]
  191. )
  192. close_status = wtforms.SelectField(
  193. 'Closed as',
  194. [wtforms.validators.Optional()],
  195. choices=[]
  196. )
  197. def __init__(self, *args, **kwargs):
  198. """ Calls the default constructor with the normal argument but
  199. uses the list of collection provided to fill the choices of the
  200. drop-down list.
  201. """
  202. super(StatusForm, self).__init__(*args, **kwargs)
  203. if 'status' in kwargs:
  204. self.status.choices = [
  205. (status, status) for status in kwargs['status']
  206. ]
  207. self.close_status.choices = []
  208. if 'close_status' in kwargs:
  209. for key in sorted(kwargs['close_status']):
  210. self.close_status.choices.append((key, key))
  211. self.close_status.choices.insert(0, ('', ''))
  212. class NewTokenForm(FlaskForm):
  213. ''' Form to add/change the status of an issue. '''
  214. acls = wtforms.SelectMultipleField(
  215. 'ACLs',
  216. [wtforms.validators.Required()],
  217. choices=[]
  218. )
  219. def __init__(self, *args, **kwargs):
  220. """ Calls the default constructor with the normal argument but
  221. uses the list of collection provided to fill the choices of the
  222. drop-down list.
  223. """
  224. super(NewTokenForm, self).__init__(*args, **kwargs)
  225. if 'acls' in kwargs:
  226. self.acls.choices = [
  227. (acl.name, acl.name) for acl in kwargs['acls']
  228. ]
  229. class UpdateIssueForm(FlaskForm):
  230. ''' Form to add a comment to an issue. '''
  231. tag = wtforms.TextField(
  232. 'tag',
  233. [
  234. wtforms.validators.Optional(),
  235. wtforms.validators.Regexp(TAGS_REGEX, flags=re.IGNORECASE),
  236. wtforms.validators.Length(max=255),
  237. ]
  238. )
  239. depends = wtforms.TextField(
  240. 'dependency issue', [wtforms.validators.Optional()]
  241. )
  242. blocks = wtforms.TextField(
  243. 'blocked issue', [wtforms.validators.Optional()]
  244. )
  245. comment = wtforms.TextAreaField(
  246. 'Comment', [wtforms.validators.Optional()]
  247. )
  248. assignee = wtforms.TextAreaField(
  249. 'Assigned to', [wtforms.validators.Optional()]
  250. )
  251. status = wtforms.SelectField(
  252. 'Status',
  253. [wtforms.validators.Optional()],
  254. choices=[]
  255. )
  256. priority = wtforms.SelectField(
  257. 'Priority',
  258. [wtforms.validators.Optional()],
  259. choices=[]
  260. )
  261. milestone = wtforms.SelectField(
  262. 'Milestone',
  263. [wtforms.validators.Optional()],
  264. choices=[],
  265. coerce=convert_value
  266. )
  267. private = wtforms.BooleanField(
  268. 'Private',
  269. [wtforms.validators.optional()],
  270. )
  271. close_status = wtforms.SelectField(
  272. 'Closed as',
  273. [wtforms.validators.Optional()],
  274. choices=[],
  275. coerce=convert_value
  276. )
  277. def __init__(self, *args, **kwargs):
  278. """ Calls the default constructor with the normal argument but
  279. uses the list of collection provided to fill the choices of the
  280. drop-down list.
  281. """
  282. super(UpdateIssueForm, self).__init__(*args, **kwargs)
  283. if 'status' in kwargs:
  284. self.status.choices = [
  285. (status, status) for status in kwargs['status']
  286. ]
  287. self.priority.choices = []
  288. if 'priorities' in kwargs:
  289. for key in sorted(kwargs['priorities']):
  290. self.priority.choices.append(
  291. (key, kwargs['priorities'][key])
  292. )
  293. self.milestone.choices = []
  294. if 'milestones' in kwargs and kwargs['milestones']:
  295. for key in sorted(kwargs['milestones']):
  296. self.milestone.choices.append((key, key))
  297. self.milestone.choices.insert(0, ('', ''))
  298. self.close_status.choices = []
  299. if 'close_status' in kwargs:
  300. for key in sorted(kwargs['close_status']):
  301. self.close_status.choices.append((key, key))
  302. self.close_status.choices.insert(0, ('', ''))
  303. class AddPullRequestCommentForm(FlaskForm):
  304. ''' Form to add a comment to a pull-request. '''
  305. commit = wtforms.HiddenField('commit identifier')
  306. filename = wtforms.HiddenField('file changed')
  307. row = wtforms.HiddenField('row')
  308. requestid = wtforms.HiddenField('requestid')
  309. tree_id = wtforms.HiddenField('treeid')
  310. comment = wtforms.TextAreaField(
  311. 'Comment<span class="error">*</span>',
  312. [wtforms.validators.Required()]
  313. )
  314. class AddPullRequestFlagForm(FlaskForm):
  315. ''' Form to add a flag to a pull-request. '''
  316. username = wtforms.TextField(
  317. 'Username', [wtforms.validators.Required()])
  318. percent = wtforms.TextField(
  319. 'Percentage of completion', [wtforms.validators.Required()])
  320. comment = wtforms.TextAreaField(
  321. 'Comment', [wtforms.validators.Required()])
  322. url = wtforms.TextField(
  323. 'URL', [wtforms.validators.Required()])
  324. uid = wtforms.TextField(
  325. 'UID', [wtforms.validators.optional()])
  326. class UserSettingsForm(FlaskForm):
  327. ''' Form to create or edit project. '''
  328. ssh_key = wtforms.TextAreaField(
  329. 'Public SSH key <span class="error">*</span>',
  330. [wtforms.validators.Required(),
  331. ssh_key_validator]
  332. )
  333. class AddUserForm(FlaskForm):
  334. ''' Form to add a user to a project. '''
  335. user = wtforms.TextField(
  336. 'Username <span class="error">*</span>',
  337. [wtforms.validators.Required()]
  338. )
  339. class AssignIssueForm(FlaskForm):
  340. ''' Form to assign an user to an issue. '''
  341. assignee = wtforms.TextField(
  342. 'Assignee <span class="error">*</span>',
  343. [wtforms.validators.Required()]
  344. )
  345. class AddGroupForm(FlaskForm):
  346. ''' Form to add a group to a project. '''
  347. group = wtforms.TextField(
  348. 'Group <span class="error">*</span>',
  349. [
  350. wtforms.validators.Required(),
  351. wtforms.validators.Regexp(STRICT_REGEX, flags=re.IGNORECASE)
  352. ]
  353. )
  354. class ConfirmationForm(FlaskForm):
  355. ''' Simple form used just for CSRF protection. '''
  356. pass
  357. class UploadFileForm(FlaskForm):
  358. ''' Form to upload a file. '''
  359. filestream = wtforms.FileField(
  360. 'File',
  361. [wtforms.validators.Required(), file_virus_validator])
  362. class UserEmailForm(FlaskForm):
  363. ''' Form to edit the description of a project. '''
  364. email = wtforms.TextField(
  365. 'email', [wtforms.validators.Required()]
  366. )
  367. def __init__(self, *args, **kwargs):
  368. super(UserEmailForm, self).__init__(*args, **kwargs)
  369. if 'emails' in kwargs:
  370. if kwargs['emails']:
  371. self.email.validators.append(
  372. wtforms.validators.NoneOf(kwargs['emails'])
  373. )
  374. else:
  375. self.email.validators = [wtforms.validators.Required()]
  376. class ProjectCommentForm(FlaskForm):
  377. ''' Form to represent project. '''
  378. objid = wtforms.TextField(
  379. 'Ticket/Request id',
  380. [wtforms.validators.Required()]
  381. )
  382. useremail = wtforms.TextField(
  383. 'Email',
  384. [wtforms.validators.Required()]
  385. )
  386. class CommentForm(FlaskForm):
  387. ''' Form to upload a file. '''
  388. comment = wtforms.FileField(
  389. 'Comment',
  390. [wtforms.validators.Required(), file_virus_validator])
  391. class EditGroupForm(FlaskForm):
  392. """ Form to ask for a password change. """
  393. display_name = wtforms.TextField(
  394. 'Group name to display',
  395. [
  396. wtforms.validators.Required(),
  397. wtforms.validators.Length(max=255),
  398. ]
  399. )
  400. description = wtforms.TextField(
  401. 'Description',
  402. [
  403. wtforms.validators.Required(),
  404. wtforms.validators.Length(max=255),
  405. ]
  406. )
  407. class NewGroupForm(EditGroupForm):
  408. """ Form to ask for a password change. """
  409. group_name = wtforms.TextField(
  410. 'Group name <span class="error">*</span>',
  411. [
  412. wtforms.validators.Required(),
  413. wtforms.validators.Length(max=16),
  414. wtforms.validators.Regexp(STRICT_REGEX, flags=re.IGNORECASE)
  415. ]
  416. )
  417. group_type = wtforms.SelectField(
  418. 'Group type',
  419. [wtforms.validators.Required()],
  420. choices=[]
  421. )
  422. def __init__(self, *args, **kwargs):
  423. """ Calls the default constructor with the normal argument but
  424. uses the list of collection provided to fill the choices of the
  425. drop-down list.
  426. """
  427. super(NewGroupForm, self).__init__(*args, **kwargs)
  428. if 'group_types' in kwargs:
  429. self.group_type.choices = [
  430. (grptype, grptype) for grptype in kwargs['group_types']
  431. ]
  432. class EditFileForm(FlaskForm):
  433. """ Form used to edit a file. """
  434. content = wtforms.TextAreaField(
  435. 'content', [wtforms.validators.Required()])
  436. commit_title = wtforms.TextField(
  437. 'Title', [wtforms.validators.Required()])
  438. commit_message = wtforms.TextAreaField(
  439. 'Commit message', [wtforms.validators.optional()])
  440. email = wtforms.SelectField(
  441. 'Email', [wtforms.validators.Required()],
  442. choices=[]
  443. )
  444. branch = wtforms.TextField(
  445. 'Branch', [wtforms.validators.Required()])
  446. def __init__(self, *args, **kwargs):
  447. """ Calls the default constructor with the normal argument but
  448. uses the list of collection provided to fill the choices of the
  449. drop-down list.
  450. """
  451. super(EditFileForm, self).__init__(*args, **kwargs)
  452. if 'emails' in kwargs:
  453. self.email.choices = [
  454. (email.email, email.email) for email in kwargs['emails']
  455. ]
  456. class DefaultBranchForm(FlaskForm):
  457. """Form to change the default branh for a repository"""
  458. branches = wtforms.SelectField(
  459. 'default_branch',
  460. [wtforms.validators.Required()],
  461. choices=[]
  462. )
  463. def __init__(self, *args, **kwargs):
  464. """ Calls the default constructor with the normal argument but
  465. uses the list of collection provided to fill the choices of the
  466. drop-down list.
  467. """
  468. super(DefaultBranchForm, self).__init__(*args, **kwargs)
  469. if 'branches' in kwargs:
  470. self.branches.choices = [
  471. (branch, branch) for branch in kwargs['branches']
  472. ]
  473. class EditCommentForm(FlaskForm):
  474. """ Form to verify that comment is not empty
  475. """
  476. update_comment = wtforms.TextAreaField(
  477. 'Comment<span class="error">*</span>',
  478. [wtforms.validators.Required()]
  479. )
  480. class ForkRepoForm(FlaskForm):
  481. ''' Form to fork a project in the API. '''
  482. repo = wtforms.TextField(
  483. 'The project name',
  484. [wtforms.validators.Required()]
  485. )
  486. username = wtforms.TextField(
  487. 'User who forked the project',
  488. [wtforms.validators.optional()])
  489. namespace = wtforms.TextField(
  490. 'The project namespace',
  491. [wtforms.validators.optional()]
  492. )
  493. class AddReportForm(FlaskForm):
  494. """ Form to verify that comment is not empty
  495. """
  496. report_name = wtforms.TextAreaField(
  497. 'Report name<span class="error">*</span>',
  498. [wtforms.validators.Required()]
  499. )
  500. class PublicNotificationForm(FlaskForm):
  501. """ Form to verify that comment is not empty
  502. """
  503. issue_notifs = wtforms.TextAreaField(
  504. 'Public issue notification<span class="error">*</span>',
  505. [wtforms.validators.optional(), MultipleEmail()]
  506. )
  507. pr_notifs = wtforms.TextAreaField(
  508. 'Public PR notification<span class="error">*</span>',
  509. [wtforms.validators.optional(), MultipleEmail()]
  510. )