launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #23942
[Merge] lp:~cjwatson/launchpad/das-filter-model into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/das-filter-model into lp:launchpad with lp:~cjwatson/launchpad/packageset-is-source-included as a prerequisite.
Commit message:
Add basic model for per-DAS filtering of build creation.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1842658 in Launchpad itself: "Support central filtering of which packages build for some architectures"
https://bugs.launchpad.net/launchpad/+bug/1842658
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/das-filter-model/+merge/372261
This isn't yet used or exposed on the webservice; that will come in future MPs.
The corresponding DB patch is in https://code.launchpad.net/~cjwatson/launchpad/db-das-filter/+merge/372260.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/das-filter-model into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2019-08-09 12:04:04 +0000
+++ database/schema/security.cfg 2019-09-04 14:01:22 +0000
@@ -176,6 +176,7 @@
public.distributionmirror = SELECT, INSERT, UPDATE, DELETE
public.distributionsourcepackage = SELECT, INSERT, UPDATE, DELETE
public.distributionsourcepackagecache = SELECT, INSERT
+public.distroarchseriesfilter = SELECT, INSERT, UPDATE, DELETE
public.distroseriesdifference = SELECT, INSERT, UPDATE
public.distroseriesdifferencemessage = SELECT, INSERT, UPDATE
public.distroserieslanguage = SELECT, INSERT, UPDATE
@@ -2221,6 +2222,7 @@
public.distributionsourcepackage = SELECT, INSERT, UPDATE, DELETE
public.distributionmirror = SELECT, UPDATE
public.distroarchseries = SELECT, UPDATE
+public.distroarchseriesfilter = SELECT, UPDATE
public.distroseries = SELECT, UPDATE
public.emailaddress = SELECT, UPDATE, DELETE
public.faq = SELECT, UPDATE
@@ -2356,6 +2358,7 @@
public.commercialsubscription = SELECT, UPDATE
public.diff = SELECT, DELETE
public.distributionsourcepackagecache = SELECT, INSERT
+public.distroarchseriesfilter = SELECT
public.distroseries = SELECT, UPDATE
public.emailaddress = SELECT, UPDATE, DELETE
public.garbojobstate = SELECT, INSERT, UPDATE, DELETE
=== modified file 'lib/lp/_schema_circular_imports.py'
--- lib/lp/_schema_circular_imports.py 2018-09-25 16:41:21 +0000
+++ lib/lp/_schema_circular_imports.py 2019-09-04 14:01:22 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Update the interface schema values due to circular imports.
@@ -496,6 +496,8 @@
patch_reference_property(IDistroArchSeries, 'main_archive', IArchive)
patch_plain_parameter_type(
IDistroArchSeries, 'setChrootFromBuild', 'livefsbuild', ILiveFSBuild)
+patch_plain_parameter_type(
+ IDistroArchSeries, 'setFilter', 'packageset', IPackageset)
# IGitRef
patch_reference_property(IGitRef, 'repository', IGitRepository)
=== modified file 'lib/lp/registry/scripts/closeaccount.py'
--- lib/lp/registry/scripts/closeaccount.py 2019-08-09 12:04:04 +0000
+++ lib/lp/registry/scripts/closeaccount.py 2019-09-04 14:01:22 +0000
@@ -114,6 +114,7 @@
('codeimport', 'owner'),
('codeimport', 'registrant'),
('codeimportevent', 'person'),
+ ('distroarchseriesfilter', 'creator'),
('faq', 'last_updated_by'),
('featureflagchangelogentry', 'person'),
('gitactivity', 'changee'),
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2019-07-01 12:48:37 +0000
+++ lib/lp/security.py 2019-09-04 14:01:22 +0000
@@ -229,6 +229,7 @@
IBinaryPackageReleaseDownloadCount,
)
from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
+from lp.soyuz.interfaces.distroarchseriesfilter import IDistroArchSeriesFilter
from lp.soyuz.interfaces.livefs import ILiveFS
from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild
from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJob
@@ -1468,6 +1469,24 @@
or user.in_admin)
+class ViewDistroArchSeriesFilter(DelegatedAuthorization):
+ permission = 'launchpad.View'
+ usedfor = IDistroArchSeriesFilter
+
+ def __init__(self, obj):
+ super(ViewDistroArchSeriesFilter, self).__init__(
+ obj, obj.distroarchseries, 'launchpad.View')
+
+
+class EditDistroArchSeriesFilter(DelegatedAuthorization):
+ permission = 'launchpad.Edit'
+ usedfor = IDistroArchSeriesFilter
+
+ def __init__(self, obj):
+ super(EditDistroArchSeriesFilter, self).__init__(
+ obj, obj.distroarchseries, 'launchpad.Moderate')
+
+
class ViewAnnouncement(AuthorizationBase):
permission = 'launchpad.View'
usedfor = IAnnouncement
=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml 2019-03-12 19:12:29 +0000
+++ lib/lp/soyuz/configure.zcml 2019-09-04 14:01:22 +0000
@@ -616,6 +616,26 @@
set_schema="lp.soyuz.interfaces.distroarchseries.IPocketChroot"/>
</class>
+ <!-- DistroArchSeriesFilter -->
+ <class
+ class="lp.soyuz.model.distroarchseriesfilter.DistroArchSeriesFilter">
+ <allow
+ interface="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilterView" />
+ <require
+ permission="launchpad.Edit"
+ interface="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilterEdit" />
+ </class>
+ <securedutility
+ class="lp.soyuz.model.distroarchseriesfilter.DistroArchSeriesFilterSet"
+ provides="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilterSet">
+ <allow
+ interface="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilterSet" />
+ </securedutility>
+ <subscriber
+ for="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilter
+ lazr.lifecycle.interfaces.IObjectModifiedEvent"
+ handler="lp.soyuz.model.distroarchseriesfilter.distro_arch_series_filter_modified" />
+
<!-- Component -->
<class
=== modified file 'lib/lp/soyuz/doc/distroarchseries.txt'
--- lib/lp/soyuz/doc/distroarchseries.txt 2019-02-13 14:39:18 +0000
+++ lib/lp/soyuz/doc/distroarchseries.txt 2019-09-04 14:01:22 +0000
@@ -255,3 +255,37 @@
>>> print_architectures(hoary.buildable_architectures)
The Hoary Hedgehog Release for hppa (hppa) (ppa)
The Hoary Hedgehog Release for i386 (386) (official, ppa)
+
+An architecture can have an associated filter that controls which packages
+are included in it. It has an `isSourceIncluded` method that allows
+querying inclusion by `SourcePackageName`.
+
+ >>> from lp.soyuz.enums import DistroArchSeriesFilterSense
+
+ >>> spns = [factory.makeSourcePackageName() for _ in range(3)]
+ >>> hoary.getDistroArchSeries('i386').isSourceIncluded(spns[0])
+ True
+
+ >>> packageset_include = factory.makePackageset(distroseries=hoary)
+ >>> packageset_include.add(spns[:2])
+ >>> hoary.getDistroArchSeries('i386').setFilter(
+ ... packageset_include, DistroArchSeriesFilterSense.INCLUDE,
+ ... factory.makePerson())
+ >>> packageset_exclude = factory.makePackageset(distroseries=hoary)
+ >>> packageset_exclude.add(spns[1:])
+ >>> hoary.getDistroArchSeries('hppa').setFilter(
+ ... packageset_exclude, DistroArchSeriesFilterSense.EXCLUDE,
+ ... factory.makePerson())
+
+ >>> hoary.getDistroArchSeries('i386').isSourceIncluded(spns[0])
+ True
+ >>> hoary.getDistroArchSeries('i386').isSourceIncluded(spns[1])
+ True
+ >>> hoary.getDistroArchSeries('i386').isSourceIncluded(spns[2])
+ False
+ >>> hoary.getDistroArchSeries('hppa').isSourceIncluded(spns[0])
+ True
+ >>> hoary.getDistroArchSeries('hppa').isSourceIncluded(spns[1])
+ False
+ >>> hoary.getDistroArchSeries('hppa').isSourceIncluded(spns[2])
+ False
=== modified file 'lib/lp/soyuz/enums.py'
--- lib/lp/soyuz/enums.py 2017-03-29 09:28:09 +0000
+++ lib/lp/soyuz/enums.py 2019-09-04 14:01:22 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2017 Canonical Ltd. This software is licensed under the
+# Copyright 2010-2019 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Enumerations used in the lp/soyuz modules."""
@@ -13,6 +13,7 @@
'archive_suffixes',
'BinaryPackageFileType',
'BinaryPackageFormat',
+ 'DistroArchSeriesFilterSense',
'IndexCompressionType',
'PackageCopyPolicy',
'PackageCopyStatus',
@@ -597,3 +598,18 @@
GZIP = DBItem(1, "gzip")
BZIP2 = DBItem(2, "bzip2")
XZ = DBItem(3, "xz")
+
+
+class DistroArchSeriesFilterSense(DBEnumeratedType):
+
+ INCLUDE = DBItem(1, """
+ Include
+
+ Packages in this package set are included in the distro arch series.
+ """)
+
+ EXCLUDE = DBItem(2, """
+ Exclude
+
+ Packages in this package set are excluded from the distro arch series.
+ """)
=== modified file 'lib/lp/soyuz/interfaces/distroarchseries.py'
--- lib/lp/soyuz/interfaces/distroarchseries.py 2019-07-30 11:38:18 +0000
+++ lib/lp/soyuz/interfaces/distroarchseries.py 2019-09-04 14:01:22 +0000
@@ -7,6 +7,7 @@
__all__ = [
'ChrootNotPublic',
+ 'FilterSeriesMismatch',
'IDistroArchSeries',
'InvalidChrootUploaded',
'IPocketChroot',
@@ -65,6 +66,19 @@
"Cannot set chroot from a private build.")
+@error_status(httplib.BAD_REQUEST)
+class FilterSeriesMismatch(Exception):
+ """DAS and packageset distroseries do not match when setting a filter."""
+
+ def __init__(self, distroarchseries, packageset):
+ super(Exception, self).__init__(
+ "The requested package set is for %s and cannot be set as a "
+ "filter for %s %s." % (
+ packageset.distroseries.fullseriesname,
+ distroarchseries.distroseries.fullseriesname,
+ distroarchseries.architecturetag))
+
+
class IDistroArchSeriesPublic(IHasBuildRecords, IHasOwner):
"""Public attributes for a DistroArchSeries."""
@@ -216,6 +230,21 @@
this distro arch series.
"""
+ def getFilter():
+ """Get the filter for packages to build for this architecture, if any.
+
+ Packages are normally built for all available architectures, subject
+ to any constraints in their `Architecture` field. If a filter is
+ set, then it applies the additional constraint that packages not
+ included by the filter will not be built for this architecture.
+ """
+
+ def isSourceIncluded(sourcepackagerelease):
+ """Is this source package included in this distro arch series?
+
+ :param sourcepackagerelease: An `ISourcePackageRelease` to check.
+ """
+
class IDistroArchSeriesModerate(Interface):
@@ -263,6 +292,36 @@
tarball".
"""
+ def setFilter(packageset, sense, creator):
+ """Set a filter for packages to build for this architecture.
+
+ Packages are normally built for all available architectures, subject
+ to any constraints in their `Architecture` field. If a filter is
+ set, then it applies the additional constraint that packages not
+ included by the filter will not be built for this architecture.
+
+ If the sense of the filter is "Include", then the filter only
+ includes packages in the given package set. If the sense of the
+ filter is "Exclude", then the filter only includes packages not in
+ the given package set.
+
+ Later changes to the given package set will also affect any filters
+ using it.
+
+ :param packageset: An `IPackageset` to use as a filter.
+ :param sense: A `DistroArchSeriesFilterSense` item indicating
+ whether the filter includes or excludes packages.
+ :param creator: The `IPerson` who is creating this filter.
+ """
+
+ def removeFilter():
+ """Remove any filter for packages to build for this architecture.
+
+ This causes packages to be built for this architecture when they
+ might previously have been filtered, subject to any constraints in
+ their `Architecture` field.
+ """
+
class IDistroArchSeries(IDistroArchSeriesPublic, IDistroArchSeriesModerate):
"""An architecture for a distroseries."""
=== added file 'lib/lp/soyuz/interfaces/distroarchseriesfilter.py'
--- lib/lp/soyuz/interfaces/distroarchseriesfilter.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/interfaces/distroarchseriesfilter.py 2019-09-04 14:01:22 +0000
@@ -0,0 +1,127 @@
+# Copyright 2019 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Distro arch series filter interfaces."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'IDistroArchSeriesFilter',
+ 'IDistroArchSeriesFilterSet',
+ 'NoSuchDistroArchSeriesFilter',
+ ]
+
+from lazr.restful.fields import Reference
+from zope.interface import Interface
+from zope.schema import (
+ Choice,
+ Datetime,
+ Int,
+ )
+
+from lp import _
+from lp.app.errors import NameLookupFailed
+from lp.services.fields import PublicPersonChoice
+from lp.soyuz.enums import DistroArchSeriesFilterSense
+from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
+from lp.soyuz.interfaces.packageset import IPackageset
+
+
+class NoSuchDistroArchSeriesFilter(NameLookupFailed):
+ """Raised when we try to look up a nonexistent DistroArchSeriesFilter."""
+ _message_prefix = (
+ "The given distro arch series has no DistroArchSeriesFilter")
+
+
+class IDistroArchSeriesFilterView(Interface):
+ """`IDistroArchSeriesFilter` attributes that require launchpad.View."""
+
+ id = Int(title=_("ID"), readonly=True, required=True)
+
+ distroarchseries = Reference(
+ title=_("Distro arch series"), required=True, readonly=True,
+ schema=IDistroArchSeries,
+ description=_("The distro arch series that this filter is for."))
+
+ packageset = Reference(
+ title=_("Package set"), required=True, readonly=True,
+ schema=IPackageset,
+ description=_(
+ "The package set to be included in or excluded from this distro "
+ "arch series."))
+
+ sense = Choice(
+ title=_("Sense"),
+ vocabulary=DistroArchSeriesFilterSense, required=True, readonly=True,
+ description=_(
+ "Whether the filter represents packages to include or exclude "
+ "from the distro arch series."))
+
+ creator = PublicPersonChoice(
+ title=_("Creator"), required=True, readonly=True,
+ vocabulary="ValidPerson",
+ description=_("The user who created this filter."))
+
+ date_created = Datetime(
+ title=_("Date created"), required=True, readonly=True,
+ description=_("The time when this filter was created."))
+
+ date_last_modified = Datetime(
+ title=_("Date last modified"), required=True, readonly=True,
+ description=_("The time when this filter was last modified."))
+
+ def isSourceIncluded(sourcepackagename):
+ """Is this source package name included by this filter?
+
+ If the sense of the filter is INCLUDE, then this returns True iff
+ the source package name is included in the related package set;
+ otherwise, it returns True iff the source package name is not
+ included in the related package set.
+
+ :param sourcepackagename: an `ISourcePackageName`.
+ :return: True if the source is included by this filter, otherwise
+ False.
+ """
+
+
+class IDistroArchSeriesFilterEdit(Interface):
+ """`IDistroArchSeriesFilter` attributes that require launchpad.Edit."""
+
+ def destroySelf():
+ """Delete this filter."""
+
+
+class IDistroArchSeriesFilter(
+ IDistroArchSeriesFilterView, IDistroArchSeriesFilterEdit):
+ """A filter for packages to be included in or excluded from a DAS.
+
+ Since package sets can include other package sets, a single package set
+ is flexible enough for this. However, one might reasonably want to
+ either include some packages ("this architecture is obsolescent or
+ experimental and we only want to build a few packages for it") or
+ exclude some packages ("this architecture can't handle some packages so
+ we want to make them go away centrally").
+ """
+
+
+class IDistroArchSeriesFilterSet(Interface):
+ """An interface for multiple distro arch series filters."""
+
+ def new(distroarchseries, packageset, sense, creator, date_created=None):
+ """Create an `IDistroArchSeriesFilter`."""
+
+ def getByDistroArchSeries(distroarchseries):
+ """Return the filter for this distro arch series, if any.
+
+ :param distroarchseries: The `IDistroArchSeries` to search for.
+ :return: An `IDistroArchSeriesFilter` instance.
+ :raises NoSuchDistroArchSeriesFilter: if no filter is found.
+ """
+
+ def findByPackageset(packageset):
+ """Return any filters using this package set.
+
+ :param packageset: The `IPackageset` to search for.
+ :return: A `ResultSet` of `IDistroArchSeriesFilter` instances.
+ """
=== modified file 'lib/lp/soyuz/model/distroarchseries.py'
--- lib/lp/soyuz/model/distroarchseries.py 2019-07-30 11:38:18 +0000
+++ lib/lp/soyuz/model/distroarchseries.py 2019-09-04 14:01:22 +0000
@@ -55,10 +55,14 @@
from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
from lp.soyuz.interfaces.distroarchseries import (
ChrootNotPublic,
+ FilterSeriesMismatch,
IDistroArchSeries,
InvalidChrootUploaded,
IPocketChroot,
)
+from lp.soyuz.interfaces.distroarchseriesfilter import (
+ IDistroArchSeriesFilterSet,
+ )
from lp.soyuz.interfaces.publishing import active_publishing_status
from lp.soyuz.model.binarypackagename import BinaryPackageName
from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
@@ -341,6 +345,32 @@
def main_archive(self):
return self.distroseries.distribution.main_archive
+ def getFilter(self):
+ """See `IDistroArchSeries`."""
+ return getUtility(IDistroArchSeriesFilterSet).getByDistroArchSeries(
+ self)
+
+ def setFilter(self, packageset, sense, creator):
+ """See `IDistroArchSeries`."""
+ if self.distroseries != packageset.distroseries:
+ raise FilterSeriesMismatch(self, packageset)
+ self.removeFilter()
+ getUtility(IDistroArchSeriesFilterSet).new(
+ self, packageset, sense, creator)
+
+ def removeFilter(self):
+ """See `IDistroArchSeries`."""
+ dasf = self.getFilter()
+ if dasf is not None:
+ dasf.destroySelf()
+
+ def isSourceIncluded(self, sourcepackagename):
+ """See `IDistroArchSeries`."""
+ dasf = self.getFilter()
+ if dasf is None:
+ return True
+ return dasf.isSourceIncluded(sourcepackagename)
+
@implementer(IPocketChroot)
class PocketChroot(SQLBase):
=== added file 'lib/lp/soyuz/model/distroarchseriesfilter.py'
--- lib/lp/soyuz/model/distroarchseriesfilter.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/model/distroarchseriesfilter.py 2019-09-04 14:01:22 +0000
@@ -0,0 +1,124 @@
+# Copyright 2019 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Distro arch series filters."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'DistroArchSeriesFilter',
+ ]
+
+import pytz
+from storm.locals import (
+ DateTime,
+ Int,
+ Reference,
+ Storm,
+ )
+from zope.interface import implementer
+from zope.security.proxy import removeSecurityProxy
+
+from lp.services.database.constants import (
+ DEFAULT,
+ UTC_NOW,
+ )
+from lp.services.database.enumcol import DBEnum
+from lp.services.database.interfaces import (
+ IMasterStore,
+ IStore,
+ )
+from lp.soyuz.enums import DistroArchSeriesFilterSense
+from lp.soyuz.interfaces.distroarchseriesfilter import (
+ IDistroArchSeriesFilter,
+ IDistroArchSeriesFilterSet,
+ )
+
+
+def distro_arch_series_filter_modified(pss, event):
+ """Update date_last_modified when a `DistroArchSeriesFilter` is modified.
+
+ This method is registered as a subscriber to `IObjectModifiedEvent`
+ events on `DistroArchSeriesFilter`s.
+ """
+ removeSecurityProxy(pss).date_last_modified = UTC_NOW
+
+
+@implementer(IDistroArchSeriesFilter)
+class DistroArchSeriesFilter(Storm):
+ """See `IDistroArchSeriesFilter`."""
+
+ __storm_table__ = "DistroArchSeriesFilter"
+
+ id = Int(primary=True)
+
+ distroarchseries_id = Int(name="distroarchseries", allow_none=False)
+ distroarchseries = Reference(distroarchseries_id, "DistroArchSeries.id")
+
+ packageset_id = Int(name="packageset", allow_none=False)
+ packageset = Reference(packageset_id, "Packageset.id")
+
+ sense = DBEnum(enum=DistroArchSeriesFilterSense, allow_none=False)
+
+ creator_id = Int(name="creator", allow_none=False)
+ creator = Reference(creator_id, "Person.id")
+
+ date_created = DateTime(
+ name="date_created", tzinfo=pytz.UTC, allow_none=False)
+ date_last_modified = DateTime(
+ name="date_last_modified", tzinfo=pytz.UTC, allow_none=False)
+
+ def __init__(self, distroarchseries, packageset, sense, creator,
+ date_created=DEFAULT):
+ """Construct a `DistroArchSeriesFilter`."""
+ super(DistroArchSeriesFilter, self).__init__()
+ self.distroarchseries = distroarchseries
+ self.packageset = packageset
+ self.sense = sense
+ self.creator = creator
+ self.date_created = date_created
+ self.date_last_modified = date_created
+
+ def __repr__(self):
+ return "<DistroArchSeriesFilter for %s>" % self.distroarchseries.title
+
+ def isSourceIncluded(self, sourcepackagename):
+ """See `IDistroArchSeriesFilter`."""
+ return (
+ (self.sense == DistroArchSeriesFilterSense.INCLUDE) ==
+ self.packageset.isSourceIncluded(sourcepackagename))
+
+ def destroySelf(self):
+ """See `IDistroArchSeriesFilter`."""
+ IStore(DistroArchSeriesFilter).remove(self)
+
+
+@implementer(IDistroArchSeriesFilterSet)
+class DistroArchSeriesFilterSet:
+ """See `IDistroArchSeriesFilterSet`."""
+
+ def new(self, distroarchseries, packageset, sense, creator,
+ date_created=DEFAULT):
+ """See `IDistroArchSeriesFilterSet`.
+
+ The caller must check that the creator has suitable permissions on
+ `distroarchseries`.
+ """
+ store = IMasterStore(DistroArchSeriesFilter)
+ dasf = DistroArchSeriesFilter(
+ distroarchseries, packageset, sense, creator,
+ date_created=date_created)
+ store.add(dasf)
+ return dasf
+
+ def getByDistroArchSeries(self, distroarchseries):
+ """See `IDistroArchSeriesFilterSet`."""
+ return IStore(DistroArchSeriesFilter).find(
+ DistroArchSeriesFilter,
+ DistroArchSeriesFilter.distroarchseries == distroarchseries).one()
+
+ def findByPackageset(self, packageset):
+ return IStore(DistroArchSeriesFilter).find(
+ DistroArchSeriesFilter,
+ DistroArchSeriesFilter.packageset == packageset)
=== added file 'lib/lp/soyuz/tests/test_distroarchseriesfilter.py'
--- lib/lp/soyuz/tests/test_distroarchseriesfilter.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/tests/test_distroarchseriesfilter.py 2019-09-04 14:01:22 +0000
@@ -0,0 +1,128 @@
+# Copyright 2019 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test distro arch series filters."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+
+from testtools.matchers import MatchesStructure
+from zope.component import getUtility
+from zope.security.interfaces import Unauthorized
+
+from lp.services.database.interfaces import IStore
+from lp.services.database.sqlbase import get_transaction_timestamp
+from lp.soyuz.enums import DistroArchSeriesFilterSense
+from lp.soyuz.interfaces.distroarchseriesfilter import (
+ IDistroArchSeriesFilter,
+ IDistroArchSeriesFilterSet,
+ )
+from lp.testing import (
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import (
+ DatabaseFunctionalLayer,
+ ZopelessDatabaseLayer,
+ )
+
+
+class TestDistroArchSeriesFilter(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_implements_interfaces(self):
+ # DistroArchSeriesFilter implements IDistroArchSeriesFilter.
+ dasf = self.factory.makeDistroArchSeriesFilter()
+ self.assertProvides(dasf, IDistroArchSeriesFilter)
+
+ def test___repr__(self):
+ # `DistroArchSeriesFilter` objects have an informative __repr__.
+ das = self.factory.makeDistroArchSeries()
+ dasf = self.factory.makeDistroArchSeriesFilter(distroarchseries=das)
+ self.assertEqual(
+ "<DistroArchSeriesFilter for %s>" % das.title, repr(dasf))
+
+ def test_isSourceIncluded_include(self):
+ # INCLUDE filters report that a source is included if it is in the
+ # packageset.
+ spns = [self.factory.makeSourcePackageName() for _ in range(3)]
+ dasf = self.factory.makeDistroArchSeriesFilter(
+ sense=DistroArchSeriesFilterSense.INCLUDE)
+ dasf.packageset.add(spns[:2])
+ self.assertTrue(dasf.isSourceIncluded(spns[0]))
+ self.assertTrue(dasf.isSourceIncluded(spns[1]))
+ self.assertFalse(dasf.isSourceIncluded(spns[2]))
+
+ def test_isSourceIncluded_exclude(self):
+ # EXCLUDE filters report that a source is included if it is not in
+ # the packageset.
+ spns = [self.factory.makeSourcePackageName() for _ in range(3)]
+ dasf = self.factory.makeDistroArchSeriesFilter(
+ sense=DistroArchSeriesFilterSense.EXCLUDE)
+ dasf.packageset.add(spns[:2])
+ self.assertFalse(dasf.isSourceIncluded(spns[0]))
+ self.assertFalse(dasf.isSourceIncluded(spns[1]))
+ self.assertTrue(dasf.isSourceIncluded(spns[2]))
+
+ def test_destroySelf_unauthorized(self):
+ # Ordinary users cannot delete a filter.
+ das = self.factory.makeDistroArchSeries()
+ self.factory.makeDistroArchSeriesFilter(distroarchseries=das)
+ dasf = das.getFilter()
+ with person_logged_in(self.factory.makePerson()):
+ self.assertRaises(Unauthorized, getattr, dasf, "destroySelf")
+
+ def test_destroySelf(self):
+ # Owners of the DAS's archive can delete a filter.
+ das = self.factory.makeDistroArchSeries()
+ self.factory.makeDistroArchSeriesFilter(distroarchseries=das)
+ dasf = das.getFilter()
+ with person_logged_in(das.main_archive.owner):
+ dasf.destroySelf()
+ self.assertIsNone(das.getFilter())
+
+
+class TestDistroArchSeriesFilterSet(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def test_class_implements_interface(self):
+ # The DistroArchSeriesFilterSet class implements
+ # IDistroArchSeriesFilterSet.
+ self.assertProvides(
+ getUtility(IDistroArchSeriesFilterSet), IDistroArchSeriesFilterSet)
+
+ def test_new(self):
+ # The arguments passed when creating a filter are present on the new
+ # object.
+ das = self.factory.makeDistroArchSeries()
+ packageset = self.factory.makePackageset(distroseries=das.distroseries)
+ sense = DistroArchSeriesFilterSense.EXCLUDE
+ creator = self.factory.makePerson()
+ dasf = getUtility(IDistroArchSeriesFilterSet).new(
+ distroarchseries=das, packageset=packageset, sense=sense,
+ creator=creator)
+ now = get_transaction_timestamp(IStore(dasf))
+ self.assertThat(dasf, MatchesStructure.byEquality(
+ distroarchseries=das, packageset=packageset, sense=sense,
+ creator=creator, date_created=now, date_last_modified=now))
+
+ def test_getByDistroArchSeries(self):
+ # getByDistroArchSeries returns the filter for a DAS, if any.
+ das = self.factory.makeDistroArchSeries()
+ dasf_set = getUtility(IDistroArchSeriesFilterSet)
+ self.assertIsNone(dasf_set.getByDistroArchSeries(das))
+ dasf = self.factory.makeDistroArchSeriesFilter(distroarchseries=das)
+ self.assertEqual(dasf, dasf_set.getByDistroArchSeries(das))
+
+ def test_findByPackageset(self):
+ # findByPackageset returns any filters using a package set.
+ packageset = self.factory.makePackageset()
+ dasf_set = getUtility(IDistroArchSeriesFilterSet)
+ self.assertContentEqual([], dasf_set.findByPackageset(packageset))
+ dasfs = [
+ self.factory.makeDistroArchSeriesFilter(packageset=packageset)
+ for _ in range(2)]
+ self.assertContentEqual(dasfs, dasf_set.findByPackageset(packageset))
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2019-07-09 12:32:22 +0000
+++ lib/lp/testing/factory.py 2019-09-04 14:01:22 +0000
@@ -297,6 +297,7 @@
ArchivePurpose,
BinaryPackageFileType,
BinaryPackageFormat,
+ DistroArchSeriesFilterSense,
PackageDiffStatus,
PackagePublishingPriority,
PackagePublishingStatus,
@@ -326,6 +327,7 @@
from lp.soyuz.model.distributionsourcepackagecache import (
DistributionSourcePackageCache,
)
+from lp.soyuz.model.distroarchseriesfilter import DistroArchSeriesFilter
from lp.soyuz.model.files import BinaryPackageFile
from lp.soyuz.model.livefsbuild import LiveFSFile
from lp.soyuz.model.packagediff import PackageDiff
@@ -4198,6 +4200,27 @@
run_with_login(owner, lambda: package_set.add(packages))
return package_set
+ def makeDistroArchSeriesFilter(self, distroarchseries=None,
+ packageset=None,
+ sense=DistroArchSeriesFilterSense.INCLUDE,
+ creator=None, date_created=DEFAULT):
+ """Make a new `DistroArchSeriesFilter`."""
+ if distroarchseries is None:
+ if packageset is not None:
+ distroseries = packageset.distroseries
+ else:
+ distroseries = None
+ distroarchseries = self.makeDistroArchSeries(
+ distroseries=distroseries)
+ if packageset is None:
+ packageset = self.makePackageset(
+ distroseries=distroarchseries.distroseries)
+ if creator is None:
+ creator = self.makePerson()
+ return DistroArchSeriesFilter(
+ distroarchseries=distroarchseries, packageset=packageset,
+ sense=sense, creator=creator, date_created=date_created)
+
def getAnyPocket(self):
return PackagePublishingPocket.BACKPORTS
Follow ups