Пример #1
0
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)
Пример #2
0
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)
Пример #3
0
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()
Пример #4
0
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))
Пример #5
0
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
Пример #6
0
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
Пример #7
0
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)
Пример #9
0
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)
Пример #10
0
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
Пример #11
0
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()
Пример #12
0
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)
Пример #13
0
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)
Пример #14
0
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)
Пример #15
0
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
Пример #17
0
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')
Пример #18
0
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)
Пример #19
0
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)]
Пример #20
0
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)
Пример #21
0
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')
Пример #22
0
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)
Пример #23
0
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
Пример #24
0
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
Пример #25
0
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)
Пример #26
0
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
Пример #29
0
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)
Пример #30
0
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