Merge lp:~cjwatson/launchpad/das-filter-model into lp:launchpad
- das-filter-model
- Merge into devel
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 |
Related bugs: |
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:/
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
1 | === modified file 'database/schema/security.cfg' |
2 | --- database/schema/security.cfg 2019-08-09 12:04:04 +0000 |
3 | +++ database/schema/security.cfg 2019-09-10 11:16:47 +0000 |
4 | @@ -176,6 +176,7 @@ |
5 | public.distributionmirror = SELECT, INSERT, UPDATE, DELETE |
6 | public.distributionsourcepackage = SELECT, INSERT, UPDATE, DELETE |
7 | public.distributionsourcepackagecache = SELECT, INSERT |
8 | +public.distroarchseriesfilter = SELECT, INSERT, UPDATE, DELETE |
9 | public.distroseriesdifference = SELECT, INSERT, UPDATE |
10 | public.distroseriesdifferencemessage = SELECT, INSERT, UPDATE |
11 | public.distroserieslanguage = SELECT, INSERT, UPDATE |
12 | @@ -2221,6 +2222,7 @@ |
13 | public.distributionsourcepackage = SELECT, INSERT, UPDATE, DELETE |
14 | public.distributionmirror = SELECT, UPDATE |
15 | public.distroarchseries = SELECT, UPDATE |
16 | +public.distroarchseriesfilter = SELECT, UPDATE |
17 | public.distroseries = SELECT, UPDATE |
18 | public.emailaddress = SELECT, UPDATE, DELETE |
19 | public.faq = SELECT, UPDATE |
20 | @@ -2356,6 +2358,7 @@ |
21 | public.commercialsubscription = SELECT, UPDATE |
22 | public.diff = SELECT, DELETE |
23 | public.distributionsourcepackagecache = SELECT, INSERT |
24 | +public.distroarchseriesfilter = SELECT |
25 | public.distroseries = SELECT, UPDATE |
26 | public.emailaddress = SELECT, UPDATE, DELETE |
27 | public.garbojobstate = SELECT, INSERT, UPDATE, DELETE |
28 | |
29 | === modified file 'lib/lp/_schema_circular_imports.py' |
30 | --- lib/lp/_schema_circular_imports.py 2018-09-25 16:41:21 +0000 |
31 | +++ lib/lp/_schema_circular_imports.py 2019-09-10 11:16:47 +0000 |
32 | @@ -1,4 +1,4 @@ |
33 | -# Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
34 | +# Copyright 2009-2019 Canonical Ltd. This software is licensed under the |
35 | # GNU Affero General Public License version 3 (see the file LICENSE). |
36 | |
37 | """Update the interface schema values due to circular imports. |
38 | @@ -496,6 +496,8 @@ |
39 | patch_reference_property(IDistroArchSeries, 'main_archive', IArchive) |
40 | patch_plain_parameter_type( |
41 | IDistroArchSeries, 'setChrootFromBuild', 'livefsbuild', ILiveFSBuild) |
42 | +patch_plain_parameter_type( |
43 | + IDistroArchSeries, 'setSourceFilter', 'packageset', IPackageset) |
44 | |
45 | # IGitRef |
46 | patch_reference_property(IGitRef, 'repository', IGitRepository) |
47 | |
48 | === modified file 'lib/lp/registry/scripts/closeaccount.py' |
49 | --- lib/lp/registry/scripts/closeaccount.py 2019-08-09 12:04:04 +0000 |
50 | +++ lib/lp/registry/scripts/closeaccount.py 2019-09-10 11:16:47 +0000 |
51 | @@ -114,6 +114,7 @@ |
52 | ('codeimport', 'owner'), |
53 | ('codeimport', 'registrant'), |
54 | ('codeimportevent', 'person'), |
55 | + ('distroarchseriesfilter', 'creator'), |
56 | ('faq', 'last_updated_by'), |
57 | ('featureflagchangelogentry', 'person'), |
58 | ('gitactivity', 'changee'), |
59 | |
60 | === modified file 'lib/lp/security.py' |
61 | --- lib/lp/security.py 2019-07-01 12:48:37 +0000 |
62 | +++ lib/lp/security.py 2019-09-10 11:16:47 +0000 |
63 | @@ -229,6 +229,7 @@ |
64 | IBinaryPackageReleaseDownloadCount, |
65 | ) |
66 | from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries |
67 | +from lp.soyuz.interfaces.distroarchseriesfilter import IDistroArchSeriesFilter |
68 | from lp.soyuz.interfaces.livefs import ILiveFS |
69 | from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild |
70 | from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJob |
71 | @@ -1468,6 +1469,24 @@ |
72 | or user.in_admin) |
73 | |
74 | |
75 | +class ViewDistroArchSeriesFilter(DelegatedAuthorization): |
76 | + permission = 'launchpad.View' |
77 | + usedfor = IDistroArchSeriesFilter |
78 | + |
79 | + def __init__(self, obj): |
80 | + super(ViewDistroArchSeriesFilter, self).__init__( |
81 | + obj, obj.distroarchseries, 'launchpad.View') |
82 | + |
83 | + |
84 | +class EditDistroArchSeriesFilter(DelegatedAuthorization): |
85 | + permission = 'launchpad.Edit' |
86 | + usedfor = IDistroArchSeriesFilter |
87 | + |
88 | + def __init__(self, obj): |
89 | + super(EditDistroArchSeriesFilter, self).__init__( |
90 | + obj, obj.distroarchseries, 'launchpad.Moderate') |
91 | + |
92 | + |
93 | class ViewAnnouncement(AuthorizationBase): |
94 | permission = 'launchpad.View' |
95 | usedfor = IAnnouncement |
96 | |
97 | === modified file 'lib/lp/soyuz/configure.zcml' |
98 | --- lib/lp/soyuz/configure.zcml 2019-03-12 19:12:29 +0000 |
99 | +++ lib/lp/soyuz/configure.zcml 2019-09-10 11:16:47 +0000 |
100 | @@ -616,6 +616,26 @@ |
101 | set_schema="lp.soyuz.interfaces.distroarchseries.IPocketChroot"/> |
102 | </class> |
103 | |
104 | + <!-- DistroArchSeriesFilter --> |
105 | + <class |
106 | + class="lp.soyuz.model.distroarchseriesfilter.DistroArchSeriesFilter"> |
107 | + <allow |
108 | + interface="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilterView" /> |
109 | + <require |
110 | + permission="launchpad.Edit" |
111 | + interface="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilterEdit" /> |
112 | + </class> |
113 | + <securedutility |
114 | + class="lp.soyuz.model.distroarchseriesfilter.DistroArchSeriesFilterSet" |
115 | + provides="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilterSet"> |
116 | + <allow |
117 | + interface="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilterSet" /> |
118 | + </securedutility> |
119 | + <subscriber |
120 | + for="lp.soyuz.interfaces.distroarchseriesfilter.IDistroArchSeriesFilter |
121 | + lazr.lifecycle.interfaces.IObjectModifiedEvent" |
122 | + handler="lp.soyuz.model.distroarchseriesfilter.distro_arch_series_filter_modified" /> |
123 | + |
124 | <!-- Component --> |
125 | |
126 | <class |
127 | |
128 | === modified file 'lib/lp/soyuz/doc/distroarchseries.txt' |
129 | --- lib/lp/soyuz/doc/distroarchseries.txt 2019-02-13 14:39:18 +0000 |
130 | +++ lib/lp/soyuz/doc/distroarchseries.txt 2019-09-10 11:16:47 +0000 |
131 | @@ -255,3 +255,37 @@ |
132 | >>> print_architectures(hoary.buildable_architectures) |
133 | The Hoary Hedgehog Release for hppa (hppa) (ppa) |
134 | The Hoary Hedgehog Release for i386 (386) (official, ppa) |
135 | + |
136 | +An architecture can have an associated filter that controls which packages |
137 | +are included in it. It has an `isSourceIncluded` method that allows |
138 | +querying inclusion by `SourcePackageName`. |
139 | + |
140 | + >>> from lp.soyuz.enums import DistroArchSeriesFilterSense |
141 | + |
142 | + >>> spns = [factory.makeSourcePackageName() for _ in range(3)] |
143 | + >>> hoary.getDistroArchSeries('i386').isSourceIncluded(spns[0]) |
144 | + True |
145 | + |
146 | + >>> packageset_include = factory.makePackageset(distroseries=hoary) |
147 | + >>> packageset_include.add(spns[:2]) |
148 | + >>> hoary.getDistroArchSeries('i386').setSourceFilter( |
149 | + ... packageset_include, DistroArchSeriesFilterSense.INCLUDE, |
150 | + ... factory.makePerson()) |
151 | + >>> packageset_exclude = factory.makePackageset(distroseries=hoary) |
152 | + >>> packageset_exclude.add(spns[1:]) |
153 | + >>> hoary.getDistroArchSeries('hppa').setSourceFilter( |
154 | + ... packageset_exclude, DistroArchSeriesFilterSense.EXCLUDE, |
155 | + ... factory.makePerson()) |
156 | + |
157 | + >>> hoary.getDistroArchSeries('i386').isSourceIncluded(spns[0]) |
158 | + True |
159 | + >>> hoary.getDistroArchSeries('i386').isSourceIncluded(spns[1]) |
160 | + True |
161 | + >>> hoary.getDistroArchSeries('i386').isSourceIncluded(spns[2]) |
162 | + False |
163 | + >>> hoary.getDistroArchSeries('hppa').isSourceIncluded(spns[0]) |
164 | + True |
165 | + >>> hoary.getDistroArchSeries('hppa').isSourceIncluded(spns[1]) |
166 | + False |
167 | + >>> hoary.getDistroArchSeries('hppa').isSourceIncluded(spns[2]) |
168 | + False |
169 | |
170 | === modified file 'lib/lp/soyuz/enums.py' |
171 | --- lib/lp/soyuz/enums.py 2017-03-29 09:28:09 +0000 |
172 | +++ lib/lp/soyuz/enums.py 2019-09-10 11:16:47 +0000 |
173 | @@ -1,4 +1,4 @@ |
174 | -# Copyright 2010-2017 Canonical Ltd. This software is licensed under the |
175 | +# Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
176 | # GNU Affero General Public License version 3 (see the file LICENSE). |
177 | |
178 | """Enumerations used in the lp/soyuz modules.""" |
179 | @@ -13,6 +13,7 @@ |
180 | 'archive_suffixes', |
181 | 'BinaryPackageFileType', |
182 | 'BinaryPackageFormat', |
183 | + 'DistroArchSeriesFilterSense', |
184 | 'IndexCompressionType', |
185 | 'PackageCopyPolicy', |
186 | 'PackageCopyStatus', |
187 | @@ -597,3 +598,18 @@ |
188 | GZIP = DBItem(1, "gzip") |
189 | BZIP2 = DBItem(2, "bzip2") |
190 | XZ = DBItem(3, "xz") |
191 | + |
192 | + |
193 | +class DistroArchSeriesFilterSense(DBEnumeratedType): |
194 | + |
195 | + INCLUDE = DBItem(1, """ |
196 | + Include |
197 | + |
198 | + Packages in this package set are included in the distro arch series. |
199 | + """) |
200 | + |
201 | + EXCLUDE = DBItem(2, """ |
202 | + Exclude |
203 | + |
204 | + Packages in this package set are excluded from the distro arch series. |
205 | + """) |
206 | |
207 | === modified file 'lib/lp/soyuz/interfaces/distroarchseries.py' |
208 | --- lib/lp/soyuz/interfaces/distroarchseries.py 2019-07-30 11:38:18 +0000 |
209 | +++ lib/lp/soyuz/interfaces/distroarchseries.py 2019-09-10 11:16:47 +0000 |
210 | @@ -7,6 +7,7 @@ |
211 | |
212 | __all__ = [ |
213 | 'ChrootNotPublic', |
214 | + 'FilterSeriesMismatch', |
215 | 'IDistroArchSeries', |
216 | 'InvalidChrootUploaded', |
217 | 'IPocketChroot', |
218 | @@ -65,6 +66,19 @@ |
219 | "Cannot set chroot from a private build.") |
220 | |
221 | |
222 | +@error_status(httplib.BAD_REQUEST) |
223 | +class FilterSeriesMismatch(Exception): |
224 | + """DAS and packageset distroseries do not match when setting a filter.""" |
225 | + |
226 | + def __init__(self, distroarchseries, packageset): |
227 | + super(Exception, self).__init__( |
228 | + "The requested package set is for %s and cannot be set as a " |
229 | + "filter for %s %s." % ( |
230 | + packageset.distroseries.fullseriesname, |
231 | + distroarchseries.distroseries.fullseriesname, |
232 | + distroarchseries.architecturetag)) |
233 | + |
234 | + |
235 | class IDistroArchSeriesPublic(IHasBuildRecords, IHasOwner): |
236 | """Public attributes for a DistroArchSeries.""" |
237 | |
238 | @@ -216,6 +230,21 @@ |
239 | this distro arch series. |
240 | """ |
241 | |
242 | + def getSourceFilter(): |
243 | + """Get the filter for packages to build for this architecture, if any. |
244 | + |
245 | + Packages are normally built for all available architectures, subject |
246 | + to any constraints in their `Architecture` field. If a filter is |
247 | + set, then it applies the additional constraint that packages not |
248 | + included by the filter will not be built for this architecture. |
249 | + """ |
250 | + |
251 | + def isSourceIncluded(sourcepackagename): |
252 | + """Is this source package included in this distro arch series? |
253 | + |
254 | + :param sourcepackagename: An `ISourcePackageName` to check. |
255 | + """ |
256 | + |
257 | |
258 | class IDistroArchSeriesModerate(Interface): |
259 | |
260 | @@ -263,6 +292,36 @@ |
261 | tarball". |
262 | """ |
263 | |
264 | + def setSourceFilter(packageset, sense, creator): |
265 | + """Set a filter for packages to build for this architecture. |
266 | + |
267 | + Packages are normally built for all available architectures, subject |
268 | + to any constraints in their `Architecture` field. If a filter is |
269 | + set, then it applies the additional constraint that packages not |
270 | + included by the filter will not be built for this architecture. |
271 | + |
272 | + If the sense of the filter is "Include", then the filter only |
273 | + includes packages in the given package set. If the sense of the |
274 | + filter is "Exclude", then the filter only includes packages not in |
275 | + the given package set. |
276 | + |
277 | + Later changes to the given package set will also affect any filters |
278 | + using it. |
279 | + |
280 | + :param packageset: An `IPackageset` to use as a filter. |
281 | + :param sense: A `DistroArchSeriesFilterSense` item indicating |
282 | + whether the filter includes or excludes packages. |
283 | + :param creator: The `IPerson` who is creating this filter. |
284 | + """ |
285 | + |
286 | + def removeSourceFilter(): |
287 | + """Remove any filter for packages to build for this architecture. |
288 | + |
289 | + This causes packages to be built for this architecture when they |
290 | + might previously have been filtered, subject to any constraints in |
291 | + their `Architecture` field. |
292 | + """ |
293 | + |
294 | |
295 | class IDistroArchSeries(IDistroArchSeriesPublic, IDistroArchSeriesModerate): |
296 | """An architecture for a distroseries.""" |
297 | |
298 | === added file 'lib/lp/soyuz/interfaces/distroarchseriesfilter.py' |
299 | --- lib/lp/soyuz/interfaces/distroarchseriesfilter.py 1970-01-01 00:00:00 +0000 |
300 | +++ lib/lp/soyuz/interfaces/distroarchseriesfilter.py 2019-09-10 11:16:47 +0000 |
301 | @@ -0,0 +1,127 @@ |
302 | +# Copyright 2019 Canonical Ltd. This software is licensed under the |
303 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
304 | + |
305 | +"""Distro arch series filter interfaces.""" |
306 | + |
307 | +from __future__ import absolute_import, print_function, unicode_literals |
308 | + |
309 | +__metaclass__ = type |
310 | +__all__ = [ |
311 | + 'IDistroArchSeriesFilter', |
312 | + 'IDistroArchSeriesFilterSet', |
313 | + 'NoSuchDistroArchSeriesFilter', |
314 | + ] |
315 | + |
316 | +from lazr.restful.fields import Reference |
317 | +from zope.interface import Interface |
318 | +from zope.schema import ( |
319 | + Choice, |
320 | + Datetime, |
321 | + Int, |
322 | + ) |
323 | + |
324 | +from lp import _ |
325 | +from lp.app.errors import NameLookupFailed |
326 | +from lp.services.fields import PublicPersonChoice |
327 | +from lp.soyuz.enums import DistroArchSeriesFilterSense |
328 | +from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries |
329 | +from lp.soyuz.interfaces.packageset import IPackageset |
330 | + |
331 | + |
332 | +class NoSuchDistroArchSeriesFilter(NameLookupFailed): |
333 | + """Raised when we try to look up a nonexistent DistroArchSeriesFilter.""" |
334 | + _message_prefix = ( |
335 | + "The given distro arch series has no DistroArchSeriesFilter") |
336 | + |
337 | + |
338 | +class IDistroArchSeriesFilterView(Interface): |
339 | + """`IDistroArchSeriesFilter` attributes that require launchpad.View.""" |
340 | + |
341 | + id = Int(title=_("ID"), readonly=True, required=True) |
342 | + |
343 | + distroarchseries = Reference( |
344 | + title=_("Distro arch series"), required=True, readonly=True, |
345 | + schema=IDistroArchSeries, |
346 | + description=_("The distro arch series that this filter is for.")) |
347 | + |
348 | + packageset = Reference( |
349 | + title=_("Package set"), required=True, readonly=True, |
350 | + schema=IPackageset, |
351 | + description=_( |
352 | + "The package set to be included in or excluded from this distro " |
353 | + "arch series.")) |
354 | + |
355 | + sense = Choice( |
356 | + title=_("Sense"), |
357 | + vocabulary=DistroArchSeriesFilterSense, required=True, readonly=True, |
358 | + description=_( |
359 | + "Whether the filter represents packages to include or exclude " |
360 | + "from the distro arch series.")) |
361 | + |
362 | + creator = PublicPersonChoice( |
363 | + title=_("Creator"), required=True, readonly=True, |
364 | + vocabulary="ValidPerson", |
365 | + description=_("The user who created this filter.")) |
366 | + |
367 | + date_created = Datetime( |
368 | + title=_("Date created"), required=True, readonly=True, |
369 | + description=_("The time when this filter was created.")) |
370 | + |
371 | + date_last_modified = Datetime( |
372 | + title=_("Date last modified"), required=True, readonly=True, |
373 | + description=_("The time when this filter was last modified.")) |
374 | + |
375 | + def isSourceIncluded(sourcepackagename): |
376 | + """Is this source package name included by this filter? |
377 | + |
378 | + If the sense of the filter is INCLUDE, then this returns True iff |
379 | + the source package name is included in the related package set; |
380 | + otherwise, it returns True iff the source package name is not |
381 | + included in the related package set. |
382 | + |
383 | + :param sourcepackagename: an `ISourcePackageName`. |
384 | + :return: True if the source is included by this filter, otherwise |
385 | + False. |
386 | + """ |
387 | + |
388 | + |
389 | +class IDistroArchSeriesFilterEdit(Interface): |
390 | + """`IDistroArchSeriesFilter` attributes that require launchpad.Edit.""" |
391 | + |
392 | + def destroySelf(): |
393 | + """Delete this filter.""" |
394 | + |
395 | + |
396 | +class IDistroArchSeriesFilter( |
397 | + IDistroArchSeriesFilterView, IDistroArchSeriesFilterEdit): |
398 | + """A filter for packages to be included in or excluded from a DAS. |
399 | + |
400 | + Since package sets can include other package sets, a single package set |
401 | + is flexible enough for this. However, one might reasonably want to |
402 | + either include some packages ("this architecture is obsolescent or |
403 | + experimental and we only want to build a few packages for it") or |
404 | + exclude some packages ("this architecture can't handle some packages so |
405 | + we want to make them go away centrally"). |
406 | + """ |
407 | + |
408 | + |
409 | +class IDistroArchSeriesFilterSet(Interface): |
410 | + """An interface for multiple distro arch series filters.""" |
411 | + |
412 | + def new(distroarchseries, packageset, sense, creator, date_created=None): |
413 | + """Create an `IDistroArchSeriesFilter`.""" |
414 | + |
415 | + def getByDistroArchSeries(distroarchseries): |
416 | + """Return the filter for this distro arch series, if any. |
417 | + |
418 | + :param distroarchseries: The `IDistroArchSeries` to search for. |
419 | + :return: An `IDistroArchSeriesFilter` instance. |
420 | + :raises NoSuchDistroArchSeriesFilter: if no filter is found. |
421 | + """ |
422 | + |
423 | + def findByPackageset(packageset): |
424 | + """Return any filters using this package set. |
425 | + |
426 | + :param packageset: The `IPackageset` to search for. |
427 | + :return: A `ResultSet` of `IDistroArchSeriesFilter` instances. |
428 | + """ |
429 | |
430 | === modified file 'lib/lp/soyuz/model/distroarchseries.py' |
431 | --- lib/lp/soyuz/model/distroarchseries.py 2019-07-30 11:38:18 +0000 |
432 | +++ lib/lp/soyuz/model/distroarchseries.py 2019-09-10 11:16:47 +0000 |
433 | @@ -55,10 +55,14 @@ |
434 | from lp.soyuz.interfaces.buildrecords import IHasBuildRecords |
435 | from lp.soyuz.interfaces.distroarchseries import ( |
436 | ChrootNotPublic, |
437 | + FilterSeriesMismatch, |
438 | IDistroArchSeries, |
439 | InvalidChrootUploaded, |
440 | IPocketChroot, |
441 | ) |
442 | +from lp.soyuz.interfaces.distroarchseriesfilter import ( |
443 | + IDistroArchSeriesFilterSet, |
444 | + ) |
445 | from lp.soyuz.interfaces.publishing import active_publishing_status |
446 | from lp.soyuz.model.binarypackagename import BinaryPackageName |
447 | from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease |
448 | @@ -341,6 +345,32 @@ |
449 | def main_archive(self): |
450 | return self.distroseries.distribution.main_archive |
451 | |
452 | + def getSourceFilter(self): |
453 | + """See `IDistroArchSeries`.""" |
454 | + return getUtility(IDistroArchSeriesFilterSet).getByDistroArchSeries( |
455 | + self) |
456 | + |
457 | + def setSourceFilter(self, packageset, sense, creator): |
458 | + """See `IDistroArchSeries`.""" |
459 | + if self.distroseries != packageset.distroseries: |
460 | + raise FilterSeriesMismatch(self, packageset) |
461 | + self.removeSourceFilter() |
462 | + getUtility(IDistroArchSeriesFilterSet).new( |
463 | + self, packageset, sense, creator) |
464 | + |
465 | + def removeSourceFilter(self): |
466 | + """See `IDistroArchSeries`.""" |
467 | + dasf = self.getSourceFilter() |
468 | + if dasf is not None: |
469 | + dasf.destroySelf() |
470 | + |
471 | + def isSourceIncluded(self, sourcepackagename): |
472 | + """See `IDistroArchSeries`.""" |
473 | + dasf = self.getSourceFilter() |
474 | + if dasf is None: |
475 | + return True |
476 | + return dasf.isSourceIncluded(sourcepackagename) |
477 | + |
478 | |
479 | @implementer(IPocketChroot) |
480 | class PocketChroot(SQLBase): |
481 | |
482 | === added file 'lib/lp/soyuz/model/distroarchseriesfilter.py' |
483 | --- lib/lp/soyuz/model/distroarchseriesfilter.py 1970-01-01 00:00:00 +0000 |
484 | +++ lib/lp/soyuz/model/distroarchseriesfilter.py 2019-09-10 11:16:47 +0000 |
485 | @@ -0,0 +1,124 @@ |
486 | +# Copyright 2019 Canonical Ltd. This software is licensed under the |
487 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
488 | + |
489 | +"""Distro arch series filters.""" |
490 | + |
491 | +from __future__ import absolute_import, print_function, unicode_literals |
492 | + |
493 | +__metaclass__ = type |
494 | +__all__ = [ |
495 | + 'DistroArchSeriesFilter', |
496 | + ] |
497 | + |
498 | +import pytz |
499 | +from storm.locals import ( |
500 | + DateTime, |
501 | + Int, |
502 | + Reference, |
503 | + Storm, |
504 | + ) |
505 | +from zope.interface import implementer |
506 | +from zope.security.proxy import removeSecurityProxy |
507 | + |
508 | +from lp.services.database.constants import ( |
509 | + DEFAULT, |
510 | + UTC_NOW, |
511 | + ) |
512 | +from lp.services.database.enumcol import DBEnum |
513 | +from lp.services.database.interfaces import ( |
514 | + IMasterStore, |
515 | + IStore, |
516 | + ) |
517 | +from lp.soyuz.enums import DistroArchSeriesFilterSense |
518 | +from lp.soyuz.interfaces.distroarchseriesfilter import ( |
519 | + IDistroArchSeriesFilter, |
520 | + IDistroArchSeriesFilterSet, |
521 | + ) |
522 | + |
523 | + |
524 | +def distro_arch_series_filter_modified(pss, event): |
525 | + """Update date_last_modified when a `DistroArchSeriesFilter` is modified. |
526 | + |
527 | + This method is registered as a subscriber to `IObjectModifiedEvent` |
528 | + events on `DistroArchSeriesFilter`s. |
529 | + """ |
530 | + removeSecurityProxy(pss).date_last_modified = UTC_NOW |
531 | + |
532 | + |
533 | +@implementer(IDistroArchSeriesFilter) |
534 | +class DistroArchSeriesFilter(Storm): |
535 | + """See `IDistroArchSeriesFilter`.""" |
536 | + |
537 | + __storm_table__ = "DistroArchSeriesFilter" |
538 | + |
539 | + id = Int(primary=True) |
540 | + |
541 | + distroarchseries_id = Int(name="distroarchseries", allow_none=False) |
542 | + distroarchseries = Reference(distroarchseries_id, "DistroArchSeries.id") |
543 | + |
544 | + packageset_id = Int(name="packageset", allow_none=False) |
545 | + packageset = Reference(packageset_id, "Packageset.id") |
546 | + |
547 | + sense = DBEnum(enum=DistroArchSeriesFilterSense, allow_none=False) |
548 | + |
549 | + creator_id = Int(name="creator", allow_none=False) |
550 | + creator = Reference(creator_id, "Person.id") |
551 | + |
552 | + date_created = DateTime( |
553 | + name="date_created", tzinfo=pytz.UTC, allow_none=False) |
554 | + date_last_modified = DateTime( |
555 | + name="date_last_modified", tzinfo=pytz.UTC, allow_none=False) |
556 | + |
557 | + def __init__(self, distroarchseries, packageset, sense, creator, |
558 | + date_created=DEFAULT): |
559 | + """Construct a `DistroArchSeriesFilter`.""" |
560 | + super(DistroArchSeriesFilter, self).__init__() |
561 | + self.distroarchseries = distroarchseries |
562 | + self.packageset = packageset |
563 | + self.sense = sense |
564 | + self.creator = creator |
565 | + self.date_created = date_created |
566 | + self.date_last_modified = date_created |
567 | + |
568 | + def __repr__(self): |
569 | + return "<DistroArchSeriesFilter for %s>" % self.distroarchseries.title |
570 | + |
571 | + def isSourceIncluded(self, sourcepackagename): |
572 | + """See `IDistroArchSeriesFilter`.""" |
573 | + return ( |
574 | + (self.sense == DistroArchSeriesFilterSense.INCLUDE) == |
575 | + self.packageset.isSourceIncluded(sourcepackagename)) |
576 | + |
577 | + def destroySelf(self): |
578 | + """See `IDistroArchSeriesFilter`.""" |
579 | + IStore(DistroArchSeriesFilter).remove(self) |
580 | + |
581 | + |
582 | +@implementer(IDistroArchSeriesFilterSet) |
583 | +class DistroArchSeriesFilterSet: |
584 | + """See `IDistroArchSeriesFilterSet`.""" |
585 | + |
586 | + def new(self, distroarchseries, packageset, sense, creator, |
587 | + date_created=DEFAULT): |
588 | + """See `IDistroArchSeriesFilterSet`. |
589 | + |
590 | + The caller must check that the creator has suitable permissions on |
591 | + `distroarchseries`. |
592 | + """ |
593 | + store = IMasterStore(DistroArchSeriesFilter) |
594 | + dasf = DistroArchSeriesFilter( |
595 | + distroarchseries, packageset, sense, creator, |
596 | + date_created=date_created) |
597 | + store.add(dasf) |
598 | + return dasf |
599 | + |
600 | + def getByDistroArchSeries(self, distroarchseries): |
601 | + """See `IDistroArchSeriesFilterSet`.""" |
602 | + return IStore(DistroArchSeriesFilter).find( |
603 | + DistroArchSeriesFilter, |
604 | + DistroArchSeriesFilter.distroarchseries == distroarchseries).one() |
605 | + |
606 | + def findByPackageset(self, packageset): |
607 | + return IStore(DistroArchSeriesFilter).find( |
608 | + DistroArchSeriesFilter, |
609 | + DistroArchSeriesFilter.packageset == packageset) |
610 | |
611 | === added file 'lib/lp/soyuz/tests/test_distroarchseriesfilter.py' |
612 | --- lib/lp/soyuz/tests/test_distroarchseriesfilter.py 1970-01-01 00:00:00 +0000 |
613 | +++ lib/lp/soyuz/tests/test_distroarchseriesfilter.py 2019-09-10 11:16:47 +0000 |
614 | @@ -0,0 +1,128 @@ |
615 | +# Copyright 2019 Canonical Ltd. This software is licensed under the |
616 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
617 | + |
618 | +"""Test distro arch series filters.""" |
619 | + |
620 | +from __future__ import absolute_import, print_function, unicode_literals |
621 | + |
622 | +__metaclass__ = type |
623 | + |
624 | +from testtools.matchers import MatchesStructure |
625 | +from zope.component import getUtility |
626 | +from zope.security.interfaces import Unauthorized |
627 | + |
628 | +from lp.services.database.interfaces import IStore |
629 | +from lp.services.database.sqlbase import get_transaction_timestamp |
630 | +from lp.soyuz.enums import DistroArchSeriesFilterSense |
631 | +from lp.soyuz.interfaces.distroarchseriesfilter import ( |
632 | + IDistroArchSeriesFilter, |
633 | + IDistroArchSeriesFilterSet, |
634 | + ) |
635 | +from lp.testing import ( |
636 | + person_logged_in, |
637 | + TestCaseWithFactory, |
638 | + ) |
639 | +from lp.testing.layers import ( |
640 | + DatabaseFunctionalLayer, |
641 | + ZopelessDatabaseLayer, |
642 | + ) |
643 | + |
644 | + |
645 | +class TestDistroArchSeriesFilter(TestCaseWithFactory): |
646 | + |
647 | + layer = DatabaseFunctionalLayer |
648 | + |
649 | + def test_implements_interfaces(self): |
650 | + # DistroArchSeriesFilter implements IDistroArchSeriesFilter. |
651 | + dasf = self.factory.makeDistroArchSeriesFilter() |
652 | + self.assertProvides(dasf, IDistroArchSeriesFilter) |
653 | + |
654 | + def test___repr__(self): |
655 | + # `DistroArchSeriesFilter` objects have an informative __repr__. |
656 | + das = self.factory.makeDistroArchSeries() |
657 | + dasf = self.factory.makeDistroArchSeriesFilter(distroarchseries=das) |
658 | + self.assertEqual( |
659 | + "<DistroArchSeriesFilter for %s>" % das.title, repr(dasf)) |
660 | + |
661 | + def test_isSourceIncluded_include(self): |
662 | + # INCLUDE filters report that a source is included if it is in the |
663 | + # packageset. |
664 | + spns = [self.factory.makeSourcePackageName() for _ in range(3)] |
665 | + dasf = self.factory.makeDistroArchSeriesFilter( |
666 | + sense=DistroArchSeriesFilterSense.INCLUDE) |
667 | + dasf.packageset.add(spns[:2]) |
668 | + self.assertTrue(dasf.isSourceIncluded(spns[0])) |
669 | + self.assertTrue(dasf.isSourceIncluded(spns[1])) |
670 | + self.assertFalse(dasf.isSourceIncluded(spns[2])) |
671 | + |
672 | + def test_isSourceIncluded_exclude(self): |
673 | + # EXCLUDE filters report that a source is included if it is not in |
674 | + # the packageset. |
675 | + spns = [self.factory.makeSourcePackageName() for _ in range(3)] |
676 | + dasf = self.factory.makeDistroArchSeriesFilter( |
677 | + sense=DistroArchSeriesFilterSense.EXCLUDE) |
678 | + dasf.packageset.add(spns[:2]) |
679 | + self.assertFalse(dasf.isSourceIncluded(spns[0])) |
680 | + self.assertFalse(dasf.isSourceIncluded(spns[1])) |
681 | + self.assertTrue(dasf.isSourceIncluded(spns[2])) |
682 | + |
683 | + def test_destroySelf_unauthorized(self): |
684 | + # Ordinary users cannot delete a filter. |
685 | + das = self.factory.makeDistroArchSeries() |
686 | + self.factory.makeDistroArchSeriesFilter(distroarchseries=das) |
687 | + dasf = das.getSourceFilter() |
688 | + with person_logged_in(self.factory.makePerson()): |
689 | + self.assertRaises(Unauthorized, getattr, dasf, "destroySelf") |
690 | + |
691 | + def test_destroySelf(self): |
692 | + # Owners of the DAS's archive can delete a filter. |
693 | + das = self.factory.makeDistroArchSeries() |
694 | + self.factory.makeDistroArchSeriesFilter(distroarchseries=das) |
695 | + dasf = das.getSourceFilter() |
696 | + with person_logged_in(das.main_archive.owner): |
697 | + dasf.destroySelf() |
698 | + self.assertIsNone(das.getSourceFilter()) |
699 | + |
700 | + |
701 | +class TestDistroArchSeriesFilterSet(TestCaseWithFactory): |
702 | + |
703 | + layer = ZopelessDatabaseLayer |
704 | + |
705 | + def test_class_implements_interface(self): |
706 | + # The DistroArchSeriesFilterSet class implements |
707 | + # IDistroArchSeriesFilterSet. |
708 | + self.assertProvides( |
709 | + getUtility(IDistroArchSeriesFilterSet), IDistroArchSeriesFilterSet) |
710 | + |
711 | + def test_new(self): |
712 | + # The arguments passed when creating a filter are present on the new |
713 | + # object. |
714 | + das = self.factory.makeDistroArchSeries() |
715 | + packageset = self.factory.makePackageset(distroseries=das.distroseries) |
716 | + sense = DistroArchSeriesFilterSense.EXCLUDE |
717 | + creator = self.factory.makePerson() |
718 | + dasf = getUtility(IDistroArchSeriesFilterSet).new( |
719 | + distroarchseries=das, packageset=packageset, sense=sense, |
720 | + creator=creator) |
721 | + now = get_transaction_timestamp(IStore(dasf)) |
722 | + self.assertThat(dasf, MatchesStructure.byEquality( |
723 | + distroarchseries=das, packageset=packageset, sense=sense, |
724 | + creator=creator, date_created=now, date_last_modified=now)) |
725 | + |
726 | + def test_getByDistroArchSeries(self): |
727 | + # getByDistroArchSeries returns the filter for a DAS, if any. |
728 | + das = self.factory.makeDistroArchSeries() |
729 | + dasf_set = getUtility(IDistroArchSeriesFilterSet) |
730 | + self.assertIsNone(dasf_set.getByDistroArchSeries(das)) |
731 | + dasf = self.factory.makeDistroArchSeriesFilter(distroarchseries=das) |
732 | + self.assertEqual(dasf, dasf_set.getByDistroArchSeries(das)) |
733 | + |
734 | + def test_findByPackageset(self): |
735 | + # findByPackageset returns any filters using a package set. |
736 | + packageset = self.factory.makePackageset() |
737 | + dasf_set = getUtility(IDistroArchSeriesFilterSet) |
738 | + self.assertContentEqual([], dasf_set.findByPackageset(packageset)) |
739 | + dasfs = [ |
740 | + self.factory.makeDistroArchSeriesFilter(packageset=packageset) |
741 | + for _ in range(2)] |
742 | + self.assertContentEqual(dasfs, dasf_set.findByPackageset(packageset)) |
743 | |
744 | === modified file 'lib/lp/testing/factory.py' |
745 | --- lib/lp/testing/factory.py 2019-09-05 13:23:34 +0000 |
746 | +++ lib/lp/testing/factory.py 2019-09-10 11:16:47 +0000 |
747 | @@ -297,6 +297,7 @@ |
748 | ArchivePurpose, |
749 | BinaryPackageFileType, |
750 | BinaryPackageFormat, |
751 | + DistroArchSeriesFilterSense, |
752 | PackageDiffStatus, |
753 | PackagePublishingPriority, |
754 | PackagePublishingStatus, |
755 | @@ -326,6 +327,7 @@ |
756 | from lp.soyuz.model.distributionsourcepackagecache import ( |
757 | DistributionSourcePackageCache, |
758 | ) |
759 | +from lp.soyuz.model.distroarchseriesfilter import DistroArchSeriesFilter |
760 | from lp.soyuz.model.files import BinaryPackageFile |
761 | from lp.soyuz.model.livefsbuild import LiveFSFile |
762 | from lp.soyuz.model.packagediff import PackageDiff |
763 | @@ -4202,6 +4204,27 @@ |
764 | run_with_login(owner, lambda: package_set.add(packages)) |
765 | return package_set |
766 | |
767 | + def makeDistroArchSeriesFilter(self, distroarchseries=None, |
768 | + packageset=None, |
769 | + sense=DistroArchSeriesFilterSense.INCLUDE, |
770 | + creator=None, date_created=DEFAULT): |
771 | + """Make a new `DistroArchSeriesFilter`.""" |
772 | + if distroarchseries is None: |
773 | + if packageset is not None: |
774 | + distroseries = packageset.distroseries |
775 | + else: |
776 | + distroseries = None |
777 | + distroarchseries = self.makeDistroArchSeries( |
778 | + distroseries=distroseries) |
779 | + if packageset is None: |
780 | + packageset = self.makePackageset( |
781 | + distroseries=distroarchseries.distroseries) |
782 | + if creator is None: |
783 | + creator = self.makePerson() |
784 | + return DistroArchSeriesFilter( |
785 | + distroarchseries=distroarchseries, packageset=packageset, |
786 | + sense=sense, creator=creator, date_created=date_created) |
787 | + |
788 | def getAnyPocket(self): |
789 | return PackagePublishingPocket.BACKPORTS |
790 |