class ReleaseAPIForm(Form): ready = ThreeStateField('ready') complete = ThreeStateField('complete') # If the client sends us a really long status we'd rather truncate # it than complain, because it's purely informational. # Use the Column length directly rather than duplicating its value. status = StringField('status', filters=[truncateFilter(Release.status.type.length)]) enUSPlatforms = JSONField('enUSPlatforms') shippedAt = DateTimeField('shippedAt', [validators.optional()]) description = TextAreaField('description', [validators.optional()]) isSecurityDriven = BooleanField('isSecurityDriven', [validators.optional()]) def validate(self, release, *args, **kwargs): valid = Form.validate(self, *args, **kwargs) # Completed releases shouldn't be altered in terms of readyness or # completeness. Status updates are OK though. if release.complete: if self.ready.data is False or self.complete.data is False: valid = False if 'ready' not in self.errors: self.errors['ready'] = [] self.errors['ready'].append( 'Cannot make a completed release not ready or incomplete.') if self.description: # We just want to update the description & isSecurityDriven valid = True else: # Check if there is an other product-version already shipped similar = getReleases(status="postrelease", productFilter=release.product, versionFilter=release.version) if similar and self.status.data != "Started": # In most of the cases, it is useless since bug 1121032 has been implemented but keeping it # in case we change/revert in the future and because we cannot always trust the client valid = False if 'postrelease' not in self.errors: self.errors['postrelease'] = [] self.errors['postrelease'].append( 'Version ' + release.version + ' already marked as shipped') # If the release isn't complete, we can accept changes to readyness or # completeness, but marking a release as not ready *and* complete at # the same time is invalid. else: if self.ready.data is False and self.complete.data is True: valid = False if 'ready' not in self.errors: self.errors['ready'] = [] self.errors['ready'].append( 'A release cannot be made ready and complete at the same time' ) return valid
class EditReleaseForm(Form): shippedAtDate = DateField('Shipped date', format='%Y/%m/%d', validators=[ validators.optional(), ]) shippedAtTime = StringField('Shipped time') isSecurityDriven = BooleanField('Is Security Driven ?') description = TextAreaField('Description') isShipped = BooleanField('Is Shipped ?') def validate_isShipped(form, field): if form.isShipped.data: dt = form.shippedAt if (not dt) or (dt > datetime.now()): raise ValidationError('Invalid Date for Shipped release') @property def shippedAt(self): dateAndTime = None if self.shippedAtDate.data: dt = self.shippedAtDate.data tm = datetime.strptime(self.shippedAtTime.data, '%H:%M:%S').time() dateAndTime = datetime.combine(dt, tm) return dateAndTime
class ReleaseForm(Form): version = StringField('Version:', validators=[ Regexp(ANY_VERSION_REGEX, message='Invalid version format.') ]) buildNumber = IntegerField( 'Build Number:', validators=[DataRequired('Build number is required.')]) branch = StringField('Branch:', validators=[DataRequired('Branch is required')]) mozillaRevision = StringField('Mozilla Revision:') mozillaRelbranch = StringField('Mozilla Relbranch:', filters=[noneFilter]) comment = TextAreaField('Extra information to release-drivers:') description = TextAreaField('Description:') isSecurityDriven = BooleanField('Is a security driven release?', default=False) mh_changeset = StringField('Mozharness Revision:') release_eta_date = DateField('Release ETA date:', format='%Y-%m-%d', validators=[validators.optional()]) release_eta_time = StringField('Release ETA time:') VALID_VERSION_PATTERN = re.compile( r"""^(\d+)\.( # Major version number (0)(a1|a2|b(\d+)|esr)? # 2-digit-versions (like 46.0, 46.0b1, 46.0esr) |( # Here begins the 3-digit-versions. ([1-9]\d*)\.(\d+)|(\d+)\.([1-9]\d*) # 46.0.0 is not correct )(esr)? # Neither is 46.2.0b1 )(build(\d+))?$""", re.VERBOSE) # See more examples of (in)valid versions in the tests def __init__(self, suggest=True, *args, **kwargs): Form.__init__(self, *args, **kwargs) if suggest: self.addSuggestions() def validate(self, *args, **kwargs): valid = Form.validate(self, *args, **kwargs) # If a relbranch has been passed revision is ignored. if self.mozillaRelbranch.data: self.mozillaRevision.data = self.mozillaRelbranch.data # However, if a relbranch hasn't been passed, revision is required. else: if not self.mozillaRevision.data: valid = False self.errors['mozillaRevision'] = [ 'Mozilla revision is required' ] if self.VALID_VERSION_PATTERN.match(self.version.data) is None: valid = False self.errors['version'] = ['Version must match either X.0 or X.Y.Z'] return valid def addSuggestions(self): table = getReleaseTable(self.product.data) recentReleases = table.getRecent() # Before we make any suggestions we need to do some preprocessing of # the data to get it into useful structures. Specifically, we need a # set containing all of the recent versions, and a dict that associates # them with the branch they were built on. recentVersions = set() recentBranches = defaultdict(list) for release in recentReleases: recentVersions.add(release.version) recentBranches[release.branch].append(MozVersion(release.version)) # Now that we have the data in the format we want it in we can start # making suggestions. suggestedVersions = set() buildNumbers = {} # This wrapper method is used to centralize the build number suggestion # logic in one place, because there's more than one spot below that # adds a version suggestion. def addVersionSuggestion(version): suggestedVersions.add(version) # We want the UI to be able to automatically set build number # to the next available one for whatever version is entered. # To make this work we need to tell it what the next available # one is for all existing versions. We don't need to add versions # that are on build1, because it uses that as the default. maxBuildNumber = table.getMaxBuildNumber(version) if maxBuildNumber: buildNumbers[version] = maxBuildNumber + 1 else: buildNumbers[version] = 1 # Every version we see will have its potential next versions # suggested, except if we already have that version. # Note that we don't look through the entire table for every # version (because that could be expensive) but any versions # which are suggested and have already happened should be in # 'recentVersions', so it's unlikely we'll be suggesting # something that has already happened. for version in recentVersions: for v in getPossibleNextVersions(version): if v not in recentVersions: addVersionSuggestion(v) # Additional, we need to suggest the most recent version for each # branch, because we may want a build2 (or higher) of it. for branchVersions in recentBranches.values(): addVersionSuggestion(str(max(branchVersions))) # Finally, attach the suggestions to their fields. self.branch.suggestions = json.dumps(list(recentBranches.keys())) self.version.suggestions = json.dumps(list(suggestedVersions)) self.buildNumber.suggestions = json.dumps(buildNumbers) def updateFromRow(self, row): self.version.data = row.version self.buildNumber.data = row.buildNumber self.branch.data = row.branch # Revision is a disabled field if relbranch is present, so we shouldn't # put any data in it. if not row.mozillaRelbranch: self.mozillaRevision.data = row.mozillaRevision self.l10nChangesets.data = row.l10nChangesets self.mozillaRelbranch.data = row.mozillaRelbranch self.mh_changeset.data = row.mh_changeset if row.release_eta: release_eta = parse_iso8601_to_date_time(row.release_eta) self.release_eta_date.data = release_eta.date() # Conversion needed because release_eta_time is a StringField self.release_eta_time.data = release_eta.strftime('%H:%M %Z') @property def release_eta(self): if self.release_eta_date.data and self.release_eta_time.data: dt = self.release_eta_date.data tm = datetime.strptime(self.release_eta_time.data, '%H:%M %Z').time() return datetime.combine(dt, tm) else: return None