Merge lp:~cjwatson/launchpad/das-filter-model into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 19058
Proposed branch: lp:~cjwatson/launchpad/das-filter-model
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/packageset-is-source-included
Diff against target: 789 lines (+588/-2)
13 files modified
database/schema/security.cfg (+3/-0)
lib/lp/_schema_circular_imports.py (+3/-1)
lib/lp/registry/scripts/closeaccount.py (+1/-0)
lib/lp/security.py (+19/-0)
lib/lp/soyuz/configure.zcml (+20/-0)
lib/lp/soyuz/doc/distroarchseries.txt (+34/-0)
lib/lp/soyuz/enums.py (+17/-1)
lib/lp/soyuz/interfaces/distroarchseries.py (+59/-0)
lib/lp/soyuz/interfaces/distroarchseriesfilter.py (+127/-0)
lib/lp/soyuz/model/distroarchseries.py (+30/-0)
lib/lp/soyuz/model/distroarchseriesfilter.py (+124/-0)
lib/lp/soyuz/tests/test_distroarchseriesfilter.py (+128/-0)
lib/lp/testing/factory.py (+23/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/das-filter-model
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+372261@code.launchpad.net

Commit message

Add basic model for per-DAS filtering of build creation.

Description of the change

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.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2019-08-09 12:04:04 +0000
+++ database/schema/security.cfg 2019-09-10 11:16:47 +0000
@@ -176,6 +176,7 @@
176public.distributionmirror = SELECT, INSERT, UPDATE, DELETE176public.distributionmirror = SELECT, INSERT, UPDATE, DELETE
177public.distributionsourcepackage = SELECT, INSERT, UPDATE, DELETE177public.distributionsourcepackage = SELECT, INSERT, UPDATE, DELETE
178public.distributionsourcepackagecache = SELECT, INSERT178public.distributionsourcepackagecache = SELECT, INSERT
179public.distroarchseriesfilter = SELECT, INSERT, UPDATE, DELETE
179public.distroseriesdifference = SELECT, INSERT, UPDATE180public.distroseriesdifference = SELECT, INSERT, UPDATE
180public.distroseriesdifferencemessage = SELECT, INSERT, UPDATE181public.distroseriesdifferencemessage = SELECT, INSERT, UPDATE
181public.distroserieslanguage = SELECT, INSERT, UPDATE182public.distroserieslanguage = SELECT, INSERT, UPDATE
@@ -2221,6 +2222,7 @@
2221public.distributionsourcepackage = SELECT, INSERT, UPDATE, DELETE2222public.distributionsourcepackage = SELECT, INSERT, UPDATE, DELETE
2222public.distributionmirror = SELECT, UPDATE2223public.distributionmirror = SELECT, UPDATE
2223public.distroarchseries = SELECT, UPDATE2224public.distroarchseries = SELECT, UPDATE
2225public.distroarchseriesfilter = SELECT, UPDATE
2224public.distroseries = SELECT, UPDATE2226public.distroseries = SELECT, UPDATE
2225public.emailaddress = SELECT, UPDATE, DELETE2227public.emailaddress = SELECT, UPDATE, DELETE
2226public.faq = SELECT, UPDATE2228public.faq = SELECT, UPDATE
@@ -2356,6 +2358,7 @@
2356public.commercialsubscription = SELECT, UPDATE2358public.commercialsubscription = SELECT, UPDATE
2357public.diff = SELECT, DELETE2359public.diff = SELECT, DELETE
2358public.distributionsourcepackagecache = SELECT, INSERT2360public.distributionsourcepackagecache = SELECT, INSERT
2361public.distroarchseriesfilter = SELECT
2359public.distroseries = SELECT, UPDATE2362public.distroseries = SELECT, UPDATE
2360public.emailaddress = SELECT, UPDATE, DELETE2363public.emailaddress = SELECT, UPDATE, DELETE
2361public.garbojobstate = SELECT, INSERT, UPDATE, DELETE2364public.garbojobstate = SELECT, INSERT, UPDATE, DELETE
23622365
=== 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-10 11:16:47 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Update the interface schema values due to circular imports.4"""Update the interface schema values due to circular imports.
@@ -496,6 +496,8 @@
496patch_reference_property(IDistroArchSeries, 'main_archive', IArchive)496patch_reference_property(IDistroArchSeries, 'main_archive', IArchive)
497patch_plain_parameter_type(497patch_plain_parameter_type(
498 IDistroArchSeries, 'setChrootFromBuild', 'livefsbuild', ILiveFSBuild)498 IDistroArchSeries, 'setChrootFromBuild', 'livefsbuild', ILiveFSBuild)
499patch_plain_parameter_type(
500 IDistroArchSeries, 'setSourceFilter', 'packageset', IPackageset)
499501
500# IGitRef502# IGitRef
501patch_reference_property(IGitRef, 'repository', IGitRepository)503patch_reference_property(IGitRef, 'repository', IGitRepository)
502504
=== 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-10 11:16:47 +0000
@@ -114,6 +114,7 @@
114 ('codeimport', 'owner'),114 ('codeimport', 'owner'),
115 ('codeimport', 'registrant'),115 ('codeimport', 'registrant'),
116 ('codeimportevent', 'person'),116 ('codeimportevent', 'person'),
117 ('distroarchseriesfilter', 'creator'),
117 ('faq', 'last_updated_by'),118 ('faq', 'last_updated_by'),
118 ('featureflagchangelogentry', 'person'),119 ('featureflagchangelogentry', 'person'),
119 ('gitactivity', 'changee'),120 ('gitactivity', 'changee'),
120121
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2019-07-01 12:48:37 +0000
+++ lib/lp/security.py 2019-09-10 11:16:47 +0000
@@ -229,6 +229,7 @@
229 IBinaryPackageReleaseDownloadCount,229 IBinaryPackageReleaseDownloadCount,
230 )230 )
231from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries231from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
232from lp.soyuz.interfaces.distroarchseriesfilter import IDistroArchSeriesFilter
232from lp.soyuz.interfaces.livefs import ILiveFS233from lp.soyuz.interfaces.livefs import ILiveFS
233from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild234from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild
234from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJob235from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJob
@@ -1468,6 +1469,24 @@
1468 or user.in_admin)1469 or user.in_admin)
14691470
14701471
1472class ViewDistroArchSeriesFilter(DelegatedAuthorization):
1473 permission = 'launchpad.View'
1474 usedfor = IDistroArchSeriesFilter
1475
1476 def __init__(self, obj):
1477 super(ViewDistroArchSeriesFilter, self).__init__(
1478 obj, obj.distroarchseries, 'launchpad.View')
1479
1480
1481class EditDistroArchSeriesFilter(DelegatedAuthorization):
1482 permission = 'launchpad.Edit'
1483 usedfor = IDistroArchSeriesFilter
1484
1485 def __init__(self, obj):
1486 super(EditDistroArchSeriesFilter, self).__init__(
1487 obj, obj.distroarchseries, 'launchpad.Moderate')
1488
1489
1471class ViewAnnouncement(AuthorizationBase):1490class ViewAnnouncement(AuthorizationBase):
1472 permission = 'launchpad.View'1491 permission = 'launchpad.View'
1473 usedfor = IAnnouncement1492 usedfor = IAnnouncement
14741493
=== 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-10 11:16:47 +0000
@@ -616,6 +616,26 @@
616 set_schema="lp.soyuz.interfaces.distroarchseries.IPocketChroot"/>616 set_schema="lp.soyuz.interfaces.distroarchseries.IPocketChroot"/>
617 </class>617 </class>
618618
619 <!-- DistroArchSeriesFilter -->
620 <class
621 class="lp.soyuz.model.distroarchseriesfilter.DistroArchSeriesFilter">
622 <allow
623 interface="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilterView" />
624 <require
625 permission="launchpad.Edit"
626 interface="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilterEdit" />
627 </class>
628 <securedutility
629 class="lp.soyuz.model.distroarchseriesfilter.DistroArchSeriesFilterSet"
630 provides="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilterSet">
631 <allow
632 interface="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilterSet" />
633 </securedutility>
634 <subscriber
635 for="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilter
636 lazr.lifecycle.interfaces.IObjectModifiedEvent"
637 handler="lp.soyuz.model.distroarchseriesfilter.distro_arch_series_filter_modified" />
638
619 <!-- Component -->639 <!-- Component -->
620640
621 <class641 <class
622642
=== 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-10 11:16:47 +0000
@@ -255,3 +255,37 @@
255 >>> print_architectures(hoary.buildable_architectures)255 >>> print_architectures(hoary.buildable_architectures)
256 The Hoary Hedgehog Release for hppa (hppa) (ppa)256 The Hoary Hedgehog Release for hppa (hppa) (ppa)
257 The Hoary Hedgehog Release for i386 (386) (official, ppa)257 The Hoary Hedgehog Release for i386 (386) (official, ppa)
258
259An architecture can have an associated filter that controls which packages
260are included in it. It has an `isSourceIncluded` method that allows
261querying inclusion by `SourcePackageName`.
262
263 >>> from lp.soyuz.enums import DistroArchSeriesFilterSense
264
265 >>> spns = [factory.makeSourcePackageName() for _ in range(3)]
266 >>> hoary.getDistroArchSeries('i386').isSourceIncluded(spns[0])
267 True
268
269 >>> packageset_include = factory.makePackageset(distroseries=hoary)
270 >>> packageset_include.add(spns[:2])
271 >>> hoary.getDistroArchSeries('i386').setSourceFilter(
272 ... packageset_include, DistroArchSeriesFilterSense.INCLUDE,
273 ... factory.makePerson())
274 >>> packageset_exclude = factory.makePackageset(distroseries=hoary)
275 >>> packageset_exclude.add(spns[1:])
276 >>> hoary.getDistroArchSeries('hppa').setSourceFilter(
277 ... packageset_exclude, DistroArchSeriesFilterSense.EXCLUDE,
278 ... factory.makePerson())
279
280 >>> hoary.getDistroArchSeries('i386').isSourceIncluded(spns[0])
281 True
282 >>> hoary.getDistroArchSeries('i386').isSourceIncluded(spns[1])
283 True
284 >>> hoary.getDistroArchSeries('i386').isSourceIncluded(spns[2])
285 False
286 >>> hoary.getDistroArchSeries('hppa').isSourceIncluded(spns[0])
287 True
288 >>> hoary.getDistroArchSeries('hppa').isSourceIncluded(spns[1])
289 False
290 >>> hoary.getDistroArchSeries('hppa').isSourceIncluded(spns[2])
291 False
258292
=== 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-10 11:16:47 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010-2017 Canonical Ltd. This software is licensed under the1# Copyright 2010-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Enumerations used in the lp/soyuz modules."""4"""Enumerations used in the lp/soyuz modules."""
@@ -13,6 +13,7 @@
13 'archive_suffixes',13 'archive_suffixes',
14 'BinaryPackageFileType',14 'BinaryPackageFileType',
15 'BinaryPackageFormat',15 'BinaryPackageFormat',
16 'DistroArchSeriesFilterSense',
16 'IndexCompressionType',17 'IndexCompressionType',
17 'PackageCopyPolicy',18 'PackageCopyPolicy',
18 'PackageCopyStatus',19 'PackageCopyStatus',
@@ -597,3 +598,18 @@
597 GZIP = DBItem(1, "gzip")598 GZIP = DBItem(1, "gzip")
598 BZIP2 = DBItem(2, "bzip2")599 BZIP2 = DBItem(2, "bzip2")
599 XZ = DBItem(3, "xz")600 XZ = DBItem(3, "xz")
601
602
603class DistroArchSeriesFilterSense(DBEnumeratedType):
604
605 INCLUDE = DBItem(1, """
606 Include
607
608 Packages in this package set are included in the distro arch series.
609 """)
610
611 EXCLUDE = DBItem(2, """
612 Exclude
613
614 Packages in this package set are excluded from the distro arch series.
615 """)
600616
=== 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-10 11:16:47 +0000
@@ -7,6 +7,7 @@
77
8__all__ = [8__all__ = [
9 'ChrootNotPublic',9 'ChrootNotPublic',
10 'FilterSeriesMismatch',
10 'IDistroArchSeries',11 'IDistroArchSeries',
11 'InvalidChrootUploaded',12 'InvalidChrootUploaded',
12 'IPocketChroot',13 'IPocketChroot',
@@ -65,6 +66,19 @@
65 "Cannot set chroot from a private build.")66 "Cannot set chroot from a private build.")
6667
6768
69@error_status(httplib.BAD_REQUEST)
70class FilterSeriesMismatch(Exception):
71 """DAS and packageset distroseries do not match when setting a filter."""
72
73 def __init__(self, distroarchseries, packageset):
74 super(Exception, self).__init__(
75 "The requested package set is for %s and cannot be set as a "
76 "filter for %s %s." % (
77 packageset.distroseries.fullseriesname,
78 distroarchseries.distroseries.fullseriesname,
79 distroarchseries.architecturetag))
80
81
68class IDistroArchSeriesPublic(IHasBuildRecords, IHasOwner):82class IDistroArchSeriesPublic(IHasBuildRecords, IHasOwner):
69 """Public attributes for a DistroArchSeries."""83 """Public attributes for a DistroArchSeries."""
7084
@@ -216,6 +230,21 @@
216 this distro arch series.230 this distro arch series.
217 """231 """
218232
233 def getSourceFilter():
234 """Get the filter for packages to build for this architecture, if any.
235
236 Packages are normally built for all available architectures, subject
237 to any constraints in their `Architecture` field. If a filter is
238 set, then it applies the additional constraint that packages not
239 included by the filter will not be built for this architecture.
240 """
241
242 def isSourceIncluded(sourcepackagename):
243 """Is this source package included in this distro arch series?
244
245 :param sourcepackagename: An `ISourcePackageName` to check.
246 """
247
219248
220class IDistroArchSeriesModerate(Interface):249class IDistroArchSeriesModerate(Interface):
221250
@@ -263,6 +292,36 @@
263 tarball".292 tarball".
264 """293 """
265294
295 def setSourceFilter(packageset, sense, creator):
296 """Set a filter for packages to build for this architecture.
297
298 Packages are normally built for all available architectures, subject
299 to any constraints in their `Architecture` field. If a filter is
300 set, then it applies the additional constraint that packages not
301 included by the filter will not be built for this architecture.
302
303 If the sense of the filter is "Include", then the filter only
304 includes packages in the given package set. If the sense of the
305 filter is "Exclude", then the filter only includes packages not in
306 the given package set.
307
308 Later changes to the given package set will also affect any filters
309 using it.
310
311 :param packageset: An `IPackageset` to use as a filter.
312 :param sense: A `DistroArchSeriesFilterSense` item indicating
313 whether the filter includes or excludes packages.
314 :param creator: The `IPerson` who is creating this filter.
315 """
316
317 def removeSourceFilter():
318 """Remove any filter for packages to build for this architecture.
319
320 This causes packages to be built for this architecture when they
321 might previously have been filtered, subject to any constraints in
322 their `Architecture` field.
323 """
324
266325
267class IDistroArchSeries(IDistroArchSeriesPublic, IDistroArchSeriesModerate):326class IDistroArchSeries(IDistroArchSeriesPublic, IDistroArchSeriesModerate):
268 """An architecture for a distroseries."""327 """An architecture for a distroseries."""
269328
=== 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-10 11:16:47 +0000
@@ -0,0 +1,127 @@
1# Copyright 2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Distro arch series filter interfaces."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 'IDistroArchSeriesFilter',
11 'IDistroArchSeriesFilterSet',
12 'NoSuchDistroArchSeriesFilter',
13 ]
14
15from lazr.restful.fields import Reference
16from zope.interface import Interface
17from zope.schema import (
18 Choice,
19 Datetime,
20 Int,
21 )
22
23from lp import _
24from lp.app.errors import NameLookupFailed
25from lp.services.fields import PublicPersonChoice
26from lp.soyuz.enums import DistroArchSeriesFilterSense
27from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
28from lp.soyuz.interfaces.packageset import IPackageset
29
30
31class NoSuchDistroArchSeriesFilter(NameLookupFailed):
32 """Raised when we try to look up a nonexistent DistroArchSeriesFilter."""
33 _message_prefix = (
34 "The given distro arch series has no DistroArchSeriesFilter")
35
36
37class IDistroArchSeriesFilterView(Interface):
38 """`IDistroArchSeriesFilter` attributes that require launchpad.View."""
39
40 id = Int(title=_("ID"), readonly=True, required=True)
41
42 distroarchseries = Reference(
43 title=_("Distro arch series"), required=True, readonly=True,
44 schema=IDistroArchSeries,
45 description=_("The distro arch series that this filter is for."))
46
47 packageset = Reference(
48 title=_("Package set"), required=True, readonly=True,
49 schema=IPackageset,
50 description=_(
51 "The package set to be included in or excluded from this distro "
52 "arch series."))
53
54 sense = Choice(
55 title=_("Sense"),
56 vocabulary=DistroArchSeriesFilterSense, required=True, readonly=True,
57 description=_(
58 "Whether the filter represents packages to include or exclude "
59 "from the distro arch series."))
60
61 creator = PublicPersonChoice(
62 title=_("Creator"), required=True, readonly=True,
63 vocabulary="ValidPerson",
64 description=_("The user who created this filter."))
65
66 date_created = Datetime(
67 title=_("Date created"), required=True, readonly=True,
68 description=_("The time when this filter was created."))
69
70 date_last_modified = Datetime(
71 title=_("Date last modified"), required=True, readonly=True,
72 description=_("The time when this filter was last modified."))
73
74 def isSourceIncluded(sourcepackagename):
75 """Is this source package name included by this filter?
76
77 If the sense of the filter is INCLUDE, then this returns True iff
78 the source package name is included in the related package set;
79 otherwise, it returns True iff the source package name is not
80 included in the related package set.
81
82 :param sourcepackagename: an `ISourcePackageName`.
83 :return: True if the source is included by this filter, otherwise
84 False.
85 """
86
87
88class IDistroArchSeriesFilterEdit(Interface):
89 """`IDistroArchSeriesFilter` attributes that require launchpad.Edit."""
90
91 def destroySelf():
92 """Delete this filter."""
93
94
95class IDistroArchSeriesFilter(
96 IDistroArchSeriesFilterView, IDistroArchSeriesFilterEdit):
97 """A filter for packages to be included in or excluded from a DAS.
98
99 Since package sets can include other package sets, a single package set
100 is flexible enough for this. However, one might reasonably want to
101 either include some packages ("this architecture is obsolescent or
102 experimental and we only want to build a few packages for it") or
103 exclude some packages ("this architecture can't handle some packages so
104 we want to make them go away centrally").
105 """
106
107
108class IDistroArchSeriesFilterSet(Interface):
109 """An interface for multiple distro arch series filters."""
110
111 def new(distroarchseries, packageset, sense, creator, date_created=None):
112 """Create an `IDistroArchSeriesFilter`."""
113
114 def getByDistroArchSeries(distroarchseries):
115 """Return the filter for this distro arch series, if any.
116
117 :param distroarchseries: The `IDistroArchSeries` to search for.
118 :return: An `IDistroArchSeriesFilter` instance.
119 :raises NoSuchDistroArchSeriesFilter: if no filter is found.
120 """
121
122 def findByPackageset(packageset):
123 """Return any filters using this package set.
124
125 :param packageset: The `IPackageset` to search for.
126 :return: A `ResultSet` of `IDistroArchSeriesFilter` instances.
127 """
0128
=== 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-10 11:16:47 +0000
@@ -55,10 +55,14 @@
55from lp.soyuz.interfaces.buildrecords import IHasBuildRecords55from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
56from lp.soyuz.interfaces.distroarchseries import (56from lp.soyuz.interfaces.distroarchseries import (
57 ChrootNotPublic,57 ChrootNotPublic,
58 FilterSeriesMismatch,
58 IDistroArchSeries,59 IDistroArchSeries,
59 InvalidChrootUploaded,60 InvalidChrootUploaded,
60 IPocketChroot,61 IPocketChroot,
61 )62 )
63from lp.soyuz.interfaces.distroarchseriesfilter import (
64 IDistroArchSeriesFilterSet,
65 )
62from lp.soyuz.interfaces.publishing import active_publishing_status66from lp.soyuz.interfaces.publishing import active_publishing_status
63from lp.soyuz.model.binarypackagename import BinaryPackageName67from lp.soyuz.model.binarypackagename import BinaryPackageName
64from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease68from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
@@ -341,6 +345,32 @@
341 def main_archive(self):345 def main_archive(self):
342 return self.distroseries.distribution.main_archive346 return self.distroseries.distribution.main_archive
343347
348 def getSourceFilter(self):
349 """See `IDistroArchSeries`."""
350 return getUtility(IDistroArchSeriesFilterSet).getByDistroArchSeries(
351 self)
352
353 def setSourceFilter(self, packageset, sense, creator):
354 """See `IDistroArchSeries`."""
355 if self.distroseries != packageset.distroseries:
356 raise FilterSeriesMismatch(self, packageset)
357 self.removeSourceFilter()
358 getUtility(IDistroArchSeriesFilterSet).new(
359 self, packageset, sense, creator)
360
361 def removeSourceFilter(self):
362 """See `IDistroArchSeries`."""
363 dasf = self.getSourceFilter()
364 if dasf is not None:
365 dasf.destroySelf()
366
367 def isSourceIncluded(self, sourcepackagename):
368 """See `IDistroArchSeries`."""
369 dasf = self.getSourceFilter()
370 if dasf is None:
371 return True
372 return dasf.isSourceIncluded(sourcepackagename)
373
344374
345@implementer(IPocketChroot)375@implementer(IPocketChroot)
346class PocketChroot(SQLBase):376class PocketChroot(SQLBase):
347377
=== 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-10 11:16:47 +0000
@@ -0,0 +1,124 @@
1# Copyright 2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Distro arch series filters."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 'DistroArchSeriesFilter',
11 ]
12
13import pytz
14from storm.locals import (
15 DateTime,
16 Int,
17 Reference,
18 Storm,
19 )
20from zope.interface import implementer
21from zope.security.proxy import removeSecurityProxy
22
23from lp.services.database.constants import (
24 DEFAULT,
25 UTC_NOW,
26 )
27from lp.services.database.enumcol import DBEnum
28from lp.services.database.interfaces import (
29 IMasterStore,
30 IStore,
31 )
32from lp.soyuz.enums import DistroArchSeriesFilterSense
33from lp.soyuz.interfaces.distroarchseriesfilter import (
34 IDistroArchSeriesFilter,
35 IDistroArchSeriesFilterSet,
36 )
37
38
39def distro_arch_series_filter_modified(pss, event):
40 """Update date_last_modified when a `DistroArchSeriesFilter` is modified.
41
42 This method is registered as a subscriber to `IObjectModifiedEvent`
43 events on `DistroArchSeriesFilter`s.
44 """
45 removeSecurityProxy(pss).date_last_modified = UTC_NOW
46
47
48@implementer(IDistroArchSeriesFilter)
49class DistroArchSeriesFilter(Storm):
50 """See `IDistroArchSeriesFilter`."""
51
52 __storm_table__ = "DistroArchSeriesFilter"
53
54 id = Int(primary=True)
55
56 distroarchseries_id = Int(name="distroarchseries", allow_none=False)
57 distroarchseries = Reference(distroarchseries_id, "DistroArchSeries.id")
58
59 packageset_id = Int(name="packageset", allow_none=False)
60 packageset = Reference(packageset_id, "Packageset.id")
61
62 sense = DBEnum(enum=DistroArchSeriesFilterSense, allow_none=False)
63
64 creator_id = Int(name="creator", allow_none=False)
65 creator = Reference(creator_id, "Person.id")
66
67 date_created = DateTime(
68 name="date_created", tzinfo=pytz.UTC, allow_none=False)
69 date_last_modified = DateTime(
70 name="date_last_modified", tzinfo=pytz.UTC, allow_none=False)
71
72 def __init__(self, distroarchseries, packageset, sense, creator,
73 date_created=DEFAULT):
74 """Construct a `DistroArchSeriesFilter`."""
75 super(DistroArchSeriesFilter, self).__init__()
76 self.distroarchseries = distroarchseries
77 self.packageset = packageset
78 self.sense = sense
79 self.creator = creator
80 self.date_created = date_created
81 self.date_last_modified = date_created
82
83 def __repr__(self):
84 return "<DistroArchSeriesFilter for %s>" % self.distroarchseries.title
85
86 def isSourceIncluded(self, sourcepackagename):
87 """See `IDistroArchSeriesFilter`."""
88 return (
89 (self.sense == DistroArchSeriesFilterSense.INCLUDE) ==
90 self.packageset.isSourceIncluded(sourcepackagename))
91
92 def destroySelf(self):
93 """See `IDistroArchSeriesFilter`."""
94 IStore(DistroArchSeriesFilter).remove(self)
95
96
97@implementer(IDistroArchSeriesFilterSet)
98class DistroArchSeriesFilterSet:
99 """See `IDistroArchSeriesFilterSet`."""
100
101 def new(self, distroarchseries, packageset, sense, creator,
102 date_created=DEFAULT):
103 """See `IDistroArchSeriesFilterSet`.
104
105 The caller must check that the creator has suitable permissions on
106 `distroarchseries`.
107 """
108 store = IMasterStore(DistroArchSeriesFilter)
109 dasf = DistroArchSeriesFilter(
110 distroarchseries, packageset, sense, creator,
111 date_created=date_created)
112 store.add(dasf)
113 return dasf
114
115 def getByDistroArchSeries(self, distroarchseries):
116 """See `IDistroArchSeriesFilterSet`."""
117 return IStore(DistroArchSeriesFilter).find(
118 DistroArchSeriesFilter,
119 DistroArchSeriesFilter.distroarchseries == distroarchseries).one()
120
121 def findByPackageset(self, packageset):
122 return IStore(DistroArchSeriesFilter).find(
123 DistroArchSeriesFilter,
124 DistroArchSeriesFilter.packageset == packageset)
0125
=== 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-10 11:16:47 +0000
@@ -0,0 +1,128 @@
1# Copyright 2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test distro arch series filters."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9
10from testtools.matchers import MatchesStructure
11from zope.component import getUtility
12from zope.security.interfaces import Unauthorized
13
14from lp.services.database.interfaces import IStore
15from lp.services.database.sqlbase import get_transaction_timestamp
16from lp.soyuz.enums import DistroArchSeriesFilterSense
17from lp.soyuz.interfaces.distroarchseriesfilter import (
18 IDistroArchSeriesFilter,
19 IDistroArchSeriesFilterSet,
20 )
21from lp.testing import (
22 person_logged_in,
23 TestCaseWithFactory,
24 )
25from lp.testing.layers import (
26 DatabaseFunctionalLayer,
27 ZopelessDatabaseLayer,
28 )
29
30
31class TestDistroArchSeriesFilter(TestCaseWithFactory):
32
33 layer = DatabaseFunctionalLayer
34
35 def test_implements_interfaces(self):
36 # DistroArchSeriesFilter implements IDistroArchSeriesFilter.
37 dasf = self.factory.makeDistroArchSeriesFilter()
38 self.assertProvides(dasf, IDistroArchSeriesFilter)
39
40 def test___repr__(self):
41 # `DistroArchSeriesFilter` objects have an informative __repr__.
42 das = self.factory.makeDistroArchSeries()
43 dasf = self.factory.makeDistroArchSeriesFilter(distroarchseries=das)
44 self.assertEqual(
45 "<DistroArchSeriesFilter for %s>" % das.title, repr(dasf))
46
47 def test_isSourceIncluded_include(self):
48 # INCLUDE filters report that a source is included if it is in the
49 # packageset.
50 spns = [self.factory.makeSourcePackageName() for _ in range(3)]
51 dasf = self.factory.makeDistroArchSeriesFilter(
52 sense=DistroArchSeriesFilterSense.INCLUDE)
53 dasf.packageset.add(spns[:2])
54 self.assertTrue(dasf.isSourceIncluded(spns[0]))
55 self.assertTrue(dasf.isSourceIncluded(spns[1]))
56 self.assertFalse(dasf.isSourceIncluded(spns[2]))
57
58 def test_isSourceIncluded_exclude(self):
59 # EXCLUDE filters report that a source is included if it is not in
60 # the packageset.
61 spns = [self.factory.makeSourcePackageName() for _ in range(3)]
62 dasf = self.factory.makeDistroArchSeriesFilter(
63 sense=DistroArchSeriesFilterSense.EXCLUDE)
64 dasf.packageset.add(spns[:2])
65 self.assertFalse(dasf.isSourceIncluded(spns[0]))
66 self.assertFalse(dasf.isSourceIncluded(spns[1]))
67 self.assertTrue(dasf.isSourceIncluded(spns[2]))
68
69 def test_destroySelf_unauthorized(self):
70 # Ordinary users cannot delete a filter.
71 das = self.factory.makeDistroArchSeries()
72 self.factory.makeDistroArchSeriesFilter(distroarchseries=das)
73 dasf = das.getSourceFilter()
74 with person_logged_in(self.factory.makePerson()):
75 self.assertRaises(Unauthorized, getattr, dasf, "destroySelf")
76
77 def test_destroySelf(self):
78 # Owners of the DAS's archive can delete a filter.
79 das = self.factory.makeDistroArchSeries()
80 self.factory.makeDistroArchSeriesFilter(distroarchseries=das)
81 dasf = das.getSourceFilter()
82 with person_logged_in(das.main_archive.owner):
83 dasf.destroySelf()
84 self.assertIsNone(das.getSourceFilter())
85
86
87class TestDistroArchSeriesFilterSet(TestCaseWithFactory):
88
89 layer = ZopelessDatabaseLayer
90
91 def test_class_implements_interface(self):
92 # The DistroArchSeriesFilterSet class implements
93 # IDistroArchSeriesFilterSet.
94 self.assertProvides(
95 getUtility(IDistroArchSeriesFilterSet), IDistroArchSeriesFilterSet)
96
97 def test_new(self):
98 # The arguments passed when creating a filter are present on the new
99 # object.
100 das = self.factory.makeDistroArchSeries()
101 packageset = self.factory.makePackageset(distroseries=das.distroseries)
102 sense = DistroArchSeriesFilterSense.EXCLUDE
103 creator = self.factory.makePerson()
104 dasf = getUtility(IDistroArchSeriesFilterSet).new(
105 distroarchseries=das, packageset=packageset, sense=sense,
106 creator=creator)
107 now = get_transaction_timestamp(IStore(dasf))
108 self.assertThat(dasf, MatchesStructure.byEquality(
109 distroarchseries=das, packageset=packageset, sense=sense,
110 creator=creator, date_created=now, date_last_modified=now))
111
112 def test_getByDistroArchSeries(self):
113 # getByDistroArchSeries returns the filter for a DAS, if any.
114 das = self.factory.makeDistroArchSeries()
115 dasf_set = getUtility(IDistroArchSeriesFilterSet)
116 self.assertIsNone(dasf_set.getByDistroArchSeries(das))
117 dasf = self.factory.makeDistroArchSeriesFilter(distroarchseries=das)
118 self.assertEqual(dasf, dasf_set.getByDistroArchSeries(das))
119
120 def test_findByPackageset(self):
121 # findByPackageset returns any filters using a package set.
122 packageset = self.factory.makePackageset()
123 dasf_set = getUtility(IDistroArchSeriesFilterSet)
124 self.assertContentEqual([], dasf_set.findByPackageset(packageset))
125 dasfs = [
126 self.factory.makeDistroArchSeriesFilter(packageset=packageset)
127 for _ in range(2)]
128 self.assertContentEqual(dasfs, dasf_set.findByPackageset(packageset))
0129
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2019-09-05 13:23:34 +0000
+++ lib/lp/testing/factory.py 2019-09-10 11:16:47 +0000
@@ -297,6 +297,7 @@
297 ArchivePurpose,297 ArchivePurpose,
298 BinaryPackageFileType,298 BinaryPackageFileType,
299 BinaryPackageFormat,299 BinaryPackageFormat,
300 DistroArchSeriesFilterSense,
300 PackageDiffStatus,301 PackageDiffStatus,
301 PackagePublishingPriority,302 PackagePublishingPriority,
302 PackagePublishingStatus,303 PackagePublishingStatus,
@@ -326,6 +327,7 @@
326from lp.soyuz.model.distributionsourcepackagecache import (327from lp.soyuz.model.distributionsourcepackagecache import (
327 DistributionSourcePackageCache,328 DistributionSourcePackageCache,
328 )329 )
330from lp.soyuz.model.distroarchseriesfilter import DistroArchSeriesFilter
329from lp.soyuz.model.files import BinaryPackageFile331from lp.soyuz.model.files import BinaryPackageFile
330from lp.soyuz.model.livefsbuild import LiveFSFile332from lp.soyuz.model.livefsbuild import LiveFSFile
331from lp.soyuz.model.packagediff import PackageDiff333from lp.soyuz.model.packagediff import PackageDiff
@@ -4202,6 +4204,27 @@
4202 run_with_login(owner, lambda: package_set.add(packages))4204 run_with_login(owner, lambda: package_set.add(packages))
4203 return package_set4205 return package_set
42044206
4207 def makeDistroArchSeriesFilter(self, distroarchseries=None,
4208 packageset=None,
4209 sense=DistroArchSeriesFilterSense.INCLUDE,
4210 creator=None, date_created=DEFAULT):
4211 """Make a new `DistroArchSeriesFilter`."""
4212 if distroarchseries is None:
4213 if packageset is not None:
4214 distroseries = packageset.distroseries
4215 else:
4216 distroseries = None
4217 distroarchseries = self.makeDistroArchSeries(
4218 distroseries=distroseries)
4219 if packageset is None:
4220 packageset = self.makePackageset(
4221 distroseries=distroarchseries.distroseries)
4222 if creator is None:
4223 creator = self.makePerson()
4224 return DistroArchSeriesFilter(
4225 distroarchseries=distroarchseries, packageset=packageset,
4226 sense=sense, creator=creator, date_created=date_created)
4227
4205 def getAnyPocket(self):4228 def getAnyPocket(self):
4206 return PackagePublishingPocket.BACKPORTS4229 return PackagePublishingPocket.BACKPORTS
42074230