Package coprs :: Module forms
[hide private]
[frames] | no frames]

Source Code for Module coprs.forms

   1  import os 
   2  import re 
   3  from six.moves.urllib.parse import urlparse 
   4   
   5  import flask 
   6  import wtforms 
   7  import json 
   8   
   9  from flask_wtf.file import FileRequired, FileField 
  10  from fnmatch import fnmatch 
  11   
  12  try: # get rid of deprecation warning with newer flask_wtf 
  13      from flask_wtf import FlaskForm 
  14  except ImportError: 
  15      from flask_wtf import Form as FlaskForm 
  16   
  17  from coprs import app 
  18  from coprs import helpers 
  19  from coprs import models 
  20  from coprs.logic.coprs_logic import CoprsLogic, MockChrootsLogic 
  21  from coprs.logic.users_logic import UsersLogic 
  22  from coprs.logic.dist_git_logic import DistGitLogic 
  23  from coprs.logic.complex_logic import ComplexLogic 
  24  from coprs import exceptions 
  25   
  26  from wtforms import ValidationError 
  27   
  28  FALSE_VALUES = {False, "false", ""} 
29 30 31 -def get_package_form_cls_by_source_type_text(source_type_text):
32 """ 33 Params 34 ------ 35 source_type_text : str 36 name of the source type (scm/pypi/rubygems/git_and_tito/mock_scm) 37 38 Returns 39 ------- 40 BasePackageForm child 41 based on source_type_text input 42 """ 43 # pylint: disable=too-many-return-statements 44 if source_type_text == 'scm': 45 return PackageFormScm 46 elif source_type_text == 'pypi': 47 return PackageFormPyPI 48 elif source_type_text == 'rubygems': 49 return PackageFormRubyGems 50 elif source_type_text == 'git_and_tito': 51 return PackageFormTito # deprecated 52 elif source_type_text == 'mock_scm': 53 return PackageFormMock # deprecated 54 elif source_type_text == "custom": 55 return PackageFormCustom 56 elif source_type_text == "distgit": 57 return PackageFormDistGitSimple 58 else: 59 raise exceptions.UnknownSourceTypeException("Invalid source type")
60
61 62 -def create_mock_bootstrap_field(level):
63 """ 64 Select-box for the bootstrap configuration in chroot/project form 65 """ 66 67 choices = [] 68 default_choices = [ 69 ('default', 'Use default configuration from mock-core-configs.rpm'), 70 ('off', 'Disable'), 71 ('on', 'Enable'), 72 ('image', 'Initialize by default pre-configured container image'), 73 ] 74 75 if level == 'chroot': 76 choices.append(("unchanged", "Use project settings")) 77 choices.extend(default_choices) 78 choices.append(('custom_image', 79 'Initialize by custom bootstrap image (specified ' 80 'in the "Mock bootstrap image" field below)')) 81 82 elif level == 'build': 83 choices.append(("unchanged", "Use project/chroot settings")) 84 choices.extend(default_choices) 85 86 else: 87 choices.extend(default_choices) 88 89 return wtforms.SelectField( 90 "Mock bootstrap", 91 choices=choices, 92 validators=[wtforms.validators.Optional()], 93 # Replace "None" with None (needed on Fedora <= 32) 94 filters=[NoneFilter(None)], 95 )
96
97 98 -def create_mock_bootstrap_image_field():
99 """ 100 Mandatory bootstrap-image field when the bootstrap select-box is set to a 101 custom image option. 102 """ 103 return wtforms.TextField( 104 "Mock bootstrap image", 105 validators=[ 106 wtforms.validators.Optional(), 107 wtforms.validators.Regexp( 108 r"^\w+(:\w+)?$", 109 message=("Enter valid bootstrap image id " 110 "(<name>[:<tag>], e.g. fedora:33)."))], 111 filters=[ 112 lambda x: None if not x else x 113 ], 114 )
115
116 117 -class MultiCheckboxField(wtforms.SelectMultipleField):
118 widget = wtforms.widgets.ListWidget(prefix_label=False) 119 option_widget = wtforms.widgets.CheckboxInput()
120
121 122 -class UrlListValidator(object):
123
124 - def __init__(self, message=None):
125 if not message: 126 message = ("A list of http[s] URLs separated by whitespace characters" 127 " is needed ('{0}' doesn't seem to be a valid URL).") 128 self.message = message
129
130 - def __call__(self, form, field):
131 urls = field.data.split() 132 for u in urls: 133 if not self.is_url(u): 134 raise wtforms.ValidationError(self.message.format(u))
135
136 - def is_url(self, url):
137 parsed = urlparse(url) 138 if not parsed.scheme.startswith("http"): 139 return False 140 if not parsed.netloc: 141 return False 142 return True
143
144 145 -class UrlRepoListValidator(UrlListValidator):
146 """ Allows also `repo://` schema"""
147 - def is_url(self, url):
148 parsed = urlparse(url) 149 if parsed.scheme not in ["http", "https", "copr"]: 150 return False 151 if not parsed.netloc: 152 return False 153 # copr://username/projectname 154 # ^^ schema ^^ netlock ^^ path 155 if parsed.scheme == "copr": 156 # check if projectname missed 157 path_split = parsed.path.split("/") 158 if len(path_split) < 2 or path_split[1] == "": 159 return False 160 161 return True
162
163 164 -class UrlSrpmListValidator(UrlListValidator):
165 - def __init__(self, message=None):
166 if not message: 167 message = ("URLs must end with .src.rpm, .nosrc.rpm, or .spec" 168 " ('{0}' doesn't seem to be a valid URL).") 169 super(UrlSrpmListValidator, self).__init__(message)
170
171 - def is_url(self, url):
172 parsed = urlparse(url) 173 if not parsed.path.endswith((".src.rpm", ".nosrc.rpm", ".spec")): 174 return False 175 return True
176
177 178 -class SrpmValidator(object):
179 - def __init__(self, message=None):
180 if not message: 181 message = "You can upload only .src.rpm, .nosrc.rpm, and .spec files" 182 self.message = message
183
184 - def __call__(self, form, field):
185 filename = field.data.filename.lower() 186 if not filename.endswith((".src.rpm", ".nosrc.rpm", ".spec")): 187 raise wtforms.ValidationError(self.message)
188
189 190 -class CoprUniqueNameValidator(object):
191
192 - def __init__(self, message=None, user=None, group=None):
193 if not message: 194 if group is None: 195 message = "You already have project named '{}'." 196 else: 197 message = "Group {} ".format(group) + "already have project named '{}'." 198 self.message = message 199 if not user: 200 user = flask.g.user 201 self.user = user 202 self.group = group
203
204 - def __call__(self, form, field):
205 if self.group: 206 existing = CoprsLogic.exists_for_group( 207 self.group, field.data).first() 208 else: 209 existing = CoprsLogic.exists_for_user( 210 self.user, field.data).first() 211 212 if existing and str(existing.id) != form.id.data: 213 raise wtforms.ValidationError(self.message.format(field.data))
214
215 216 -class NameCharactersValidator(object):
217 - def __init__(self, message=None):
218 if not message: 219 message = "Name must contain only letters, digits, underscores, dashes and dots." 220 self.message = message
221
222 - def __call__(self, form, field):
223 validator = wtforms.validators.Regexp( 224 re.compile(r"^[\w.-]+$"), 225 message=self.message) 226 validator(form, field)
227
228 -class ModuleEnableNameValidator(object):
229
230 - def __call__(self, form, field):
231 already_enabled = {} 232 for module in form.module_toggle.data.split(","): 233 if module == "": 234 return True 235 236 try: 237 module_name, stream = module.strip().split(":") 238 except ValueError: 239 raise ValidationError( 240 message=( 241 "Module name '{0}' must consist of two parts separated " 242 "with colon, e.g. module:stream" 243 ).format(module)) 244 245 pattern = re.compile(re.compile(r"^([a-zA-Z0-9-_!][^\ ]*)$")) 246 if pattern.match(module_name) is None: 247 raise ValidationError(message=( 248 "Module name '{0}' must contain only letters, digits, " 249 "dashes, underscores.").format(module_name)) 250 251 if module_name in already_enabled: 252 raise ValidationError("Module name '{0}' specified multiple " 253 "times".format(module_name)) 254 else: 255 already_enabled[module_name] = True 256 257 if pattern.match(stream) is None: 258 raise ValidationError(message=( 259 "Stream part of module name '{0}' must contain only " 260 "letters, digits, dashes, underscores.").format(stream))
261
262 -class ChrootsValidator(object):
263 - def __call__(self, form, field):
264 # Allow it to be truly optional and has None value 265 if not field.data: 266 return 267 268 selected = set(field.data.split()) 269 enabled = set(MockChrootsLogic.active_names()) 270 271 if selected - enabled: 272 raise wtforms.ValidationError("Such chroot is not available: {}".format(", ".join(selected - enabled)))
273
274 275 -class NameNotNumberValidator(object):
276
277 - def __init__(self, message=None):
278 if not message: 279 message = "Project's name can not be just number." 280 self.message = message
281
282 - def __call__(self, form, field):
283 if field.data.isdigit(): 284 raise wtforms.ValidationError(self.message.format(field.data))
285
286 287 -class EmailOrURL(object):
288
289 - def __init__(self, message=None):
290 if not message: 291 message = "{} must be email address or URL" 292 self.message = message
293
294 - def __call__(self, form, field):
295 for validator in [wtforms.validators.Email(), wtforms.validators.URL()]: 296 try: 297 validator(form, field) 298 return True 299 except wtforms.ValidationError: 300 pass 301 raise wtforms.ValidationError(self.message.format(field.name.capitalize()))
302
303 304 -class StringListFilter(object):
305
306 - def __call__(self, value):
307 if not value: 308 return '' 309 # Replace every whitespace string with one newline 310 # Formats ideally for html form filling, use replace('\n', ' ') 311 # to get space-separated values or split() to get list 312 result = value.strip() 313 regex = re.compile(r"\s+") 314 return regex.sub(lambda x: '\n', result)
315
316 -class StringWhiteCharactersFilter(object):
317
318 - def __call__(self, value):
319 if not value: 320 return '' 321 322 modules = [module.strip() for module in value.split(",")] 323 # to remove empty strings 324 modules = [m for m in modules if m] 325 326 return ", ".join(module for module in modules if module != "")
327
328 -class ValueToPermissionNumberFilter(object):
329
330 - def __call__(self, value):
331 if value: 332 return helpers.PermissionEnum("request") 333 return helpers.PermissionEnum("nothing")
334
335 -def _optional_checkbox_filter(data):
336 if data in [True, 'true']: 337 return True 338 if data in [False, 'false']: 339 return False 340 return None
341
342 -class CoprFormFactory(object):
343 344 @staticmethod
345 - def create_form_cls(mock_chroots=None, user=None, group=None, copr=None):
346 class F(FlaskForm): 347 # also use id here, to be able to find out whether user 348 # is updating a copr if so, we don't want to shout 349 # that name already exists 350 id = wtforms.HiddenField() 351 group_id = wtforms.HiddenField() 352 353 name = wtforms.StringField( 354 "Name", 355 validators=[ 356 wtforms.validators.DataRequired(), 357 NameCharactersValidator(), 358 CoprUniqueNameValidator(user=user, group=group), 359 NameNotNumberValidator() 360 ]) 361 362 homepage = wtforms.StringField( 363 "Homepage", 364 validators=[ 365 wtforms.validators.Optional(), 366 wtforms.validators.URL()]) 367 368 contact = wtforms.StringField( 369 "Contact", 370 validators=[ 371 wtforms.validators.Optional(), 372 EmailOrURL()]) 373 374 description = wtforms.TextAreaField("Description") 375 376 instructions = wtforms.TextAreaField("Instructions") 377 378 delete_after_days = wtforms.IntegerField( 379 "Delete after days", 380 validators=[ 381 wtforms.validators.Optional(), 382 wtforms.validators.NumberRange(min=0, max=60), 383 ], 384 render_kw={'disabled': bool(copr and copr.persistent)}) 385 386 repos = wtforms.TextAreaField( 387 "External Repositories", 388 validators=[UrlRepoListValidator()], 389 filters=[StringListFilter()]) 390 391 runtime_dependencies = wtforms.TextAreaField( 392 "Runtime dependencies", 393 validators=[UrlRepoListValidator()], 394 filters=[StringListFilter()]) 395 396 initial_pkgs = wtforms.TextAreaField( 397 "Initial packages to build", 398 validators=[ 399 UrlListValidator(), 400 UrlSrpmListValidator()], 401 filters=[StringListFilter()]) 402 403 disable_createrepo = wtforms.BooleanField(default=False, 404 label="Create repositories manually", 405 description="""Repository meta data is normally refreshed 406 after each build. If you want to do this manually, turn 407 this option on.""", 408 false_values=FALSE_VALUES) 409 410 unlisted_on_hp = wtforms.BooleanField( 411 "Project will not be listed on home page", 412 default=False, 413 false_values=FALSE_VALUES) 414 415 persistent = wtforms.BooleanField( 416 "Protect project and its builds against deletion", 417 description="""Project's builds and the project itself 418 cannot be deleted by any means. This option is set once and 419 for all (this option can not be changed after project is 420 created).""", 421 render_kw={'disabled': bool(copr)}, 422 default=False, false_values=FALSE_VALUES) 423 424 auto_prune = wtforms.BooleanField( 425 "Old builds will be deleted automatically", 426 default=True, false_values=FALSE_VALUES, 427 description="""Build will be deleted only if there is a 428 newer build (with respect to package version) and it is 429 older than 14 days""") 430 431 use_bootstrap_container = wtforms.StringField( 432 "backward-compat-only: old bootstrap", 433 validators=[wtforms.validators.Optional()], 434 filters=[_optional_checkbox_filter]) 435 436 bootstrap = create_mock_bootstrap_field("project") 437 438 follow_fedora_branching = wtforms.BooleanField( 439 "Follow Fedora branching", 440 description="""When Fedora is branched from rawhide, the 441 respective chroots for the new branch are automatically 442 created for you (as soon as they are available) as rawhide 443 chroot forks.""", 444 default=True, 445 false_values=FALSE_VALUES) 446 447 multilib = wtforms.BooleanField( 448 "Multilib support", 449 description="""When users enable this copr repository on 450 64bit variant of multilib capable architecture (e.g. 451 x86_64), they will be able to install 32bit variants of the 452 packages (e.g. i386 for x86_64 arch)""", 453 default=False, 454 false_values=FALSE_VALUES) 455 456 # Deprecated, use `enable_net` instead 457 build_enable_net = wtforms.BooleanField( 458 "Enable internet access during builds", 459 default=False, false_values=FALSE_VALUES) 460 461 enable_net = wtforms.BooleanField( 462 "Enable internet access during builds", 463 default=False, false_values=FALSE_VALUES) 464 465 module_hotfixes = wtforms.BooleanField( 466 "This repository contains module hotfixes", 467 description="""This will make packages from this project 468 available on along with packages from the active module 469 streams.""", 470 default=False, false_values=FALSE_VALUES) 471 472 @property 473 def selected_chroots(self): 474 selected = [] 475 for ch in self.chroots_list: 476 if getattr(self, ch).data: 477 selected.append(ch) 478 return selected
479 480 def validate(self): 481 if not super(F, self).validate(): 482 return False 483 484 if not self.validate_mock_chroots_not_empty(): 485 self.form_errors = ["At least one chroot must be selected"] 486 return False 487 488 if self.persistent.data and self.delete_after_days.data: 489 self.delete_after_days.errors.append( 490 "'delete after' can not be combined with persistent") 491 return False 492 493 return True
494 495 @property 496 def errors(self): 497 """ 498 Current stable version of WTForms's `Form` doesn't allow to set 499 form-level errors. Let's workaround it in a way, that is 500 implemented in the development branch. 501 502 2.2.1 (Fedora 31/32) 503 `form.errors["whatever"] = ["Some message"]` could be done 504 505 2.3.1 (Fedora 33) 506 The previous solution does nothing and there is no way to 507 have form-level errors. The only way to set errors is via 508 `form.some_field.errors.append("Some message")`. We are 509 reimplementing `errors` property to behave like in 3.0.0 510 511 3.0.0 (Fedora ??) 512 The `form.form_errors` field can be set. This list will be 513 added to the resulting `errors` value and accessible as 514 `form.errors[None]`. 515 516 RFE: https://github.com/wtforms/wtforms/issues/55 517 PR: https://github.com/wtforms/wtforms/pull/595 518 Release notes: https://github.com/wtforms/wtforms/blob/master/CHANGES.rst#version-300 519 """ 520 errors = super().errors.copy() 521 if hasattr(self, "form_errors"): 522 errors[None] = self.form_errors 523 return errors 524 525 def validate_mock_chroots_not_empty(self): 526 have_any = False 527 for c in self.chroots_list: 528 if getattr(self, c).data: 529 have_any = True 530 return have_any 531 532 F.chroots_list = MockChrootsLogic.active_names() 533 F.chroots_list.sort() 534 # sets of chroots according to how we should print them in columns 535 F.chroots_sets = {} 536 for ch in F.chroots_list: 537 checkbox_default = False 538 if mock_chroots and ch in [x.name for x in mock_chroots]: 539 checkbox_default = True 540 541 setattr(F, ch, wtforms.BooleanField(ch, default=checkbox_default, false_values=FALSE_VALUES)) 542 if ch[0] in F.chroots_sets: 543 F.chroots_sets[ch[0]].append(ch) 544 else: 545 F.chroots_sets[ch[0]] = [ch] 546 547 return F 548
549 550 -class CoprDeleteForm(FlaskForm):
551 verify = wtforms.StringField( 552 "Confirm deleting by typing 'yes'", 553 validators=[ 554 wtforms.validators.DataRequired(), 555 wtforms.validators.Regexp( 556 r"^yes$", 557 message="Type 'yes' - without the quotes, lowercase.") 558 ])
559
560 561 -class APICoprDeleteForm(CoprDeleteForm):
562 verify = wtforms.BooleanField("Confirm deleting", false_values=FALSE_VALUES)
563
564 -def seconds_to_pretty_hours(sec):
565 minutes = round(sec / 60) 566 hours = minutes // 60 567 minutes = minutes % 60 568 return hours if not minutes else "{}:{:02d}".format(hours, minutes)
569
570 571 -class BuildFormRebuildFactory(object):
572 # TODO: drop, and use BaseBuildFormFactory directly 573 @staticmethod
574 - def create_form_cls(active_chroots):
575 return BaseBuildFormFactory(active_chroots, FlaskForm)
576
577 578 -class RebuildPackageFactory(object):
579 @staticmethod
580 - def create_form_cls(active_chroots):
581 form = BuildFormRebuildFactory.create_form_cls(active_chroots) 582 # pylint: disable=attribute-defined-outside-init 583 form.package_name = wtforms.StringField( 584 "Package name", 585 validators=[wtforms.validators.DataRequired()]) 586 return form
587
588 589 -def cleanup_chroot_blacklist(string):
590 if not string: 591 return string 592 fields = [x.lstrip().rstrip() for x in string.split(',')] 593 return ', '.join(fields)
594
595 596 -def validate_chroot_blacklist(form, field):
597 if field.data: 598 string = field.data 599 fields = [x.lstrip().rstrip() for x in string.split(',')] 600 for field in fields: 601 pattern = r'^[a-z0-9-*]+$' 602 if not re.match(pattern, field): 603 raise wtforms.ValidationError('Pattern "{0}" does not match "{1}"'.format(field, pattern)) 604 605 matched = set() 606 all_chroots = MockChrootsLogic.active_names() 607 for chroot in all_chroots: 608 if fnmatch(chroot, field): 609 matched.add(chroot) 610 611 if not matched: 612 raise wtforms.ValidationError('no chroot matched by pattern "{0}"'.format(field)) 613 614 if matched == all_chroots: 615 raise wtforms.ValidationError('patterns are black-listing all chroots')
616
617 618 -class BasePackageForm(FlaskForm):
619 package_name_regex = r"^[-+_.a-zA-Z0-9]+$" 620 621 package_name = wtforms.StringField( 622 "Package name", 623 validators=[ 624 wtforms.validators.Regexp( 625 re.compile(package_name_regex), 626 message="Please enter a valid package name in " \ 627 + package_name_regex)] 628 ) 629 630 webhook_rebuild = wtforms.BooleanField(default=False, false_values=FALSE_VALUES) 631 chroot_blacklist = wtforms.StringField( 632 "Chroot blacklist", 633 filters=[cleanup_chroot_blacklist], 634 validators=[ 635 wtforms.validators.Optional(), 636 validate_chroot_blacklist, 637 ], 638 ) 639 max_builds = wtforms.IntegerField( 640 "Max number of builds", 641 description="""Keep only the specified number of the newest-by-id builds 642 (garbage collector is run daily)""", 643 render_kw={'placeholder': 'Optional - integer, e.g. 10, zero/empty disables'}, 644 validators=[ 645 wtforms.validators.Optional(), 646 wtforms.validators.NumberRange(min=0, max=100)], 647 default=None, 648 )
649
650 651 -class PackageFormScm(BasePackageForm):
652 scm_type = wtforms.SelectField( 653 "Type", 654 choices=[("git", "Git"), ("svn", "SVN")], 655 default="git") 656 657 clone_url = wtforms.StringField( 658 "Clone url", 659 validators=[ 660 wtforms.validators.DataRequired(), 661 wtforms.validators.URL()]) 662 663 committish = wtforms.StringField( 664 "Committish", 665 validators=[ 666 wtforms.validators.Optional()]) 667 668 subdirectory = wtforms.StringField( 669 "Subdirectory", 670 validators=[ 671 wtforms.validators.Optional()]) 672 673 spec = wtforms.StringField( 674 "Spec File", 675 validators=[ 676 wtforms.validators.Optional(), 677 wtforms.validators.Regexp( 678 r"^.+\.spec$", 679 message="RPM spec file must end with .spec")]) 680 681 srpm_build_method = wtforms.SelectField( 682 "SRPM build method", 683 choices=[(x, x) for x in ["rpkg", "tito", "tito_test", "make_srpm"]], 684 default="rpkg") 685 686 @property
687 - def source_json(self):
688 return json.dumps({ 689 "type": self.scm_type.data, 690 "clone_url": self.clone_url.data, 691 "subdirectory": self.subdirectory.data, 692 "committish": self.committish.data, 693 "spec": self.spec.data, 694 "srpm_build_method": self.srpm_build_method.data, 695 })
696
697 698 -class PackageFormPyPI(BasePackageForm):
699 pypi_package_name = wtforms.StringField( 700 "PyPI package name", 701 validators=[wtforms.validators.DataRequired()]) 702 703 pypi_package_version = wtforms.StringField( 704 "PyPI package version", 705 validators=[ 706 wtforms.validators.Optional(), 707 ]) 708 709 spec_template = wtforms.SelectField( 710 "Spec template", 711 choices=[ 712 ("", "default"), 713 ("fedora", "fedora"), 714 ("epel7", "epel7"), 715 ("mageia", "mageia"), 716 ("pld", "pld"), 717 ], default="") 718 719 python_versions = MultiCheckboxField( 720 'Build for Python', 721 choices=[ 722 ('3', 'python3'), 723 ('2', 'python2') 724 ], 725 default=['3', '2']) 726 727 @property
728 - def source_json(self):
729 return json.dumps({ 730 "pypi_package_name": self.pypi_package_name.data, 731 "pypi_package_version": self.pypi_package_version.data, 732 "spec_template": self.spec_template.data, 733 "python_versions": self.python_versions.data 734 })
735
736 737 -class PackageFormRubyGems(BasePackageForm):
738 gem_name = wtforms.StringField( 739 "Gem Name", 740 validators=[wtforms.validators.DataRequired()]) 741 742 @property
743 - def source_json(self):
744 return json.dumps({ 745 "gem_name": self.gem_name.data 746 })
747
748 749 -class PackageFormTito(BasePackageForm):
750 """ 751 @deprecated 752 """ 753 git_url = wtforms.StringField( 754 "Git URL", 755 validators=[ 756 wtforms.validators.DataRequired(), 757 wtforms.validators.URL()]) 758 759 git_directory = wtforms.StringField( 760 "Git Directory", 761 validators=[ 762 wtforms.validators.Optional()]) 763 764 git_branch = wtforms.StringField( 765 "Git Branch", 766 validators=[ 767 wtforms.validators.Optional()]) 768 769 tito_test = wtforms.BooleanField(default=False, false_values=FALSE_VALUES) 770 771 @property
772 - def source_json(self):
773 return json.dumps({ 774 "type": 'git', 775 "clone_url": self.git_url.data, 776 "committish": self.git_branch.data, 777 "subdirectory": self.git_directory.data, 778 "spec": '', 779 "srpm_build_method": 'tito_test' if self.tito_test.data else 'tito', 780 })
781
782 783 -class PackageFormMock(BasePackageForm):
784 """ 785 @deprecated 786 """ 787 scm_type = wtforms.SelectField( 788 "SCM Type", 789 choices=[("git", "Git"), ("svn", "SVN")]) 790 791 scm_url = wtforms.StringField( 792 "SCM URL", 793 validators=[ 794 wtforms.validators.DataRequired(), 795 wtforms.validators.URL()]) 796 797 scm_branch = wtforms.StringField( 798 "Git Branch", 799 validators=[ 800 wtforms.validators.Optional()]) 801 802 scm_subdir = wtforms.StringField( 803 "Subdirectory", 804 validators=[ 805 wtforms.validators.Optional()]) 806 807 spec = wtforms.StringField( 808 "Spec File", 809 validators=[ 810 wtforms.validators.Optional(), 811 wtforms.validators.Regexp( 812 r"^.+\.spec$", 813 message="RPM spec file must end with .spec")]) 814 815 @property
816 - def source_json(self):
817 return json.dumps({ 818 "type": self.scm_type.data, 819 "clone_url": self.scm_url.data, 820 "committish": self.scm_branch.data, 821 "subdirectory": self.scm_subdir.data, 822 "spec": self.spec.data, 823 "srpm_build_method": 'rpkg', 824 })
825
826 827 -class PackageFormDistGit(BasePackageForm):
828 """ 829 @deprecated 830 """ 831 clone_url = wtforms.StringField( 832 "Clone Url", 833 validators=[wtforms.validators.DataRequired()]) 834 835 branch = wtforms.StringField( 836 "Branch", 837 validators=[wtforms.validators.Optional()]) 838 839 @property
840 - def source_json(self):
841 return json.dumps({ 842 "type": 'git', 843 "clone_url": self.clone_url.data, 844 "committish": self.branch.data, 845 "subdirectory": '', 846 "spec": '', 847 "srpm_build_method": 'rpkg', 848 })
849
850 851 -def cleanup_script(string):
852 if not string: 853 return string 854 855 if string.split('\n')[0].endswith('\r'): 856 # This script is most probably coming from the web-UI, where 857 # web-browsers mistakenly put '\r\n' as EOL; and that would just 858 # mean that the script is not executable (any line can mean 859 # syntax error, but namely shebang would cause 100% fail) 860 string = string.replace('\r\n', '\n') 861 862 # And append newline to have a valid unix file. 863 if not string.endswith('\n'): 864 string += '\n' 865 866 return string
867
868 869 -class PackageFormCustom(BasePackageForm):
870 script = wtforms.TextAreaField( 871 "Script", 872 validators=[ 873 wtforms.validators.DataRequired(), 874 wtforms.validators.Length( 875 max=4096, 876 message="Maximum script size is 4kB"), 877 ], 878 filters=[cleanup_script], 879 ) 880 881 builddeps = wtforms.StringField( 882 "Build dependencies", 883 validators=[wtforms.validators.Optional()]) 884 885 chroot = wtforms.SelectField( 886 'Mock chroot', 887 choices=[], 888 default='fedora-latest-x86_64', 889 ) 890 891 resultdir = wtforms.StringField( 892 "Result directory", 893 validators=[wtforms.validators.Optional()]) 894
895 - def __init__(self, *args, **kwargs):
896 super(PackageFormCustom, self).__init__(*args, **kwargs) 897 chroot_objects = models.MockChroot.query.filter(models.MockChroot.is_active).all() 898 899 chroots = [c.name for c in chroot_objects] 900 chroots.sort() 901 chroots = [(name, name) for name in chroots] 902 903 arches = set() 904 for ch in chroot_objects: 905 if ch.os_release == 'fedora': 906 arches.add(ch.arch) 907 908 self.chroot.choices = [] 909 if arches: 910 self.chroot.choices += [('fedora-latest-' + l, 'fedora-latest-' + l) for l in arches] 911 912 self.chroot.choices += chroots
913 914 @property
915 - def source_json(self):
916 return json.dumps({ 917 "script": self.script.data, 918 "chroot": self.chroot.data, 919 "builddeps": self.builddeps.data, 920 "resultdir": self.resultdir.data, 921 })
922
923 924 -class DistGitValidator(object):
925 - def __call__(self, form, field):
926 if field.data not in field.distgit_choices: 927 message = "DistGit ID must be one of: {}".format( 928 ", ".join(field.distgit_choices)) 929 raise wtforms.ValidationError(message)
930
931 932 -class NoneFilter():
933 - def __init__(self, default):
934 self.default = default
935
936 - def __call__(self, value):
937 if value in [None, 'None']: 938 return self.default 939 return value
940
941 942 -class DistGitSelectField(wtforms.SelectField):
943 """ Select-box for picking (default) dist git instance """ 944 945 # pylint: disable=too-few-public-methods
946 - def __init__(self, validators=None, filters=None, **kwargs):
947 if not validators: 948 validators = [] 949 if not filters: 950 filters = [] 951 952 self.distgit_choices = [x.name for x in DistGitLogic.ordered().all()] 953 self.distgit_default = self.distgit_choices[0] 954 955 validators.append(DistGitValidator()) 956 filters.append(NoneFilter(self.distgit_default)) 957 958 super().__init__( 959 label="DistGit instance", 960 validators=validators, 961 filters=filters, 962 choices=[(x, x) for x in self.distgit_choices], 963 **kwargs, 964 )
965
966 967 -class PackageFormDistGitSimple(BasePackageForm):
968 """ 969 This represents basically a variant of the SCM method, but with a very 970 trivial user interface. 971 """ 972 distgit = DistGitSelectField() 973 974 committish = wtforms.StringField( 975 "Committish", 976 validators=[wtforms.validators.Optional()], 977 render_kw={ 978 "placeholder": "Optional - Specific branch, tag, or commit that " 979 "you want to build from"}, 980 ) 981 982 namespace = wtforms.StringField( 983 "DistGit namespace", 984 validators=[wtforms.validators.Optional()], 985 default=None, 986 filters=[lambda x: None if not x else os.path.normpath(x)], 987 description=( 988 "Some dist-git instances have the git repositories " 989 "namespaced - e.g. you need to specify '@copr/copr' for " 990 "the <a href='https://copr-dist-git.fedorainfracloud.org/" 991 "cgit/@copr/copr/copr-cli.git/tree/copr-cli.spec'>" 992 "@copr/copr/copr-cli</a> Fedora Copr package"), 993 render_kw={ 994 "placeholder": "Optional - string, e.g. '@copr/copr'"}, 995 ) 996 997 build_requires_package_name = True 998 999 @property
1000 - def source_json(self):
1001 """ Source json stored in DB in Package.source_json """ 1002 data = { 1003 "clone_url": self.clone_url(), 1004 } 1005 1006 if self.distgit.data: 1007 data["distgit"] = self.distgit.data 1008 1009 for field_name in ["distgit", "namespace", "committish"]: 1010 field = getattr(self, field_name) 1011 if field.data: 1012 data[field_name] = field.data 1013 1014 return json.dumps(data)
1015
1016 - def clone_url(self):
1017 """ One-time generate the clone_url from the form data """ 1018 return DistGitLogic.get_clone_url(self.distgit.data, 1019 self.package_name.data, 1020 self.namespace.data)
1021
1022 - def validate(self):
1023 """ 1024 Try to check that we can generate clone_url from distgit, namespace and 1025 package. This can not be done by single-field-context validator. 1026 """ 1027 if not super().validate(): 1028 return False 1029 1030 try: 1031 self.clone_url() 1032 except Exception as e: # pylint: disable=broad-except 1033 self.distgit.errors.append( 1034 "Can not validate DistGit input: {}".format(str(e)) 1035 ) 1036 return False 1037 1038 return True
1039
1040 1041 -class RebuildAllPackagesFormFactory(object):
1042 - def __new__(cls, active_chroots, package_names):
1043 form_cls = BaseBuildFormFactory(active_chroots, FlaskForm) 1044 form_cls.packages = MultiCheckboxField( 1045 "Packages", 1046 choices=[(name, name) for name in package_names], 1047 default=package_names, 1048 validators=[wtforms.validators.DataRequired()]) 1049 return form_cls
1050
1051 1052 -class BaseBuildFormFactory(object):
1053 # TODO: Change this to just 'def get_build_form(...)'. The __new__ 1054 # hack confuses not only PyLint (on each calling place it claims 1055 # that the return value is not callable. __new__ isn't supposed 1056 # to return classes, but instances.
1057 - def __new__(cls, active_chroots, form, package=None):
1058 class F(form): 1059 @property 1060 def selected_chroots(self): 1061 selected = [] 1062 for ch in self.chroots_list: 1063 if getattr(self, ch).data: 1064 selected.append(ch) 1065 return selected
1066 1067 F.timeout = wtforms.IntegerField( 1068 "Timeout", 1069 description="Optional - number of seconds we allow the builds to run, default is {0} ({1}h)".format( 1070 app.config["DEFAULT_BUILD_TIMEOUT"], seconds_to_pretty_hours(app.config["DEFAULT_BUILD_TIMEOUT"])), 1071 validators=[ 1072 wtforms.validators.Optional(), 1073 wtforms.validators.NumberRange( 1074 min=app.config["MIN_BUILD_TIMEOUT"], 1075 max=app.config["MAX_BUILD_TIMEOUT"])], 1076 default=app.config["DEFAULT_BUILD_TIMEOUT"]) 1077 1078 F.enable_net = wtforms.BooleanField(false_values=FALSE_VALUES) 1079 F.background = wtforms.BooleanField(default=False, false_values=FALSE_VALUES) 1080 F.project_dirname = wtforms.StringField(default=None) 1081 F.bootstrap = create_mock_bootstrap_field("build") 1082 1083 # Overrides BasePackageForm.package_name, it is usually unused for 1084 # building 1085 if not getattr(F, "build_requires_package_name", None): 1086 F.package_name = wtforms.StringField() 1087 1088 # fill chroots based on project settings 1089 F.chroots_list = [x.name for x in active_chroots] 1090 F.chroots_list.sort() 1091 F.chroots_sets = {} 1092 1093 package_chroots = set(F.chroots_list) 1094 if package: 1095 package_chroots = set([ch.name for ch in package.chroots]) 1096 1097 for ch in F.chroots_list: 1098 default = ch in package_chroots 1099 setattr(F, ch, wtforms.BooleanField(ch, default=default, false_values=FALSE_VALUES)) 1100 if ch[0] in F.chroots_sets: 1101 F.chroots_sets[ch[0]].append(ch) 1102 else: 1103 F.chroots_sets[ch[0]] = [ch] 1104 1105 F.after_build_id = wtforms.IntegerField( 1106 "Batch-build after", 1107 description=( 1108 "Optional - Build after the batch containing " 1109 "the Build ID build." 1110 ), 1111 validators=[ 1112 wtforms.validators.Optional()], 1113 render_kw={'placeholder': 'Build ID'}, 1114 filters=[NoneFilter(None)], 1115 ) 1116 1117 F.with_build_id = wtforms.IntegerField( 1118 "Batch-build with", 1119 description=( 1120 "Optional - Build in the same batch with the Build ID build" 1121 ), 1122 render_kw={'placeholder': 'Build ID'}, 1123 validators=[ 1124 wtforms.validators.Optional()], 1125 filters=[NoneFilter(None)], 1126 ) 1127 1128 def _validate_batch_opts(form, field): 1129 counterpart = form.with_build_id 1130 modifies = False 1131 if counterpart == field: 1132 counterpart = form.after_build_id 1133 modifies = True 1134 1135 if counterpart.data: 1136 raise wtforms.ValidationError( 1137 "Only one batch option can be specified") 1138 1139 build_id = field.data 1140 if not build_id: 1141 return 1142 1143 build_id = int(build_id) 1144 build = models.Build.query.get(build_id) 1145 if not build: 1146 raise wtforms.ValidationError( 1147 "Build {} not found".format(build_id)) 1148 batch_error = build.batching_user_error(flask.g.user, modifies) 1149 if batch_error: 1150 raise wtforms.ValidationError(batch_error) 1151 1152 F.validate_with_build_id = _validate_batch_opts 1153 F.validate_after_build_id = _validate_batch_opts 1154 1155 return F 1156
1157 1158 -class BuildFormScmFactory(object):
1159 - def __new__(cls, active_chroots, package=None):
1161
1162 1163 -class BuildFormTitoFactory(object):
1164 """ 1165 @deprecated 1166 """
1167 - def __new__(cls, active_chroots):
1169
1170 1171 -class BuildFormMockFactory(object):
1172 """ 1173 @deprecated 1174 """
1175 - def __new__(cls, active_chroots):
1177
1178 1179 -class BuildFormPyPIFactory(object):
1180 - def __new__(cls, active_chroots, package=None):
1182
1183 1184 -class BuildFormRubyGemsFactory(object):
1185 - def __new__(cls, active_chroots, package=None):
1187
1188 1189 -class BuildFormDistGitFactory(object):
1190 - def __new__(cls, active_chroots):
1192
1193 1194 -class BuildFormUploadFactory(object):
1195 - def __new__(cls, active_chroots):
1196 form = BaseBuildFormFactory(active_chroots, FlaskForm) 1197 form.pkgs = FileField('srpm', validators=[ 1198 FileRequired(), 1199 SrpmValidator()]) 1200 return form
1201
1202 1203 -class BuildFormCustomFactory(object):
1204 - def __new__(cls, active_chroots, package=None):
1206
1207 1208 -class BuildFormDistGitSimpleFactory:
1209 """ 1210 Transform DistGitSimple package form into build form 1211 """
1212 - def __new__(cls, active_chroots, package=None):
1215
1216 -class BuildFormUrlFactory(object):
1217 - def __new__(cls, active_chroots):
1218 form = BaseBuildFormFactory(active_chroots, FlaskForm) 1219 form.pkgs = wtforms.TextAreaField( 1220 "Pkgs", 1221 validators=[ 1222 wtforms.validators.DataRequired(message="URLs to packages are required"), 1223 UrlListValidator(), 1224 UrlSrpmListValidator()], 1225 filters=[StringListFilter()]) 1226 return form
1227
1228 1229 -class ModuleFormUploadFactory(FlaskForm):
1230 modulemd = FileField("modulemd", validators=[ 1231 FileRequired(), 1232 # @TODO Validate modulemd.yaml file 1233 ]) 1234 1235 create = wtforms.BooleanField("create", default=True, false_values=FALSE_VALUES) 1236 build = wtforms.BooleanField("build", default=True, false_values=FALSE_VALUES)
1237
1238 1239 -def get_module_build_form(*args, **kwargs):
1240 class ModuleBuildForm(FlaskForm): 1241 modulemd = FileField("modulemd") 1242 scmurl = wtforms.StringField() 1243 branch = wtforms.StringField() 1244 1245 distgit = DistGitSelectField()
1246 1247 return ModuleBuildForm(*args, **kwargs) 1248
1249 1250 -class PagureIntegrationForm(FlaskForm):
1251 repo_url = wtforms.StringField("repo_url", default='') 1252 api_key = wtforms.StringField("api_key", default='') 1253
1254 - def __init__(self, api_key=None, repo_url=None, *args, **kwargs):
1255 super(PagureIntegrationForm, self).__init__(*args, **kwargs) 1256 if api_key != None: 1257 self.api_key.data = api_key 1258 if repo_url != None: 1259 self.repo_url.data = repo_url
1260
1261 1262 -class ChrootForm(FlaskForm):
1263 1264 """ 1265 Validator for editing chroots in project 1266 (adding packages to minimal chroot) 1267 """ 1268 1269 buildroot_pkgs = wtforms.StringField("Packages") 1270 1271 repos = wtforms.TextAreaField('Repos', 1272 validators=[UrlRepoListValidator(), 1273 wtforms.validators.Optional()], 1274 filters=[StringListFilter()]) 1275 1276 comps = FileField("comps_xml") 1277 1278 module_toggle = wtforms.StringField("Enable module", 1279 validators=[ModuleEnableNameValidator()], 1280 filters=[StringWhiteCharactersFilter()] 1281 ) 1282 1283 with_opts = wtforms.StringField("With options") 1284 without_opts = wtforms.StringField("Without options") 1285 1286 bootstrap = create_mock_bootstrap_field("chroot") 1287 bootstrap_image = create_mock_bootstrap_image_field() 1288
1289 - def validate(self, *args, **kwargs): # pylint: disable=signature-differs
1290 """ We need to special-case custom_image configuration """ 1291 result = super().validate(*args, **kwargs) 1292 if self.bootstrap.data != "custom_image": 1293 return result 1294 if not self.bootstrap_image.data: 1295 self.bootstrap_image.errors.append( 1296 "Custom image is selected, but not specified") 1297 return False 1298 return result
1299
1300 1301 -class CoprChrootExtend(FlaskForm):
1302 extend = wtforms.StringField("Chroot name") 1303 expire = wtforms.StringField("Chroot name") 1304 ownername = wtforms.HiddenField("Owner name") 1305 projectname = wtforms.HiddenField("Project name")
1306
1307 1308 -class CoprLegalFlagForm(FlaskForm):
1309 comment = wtforms.TextAreaField("Comment")
1310
1311 1312 -class PermissionsApplierFormFactory(object):
1313 1314 @staticmethod
1315 - def create_form_cls(permission=None):
1316 class F(FlaskForm): 1317 pass
1318 1319 builder_default = False 1320 admin_default = False 1321 1322 if permission: 1323 if permission.copr_builder != helpers.PermissionEnum("nothing"): 1324 builder_default = True 1325 if permission.copr_admin != helpers.PermissionEnum("nothing"): 1326 admin_default = True 1327 1328 setattr(F, "copr_builder", 1329 wtforms.BooleanField( 1330 default=builder_default, 1331 false_values=FALSE_VALUES, 1332 filters=[ValueToPermissionNumberFilter()])) 1333 1334 setattr(F, "copr_admin", 1335 wtforms.BooleanField( 1336 default=admin_default, 1337 false_values=FALSE_VALUES, 1338 filters=[ValueToPermissionNumberFilter()])) 1339 1340 return F
1341
1342 1343 -class PermissionsFormFactory(object):
1344 1345 """Creates a dynamic form for given set of copr permissions""" 1346 @staticmethod
1347 - def create_form_cls(permissions):
1348 class F(FlaskForm): 1349 pass
1350 1351 for perm in permissions: 1352 builder_choices = helpers.PermissionEnum.choices_list() 1353 admin_choices = helpers.PermissionEnum.choices_list() 1354 1355 builder_default = perm.copr_builder 1356 admin_default = perm.copr_admin 1357 1358 setattr(F, "copr_builder_{0}".format(perm.user.id), 1359 wtforms.SelectField( 1360 choices=builder_choices, 1361 default=builder_default, 1362 coerce=int)) 1363 1364 setattr(F, "copr_admin_{0}".format(perm.user.id), 1365 wtforms.SelectField( 1366 choices=admin_choices, 1367 default=admin_default, 1368 coerce=int)) 1369 1370 return F
1371
1372 1373 -class CoprModifyForm(FlaskForm):
1374 description = wtforms.TextAreaField('Description', 1375 validators=[wtforms.validators.Optional()]) 1376 1377 instructions = wtforms.TextAreaField('Instructions', 1378 validators=[wtforms.validators.Optional()]) 1379 1380 chroots = wtforms.TextAreaField('Chroots', 1381 validators=[wtforms.validators.Optional(), ChrootsValidator()]) 1382 1383 repos = wtforms.TextAreaField('External Repositories', 1384 validators=[UrlRepoListValidator(), 1385 wtforms.validators.Optional()], 1386 filters=[StringListFilter()]) 1387 1388 disable_createrepo = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES) 1389 unlisted_on_hp = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES) 1390 auto_prune = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES) 1391 bootstrap = create_mock_bootstrap_field("project") 1392 use_bootstrap_container = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES) 1393 follow_fedora_branching = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES) 1394 follow_fedora_branching = wtforms.BooleanField(default=True, false_values=FALSE_VALUES) 1395 delete_after_days = wtforms.IntegerField( 1396 validators=[wtforms.validators.Optional(), 1397 wtforms.validators.NumberRange(min=-1, max=60)], 1398 filters=[(lambda x : -1 if x is None else x)]) 1399 1400 # Deprecated, use `enable_net` instead 1401 build_enable_net = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES) 1402 enable_net = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES) 1403 multilib = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES) 1404 module_hotfixes = wtforms.BooleanField(validators=[wtforms.validators.Optional()], false_values=FALSE_VALUES)
1405
1406 1407 -class CoprForkFormFactory(object):
1408 @staticmethod
1409 - def create_form_cls(copr, user, groups):
1410 class F(FlaskForm): 1411 source = wtforms.StringField( 1412 "Source", 1413 default=copr.full_name) 1414 1415 owner = wtforms.SelectField( 1416 "Fork owner", 1417 choices=[(user.name, user.name)] + [(g.at_name, g.at_name) for g in groups], 1418 default=user.name, 1419 validators=[wtforms.validators.DataRequired()]) 1420 1421 name = wtforms.StringField( 1422 "Fork name", 1423 default=copr.name, 1424 validators=[wtforms.validators.DataRequired(), NameCharactersValidator()]) 1425 1426 confirm = wtforms.BooleanField( 1427 "Confirm", 1428 false_values=FALSE_VALUES, 1429 default=False)
1430 return F
1431
1432 1433 -class ModifyChrootForm(ChrootForm):
1434 buildroot_pkgs = wtforms.StringField('Additional packages to be always present in minimal buildroot') 1435 repos = wtforms.TextAreaField('Additional repos to be used for builds in chroot', 1436 validators=[UrlRepoListValidator(), 1437 wtforms.validators.Optional()], 1438 filters=[StringListFilter()]) 1439 comps = None 1440 upload_comps = FileField("Upload comps.xml") 1441 delete_comps = wtforms.BooleanField("Delete comps.xml", false_values=FALSE_VALUES)
1442
1443 1444 -class SelectMultipleFieldNoValidation(wtforms.SelectMultipleField):
1445 """ 1446 Otherwise choices are required and in some cases we don't know them beforehand 1447 """
1448 - def pre_validate(self, form):
1449 pass
1450
1451 1452 -class PinnedCoprsForm(FlaskForm):
1453 copr_ids = SelectMultipleFieldNoValidation(wtforms.IntegerField("Pinned Copr ID")) 1454
1455 - def __init__(self, owner, *args, **kwargs):
1456 super().__init__(*args, **kwargs) 1457 self.owner = owner
1458
1459 - def validate(self):
1460 super().validate() 1461 1462 choices = [str(c.id) for c in ComplexLogic.get_coprs_pinnable_by_owner(self.owner)] 1463 if any([i and i not in choices for i in self.copr_ids.data]): 1464 self.copr_ids.errors.append("Unexpected value selected") 1465 return False 1466 1467 limit = app.config["PINNED_PROJECTS_LIMIT"] 1468 if len(self.copr_ids.data) > limit: 1469 self.copr_ids.errors.append("Too many pinned projects. Limit is {}!".format(limit)) 1470 return False 1471 1472 if len(list(filter(None, self.copr_ids.data))) != len(set(filter(None, self.copr_ids.data))): 1473 self.copr_ids.errors.append("You can pin a particular project only once") 1474 return False 1475 1476 return True
1477
1478 1479 -class VoteForCopr(FlaskForm):
1480 """ 1481 Form for upvoting and downvoting projects 1482 """ 1483 upvote = wtforms.SubmitField("Upvote") 1484 downvote = wtforms.SubmitField("Downvote") 1485 reset = wtforms.SubmitField("Reset vote")
1486
1487 1488 -class AdminPlaygroundForm(FlaskForm):
1489 playground = wtforms.BooleanField("Playground", false_values=FALSE_VALUES)
1490
1491 1492 -class AdminPlaygroundSearchForm(FlaskForm):
1493 project = wtforms.StringField("Project")
1494
1495 1496 -class GroupUniqueNameValidator(object):
1497
1498 - def __init__(self, message=None):
1499 if not message: 1500 message = "Group with the alias '{}' already exists." 1501 self.message = message
1502
1503 - def __call__(self, form, field):
1504 if UsersLogic.group_alias_exists(field.data): 1505 raise wtforms.ValidationError(self.message.format(field.data))
1506
1507 1508 -class ActivateFasGroupForm(FlaskForm):
1509 1510 name = wtforms.StringField( 1511 validators=[ 1512 wtforms.validators.Regexp( 1513 re.compile(r"^[\w.-]+$"), 1514 message="Name must contain only letters," 1515 "digits, underscores, dashes and dots."), 1516 GroupUniqueNameValidator() 1517 ] 1518 )
1519
1520 1521 -class CreateModuleForm(FlaskForm):
1522 builds = wtforms.FieldList(wtforms.StringField("Builds ID list")) 1523 packages = wtforms.FieldList(wtforms.StringField("Packages list")) 1524 filter = wtforms.FieldList(wtforms.StringField("Package Filter"), 1525 validators=[wtforms.validators.DataRequired("You must select some packages from this project")]) 1526 api = wtforms.FieldList(wtforms.StringField("Module API")) 1527 profile_names = wtforms.FieldList(wtforms.StringField("Install Profiles"), min_entries=2) 1528 profile_pkgs = wtforms.FieldList(wtforms.FieldList(wtforms.StringField("Install Profiles")), min_entries=2) 1529
1530 - def __init__(self, copr=None, *args, **kwargs):
1531 self.copr = copr 1532 super(CreateModuleForm, self).__init__(*args, **kwargs)
1533
1534 - def validate(self):
1535 if not FlaskForm.validate(self): 1536 return False 1537 1538 # Profile names should be unique 1539 names = [x for x in self.profile_names.data if x] 1540 if len(set(names)) < len(names): 1541 self.profile_names.errors.append("Profile names must be unique") 1542 return False 1543 1544 # WORKAROUND 1545 # profile_pkgs are somehow sorted so if I fill profile_name in the first box and 1546 # profile_pkgs in seconds box, it is sorted and validated correctly 1547 for i in range(0, len(self.profile_names.data)): 1548 # If profile name is not set, then there should not be any packages in this profile 1549 if not flask.request.form["profile_names-{}".format(i)]: 1550 if [j for j in range(0, len(self.profile_names)) if "profile_pkgs-{}-{}".format(i, j) in flask.request.form]: 1551 self.profile_names.errors.append("Missing profile name") 1552 return False 1553 return True
1554
1555 1556 -class ModuleRepo(FlaskForm):
1557 owner = wtforms.StringField("Owner Name", validators=[wtforms.validators.DataRequired()]) 1558 copr = wtforms.StringField("Copr Name", validators=[wtforms.validators.DataRequired()]) 1559 name = wtforms.StringField("Name", validators=[wtforms.validators.DataRequired()]) 1560 stream = wtforms.StringField("Stream", validators=[wtforms.validators.DataRequired()]) 1561 version = wtforms.IntegerField("Version", validators=[wtforms.validators.DataRequired()]) 1562 arch = wtforms.StringField("Arch", validators=[wtforms.validators.DataRequired()])
1563