class PersonTransferJob(StormBase): """Base class for team membership and person merge jobs.""" implements(IPersonTransferJob) __storm_table__ = 'PersonTransferJob' id = Int(primary=True) job_id = Int(name='job') job = Reference(job_id, Job.id) major_person_id = Int(name='major_person') major_person = Reference(major_person_id, Person.id) minor_person_id = Int(name='minor_person') minor_person = Reference(minor_person_id, Person.id) job_type = EnumCol(enum=PersonTransferJobType, notNull=True) _json_data = Unicode('json_data') @property def metadata(self): return simplejson.loads(self._json_data) def __init__(self, minor_person, major_person, job_type, metadata, requester=None): """Constructor. :param minor_person: The person or team being added to or removed from the major_person. :param major_person: The person or team that is receiving or losing the minor person. :param job_type: The specific membership action being performed. :param metadata: The type-specific variables, as a JSON-compatible dict. """ super(PersonTransferJob, self).__init__() self.job = Job(requester=requester) self.job_type = job_type self.major_person = major_person self.minor_person = minor_person json_data = simplejson.dumps(metadata) # XXX AaronBentley 2009-01-29 bug=322819: This should be a bytestring, # but the DB representation is unicode. self._json_data = json_data.decode('utf-8') def makeDerived(self): return PersonTransferJobDerived.makeSubclass(self)
class Cve(SQLBase, BugLinkTargetMixin): """A CVE database record.""" implements(ICve, IBugLinkTarget) _table = 'Cve' sequence = StringCol(notNull=True, alternateID=True) status = EnumCol(dbName='status', schema=CveStatus, notNull=True) description = StringCol(notNull=True) datecreated = UtcDateTimeCol(notNull=True, default=UTC_NOW) datemodified = UtcDateTimeCol(notNull=True, default=UTC_NOW) # joins bugs = SQLRelatedJoin('Bug', intermediateTable='BugCve', joinColumn='cve', otherColumn='bug', orderBy='id') bug_links = SQLMultipleJoin('BugCve', joinColumn='cve', orderBy='id') references = SQLMultipleJoin( 'CveReference', joinColumn='cve', orderBy='id') @property def url(self): """See ICve.""" return ('http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=%s' % self.sequence) @property def displayname(self): return 'CVE-%s' % self.sequence @property def title(self): return 'CVE-%s (%s)' % (self.sequence, self.status.title) # CveReference's def createReference(self, source, content, url=None): """See ICveReference.""" return CveReference(cve=self, source=source, content=content, url=url) def removeReference(self, ref): assert ref.cve == self CveReference.delete(ref.id) # Template methods for BugLinkTargetMixin buglinkClass = BugCve def createBugLink(self, bug): """See BugLinkTargetMixin.""" return BugCve(cve=self, bug=bug)
class BugWatchActivity(StormBase): """See `IBugWatchActivity`.""" implements(IBugWatchActivity) __storm_table__ = 'BugWatchActivity' id = Int(primary=True) bug_watch_id = Int(name='bug_watch') bug_watch = Reference(bug_watch_id, BugWatch.id) activity_date = UtcDateTimeCol(notNull=True) result = EnumCol(enum=BugWatchActivityStatus, notNull=False) message = Unicode() oops_id = Unicode()
class BranchSubscription(SQLBase): """A relationship between a person and a branch.""" implements(IBranchSubscription, IBranchNavigationMenu, IHasBranchTarget) _table = 'BranchSubscription' person = ForeignKey(dbName='person', foreignKey='Person', storm_validator=validate_person, notNull=True) branch = ForeignKey(dbName='branch', foreignKey='Branch', notNull=True) notification_level = EnumCol(enum=BranchSubscriptionNotificationLevel, notNull=True, default=DEFAULT) max_diff_lines = EnumCol(enum=BranchSubscriptionDiffSize, notNull=False, default=DEFAULT) review_level = EnumCol(enum=CodeReviewNotificationLevel, notNull=True, default=DEFAULT) subscribed_by = ForeignKey(dbName='subscribed_by', foreignKey='Person', storm_validator=validate_person, notNull=True) @property def target(self): """See `IHasBranchTarget`.""" return self.branch.target def canBeUnsubscribedByUser(self, user): """See `IBranchSubscription`.""" if user is None: return False permission_check = BranchSubscriptionEdit(self) return permission_check.checkAuthenticated(IPersonRoles(user))
class CodeImportJob(SQLBase): """See `ICodeImportJob`.""" implements(ICodeImportJob) date_created = UtcDateTimeCol(notNull=True, default=UTC_NOW) code_import = ForeignKey( dbName='code_import', foreignKey='CodeImport', notNull=True) machine = ForeignKey( dbName='machine', foreignKey='CodeImportMachine', notNull=False, default=None) date_due = UtcDateTimeCol(notNull=True) state = EnumCol( enum=CodeImportJobState, notNull=True, default=CodeImportJobState.PENDING) requesting_user = ForeignKey( dbName='requesting_user', foreignKey='Person', storm_validator=validate_public_person, notNull=False, default=None) ordering = IntCol(notNull=False, default=None) heartbeat = UtcDateTimeCol(notNull=False, default=None) logtail = StringCol(notNull=False, default=None) date_started = UtcDateTimeCol(notNull=False, default=None) def isOverdue(self): """See `ICodeImportJob`.""" # SQLObject offers no easy way to compare a timestamp to UTC_NOW, so # we must use trickery here. # First we flush any pending update to self to ensure that the # following database query will give the correct result even if # date_due was modified in this transaction. self.syncUpdate() # Then, we try to find a CodeImportJob object with the id of self, and # a date_due of now or past. If we find one, this means self is # overdue. import_job = CodeImportJob.selectOne( "id = %s AND date_due <= %s" % sqlvalues(self.id, UTC_NOW)) return import_job is not None
class GitActivity(StormBase): """See IGitActivity`.""" __storm_table__ = 'GitActivity' id = Int(primary=True) repository_id = Int(name='repository', allow_none=False) repository = Reference(repository_id, 'GitRepository.id') date_changed = DateTime( name='date_changed', tzinfo=pytz.UTC, allow_none=False) changer_id = Int( name='changer', allow_none=False, validator=validate_public_person) changer = Reference(changer_id, 'Person.id') changee_id = Int( name='changee', allow_none=True, validator=validate_person) changee = Reference(changee_id, 'Person.id') what_changed = EnumCol( dbName='what_changed', enum=GitActivityType, notNull=True) old_value = JSON(name='old_value', allow_none=True) new_value = JSON(name='new_value', allow_none=True) def __init__(self, repository, changer, what_changed, changee=None, old_value=None, new_value=None, date_changed=DEFAULT): super(GitActivity, self).__init__() self.repository = repository self.date_changed = date_changed self.changer = changer self.changee = changee self.what_changed = what_changed self.old_value = old_value self.new_value = new_value @property def changee_description(self): if self.changee is not None: return "~" + self.changee.name elif self.new_value is not None and "changee_type" in self.new_value: return self.new_value["changee_type"].lower() elif self.old_value is not None and "changee_type" in self.old_value: return self.old_value["changee_type"].lower() else: return None
class QuestionJob(StormBase): """A Job for queued question emails.""" implements(IQuestionJob) __storm_table__ = 'QuestionJob' id = Int(primary=True) job_id = Int(name='job') job = Reference(job_id, Job.id) job_type = EnumCol(enum=QuestionJobType, notNull=True) question_id = Int(name='question') question = Reference(question_id, Question.id) _json_data = Unicode('json_data') def __init__(self, question, job_type, metadata): """Constructor. :param question: The question related to this job. :param job_type: The specific job being performed for the question. :param metadata: The type-specific variables, as a JSON-compatible dict. """ super(QuestionJob, self).__init__() self.job = Job() self.job_type = job_type self.question = question json_data = simplejson.dumps(metadata) self._json_data = json_data.decode('utf-8') def __repr__(self): return ( "<{self.__class__.__name__} for question {self.question.id}; " "status={self.job.status}>").format(self=self) @property def metadata(self): """See `IQuestionJob`.""" return simplejson.loads(self._json_data) def makeDerived(self): if self.job_type != QuestionJobType.EMAIL: raise ValueError('Unsupported Job type') return QuestionEmailJob(self)
class ArchiveDependency(SQLBase): """See `IArchiveDependency`.""" implements(IArchiveDependency) _table = 'ArchiveDependency' _defaultOrder = 'id' date_created = UtcDateTimeCol(dbName='date_created', notNull=True, default=UTC_NOW) archive = ForeignKey(foreignKey='Archive', dbName='archive', notNull=True) dependency = ForeignKey(foreignKey='Archive', dbName='dependency', notNull=True) pocket = EnumCol(dbName='pocket', notNull=True, schema=PackagePublishingPocket) component = ForeignKey(foreignKey='Component', dbName='component') @property def component_name(self): """See `IArchiveDependency`""" if self.component: return self.component.name else: return None @property def title(self): """See `IArchiveDependency`.""" if self.dependency.is_ppa: return self.dependency.displayname pocket_title = "%s - %s" % (self.dependency.displayname, self.pocket.name) if self.component is None: return pocket_title component_part = ", ".join(component_dependencies[self.component.name]) return "%s (%s)" % (pocket_title, component_part)
class POExportRequest(SQLBase): _table = 'POExportRequest' person = ForeignKey(dbName='person', foreignKey='Person', storm_validator=validate_public_person, notNull=True) date_created = UtcDateTimeCol(dbName='date_created', default=DEFAULT) potemplate = ForeignKey(dbName='potemplate', foreignKey='POTemplate', notNull=True) pofile = ForeignKey(dbName='pofile', foreignKey='POFile') format = EnumCol(dbName='format', schema=TranslationFileFormat, default=TranslationFileFormat.PO, notNull=True)
class SprintSpecification(SQLBase): """A link between a sprint and a specification.""" _table = 'SprintSpecification' sprint = ForeignKey(dbName='sprint', foreignKey='Sprint', notNull=True) specification = ForeignKey(dbName='specification', foreignKey='Specification', notNull=True) status = EnumCol(schema=SprintSpecificationStatus, notNull=True, default=SprintSpecificationStatus.PROPOSED) whiteboard = StringCol(notNull=False, default=None) registrant = ForeignKey(dbName='registrant', foreignKey='Person', storm_validator=validate_public_person, notNull=True) date_created = UtcDateTimeCol(notNull=True, default=DEFAULT) decider = ForeignKey(dbName='decider', foreignKey='Person', storm_validator=validate_public_person, notNull=False, default=None) date_decided = UtcDateTimeCol(notNull=False, default=None) @property def is_confirmed(self): """See ISprintSpecification.""" return self.status == SprintSpecificationStatus.ACCEPTED @property def is_decided(self): """See ISprintSpecification.""" return self.status != SprintSpecificationStatus.PROPOSED def acceptBy(self, decider): """See ISprintSpecification.""" self.status = SprintSpecificationStatus.ACCEPTED self.decider = decider self.date_decided = UTC_NOW def declineBy(self, decider): """See ISprintSpecification.""" self.status = SprintSpecificationStatus.DECLINED self.decider = decider self.date_decided = UTC_NOW
class EmailAddress(SQLBase, HasOwnerMixin): implements(IEmailAddress) _table = 'EmailAddress' _defaultOrder = ['email'] email = StringCol(dbName='email', notNull=True, unique=True, alternateID=True) status = EnumCol(dbName='status', schema=EmailAddressStatus, notNull=True) person = ForeignKey(dbName='person', foreignKey='Person', notNull=False) def __repr__(self): return '<EmailAddress at 0x%x <%s> [%s]>' % (id(self), self.email, self.status) def destroySelf(self): """See `IEmailAddress`.""" # Import this here to avoid circular references. from lp.registry.interfaces.mailinglist import MailingListStatus from lp.registry.model.mailinglist import (MailingListSubscription) if self.status == EmailAddressStatus.PREFERRED: raise UndeletableEmailAddress( "This is a person's preferred email, so it can't be deleted.") mailing_list = self.person and self.person.mailing_list if (mailing_list is not None and mailing_list.status != MailingListStatus.PURGED and mailing_list.address == self.email): raise UndeletableEmailAddress( "This is the email address of a team's mailing list, so it " "can't be deleted.") # XXX 2009-05-04 jamesh bug=371567: This function should not # be responsible for removing subscriptions, since the SSO # server can't write to that table. for subscription in MailingListSubscription.selectBy( email_address=self): subscription.destroySelf() super(EmailAddress, self).destroySelf() @property def rdf_sha1(self): """See `IEmailAddress`.""" return hashlib.sha1('mailto:' + self.email).hexdigest().upper()
class Account(SQLBase): """An Account.""" date_created = UtcDateTimeCol(notNull=True, default=UTC_NOW) displayname = StringCol(dbName='displayname', notNull=True) creation_rationale = EnumCol(dbName='creation_rationale', schema=AccountCreationRationale, notNull=True) status = AccountStatusEnumCol(enum=AccountStatus, default=AccountStatus.NOACCOUNT, notNull=True) date_status_set = UtcDateTimeCol(notNull=True, default=UTC_NOW) status_history = StringCol(dbName='status_comment', default=None) openid_identifiers = ReferenceSet("Account.id", OpenIdIdentifier.account_id) def __repr__(self): displayname = self.displayname.encode('ASCII', 'backslashreplace') return "<%s '%s' (%s)>" % (self.__class__.__name__, displayname, self.status) def addStatusComment(self, user, comment): """See `IAccountModerateRestricted`.""" prefix = datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') if user is not None: prefix += ' %s' % user.name old_lines = (self.status_history.splitlines() if self.status_history else []) self.status_history = '\n'.join(old_lines + ['%s: %s' % (prefix, comment), '']) def setStatus(self, status, user, comment): """See `IAccountModerateRestricted`.""" comment = comment or '' self.addStatusComment( user, '%s -> %s: %s' % (self.status.title, status.title, comment)) # date_status_set is maintained by a DB trigger. self.status = status def reactivate(self, comment): """See `IAccountSpecialRestricted`.""" self.setStatus(AccountStatus.ACTIVE, None, comment)
class BranchJob(SQLBase): """Base class for jobs related to branches.""" implements(IBranchJob) _table = 'BranchJob' job = ForeignKey(foreignKey='Job', notNull=True) branch = ForeignKey(foreignKey='Branch') job_type = EnumCol(enum=BranchJobType, notNull=True) _json_data = StringCol(dbName='json_data') @property def metadata(self): return simplejson.loads(self._json_data) def __init__(self, branch, job_type, metadata, **job_args): """Constructor. Extra keyword parameters are used to construct the underlying Job object. :param branch: The database branch this job relates to. :param job_type: The BranchJobType of this job. :param metadata: The type-specific variables, as a JSON-compatible dict. """ json_data = simplejson.dumps(metadata) SQLBase.__init__(self, job=Job(**job_args), branch=branch, job_type=job_type, _json_data=json_data) def destroySelf(self): """See `IBranchJob`.""" SQLBase.destroySelf(self) self.job.destroySelf() def makeDerived(self): return BranchJobDerived.makeSubclass(self)
class QuestionReopening(SQLBase): """A table recording each time a question is re-opened.""" _table = 'QuestionReopening' question = ForeignKey(dbName='question', foreignKey='Question', notNull=True) datecreated = UtcDateTimeCol(notNull=True, default=DEFAULT) reopener = ForeignKey(dbName='reopener', foreignKey='Person', storm_validator=validate_public_person, notNull=True) answerer = ForeignKey(dbName='answerer', foreignKey='Person', storm_validator=validate_public_person, notNull=False, default=None) date_solved = UtcDateTimeCol(notNull=False, default=None) priorstate = EnumCol(schema=QuestionStatus, notNull=True)
class HWDriver(SQLBase): """See `IHWDriver`.""" _table = 'HWDriver' # XXX: Abel Deuring 2008-12-10 bug=306265: package_name should # be declared notNull=True. This fixes the ambiguity that # "package_name is None" as well as "package_name == ''" can # indicate "we don't know to which package this driver belongs", # moreover, it gives a more clear meaning to the parameter value #package_name='' in webservice API calls. package_name = StringCol(notNull=False) name = StringCol(notNull=True) license = EnumCol(enum=License, notNull=False) def getSubmissions(self, distribution=None, distroseries=None, architecture=None, owner=None): """See `IHWDriver.`""" return HWSubmissionSet().search( driver=self, distribution=distribution, distroseries=distroseries, architecture=architecture, owner=owner)
class SpecificationWorkItem(StormBase): implements(ISpecificationWorkItem) __storm_table__ = 'SpecificationWorkItem' __storm_order__ = 'id' id = Int(primary=True) title = Unicode(allow_none=False) specification_id = Int(name='specification') specification = Reference(specification_id, 'Specification.id') assignee_id = Int(name='assignee', validator=validate_public_person) assignee = Reference(assignee_id, 'Person.id') milestone_id = Int(name='milestone') milestone = Reference(milestone_id, 'Milestone.id') status = EnumCol(schema=SpecificationWorkItemStatus, notNull=True, default=SpecificationWorkItemStatus.TODO) date_created = UtcDateTimeCol(notNull=True, default=DEFAULT) sequence = Int(allow_none=False) deleted = Bool(allow_none=False, default=False) def __repr__(self): title = self.title.encode('ASCII', 'backslashreplace') assignee = getattr(self.assignee, 'name', None) return '<SpecificationWorkItem [%s] %s: %s of %s>' % ( assignee, title, self.status.name, self.specification) def __init__(self, title, status, specification, assignee, milestone, sequence): self.title = title self.status = status self.specification = specification self.assignee = assignee self.milestone = milestone self.sequence = sequence @property def is_complete(self): """See `ISpecificationWorkItem`.""" return self.status == SpecificationWorkItemStatus.DONE
class LatestPersonSourcePackageReleaseCache(Storm): """See `LatestPersonSourcePackageReleaseCache`.""" __storm_table__ = 'LatestPersonSourcePackageReleaseCache' cache_id = Int(name='id', primary=True) publication_id = Int(name='publication') publication = Reference(publication_id, 'SourcePackagePublishingHistory.id') dateuploaded = DateTime(name='date_uploaded') creator_id = Int(name='creator') maintainer_id = Int(name='maintainer') upload_archive_id = Int(name='upload_archive') upload_archive = Reference(upload_archive_id, 'Archive.id') archive_purpose = EnumCol(schema=ArchivePurpose) upload_distroseries_id = Int(name='upload_distroseries') upload_distroseries = Reference(upload_distroseries_id, 'DistroSeries.id') sourcepackagename_id = Int(name='sourcepackagename') sourcepackagename = Reference(sourcepackagename_id, 'SourcePackageName.id') sourcepackagerelease_id = Int(name='sourcepackagerelease') sourcepackagerelease = Reference(sourcepackagerelease_id, 'SourcePackageRelease.id')
class LanguagePack(SQLBase): _table = 'LanguagePack' file = ForeignKey(foreignKey='LibraryFileAlias', dbName='file', notNull=True) date_exported = UtcDateTimeCol(notNull=True, default=UTC_NOW) distroseries = ForeignKey(foreignKey='DistroSeries', dbName='distroseries', notNull=True) type = EnumCol(enum=LanguagePackType, notNull=True, default=LanguagePackType.FULL) updates = ForeignKey(foreignKey='LanguagePack', dbName='updates', notNull=False, default=None)
class CodeImportEvent(SQLBase): """See `ICodeImportEvent`.""" _table = 'CodeImportEvent' date_created = UtcDateTimeCol(notNull=True, default=DEFAULT) event_type = EnumCol(dbName='entry_type', enum=CodeImportEventType, notNull=True) code_import = ForeignKey(dbName='code_import', foreignKey='CodeImport', default=None) person = ForeignKey(dbName='person', foreignKey='Person', storm_validator=validate_public_person, default=None) machine = ForeignKey(dbName='machine', foreignKey='CodeImportMachine', default=None) def items(self): """See `ICodeImportEvent`.""" return [(data.data_type, data.data_value) for data in _CodeImportEventData.selectBy(event=self)]
class HWVendorID(SQLBase): """See `IHWVendorID`.""" _table = 'HWVendorID' bus = EnumCol(enum=HWBus, notNull=True) vendor_id_for_bus = StringCol(notNull=True) vendor_name = ForeignKey(dbName='vendor_name', foreignKey='HWVendorName', notNull=True) def _create(self, id, **kw): bus = kw.get('bus') if bus is None: raise TypeError('HWVendorID() did not get expected keyword ' 'argument bus') vendor_id_for_bus = kw.get('vendor_id_for_bus') if vendor_id_for_bus is None: raise TypeError('HWVendorID() did not get expected keyword ' 'argument vendor_id_for_bus') if not isValidVendorID(bus, vendor_id_for_bus): raise ParameterError( '%s is not a valid vendor ID for %s' % (repr(vendor_id_for_bus), bus.title)) SQLBase._create(self, id, **kw)
class ProductJob(StormBase): """Base class for product jobs.""" implements(IProductJob) __storm_table__ = 'ProductJob' id = Int(primary=True) job_id = Int(name='job') job = Reference(job_id, Job.id) product_id = Int(name='product') product = Reference(product_id, Product.id) job_type = EnumCol(enum=ProductJobType, notNull=True) _json_data = Unicode('json_data') @property def metadata(self): return simplejson.loads(self._json_data) def __init__(self, product, job_type, metadata): """Constructor. :param product: The product the job is for. :param job_type: The type job the product needs run. :param metadata: A dict of JSON-compatible data to pass to the job. """ super(ProductJob, self).__init__() self.job = Job() self.product = product self.job_type = job_type json_data = simplejson.dumps(metadata) self._json_data = json_data.decode('utf-8')
class GitJob(StormBase): """See `IGitJob`.""" __storm_table__ = 'GitJob' job_id = Int(name='job', primary=True, allow_none=False) job = Reference(job_id, 'Job.id') repository_id = Int(name='repository', allow_none=True) repository = Reference(repository_id, 'GitRepository.id') job_type = EnumCol(enum=GitJobType, notNull=True) metadata = JSON('json_data') def __init__(self, repository, job_type, metadata, **job_args): """Constructor. Extra keyword arguments are used to construct the underlying Job object. :param repository: The database repository this job relates to. :param job_type: The `GitJobType` of this job. :param metadata: The type-specific variables, as a JSON-compatible dict. """ super(GitJob, self).__init__() self.job = Job(**job_args) self.repository = repository self.job_type = job_type self.metadata = metadata if repository is not None: self.metadata["repository_name"] = repository.unique_name def makeDerived(self): return GitJobDerived.makeSubclass(self)
class CodeImportResult(SQLBase): """See `ICodeImportResult`.""" date_created = UtcDateTimeCol(notNull=True, default=UTC_NOW) code_import = ForeignKey(dbName='code_import', foreignKey='CodeImport', notNull=True) machine = ForeignKey(dbName='machine', foreignKey='CodeImportMachine', notNull=True) requesting_user = ForeignKey(dbName='requesting_user', foreignKey='Person', storm_validator=validate_public_person, default=None) log_excerpt = StringCol(default=None) log_file = ForeignKey(dbName='log_file', foreignKey='LibraryFileAlias', default=None) status = EnumCol(enum=CodeImportResultStatus, notNull=True) date_job_started = UtcDateTimeCol(notNull=True) @property def date_job_finished(self): """See `ICodeImportResult`.""" return self.date_created @property def job_duration(self): return self.date_job_finished - self.date_job_started
class BugNomination(SQLBase): implements(IBugNomination) _table = "BugNomination" owner = ForeignKey(dbName='owner', foreignKey='Person', storm_validator=validate_public_person, notNull=True) decider = ForeignKey(dbName='decider', foreignKey='Person', storm_validator=validate_public_person, notNull=False, default=None) date_created = UtcDateTimeCol(notNull=True, default=UTC_NOW) date_decided = UtcDateTimeCol(notNull=False, default=None) distroseries = ForeignKey(dbName='distroseries', foreignKey='DistroSeries', notNull=False, default=None) productseries = ForeignKey(dbName='productseries', foreignKey='ProductSeries', notNull=False, default=None) bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True) status = EnumCol(dbName='status', notNull=True, schema=BugNominationStatus, default=BugNominationStatus.PROPOSED) @property def target(self): """See IBugNomination.""" return self.distroseries or self.productseries def approve(self, approver): """See IBugNomination.""" if self.isApproved(): # Approving an approved nomination is a no-op. return self.status = BugNominationStatus.APPROVED self.decider = approver self.date_decided = datetime.now(pytz.timezone('UTC')) targets = [] if self.distroseries: # Figure out which packages are affected in this distro for # this bug. distribution = self.distroseries.distribution distroseries = self.distroseries for task in self.bug.bugtasks: if not task.distribution == distribution: continue if task.sourcepackagename is not None: targets.append( distroseries.getSourcePackage(task.sourcepackagename)) else: targets.append(distroseries) else: targets.append(self.productseries) bugtasks = getUtility(IBugTaskSet).createManyTasks( self.bug, approver, targets) for bug_task in bugtasks: self.bug.addChange(BugTaskAdded(UTC_NOW, approver, bug_task)) def decline(self, decliner): """See IBugNomination.""" if self.isApproved(): raise BugNominationStatusError( "Cannot decline an approved nomination.") self.status = BugNominationStatus.DECLINED self.decider = decliner self.date_decided = datetime.now(pytz.timezone('UTC')) def isProposed(self): """See IBugNomination.""" return self.status == BugNominationStatus.PROPOSED def isDeclined(self): """See IBugNomination.""" return self.status == BugNominationStatus.DECLINED def isApproved(self): """See IBugNomination.""" return self.status == BugNominationStatus.APPROVED def canApprove(self, person): """See IBugNomination.""" # Use the class method to check permissions because there is not # yet a bugtask instance with the this target. BugTask = self.bug.bugtasks[0].__class__ if BugTask.userHasDriverPrivilegesContext(self.target, person): return True if self.distroseries is not None: distribution = self.distroseries.distribution # An uploader to any of the packages can approve the # nomination. Compile a list of possibilities, and check # them all. package_names = [] for bugtask in self.bug.bugtasks: if (bugtask.distribution == distribution and bugtask.sourcepackagename is not None): package_names.append(bugtask.sourcepackagename) if len(package_names) == 0: # If the bug isn't targeted to a source package, allow # any component uploader to approve the nomination, like # a new package. return distribution.main_archive.verifyUpload( person, None, None, None, strict_component=False) is None for name in package_names: component = self.distroseries.getSourcePackage( name).latest_published_component if distribution.main_archive.verifyUpload( person, name, component, self.distroseries) is None: return True return False
class BuildQueue(SQLBase): implements(IBuildQueue) _table = "BuildQueue" _defaultOrder = "id" def __init__(self, job, job_type=DEFAULT, estimated_duration=DEFAULT, virtualized=DEFAULT, processor=DEFAULT, lastscore=None): super(BuildQueue, self).__init__(job_type=job_type, job=job, virtualized=virtualized, processor=processor, estimated_duration=estimated_duration, lastscore=lastscore) if lastscore is None and self.specific_job is not None: self.score() job = ForeignKey(dbName='job', foreignKey='Job', notNull=True) job_type = EnumCol(enum=BuildFarmJobType, notNull=True, default=BuildFarmJobType.PACKAGEBUILD, dbName='job_type') builder = ForeignKey(dbName='builder', foreignKey='Builder', default=None) logtail = StringCol(dbName='logtail', default=None) lastscore = IntCol(dbName='lastscore', default=0) manual = BoolCol(dbName='manual', default=False) estimated_duration = IntervalCol() processor = ForeignKey(dbName='processor', foreignKey='Processor') virtualized = BoolCol(dbName='virtualized') @cachedproperty def specific_job(self): """See `IBuildQueue`.""" specific_class = specific_job_classes()[self.job_type] return specific_class.getByJob(self.job) def _clear_specific_job_cache(self): del get_property_cache(self).specific_job @staticmethod def preloadSpecificJobData(queues): key = attrgetter('job_type') for job_type, grouped_queues in groupby(queues, key=key): specific_class = specific_job_classes()[job_type] queue_subset = list(grouped_queues) job_subset = load_related(Job, queue_subset, ['jobID']) # We need to preload the build farm jobs early to avoid # the call to _set_build_farm_job to look up BuildFarmBuildJobs # one by one. specific_class.preloadBuildFarmJobs(job_subset) specific_jobs = list(specific_class.getByJobs(job_subset)) if len(specific_jobs) == 0: continue specific_class.preloadJobsData(specific_jobs) specific_jobs_dict = dict((specific_job.job, specific_job) for specific_job in specific_jobs) for queue in queue_subset: cache = get_property_cache(queue) cache.specific_job = specific_jobs_dict[queue.job] @property def date_started(self): """See `IBuildQueue`.""" return self.job.date_started @property def current_build_duration(self): """See `IBuildQueue`.""" date_started = self.date_started if date_started is None: return None else: return self._now() - date_started def destroySelf(self): """Remove this record and associated job/specific_job.""" job = self.job specific_job = self.specific_job builder = self.builder SQLBase.destroySelf(self) specific_job.cleanUp() job.destroySelf() if builder is not None: del get_property_cache(builder).currentjob self._clear_specific_job_cache() def manualScore(self, value): """See `IBuildQueue`.""" self.lastscore = value self.manual = True def score(self): """See `IBuildQueue`.""" if self.manual: return # Allow the `IBuildFarmJob` instance with the data/logic specific to # the job at hand to calculate the score as appropriate. self.lastscore = self.specific_job.score() def markAsBuilding(self, builder): """See `IBuildQueue`.""" self.builder = builder if self.job.status != JobStatus.RUNNING: self.job.start() self.specific_job.jobStarted() if builder is not None: del get_property_cache(builder).currentjob def reset(self): """See `IBuildQueue`.""" builder = self.builder self.builder = None if self.job.status != JobStatus.WAITING: self.job.queue() self.job.date_started = None self.job.date_finished = None self.logtail = None self.specific_job.jobReset() if builder is not None: del get_property_cache(builder).currentjob def cancel(self): """See `IBuildQueue`.""" self.specific_job.jobCancel() self.destroySelf() def _getFreeBuildersCount(self, processor, virtualized): """How many builders capable of running jobs for the given processor and virtualization combination are idle/free at present?""" query = """ SELECT COUNT(id) FROM builder WHERE builderok = TRUE AND manual = FALSE AND id NOT IN ( SELECT builder FROM BuildQueue WHERE builder IS NOT NULL) AND virtualized = %s """ % sqlvalues(normalize_virtualization(virtualized)) if processor is not None: query += """ AND processor = %s """ % sqlvalues(processor) result_set = IStore(BuildQueue).execute(query) free_builders = result_set.get_one()[0] return free_builders def _estimateTimeToNextBuilder(self): """Estimate time until next builder becomes available. For the purpose of estimating the dispatch time of the job of interest (JOI) we need to know how long it will take until the job at the head of JOI's queue is dispatched. There are two cases to consider here: the head job is - processor dependent: only builders with the matching processor/virtualization combination should be considered. - *not* processor dependent: all builders with the matching virtualization setting should be considered. :return: The estimated number of seconds untils a builder capable of running the head job becomes available. """ head_job_platform = self._getHeadJobPlatform() # Return a zero delay if we still have free builders available for the # given platform/virtualization combination. free_builders = self._getFreeBuildersCount(*head_job_platform) if free_builders > 0: return 0 head_job_processor, head_job_virtualized = head_job_platform now = self._now() delay_query = """ SELECT MIN( CASE WHEN EXTRACT(EPOCH FROM (BuildQueue.estimated_duration - (((%s AT TIME ZONE 'UTC') - Job.date_started)))) >= 0 THEN EXTRACT(EPOCH FROM (BuildQueue.estimated_duration - (((%s AT TIME ZONE 'UTC') - Job.date_started)))) ELSE -- Assume that jobs that have overdrawn their estimated -- duration time budget will complete within 2 minutes. -- This is a wild guess but has worked well so far. -- -- Please note that this is entirely innocuous i.e. if our -- guess is off nothing bad will happen but our estimate will -- not be as good as it could be. 120 END) FROM BuildQueue, Job, Builder WHERE BuildQueue.job = Job.id AND BuildQueue.builder = Builder.id AND Builder.manual = False AND Builder.builderok = True AND Job.status = %s AND Builder.virtualized = %s """ % sqlvalues(now, now, JobStatus.RUNNING, normalize_virtualization(head_job_virtualized)) if head_job_processor is not None: # Only look at builders with specific processor types. delay_query += """ AND Builder.processor = %s """ % sqlvalues(head_job_processor) result_set = IStore(BuildQueue).execute(delay_query) head_job_delay = result_set.get_one()[0] return (0 if head_job_delay is None else int(head_job_delay)) def _getPendingJobsClauses(self): """WHERE clauses for pending job queries, used for dipatch time estimation.""" virtualized = normalize_virtualization(self.virtualized) clauses = """ BuildQueue.job = Job.id AND Job.status = %s AND ( -- The score must be either above my score or the -- job must be older than me in cases where the -- score is equal. BuildQueue.lastscore > %s OR (BuildQueue.lastscore = %s AND Job.id < %s)) -- The virtualized values either match or the job -- does not care about virtualization and the job -- of interest (JOI) is to be run on a virtual builder -- (we want to prevent the execution of untrusted code -- on native builders). AND COALESCE(buildqueue.virtualized, TRUE) = %s """ % sqlvalues(JobStatus.WAITING, self.lastscore, self.lastscore, self.job, virtualized) processor_clause = """ AND ( -- The processor values either match or the candidate -- job is processor-independent. buildqueue.processor = %s OR buildqueue.processor IS NULL) """ % sqlvalues(self.processor) # We don't care about processors if the estimation is for a # processor-independent job. if self.processor is not None: clauses += processor_clause return clauses def _getHeadJobPlatform(self): """Find the processor and virtualization setting for the head job. Among the jobs that compete with the job of interest (JOI) for builders and are queued ahead of it the head job is the one in pole position i.e. the one to be dispatched to a builder next. :return: A (processor, virtualized) tuple which is the head job's platform or None if the JOI is the head job. """ my_platform = (getattr(self.processor, 'id', None), normalize_virtualization(self.virtualized)) query = """ SELECT processor, virtualized FROM BuildQueue, Job WHERE """ query += self._getPendingJobsClauses() query += """ ORDER BY lastscore DESC, job LIMIT 1 """ result = IStore(BuildQueue).execute(query).get_one() return (my_platform if result is None else result) def _estimateJobDelay(self, builder_stats): """Sum of estimated durations for *pending* jobs ahead in queue. For the purpose of estimating the dispatch time of the job of interest (JOI) we need to know the delay caused by all the pending jobs that are ahead of the JOI in the queue and that compete with it for builders. :param builder_stats: A dictionary with builder counts where the key is a (processor, virtualized) combination (aka "platform") and the value is the number of builders that can take on jobs requiring that combination. :return: An integer value holding the sum of delays (in seconds) caused by the jobs that are ahead of and competing with the JOI. """ def jobs_compete_for_builders(a, b): """True if the two jobs compete for builders.""" a_processor, a_virtualized = a b_processor, b_virtualized = b if a_processor is None or b_processor is None: # If either of the jobs is platform-independent then the two # jobs compete for the same builders if the virtualization # settings match. if a_virtualized == b_virtualized: return True else: # Neither job is platform-independent, match processor and # virtualization settings. return a == b my_platform = (getattr(self.processor, 'id', None), normalize_virtualization(self.virtualized)) query = """ SELECT BuildQueue.processor, BuildQueue.virtualized, COUNT(BuildQueue.job), CAST(EXTRACT( EPOCH FROM SUM(BuildQueue.estimated_duration)) AS INTEGER) FROM BuildQueue, Job WHERE """ query += self._getPendingJobsClauses() query += """ GROUP BY BuildQueue.processor, BuildQueue.virtualized """ delays_by_platform = IStore(BuildQueue).execute(query).get_all() # This will be used to capture per-platform delay totals. delays = defaultdict(int) # This will be used to capture per-platform job counts. job_counts = defaultdict(int) # Divide the estimated duration of the jobs as follows: # - if a job is tied to a processor TP then divide the estimated # duration of that job by the number of builders that target TP # since only these can build the job. # - if the job is processor-independent then divide its estimated # duration by the total number of builders with the same # virtualization setting because any one of them may run it. for processor, virtualized, job_count, delay in delays_by_platform: virtualized = normalize_virtualization(virtualized) platform = (processor, virtualized) builder_count = builder_stats.get(platform, 0) if builder_count == 0: # There is no builder that can run this job, ignore it # for the purpose of dispatch time estimation. continue if jobs_compete_for_builders(my_platform, platform): # The jobs that target the platform at hand compete with # the JOI for builders, add their delays. delays[platform] += delay job_counts[platform] += job_count sum_of_delays = 0 # Now devide the delays based on a jobs/builders comparison. for platform, duration in delays.iteritems(): jobs = job_counts[platform] builders = builder_stats[platform] # If there are less jobs than builders that can take them on, # the delays should be averaged/divided by the number of jobs. denominator = (jobs if jobs < builders else builders) if denominator > 1: duration = int(duration / float(denominator)) sum_of_delays += duration return sum_of_delays def getEstimatedJobStartTime(self): """See `IBuildQueue`. The estimated dispatch time for the build farm job at hand is calculated from the following ingredients: * the start time for the head job (job at the head of the respective build queue) * the estimated build durations of all jobs that precede the job of interest (JOI) in the build queue (divided by the number of machines in the respective build pool) """ # This method may only be invoked for pending jobs. if self.job.status != JobStatus.WAITING: raise AssertionError( "The start time is only estimated for pending jobs.") builder_stats = get_builder_data() platform = (getattr(self.processor, 'id', None), self.virtualized) if builder_stats[platform] == 0: # No builders that can run the job at hand # -> no dispatch time estimation available. return None # Get the sum of the estimated run times for *pending* jobs that are # ahead of us in the queue. sum_of_delays = self._estimateJobDelay(builder_stats) # Get the minimum time duration until the next builder becomes # available. min_wait_time = self._estimateTimeToNextBuilder() # A job will not get dispatched in less than 5 seconds no matter what. start_time = max(5, min_wait_time + sum_of_delays) result = self._now() + timedelta(seconds=start_time) return result @staticmethod def _now(): """Return current time (UTC). Overridable for test purposes.""" return datetime.now(pytz.UTC)
class TeamMembership(SQLBase): """See `ITeamMembership`.""" implements(ITeamMembership) _table = 'TeamMembership' _defaultOrder = 'id' team = ForeignKey(dbName='team', foreignKey='Person', notNull=True) person = ForeignKey( dbName='person', foreignKey='Person', storm_validator=validate_person, notNull=True) last_changed_by = ForeignKey( dbName='last_changed_by', foreignKey='Person', storm_validator=validate_public_person, default=None) proposed_by = ForeignKey( dbName='proposed_by', foreignKey='Person', storm_validator=validate_public_person, default=None) acknowledged_by = ForeignKey( dbName='acknowledged_by', foreignKey='Person', storm_validator=validate_public_person, default=None) reviewed_by = ForeignKey( dbName='reviewed_by', foreignKey='Person', storm_validator=validate_public_person, default=None) status = EnumCol( dbName='status', notNull=True, enum=TeamMembershipStatus) # XXX: salgado, 2008-03-06: Need to rename datejoined and dateexpires to # match their db names. datejoined = UtcDateTimeCol(dbName='date_joined', default=None) dateexpires = UtcDateTimeCol(dbName='date_expires', default=None) date_created = UtcDateTimeCol(default=UTC_NOW) date_proposed = UtcDateTimeCol(default=None) date_acknowledged = UtcDateTimeCol(default=None) date_reviewed = UtcDateTimeCol(default=None) date_last_changed = UtcDateTimeCol(default=None) last_change_comment = StringCol(default=None) proponent_comment = StringCol(default=None) acknowledger_comment = StringCol(default=None) reviewer_comment = StringCol(default=None) def isExpired(self): """See `ITeamMembership`.""" return self.status == TeamMembershipStatus.EXPIRED def canBeRenewedByMember(self): """See `ITeamMembership`.""" ondemand = TeamMembershipRenewalPolicy.ONDEMAND admin = TeamMembershipStatus.APPROVED approved = TeamMembershipStatus.ADMIN date_limit = datetime.now(pytz.UTC) + timedelta( days=DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT) return (self.status in (admin, approved) and self.team.renewal_policy == ondemand and self.dateexpires is not None and self.dateexpires < date_limit) def sendSelfRenewalNotification(self): """See `ITeamMembership`.""" team = self.team member = self.person assert team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND from_addr = format_address( team.displayname, config.canonical.noreply_from_address) replacements = {'member_name': member.unique_displayname, 'team_name': team.unique_displayname, 'team_url': canonical_url(team), 'dateexpires': self.dateexpires.strftime('%Y-%m-%d')} subject = '%s extended their membership' % member.name template = get_email_template( 'membership-member-renewed.txt', app='registry') admins_addrs = self.team.getTeamAdminsEmailAddresses() for address in admins_addrs: recipient = getUtility(IPersonSet).getByEmail(address) replacements['recipient_name'] = recipient.displayname msg = MailWrapper().format( template % replacements, force_wrap=True) simple_sendmail(from_addr, address, subject, msg) def canChangeStatusSilently(self, user): """Ensure that the user is in the Launchpad Administrators group. Then the user can silently make changes to their membership status. """ return user.inTeam(getUtility(ILaunchpadCelebrities).admin) def canChangeExpirationDate(self, person): """See `ITeamMembership`.""" person_is_team_admin = self.team in person.getAdministratedTeams() person_is_lp_admin = IPersonRoles(person).in_admin return person_is_team_admin or person_is_lp_admin def setExpirationDate(self, date, user): """See `ITeamMembership`.""" if date == self.dateexpires: return assert self.canChangeExpirationDate(user), ( "This user can't change this membership's expiration date.") self._setExpirationDate(date, user) def _setExpirationDate(self, date, user): UTC = pytz.timezone('UTC') assert date is None or date.date() >= datetime.now(UTC).date(), ( "The given expiration date must be None or be in the future: %s" % date.strftime('%Y-%m-%d')) self.dateexpires = date self.last_changed_by = user def sendExpirationWarningEmail(self): """See `ITeamMembership`.""" if self.dateexpires is None: raise AssertionError( '%s in team %s has no membership expiration date.' % (self.person.name, self.team.name)) if self.dateexpires < datetime.now(pytz.timezone('UTC')): # The membership has reached expiration. Silently return because # there is nothing to do. The member will have received emails # from previous calls by flag-expired-memberships.py return member = self.person team = self.team if member.is_team: recipient = member.teamowner templatename = 'membership-expiration-warning-bulk.txt' subject = '%s will expire soon from %s' % (member.name, team.name) else: recipient = member templatename = 'membership-expiration-warning-personal.txt' subject = 'Your membership in %s is about to expire' % team.name if team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND: how_to_renew = ( "If you want, you can renew this membership at\n" "<%s/+expiringmembership/%s>" % (canonical_url(member), team.name)) elif not self.canChangeExpirationDate(recipient): admins_names = [] admins = team.getDirectAdministrators() assert admins.count() >= 1 if admins.count() == 1: admin = admins[0] how_to_renew = ( "To prevent this membership from expiring, you should " "contact the\nteam's administrator, %s.\n<%s>" % (admin.unique_displayname, canonical_url(admin))) else: for admin in admins: admins_names.append( "%s <%s>" % (admin.unique_displayname, canonical_url(admin))) how_to_renew = ( "To prevent this membership from expiring, you should " "get in touch\nwith one of the team's administrators:\n") how_to_renew += "\n".join(admins_names) else: how_to_renew = ( "To stay a member of this team you should extend your " "membership at\n<%s/+member/%s>" % (canonical_url(team), member.name)) to_addrs = get_contact_email_addresses(recipient) if len(to_addrs) == 0: # The user does not have a preferred email address, he was # probably suspended. return formatter = DurationFormatterAPI( self.dateexpires - datetime.now(pytz.timezone('UTC'))) replacements = { 'recipient_name': recipient.displayname, 'member_name': member.unique_displayname, 'team_url': canonical_url(team), 'how_to_renew': how_to_renew, 'team_name': team.unique_displayname, 'expiration_date': self.dateexpires.strftime('%Y-%m-%d'), 'approximate_duration': formatter.approximateduration()} msg = get_email_template(templatename, app='registry') % replacements from_addr = format_address( team.displayname, config.canonical.noreply_from_address) simple_sendmail(from_addr, to_addrs, subject, msg) def setStatus(self, status, user, comment=None, silent=False): """See `ITeamMembership`.""" if status == self.status: return False if silent and not self.canChangeStatusSilently(user): raise UserCannotChangeMembershipSilently( "Only Launchpad administrators may change membership " "statuses silently.") approved = TeamMembershipStatus.APPROVED admin = TeamMembershipStatus.ADMIN expired = TeamMembershipStatus.EXPIRED declined = TeamMembershipStatus.DECLINED deactivated = TeamMembershipStatus.DEACTIVATED proposed = TeamMembershipStatus.PROPOSED invited = TeamMembershipStatus.INVITED invitation_declined = TeamMembershipStatus.INVITATION_DECLINED self.person.clearInTeamCache() # Make sure the transition from the current status to the given one # is allowed. All allowed transitions are in the TeamMembership spec. state_transition = { admin: [approved, expired, deactivated], approved: [admin, expired, deactivated], deactivated: [proposed, approved, admin, invited], expired: [proposed, approved, admin, invited], proposed: [approved, admin, declined], declined: [proposed, approved, admin, invited], invited: [approved, admin, invitation_declined], invitation_declined: [invited, approved, admin]} if self.status not in state_transition: raise TeamMembershipTransitionError( "Unknown status: %s" % self.status.name) if status not in state_transition[self.status]: raise TeamMembershipTransitionError( "Bad state transition from %s to %s" % (self.status.name, status.name)) if status in ACTIVE_STATES and self.team in self.person.allmembers: raise CyclicalTeamMembershipError( "Cannot make %(person)s a member of %(team)s because " "%(team)s is a member of %(person)s." % dict(person=self.person.name, team=self.team.name)) old_status = self.status self.status = status now = datetime.now(pytz.timezone('UTC')) if status in [proposed, invited]: self.proposed_by = user self.proponent_comment = comment self.date_proposed = now elif ((status in ACTIVE_STATES and old_status not in ACTIVE_STATES) or status == declined): self.reviewed_by = user self.reviewer_comment = comment self.date_reviewed = now if self.datejoined is None and status in ACTIVE_STATES: # This is the first time this membership is made active. self.datejoined = now else: # No need to set proponent or reviewer. pass if old_status == invited: # This member has been invited by an admin and is now accepting or # declining the invitation. self.acknowledged_by = user self.date_acknowledged = now self.acknowledger_comment = comment self.last_changed_by = user self.last_change_comment = comment self.date_last_changed = now if status in ACTIVE_STATES: _fillTeamParticipation(self.person, self.team) elif old_status in ACTIVE_STATES: _cleanTeamParticipation(self.person, self.team) # A person has left the team so they may no longer have access # to some artifacts shared with the team. We need to run a job # to remove any subscriptions to such artifacts. getUtility(IRemoveArtifactSubscriptionsJobSource).create( user, grantee=self.person) else: # Changed from an inactive state to another inactive one, so no # need to fill/clean the TeamParticipation table. pass # Flush all updates to ensure any subsequent calls to this method on # the same transaction will operate on the correct data. That is the # case with our script to expire team memberships. flush_database_updates() # When a member proposes himself, a more detailed notification is # sent to the team admins by a subscriber of JoinTeamEvent; that's # why we don't send anything here. if ((self.person != self.last_changed_by or self.status != proposed) and not silent): self._sendStatusChangeNotification(old_status) return True def _sendStatusChangeNotification(self, old_status): """Send a status change notification to all team admins and the member whose membership's status changed. """ reviewer = self.last_changed_by new_status = self.status getUtility(IMembershipNotificationJobSource).create( self.person, self.team, reviewer, old_status, new_status, self.last_change_comment)
class BinaryPackageRelease(SQLBase): implements(IBinaryPackageRelease) _table = 'BinaryPackageRelease' binarypackagename = ForeignKey(dbName='binarypackagename', notNull=True, foreignKey='BinaryPackageName') version = StringCol(dbName='version', notNull=True) summary = StringCol(dbName='summary', notNull=True, default="") description = StringCol(dbName='description', notNull=True) build = ForeignKey(dbName='build', foreignKey='BinaryPackageBuild', notNull=True) binpackageformat = EnumCol(dbName='binpackageformat', notNull=True, schema=BinaryPackageFormat) component = ForeignKey(dbName='component', foreignKey='Component', notNull=True) section = ForeignKey(dbName='section', foreignKey='Section', notNull=True) priority = EnumCol(dbName='priority', notNull=True, schema=PackagePublishingPriority) shlibdeps = StringCol(dbName='shlibdeps') depends = StringCol(dbName='depends') recommends = StringCol(dbName='recommends') suggests = StringCol(dbName='suggests') conflicts = StringCol(dbName='conflicts') replaces = StringCol(dbName='replaces') provides = StringCol(dbName='provides') pre_depends = StringCol(dbName='pre_depends') enhances = StringCol(dbName='enhances') breaks = StringCol(dbName='breaks') essential = BoolCol(dbName='essential', default=False) installedsize = IntCol(dbName='installedsize') architecturespecific = BoolCol(dbName='architecturespecific', notNull=True) homepage = StringCol(dbName='homepage') datecreated = UtcDateTimeCol(notNull=True, default=UTC_NOW) debug_package = ForeignKey(dbName='debug_package', foreignKey='BinaryPackageRelease') _user_defined_fields = StringCol(dbName='user_defined_fields') def __init__(self, *args, **kwargs): if 'user_defined_fields' in kwargs: kwargs['_user_defined_fields'] = simplejson.dumps( kwargs['user_defined_fields']) del kwargs['user_defined_fields'] super(BinaryPackageRelease, self).__init__(*args, **kwargs) @property def user_defined_fields(self): """See `IBinaryPackageRelease`.""" if self._user_defined_fields is None: return [] return simplejson.loads(self._user_defined_fields) @property def title(self): """See `IBinaryPackageRelease`.""" return '%s-%s' % (self.binarypackagename.name, self.version) @property def name(self): """See `IBinaryPackageRelease`.""" return self.binarypackagename.name @property def distributionsourcepackagerelease(self): """See `IBinaryPackageRelease`.""" # import here to avoid circular import problems from lp.soyuz.model.distributionsourcepackagerelease \ import DistributionSourcePackageRelease return DistributionSourcePackageRelease( distribution=self.build.distribution, sourcepackagerelease=self.build.source_package_release) @property def sourcepackagename(self): """See `IBinaryPackageRelease`.""" return self.build.source_package_release.sourcepackagename.name @property def is_new(self): """See `IBinaryPackageRelease`.""" distroarchseries = self.build.distro_arch_series distroarchseries_binary_package = distroarchseries.getBinaryPackage( self.binarypackagename) return distroarchseries_binary_package.currentrelease is None @property def properties(self): """See `IBinaryPackageRelease`.""" return { "name": self.name, "version": self.version, "is_new": self.is_new, "architecture": self.build.arch_tag, "component": self.component.name, "section": self.section.name, "priority": self.priority.name, } @cachedproperty def files(self): return list( Store.of(self).find(BinaryPackageFile, binarypackagerelease=self)) def addFile(self, file): """See `IBinaryPackageRelease`.""" determined_filetype = None if file.filename.endswith(".deb"): determined_filetype = BinaryPackageFileType.DEB elif file.filename.endswith(".rpm"): determined_filetype = BinaryPackageFileType.RPM elif file.filename.endswith(".udeb"): determined_filetype = BinaryPackageFileType.UDEB elif file.filename.endswith(".ddeb"): determined_filetype = BinaryPackageFileType.DDEB else: raise AssertionError('Unsupported file type: %s' % file.filename) del get_property_cache(self).files return BinaryPackageFile(binarypackagerelease=self, filetype=determined_filetype, libraryfile=file) def override(self, component=None, section=None, priority=None): """See `IBinaryPackageRelease`.""" if component is not None: self.component = component if section is not None: self.section = section if priority is not None: self.priority = priority
class _SourcePackageRecipeDataInstruction(Storm): """A single line from a recipe.""" __storm_table__ = "SourcePackageRecipeDataInstruction" def __init__(self, name, type, comment, line_number, branch_or_repository, revspec, directory, recipe_data, parent_instruction, source_directory): super(_SourcePackageRecipeDataInstruction, self).__init__() self.name = unicode(name) self.type = type self.comment = comment self.line_number = line_number if IGitRepository.providedBy(branch_or_repository): self.git_repository = branch_or_repository elif IGitRef.providedBy(branch_or_repository): self.git_repository = branch_or_repository if revspec is None: revspec = branch_or_repository.name elif IBranch.providedBy(branch_or_repository): self.branch = branch_or_repository else: raise AssertionError("Unsupported source: %r" % (branch_or_repository, )) if revspec is not None: revspec = unicode(revspec) self.revspec = revspec if directory is not None: directory = unicode(directory) self.directory = directory self.source_directory = source_directory self.recipe_data = recipe_data self.parent_instruction = parent_instruction id = Int(primary=True) name = Unicode(allow_none=False) type = EnumCol(notNull=True, schema=InstructionType) comment = Unicode(allow_none=True) line_number = Int(allow_none=False) branch_id = Int(name='branch', allow_none=True) branch = Reference(branch_id, 'Branch.id') git_repository_id = Int(name='git_repository', allow_none=True) git_repository = Reference(git_repository_id, 'GitRepository.id') revspec = Unicode(allow_none=True) directory = Unicode(allow_none=True) source_directory = Unicode(allow_none=True) recipe_data_id = Int(name='recipe_data', allow_none=False) recipe_data = Reference(recipe_data_id, 'SourcePackageRecipeData.id') parent_instruction_id = Int(name='parent_instruction', allow_none=True) parent_instruction = Reference(parent_instruction_id, '_SourcePackageRecipeDataInstruction.id') def appendToRecipe(self, recipe_branch): """Append a bzr-builder instruction to the recipe_branch object.""" if self.branch is not None: identity = self.branch.identity else: assert self.git_repository is not None identity = self.git_repository.identity branch = RecipeBranch(self.name, identity, self.revspec) if self.type == InstructionType.MERGE: recipe_branch.merge_branch(branch) elif self.type == InstructionType.NEST: recipe_branch.nest_branch(self.directory, branch) elif self.type == InstructionType.NEST_PART: recipe_branch.nest_part_branch(branch, self.source_directory, self.directory) else: raise AssertionError("Unknown type %r" % self.type) return branch
class TeamMembership(SQLBase): """See `ITeamMembership`.""" _table = 'TeamMembership' _defaultOrder = 'id' team = ForeignKey(dbName='team', foreignKey='Person', notNull=True) person = ForeignKey(dbName='person', foreignKey='Person', storm_validator=validate_person, notNull=True) last_changed_by = ForeignKey(dbName='last_changed_by', foreignKey='Person', storm_validator=validate_public_person, default=None) proposed_by = ForeignKey(dbName='proposed_by', foreignKey='Person', storm_validator=validate_public_person, default=None) acknowledged_by = ForeignKey(dbName='acknowledged_by', foreignKey='Person', storm_validator=validate_public_person, default=None) reviewed_by = ForeignKey(dbName='reviewed_by', foreignKey='Person', storm_validator=validate_public_person, default=None) status = EnumCol(dbName='status', notNull=True, enum=TeamMembershipStatus) # XXX: salgado, 2008-03-06: Need to rename datejoined and dateexpires to # match their db names. datejoined = UtcDateTimeCol(dbName='date_joined', default=None) dateexpires = UtcDateTimeCol(dbName='date_expires', default=None) date_created = UtcDateTimeCol(default=UTC_NOW) date_proposed = UtcDateTimeCol(default=None) date_acknowledged = UtcDateTimeCol(default=None) date_reviewed = UtcDateTimeCol(default=None) date_last_changed = UtcDateTimeCol(default=None) last_change_comment = StringCol(default=None) proponent_comment = StringCol(default=None) acknowledger_comment = StringCol(default=None) reviewer_comment = StringCol(default=None) def isExpired(self): """See `ITeamMembership`.""" return self.status == TeamMembershipStatus.EXPIRED def canBeRenewedByMember(self): """See `ITeamMembership`.""" ondemand = TeamMembershipRenewalPolicy.ONDEMAND admin = TeamMembershipStatus.APPROVED approved = TeamMembershipStatus.ADMIN date_limit = datetime.now( pytz.UTC) + timedelta(days=DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT) return (self.status in (admin, approved) and self.team.renewal_policy == ondemand and self.dateexpires is not None and self.dateexpires < date_limit) def sendSelfRenewalNotification(self): """See `ITeamMembership`.""" getUtility(ISelfRenewalNotificationJobSource).create( self.person, self.team, self.dateexpires) def canChangeStatusSilently(self, user): """Ensure that the user is in the Launchpad Administrators group. Then the user can silently make changes to their membership status. """ return user.inTeam(getUtility(ILaunchpadCelebrities).admin) def canChangeExpirationDate(self, person): """See `ITeamMembership`.""" person_is_team_admin = self.team in person.getAdministratedTeams() person_is_lp_admin = IPersonRoles(person).in_admin return person_is_team_admin or person_is_lp_admin def setExpirationDate(self, date, user): """See `ITeamMembership`.""" if date == self.dateexpires: return assert self.canChangeExpirationDate(user), ( "This user can't change this membership's expiration date.") self._setExpirationDate(date, user) def _setExpirationDate(self, date, user): UTC = pytz.timezone('UTC') assert date is None or date.date() >= datetime.now(UTC).date(), ( "The given expiration date must be None or be in the future: %s" % date.strftime('%Y-%m-%d')) self.dateexpires = date self.last_changed_by = user def sendExpirationWarningEmail(self): """See `ITeamMembership`.""" if self.dateexpires is None: raise AssertionError( '%s in team %s has no membership expiration date.' % (self.person.name, self.team.name)) if self.dateexpires < datetime.now(pytz.timezone('UTC')): # The membership has reached expiration. Silently return because # there is nothing to do. The member will have received emails # from previous calls by flag-expired-memberships.py return getUtility(IExpiringMembershipNotificationJobSource).create( self.person, self.team, self.dateexpires) def setStatus(self, status, user, comment=None, silent=False): """See `ITeamMembership`.""" if status == self.status: return False if silent and not self.canChangeStatusSilently(user): raise UserCannotChangeMembershipSilently( "Only Launchpad administrators may change membership " "statuses silently.") approved = TeamMembershipStatus.APPROVED admin = TeamMembershipStatus.ADMIN expired = TeamMembershipStatus.EXPIRED declined = TeamMembershipStatus.DECLINED deactivated = TeamMembershipStatus.DEACTIVATED proposed = TeamMembershipStatus.PROPOSED invited = TeamMembershipStatus.INVITED invitation_declined = TeamMembershipStatus.INVITATION_DECLINED self.person.clearInTeamCache() # Make sure the transition from the current status to the given one # is allowed. All allowed transitions are in the TeamMembership spec. state_transition = { admin: [approved, expired, deactivated], approved: [admin, expired, deactivated], deactivated: [proposed, approved, admin, invited], expired: [proposed, approved, admin, invited], proposed: [approved, admin, declined], declined: [proposed, approved, admin, invited], invited: [approved, admin, invitation_declined], invitation_declined: [invited, approved, admin] } if self.status not in state_transition: raise TeamMembershipTransitionError("Unknown status: %s" % self.status.name) if status not in state_transition[self.status]: raise TeamMembershipTransitionError( "Bad state transition from %s to %s" % (self.status.name, status.name)) if status in ACTIVE_STATES and self.team in self.person.allmembers: raise CyclicalTeamMembershipError( "Cannot make %(person)s a member of %(team)s because " "%(team)s is a member of %(person)s." % dict(person=self.person.name, team=self.team.name)) old_status = self.status self.status = status now = datetime.now(pytz.timezone('UTC')) if status in [proposed, invited]: self.proposed_by = user self.proponent_comment = comment self.date_proposed = now elif ((status in ACTIVE_STATES and old_status not in ACTIVE_STATES) or status == declined): self.reviewed_by = user self.reviewer_comment = comment self.date_reviewed = now if self.datejoined is None and status in ACTIVE_STATES: # This is the first time this membership is made active. self.datejoined = now else: # No need to set proponent or reviewer. pass if old_status == invited: # This member has been invited by an admin and is now accepting or # declining the invitation. self.acknowledged_by = user self.date_acknowledged = now self.acknowledger_comment = comment self.last_changed_by = user self.last_change_comment = comment self.date_last_changed = now if status in ACTIVE_STATES: _fillTeamParticipation(self.person, self.team) elif old_status in ACTIVE_STATES: _cleanTeamParticipation(self.person, self.team) # A person has left the team so they may no longer have access # to some artifacts shared with the team. We need to run a job # to remove any subscriptions to such artifacts. getUtility(IRemoveArtifactSubscriptionsJobSource).create( user, grantee=self.person) else: # Changed from an inactive state to another inactive one, so no # need to fill/clean the TeamParticipation table. pass # Flush all updates to ensure any subsequent calls to this method on # the same transaction will operate on the correct data. That is the # case with our script to expire team memberships. flush_database_updates() # When a member proposes themselves, a more detailed notification is # sent to the team admins by a subscriber of JoinTeamEvent; that's # why we don't send anything here. if ((self.person != self.last_changed_by or self.status != proposed) and not silent): self._sendStatusChangeNotification(old_status) return True def _sendStatusChangeNotification(self, old_status): """Send a status change notification to all team admins and the member whose membership's status changed. """ reviewer = self.last_changed_by new_status = self.status getUtility(IMembershipNotificationJobSource).create( self.person, self.team, reviewer, old_status, new_status, self.last_change_comment)
class ProjectGroup(SQLBase, BugTargetBase, HasSpecificationsMixin, MakesAnnouncements, HasSprintsMixin, HasAliasMixin, KarmaContextMixin, StructuralSubscriptionTargetMixin, HasBranchesMixin, HasMergeProposalsMixin, HasMilestonesMixin, HasDriversMixin, TranslationPolicyMixin): """A ProjectGroup""" _table = "Project" # db field names owner = ForeignKey(dbName='owner', foreignKey='Person', storm_validator=validate_person_or_closed_team, notNull=True) registrant = ForeignKey(dbName='registrant', foreignKey='Person', storm_validator=validate_public_person, notNull=True) name = StringCol(dbName='name', notNull=True) display_name = StringCol(dbName='displayname', notNull=True) _title = StringCol(dbName='title', notNull=True) summary = StringCol(dbName='summary', notNull=True) description = StringCol(dbName='description', notNull=True) datecreated = UtcDateTimeCol(dbName='datecreated', notNull=True, default=UTC_NOW) driver = ForeignKey(dbName="driver", foreignKey="Person", storm_validator=validate_public_person, notNull=False, default=None) homepageurl = StringCol(dbName='homepageurl', notNull=False, default=None) homepage_content = StringCol(default=None) icon = ForeignKey(dbName='icon', foreignKey='LibraryFileAlias', default=None) logo = ForeignKey(dbName='logo', foreignKey='LibraryFileAlias', default=None) mugshot = ForeignKey(dbName='mugshot', foreignKey='LibraryFileAlias', default=None) wikiurl = StringCol(dbName='wikiurl', notNull=False, default=None) sourceforgeproject = StringCol(dbName='sourceforgeproject', notNull=False, default=None) freshmeatproject = None lastdoap = StringCol(dbName='lastdoap', notNull=False, default=None) translationgroup = ForeignKey(dbName='translationgroup', foreignKey='TranslationGroup', notNull=False, default=None) translationpermission = EnumCol(dbName='translationpermission', notNull=True, schema=TranslationPermission, default=TranslationPermission.OPEN) active = BoolCol(dbName='active', notNull=True, default=True) reviewed = BoolCol(dbName='reviewed', notNull=True, default=False) bugtracker = ForeignKey(foreignKey="BugTracker", dbName="bugtracker", notNull=False, default=None) bug_reporting_guidelines = StringCol(default=None) bug_reported_acknowledgement = StringCol(default=None) @property def displayname(self): return self.display_name @property def title(self): return self.display_name @property def pillar_category(self): """See `IPillar`.""" return "Project Group" def getProducts(self, user): results = Store.of(self).find(Product, Product.projectgroup == self, Product.active == True, ProductSet.getProductPrivacyFilter(user)) return results.order_by(Product.display_name) @cachedproperty def products(self): return list(self.getProducts(getUtility(ILaunchBag).user)) def getProduct(self, name): return Product.selectOneBy(projectgroup=self, name=name) def getConfigurableProducts(self): return [ product for product in self.products if check_permission('launchpad.Edit', product) ] @property def drivers(self): """See `IHasDrivers`.""" if self.driver is not None: return [self.driver] return [] def getTranslatables(self): """Return an iterator over products that are translatable in LP. Only products with IProduct.translations_usage set to ServiceUsage.LAUNCHPAD are considered translatable. """ store = Store.of(self) origin = [ Product, Join(ProductSeries, Product.id == ProductSeries.productID), Join(POTemplate, ProductSeries.id == POTemplate.productseriesID), ] return store.using(*origin).find( Product, Product.projectgroup == self.id, Product.translations_usage == ServiceUsage.LAUNCHPAD, ).config(distinct=True) @cachedproperty def translatables(self): """See `IProjectGroup`.""" return list(self.getTranslatables()) def has_translatable(self): """See `IProjectGroup`.""" return len(self.translatables) > 0 def sharesTranslationsWithOtherSide(self, person, language, sourcepackage=None, purportedly_upstream=False): """See `ITranslationPolicy`.""" assert sourcepackage is None, ( "Got a SourcePackage for a ProjectGroup!") # ProjectGroup translations are considered upstream. They are # automatically shared. return True def has_branches(self): """ See `IProjectGroup`.""" return not self.getBranches().is_empty() def _getBaseQueryAndClauseTablesForQueryingSprints(self): query = """ Product.project = %s AND Specification.product = Product.id AND Specification.id = SprintSpecification.specification AND SprintSpecification.sprint = Sprint.id AND SprintSpecification.status = %s """ % sqlvalues(self, SprintSpecificationStatus.ACCEPTED) return query, ['Product', 'Specification', 'SprintSpecification'] def specifications(self, user, sort=None, quantity=None, filter=None, series=None, need_people=True, need_branches=True, need_workitems=False): """See `IHasSpecifications`.""" base_clauses = [ Specification.productID == Product.id, Product.projectgroupID == self.id ] tables = [Specification] if series: base_clauses.append(ProductSeries.name == series) tables.append( Join(ProductSeries, Specification.productseriesID == ProductSeries.id)) return search_specifications(self, base_clauses, user, sort, quantity, filter, tables=tables, need_people=need_people, need_branches=need_branches, need_workitems=need_workitems) def _customizeSearchParams(self, search_params): """Customize `search_params` for this milestone.""" search_params.setProjectGroup(self) def _getOfficialTagClause(self): """See `OfficialBugTagTargetMixin`.""" And(ProjectGroup.id == Product.projectgroupID, Product.id == OfficialBugTag.productID) @property def official_bug_tags(self): """See `IHasBugs`.""" store = Store.of(self) result = store.find( OfficialBugTag.tag, OfficialBugTag.product == Product.id, Product.projectgroup == self.id).order_by(OfficialBugTag.tag) result.config(distinct=True) return result def getBugSummaryContextWhereClause(self): """See BugTargetBase.""" # Circular fail. from lp.bugs.model.bugsummary import BugSummary product_ids = [product.id for product in self.products] if not product_ids: return False return BugSummary.product_id.is_in(product_ids) # IQuestionCollection def searchQuestions(self, search_text=None, status=QUESTION_STATUS_DEFAULT_SEARCH, language=None, sort=None, owner=None, needs_attention_from=None, unsupported=False): """See `IQuestionCollection`.""" if unsupported: unsupported_target = self else: unsupported_target = None return QuestionTargetSearch( projectgroup=self, search_text=search_text, status=status, language=language, sort=sort, owner=owner, needs_attention_from=needs_attention_from, unsupported_target=unsupported_target).getResults() def getQuestionLanguages(self): """See `IQuestionCollection`.""" return set( Language.select(""" Language.id = Question.language AND Question.product = Product.id AND Product.project = %s""" % sqlvalues(self.id), clauseTables=['Question', 'Product'], distinct=True)) @property def bugtargetname(self): """See IBugTarget.""" return self.name # IFAQCollection def getFAQ(self, id): """See `IQuestionCollection`.""" faq = FAQ.getForTarget(id, None) if (faq is not None and IProduct.providedBy(faq.target) and faq.target in self.products): # Filter out faq not related to this project. return faq else: return None def searchFAQs(self, search_text=None, owner=None, sort=None): """See `IQuestionCollection`.""" return FAQSearch(search_text=search_text, owner=owner, sort=sort, projectgroup=self).getResults() def hasProducts(self): """Returns True if a project group has products associated with it, False otherwise. If the project group has < 1 product, selected links will be disabled. This is to avoid situations where users try to file bugs against empty project groups (Malone bug #106523). """ return len(self.products) != 0 def _getMilestoneCondition(self): """See `HasMilestonesMixin`.""" user = getUtility(ILaunchBag).user privacy_filter = ProductSet.getProductPrivacyFilter(user) return And(Milestone.productID == Product.id, Product.projectgroupID == self.id, privacy_filter) def _getMilestones(self, user, only_active): """Return a list of milestones for this project group. If only_active is True, only active milestones are returned, else all milestones. A project group has a milestone named 'A', if at least one of its products has a milestone named 'A'. """ store = Store.of(self) columns = ( Milestone.name, SQL('MIN(Milestone.dateexpected)'), SQL('BOOL_OR(Milestone.active)'), ) privacy_filter = ProductSet.getProductPrivacyFilter(user) conditions = And(Milestone.product == Product.id, Product.projectgroup == self, Product.active == True, privacy_filter) result = store.find(columns, conditions) result.group_by(Milestone.name) if only_active: result.having('BOOL_OR(Milestone.active) = TRUE') # MIN(Milestone.dateexpected) has to be used to match the # aggregate function in the `columns` variable. result.order_by( 'milestone_sort_key(MIN(Milestone.dateexpected), Milestone.name) ' 'DESC') # An extra query is required here in order to get the correct # products without affecting the group/order of the query above. products_by_name = {} if result.any() is not None: milestone_names = [data[0] for data in result] product_conditions = And(Product.projectgroup == self, Milestone.product == Product.id, Product.active == True, privacy_filter, In(Milestone.name, milestone_names)) for product, name in (store.find((Product, Milestone.name), product_conditions)): if name not in products_by_name.keys(): products_by_name[name] = product return shortlist([ ProjectMilestone(self, name, dateexpected, active, products_by_name.get(name, None)) for name, dateexpected, active in result ]) @property def has_milestones(self): """See `IHasMilestones`.""" store = Store.of(self) result = store.find( Milestone.id, And(Milestone.product == Product.id, Product.projectgroup == self, Product.active == True)) return result.any() is not None @property def milestones(self): """See `IProjectGroup`.""" user = getUtility(ILaunchBag).user return self._getMilestones(user, only_active=True) @property def product_milestones(self): """Hack to avoid the ProjectMilestone in MilestoneVocabulary.""" # XXX: bug=644977 Robert Collins - this is a workaround for # inconsistency in project group milestone use. return self._get_milestones() @property def all_milestones(self): """See `IProjectGroup`.""" user = getUtility(ILaunchBag).user return self._getMilestones(user, only_active=False) def getMilestone(self, name): """See `IProjectGroup`.""" for milestone in self.all_milestones: if milestone.name == name: return milestone return None def getSeries(self, series_name): """See `IProjectGroup.`""" has_series = ProductSeries.selectFirst(AND( ProductSeries.q.productID == Product.q.id, ProductSeries.q.name == series_name, Product.q.projectgroupID == self.id), orderBy='id') if has_series is None: return None return ProjectGroupSeries(self, series_name) def _get_usage(self, attr): """Determine ProjectGroup usage based on individual projects. By default, return ServiceUsage.UNKNOWN. If any project uses Launchpad, return ServiceUsage.LAUNCHPAD. Otherwise, return the ServiceUsage of the last project that was not ServiceUsage.UNKNOWN. """ result = ServiceUsage.UNKNOWN for product in self.products: product_usage = getattr(product, attr) if product_usage != ServiceUsage.UNKNOWN: result = product_usage if product_usage == ServiceUsage.LAUNCHPAD: break return result @property def answers_usage(self): return self._get_usage('answers_usage') @property def blueprints_usage(self): return self._get_usage('blueprints_usage') @property def translations_usage(self): if self.has_translatable(): return ServiceUsage.LAUNCHPAD return ServiceUsage.UNKNOWN @property def codehosting_usage(self): # Project groups do not support submitting code. return ServiceUsage.NOT_APPLICABLE @property def bug_tracking_usage(self): return self._get_usage('bug_tracking_usage') @property def uses_launchpad(self): if (self.answers_usage == ServiceUsage.LAUNCHPAD or self.blueprints_usage == ServiceUsage.LAUNCHPAD or self.translations_usage == ServiceUsage.LAUNCHPAD or self.codehosting_usage == ServiceUsage.LAUNCHPAD or self.bug_tracking_usage == ServiceUsage.LAUNCHPAD): return True return False