Merge lp:~stefanor/launchpad/edit-packagesets into lp:launchpad

Proposed by Stefano Rivera
Status: Merged
Approved by: Stuart Bishop
Approved revision: no longer in the source branch.
Merged at revision: 16056
Proposed branch: lp:~stefanor/launchpad/edit-packagesets
Merge into: lp:launchpad
Diff against target: 2229 lines (+870/-1179)
6 files modified
lib/lp/soyuz/configure.zcml (+2/-1)
lib/lp/soyuz/doc/packageset.txt (+0/-1156)
lib/lp/soyuz/interfaces/packageset.py (+9/-2)
lib/lp/soyuz/model/packageset.py (+10/-0)
lib/lp/soyuz/stories/webservice/xx-packageset.txt (+32/-1)
lib/lp/soyuz/tests/test_packageset.py (+817/-19)
To merge this branch: bzr merge lp:~stefanor/launchpad/edit-packagesets
Reviewer Review Type Date Requested Status
Stuart Bishop (community) db Approve
Benji York (community) code Approve
Robert Collins db Pending
Review via email: mp+124555@code.launchpad.net

Commit message

Allow deletion and modification of packagesets. Expose this through the API.

Description of the change

== Summary ==

The developer membership board owns the Ubuntu packagesets, allowing the DMB to add and remove packages from them. However, the DMB cannot modify packageset descriptions or delete packagesets. These changes have have to be made by a LP admin with SQL access.
The DMB has some packagesets that are no longer needed, and has descriptions of packagesets that we'd like to set.

== Proposed Fix ==

This proposed branch exposes write access to the name, description, and owner attributes of packagesets to the API, and adds an (exposed) deletion function.

The patches for this are trivial, and minimal.

I believe that allowing packageset deletion is safe, although there are a couple of corner cases, that I could use some guidance on:

1. I emptied packagesets on deletion, but alternatively, that could abort deletion.
2. Question 1 again, but for hierarchy and upload rights. I didn't do any cleaning up here, and assumed the DB's transactional integrity would abort / cleanup here.
3. Should we disallow deletion of packagesets in stable series?
4. I believe allowing renames is safe?
5. If we change the owner of a packageset, who should own its associated packageset group? This is simple in the case of a standalone packageset, but there is no obvious owner if the group contains other packagesets.
6. Can I make deletion not return a 404 on success?

== Complexity rationale ==

I re-wrote the packagesets' doctests as unit tests, resulting in a nett LoC reduction, and less doctests \o/

== Tests ==

In lib/lp/soyuz/stories/webservice/xx-packageset.txt and lib/lp/soyuz/tests/test_packageset.py

== QA ==

edit-acl in lp:~stefanor/ubuntu-archive-tools/edit-packagesets has been taught how to exercise these new API functions.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good to me. I would like a DB review so your assumptions (#2
in particular) can be verified.

Regarding the 404 issue, the other "destroy" methods seem to share the same
behavior, so while it may not be entirely desirable, it is at least consistent
with current practice.

We try to keep review diffs below 800 lines because reviews seem to get
super-linearly more taxing as the number of lines increase. Next time see if
there is a way you can make a change in multiple branches.

review: Approve (code)
Revision history for this message
Benji York (benji) wrote :

Oh, another thing I noticed but forgot to include the the first comment:

The comment on lines 1955-1956 of the diff (in
test_new_packageset_uploader_repeated) contain an error:

# Creating the same permission repeatedly should return the re-use the
# existing permission.

I believe this should read

# Creating the same permission repeatedly should re-use the existing
# permission.

Revision history for this message
Stuart Bishop (stub) wrote :

The triggers and foreign key relationships should take care of things. When in doubt, adding tests is the most reliable method of confirming this ;)

review: Approve (db)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/soyuz/configure.zcml'
2--- lib/lp/soyuz/configure.zcml 2012-09-02 00:11:13 +0000
3+++ lib/lp/soyuz/configure.zcml 2012-09-28 14:54:21 +0000
4@@ -801,7 +801,8 @@
5 interface="lp.soyuz.interfaces.packageset.IPackagesetRestricted"/>
6 <require
7 permission="launchpad.Edit"
8- interface="lp.soyuz.interfaces.packageset.IPackagesetEdit"/>
9+ interface="lp.soyuz.interfaces.packageset.IPackagesetEdit"
10+ set_attributes="name description owner"/>
11 <require
12 permission="launchpad.Moderate"
13 set_schema="lp.soyuz.interfaces.packageset.IPackagesetRestricted"/>
14
15=== removed file 'lib/lp/soyuz/doc/packageset.txt'
16--- lib/lp/soyuz/doc/packageset.txt 2012-09-28 06:34:26 +0000
17+++ lib/lp/soyuz/doc/packageset.txt 1970-01-01 00:00:00 +0000
18@@ -1,1156 +0,0 @@
19-= Package sets =
20-
21-The `Packageset` table allows the specification of a package set. These
22-facilitate the grouping of packages for purposes like the control of upload
23-permissions, the calculation of build and runtime package dependencies etc.
24-
25-Initially, package sets will be used to enforce upload permissions to source
26-packages. Later they may be put to other uses as well. Please see also the
27-following URL for user stories and scenarios:
28-https://dev.launchpad.net/VersionThreeDotO/Soyuz/StoryCards#packagesetacl
29-
30-It is also possible to define hierarchical relationships between package
31-sets i.e. include package sets into other package sets and remove them
32-respectively.
33-
34-This effectively allows the users to arrange package sets in a directed
35-acyclic graph (DAG, http://en.wikipedia.org/wiki/Directed_acyclic_graph)
36-where each arc/edge (A,B) carries the following meaning:
37-package set 'A' includes another package set 'B' as a subset.
38-
39-The following passage may also make it easier to understand the nomenclature
40-used (from http://en.wikipedia.org/wiki/Glossary_of_graph_theory):
41-
42- "If v is reachable from u, then u is a predecessor of v and v is a
43- successor of u. If there is an arc/edge from u to v, then u is a direct
44- predecessor of v, and v is a direct successor of u."
45-
46-
47-== Package set basics ==
48-
49-So, let's start by creating a few package sets.
50-
51- >>> from zope.component import getUtility
52- >>> from lp.soyuz.interfaces.packageset import (
53- ... IPackagesetSet)
54-
55- >>> login('foo.bar@canonical.com')
56-
57- >>> person1 = factory.makePerson(
58- ... name='hacker', displayname=u'Happy Hacker')
59- >>> person2 = factory.makePerson(
60- ... name='juergen', displayname=u'J\xc3\xbcrgen Schmidt',
61- ... email='js@example.com')
62- >>> ps_factory = getUtility(IPackagesetSet)
63- >>> umbrella_ps = ps_factory.new(
64- ... u'umbrella', u'Umbrella set, contains all packages', person1)
65- >>> kernel_ps = ps_factory.new(
66- ... u'kernel', u'Contains all OS kernel packages', person2)
67-
68-Now 'juergen' and 'hacker' have a package set each.
69-
70- >>> ps_factory.getByOwner(person1).count() == 1
71- True
72- >>> ps_factory.getByOwner(person2).count() == 1
73- True
74-
75-We need to define a few functions that make it easy to look at package set
76-related data.
77-
78- >>> import operator
79- >>> def sort_by_id(iterable):
80- ... return sorted(iterable, key=operator.attrgetter('id'))
81- >>> def print_data(iterable):
82- ... for datum in sort_by_id(iterable):
83- ... print('%3d -> %s' % (datum.id, datum.name))
84- >>> def resultsets_are_equal(rs1, rs2):
85- ... rs1 = list(rs1.order_by('name'))
86- ... rs2 = list(rs2.order_by('name'))
87- ... return rs1 == rs2
88-
89-
90-Package sets can be looked up by name as follows:
91-
92- >>> umbrella = ps_factory['umbrella']
93- >>> print_data((umbrella,))
94- 1 -> umbrella
95-
96-In order to facilitate utilisation of package set related functionality
97-via the web services API we need to have a get() method that returns the
98-first N (N=50 by default) package sets sorted by name.
99-Since we only have 2 package sets at this point only these will be shown.
100-
101- >>> for datum in ps_factory.get():
102- ... print('%3d -> %s' % (datum.id, datum.name))
103- 2 -> kernel
104- 1 -> umbrella
105-
106-In a next step we will associate source package names with the package sets
107-just created.
108-
109- >>> from lp.services.database.interfaces import (
110- ... IStoreSelector, MAIN_STORE, DEFAULT_FLAVOR, MASTER_FLAVOR)
111- >>> from lp.registry.model.sourcepackagename import SourcePackageName
112- >>> store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
113-
114-First associate *all* source package names with the umbrella package set.
115-
116- >>> all_spns = store.find(SourcePackageName)
117- >>> umbrella_ps.add(all_spns)
118-
119-Let's see what we got:
120-
121- >>> umbrella_spns = umbrella_ps.sourcesIncluded(direct_inclusion=True)
122- >>> umbrella_src_names = sorted(spn.name for spn in umbrella_spns)
123- >>> print_data(umbrella_spns)
124- 1 -> mozilla-firefox
125- 9 -> evolution
126- 10 -> netapplet
127- 14 -> pmount
128- 15 -> a52dec
129- 16 -> mozilla
130- 17 -> at
131- 18 -> thunderbird
132- 19 -> alsa-utils
133- 20 -> cnews
134- 21 -> libstdc++
135- 22 -> linux-source-2.6.15
136- 23 -> foobar
137- 24 -> cdrkit
138- 25 -> language-pack-de
139- 26 -> iceweasel
140- 27 -> commercialpackage
141-
142-Now let's put a few selected source package names into the 'kernel' package
143-set:
144-
145- >>> kernel_spns = store.find(
146- ... SourcePackageName, SourcePackageName.name.like('li%'))
147- >>> kernel_ps.add(kernel_spns)
148- >>> kernel_spns = kernel_ps.sourcesIncluded(direct_inclusion=True)
149- >>> print_data(kernel_spns)
150- 21 -> libstdc++
151- 22 -> linux-source-2.6.15
152-
153-Adding source package names to a package set repeatedly has no effect.
154-
155- >>> umbrella_ps.add(kernel_spns)
156- >>> umbrella_spns2 = umbrella_ps.sourcesIncluded(direct_inclusion=True)
157- >>> print_data(umbrella_spns2)
158- 1 -> mozilla-firefox
159- 9 -> evolution
160- 10 -> netapplet
161- 14 -> pmount
162- 15 -> a52dec
163- 16 -> mozilla
164- 17 -> at
165- 18 -> thunderbird
166- 19 -> alsa-utils
167- 20 -> cnews
168- 21 -> libstdc++
169- 22 -> linux-source-2.6.15
170- 23 -> foobar
171- 24 -> cdrkit
172- 25 -> language-pack-de
173- 26 -> iceweasel
174- 27 -> commercialpackage
175-
176-Removing source package names is easy.
177-
178- >>> umbrella_ps.remove(kernel_spns)
179- >>> remaining_spns = umbrella_ps.sourcesIncluded(direct_inclusion=True)
180-
181-The 'umbrella' package set includes all source names. From that set the
182-'kernel' package set (that includes merely two source names) is deducted.
183-What the following shows is that the package set substraction method works.
184-
185- >>> print_data(remaining_spns)
186- 1 -> mozilla-firefox
187- 9 -> evolution
188- 10 -> netapplet
189- 14 -> pmount
190- 15 -> a52dec
191- 16 -> mozilla
192- 17 -> at
193- 18 -> thunderbird
194- 19 -> alsa-utils
195- 20 -> cnews
196- 23 -> foobar
197- 24 -> cdrkit
198- 25 -> language-pack-de
199- 26 -> iceweasel
200- 27 -> commercialpackage
201-
202-Trying to remove source package names that are *not* associated with a
203-package set from the latter has no effect.
204-
205- >>> umbrella_ps.remove(kernel_spns)
206- >>> remaining_spns = umbrella_ps.sourcesIncluded(direct_inclusion=True)
207- >>> print_data(remaining_spns)
208- 1 -> mozilla-firefox
209- 9 -> evolution
210- 10 -> netapplet
211- 14 -> pmount
212- 15 -> a52dec
213- 16 -> mozilla
214- 17 -> at
215- 18 -> thunderbird
216- 19 -> alsa-utils
217- 20 -> cnews
218- 23 -> foobar
219- 24 -> cdrkit
220- 25 -> language-pack-de
221- 26 -> iceweasel
222- 27 -> commercialpackage
223-
224-Add the removed source package names back to 'umbrella'.
225-
226- >>> umbrella_ps.add(kernel_spns)
227-
228-
229-== Package set hierarchies ==
230-
231-The next step in organizing package sets is to arrange them in a hierarchy
232-i.e. for package sets to include others as subsets.
233-
234-We need more package sets to play with however.
235-
236- >>> gnome_ps = ps_factory.new(
237- ... u'gnome', u'Contains all gnome desktop packages', person2)
238- >>> mozilla_ps = ps_factory.new(
239- ... u'mozilla', u'Contains all mozilla packages', person2)
240- >>> firefox_ps = ps_factory.new(
241- ... u'firefox', u'Contains all firefox packages', person2)
242- >>> thunderbird_ps = ps_factory.new(
243- ... u'thunderbird', u'Contains all thunderbird packages', person2)
244- >>> languagepack_ps = ps_factory.new(
245- ... u'languagepack', u'Contains all language packs', person2)
246- >>> store.commit()
247-
248-Now we can set up the package set hierarchy.
249-
250- >>> umbrella_ps.add((gnome_ps,))
251- >>> mozilla_ps.add((firefox_ps, thunderbird_ps, languagepack_ps))
252- >>> umbrella_ps.add((mozilla_ps,))
253- >>> gnome_ps.add((languagepack_ps,))
254-
255-The 'umbrella' package set has two *direct* successors..
256-
257- >>> print_data(umbrella_ps.setsIncluded(direct_inclusion=True))
258- 3 -> gnome
259- 4 -> mozilla
260-
261-.. but five successors in total. The 'firefox' and 'thunderbird' package
262-sets are included via 'mozilla' whereas the 'languagepack' package set comes
263-in via 'gnome' and/or 'mozilla'.
264-
265- >>> u_successors = umbrella_ps.setsIncluded()
266- >>> print_data(u_successors)
267- 3 -> gnome
268- 4 -> mozilla
269- 5 -> firefox
270- 6 -> thunderbird
271- 7 -> languagepack
272-
273-These are the *direct* predecessors of the 'languagepack' package set.
274-
275- >>> print_data(languagepack_ps.setsIncludedBy(direct_inclusion=True))
276- 3 -> gnome
277- 4 -> mozilla
278-
279-These are *all* predecessors of the 'languagepack' package set. Please not
280-that the 'umbrella' package set is listed as well.
281-
282- >>> print_data(languagepack_ps.setsIncludedBy())
283- 1 -> umbrella
284- 3 -> gnome
285- 4 -> mozilla
286-
287-When 'mozilla' stops including 'languagepack' it is still included by
288-'umbrella' (via the 'gnome' package set).
289-
290- >>> mozilla_ps.remove((languagepack_ps,))
291- >>> print_data(mozilla_ps.setsIncluded())
292- 5 -> firefox
293- 6 -> thunderbird
294-
295- >>> print_data(languagepack_ps.setsIncludedBy(direct_inclusion=True))
296- 3 -> gnome
297- >>> print_data(languagepack_ps.setsIncludedBy())
298- 1 -> umbrella
299- 3 -> gnome
300-
301-The 'umbrella' successors are still the same ('languagepack' is still
302-included via 'gnome').
303-
304- >>> sort_by_id(u_successors) == sort_by_id(umbrella_ps.setsIncluded())
305- True
306-
307-Removing direct successors is pretty tolerant i.e. if you try to remove 'Q'
308-from 'P' and 'Q' is *not* a *direct* successor of 'P' nothing will happen.
309-
310- >>> umbrella_ps.remove((languagepack_ps,))
311- >>> sort_by_id(u_successors) == sort_by_id(umbrella_ps.setsIncluded())
312- True
313-
314-What happens if we remove a package set from the database?
315-
316- >>> print_data(umbrella_ps.setsIncluded())
317- 3 -> gnome
318- 4 -> mozilla
319- 5 -> firefox
320- 6 -> thunderbird
321- 7 -> languagepack
322-
323- >>> store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
324- >>> store.remove(gnome_ps)
325-
326-We removed the 'gnome' package set and see that all the relationships it
327-participated in were cleaned up as well.
328-
329-It does not show up as a predecessor for the 'languagepack' package set
330-any more.
331-
332- >>> print_data(languagepack_ps.setsIncludedBy(direct_inclusion=True))
333- >>> print_data(languagepack_ps.setsIncludedBy())
334-
335-It is also not included by the 'umbrella' package set any longer. Please
336-note that 'languagepack' also ceased to be included by 'umbrella' because
337-the link between them ('gnome') is gone.
338-
339- >>> print_data(umbrella_ps.setsIncluded())
340- 4 -> mozilla
341- 5 -> firefox
342- 6 -> thunderbird
343-
344-
345-== Package set hierarchies and source names ==
346-
347-In order to demonstrate how included package sets play together
348-with source package names we'll "populate" the former.
349-
350- >>> def populate(packageset, pattern):
351- ... rs = store.find(
352- ... SourcePackageName, SourcePackageName.name.like(pattern))
353- ... packageset.add(rs)
354-
355- >>> populate(mozilla_ps, 'moz%a')
356- >>> print_data(mozilla_ps.sourcesIncluded(direct_inclusion=True))
357- 16 -> mozilla
358-
359- >>> populate(firefox_ps, '%fire%')
360- >>> populate(firefox_ps, '%ice%')
361- >>> print_data(firefox_ps.sourcesIncluded(direct_inclusion=True))
362- 1 -> mozilla-firefox
363- 26 -> iceweasel
364-
365-If we wanted the string source names as opposed to the `ISourcePackageName`
366-instances we could get them as follows:
367-
368- >>> sorted(firefox_ps.getSourcesIncluded(direct_inclusion=True))
369- [u'iceweasel', u'mozilla-firefox']
370-
371-Populate the 'thunderbird' package set with sources.
372-
373- >>> populate(thunderbird_ps , '%thunder%')
374- >>> populate(thunderbird_ps , '%ice%')
375- >>> print_data(thunderbird_ps.sourcesIncluded(direct_inclusion=True))
376- 18 -> thunderbird
377- 26 -> iceweasel
378-
379-When looking at *all* source package names the 'mozilla' package set is
380-associated with we see
381-
382- * 'mozilla' i.e. the source package name it is *directly* associated
383- with but also
384- * the union set of source package names of its successor package sets
385-
386- >>> print_data(mozilla_ps.sourcesIncluded())
387- 1 -> mozilla-firefox
388- 16 -> mozilla
389- 18 -> thunderbird
390- 26 -> iceweasel
391-
392-We can get the string source package names as follows:
393-
394- >>> sorted(mozilla_ps.getSourcesIncluded())
395- [u'iceweasel', u'mozilla', u'mozilla-firefox', u'thunderbird']
396-
397-We extend the package set hierarchy by including 'languagepack' into
398-'thunderbird' ..
399-
400- >>> populate(languagepack_ps , 'lang%')
401- >>> thunderbird_ps.add((languagepack_ps,))
402-
403-.. and see that the 'thunderbird' package set is (indirectly) associated
404-with the 'language-pack-de' source package name.
405-
406- >>> print_data(thunderbird_ps.sourcesIncluded(direct_inclusion=True))
407- 18 -> thunderbird
408- 26 -> iceweasel
409- >>> print_data(thunderbird_ps.sourcesIncluded())
410- 18 -> thunderbird
411- 25 -> language-pack-de
412- 26 -> iceweasel
413-
414-Furthermore, the 'language-pack-de' source package name is picked up by its
415-predecessor 'mozilla' as well.
416-
417- >>> print_data(mozilla_ps.sourcesIncluded())
418- 1 -> mozilla-firefox
419- 16 -> mozilla
420- 18 -> thunderbird
421- 25 -> language-pack-de
422- 26 -> iceweasel
423-
424-Let's see what sources the 'umbrella' and the 'mozilla' package set have in
425-common:
426-
427- >>> print_data(umbrella_ps.sourcesSharedBy(mozilla_ps))
428- 1 -> mozilla-firefox
429- 16 -> mozilla
430- 18 -> thunderbird
431- 25 -> language-pack-de
432- 26 -> iceweasel
433-
434-The same shared sources (but in string form) are obtained as follows:
435-
436- >>> sorted(umbrella_ps.getSourcesSharedBy(mozilla_ps))
437- [u'iceweasel', u'language-pack-de', u'mozilla', u'mozilla-firefox',
438- u'thunderbird']
439-
440-If we ask the question the other way around the answer should be the same.
441-
442- >>> print_data(mozilla_ps.sourcesSharedBy(umbrella_ps))
443- 1 -> mozilla-firefox
444- 16 -> mozilla
445- 18 -> thunderbird
446- 25 -> language-pack-de
447- 26 -> iceweasel
448-
449-Now we only want to see the directly included sources they have in common.
450-
451- >>> print_data(
452- ... umbrella_ps.sourcesSharedBy(mozilla_ps, direct_inclusion=True))
453- 16 -> mozilla
454-
455-The same shared sources (but in string form) are obtained as follows:
456-
457- >>> sorted(
458- ... umbrella_ps.getSourcesSharedBy(mozilla_ps, direct_inclusion=True))
459- [u'mozilla']
460-
461-Again, asking the question the other way around works as well.
462-
463- >>> print_data(
464- ... mozilla_ps.sourcesSharedBy(umbrella_ps, direct_inclusion=True))
465- 16 -> mozilla
466-
467-How many sources are in the 'mozilla' package set but not in 'umbrella'?
468-
469- >>> mozilla_ps.sourcesNotSharedBy(umbrella_ps).count()
470- 0
471-
472- >>> len(list(mozilla_ps.getSourcesNotSharedBy(umbrella_ps)))
473- 0
474-
475-What sources are included by the 'mozilla' package set but not by 'firefox'?
476-
477- >>> print_data(mozilla_ps.sourcesNotSharedBy(firefox_ps))
478- 16 -> mozilla
479- 18 -> thunderbird
480- 25 -> language-pack-de
481-
482- >>> sorted(mozilla_ps.getSourcesNotSharedBy(firefox_ps))
483- [u'language-pack-de', u'mozilla', u'thunderbird']
484-
485-What sources are *directly* included by 'mozilla' but not by 'firefox'?
486-
487- >>> print_data(
488- ... mozilla_ps.sourcesNotSharedBy(firefox_ps, direct_inclusion=True))
489- 16 -> mozilla
490-
491- >>> sorted(
492- ... mozilla_ps.getSourcesNotSharedBy(
493- ... firefox_ps, direct_inclusion=True))
494- [u'mozilla']
495-
496-Sometimes it's interesting to see what package sets include a certain
497-source package name.
498-
499- >>> from lp.soyuz.interfaces.packageset import (
500- ... IPackagesetSet)
501- >>> from lp.registry.interfaces.sourcepackagename import (
502- ... ISourcePackageNameSet)
503-
504-Which package sets include 'mozilla-firefox' either directly or indirectly?
505-
506- >>> firefox_spn = getUtility(ISourcePackageNameSet)['mozilla-firefox']
507- >>> ps_set = getUtility(IPackagesetSet)
508- >>> print_data(ps_set.setsIncludingSource(firefox_spn))
509- 1 -> umbrella
510- 4 -> mozilla
511- 5 -> firefox
512-
513-Which package sets include 'mozilla-firefox' directly? Remember that
514-'umbrella' includes *all* source package names directly.
515-
516- >>> print_data(
517- ... ps_set.setsIncludingSource(firefox_spn, direct_inclusion=True))
518- 1 -> umbrella
519- 5 -> firefox
520-
521-We can filter the package sets by series:
522-
523- >>> from lp.registry.interfaces.distribution import IDistributionSet
524- >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
525- >>> print_data(
526- ... ps_set.setsIncludingSource(firefox_spn,
527- ... distroseries=ubuntu['hoary']))
528- 1 -> umbrella
529- 4 -> mozilla
530- 5 -> firefox
531- >>> print_data(
532- ... ps_set.setsIncludingSource(firefox_spn,
533- ... distroseries=ubuntu['warty']))
534-
535-It is also possible to ask the same question by providing the mere name of
536-the source package.
537-
538- >>> print_data(
539- ... ps_set.setsIncludingSource('mozilla-firefox',
540- ... direct_inclusion=True))
541- 1 -> umbrella
542- 5 -> firefox
543-
544-If the source package for a given name cannot be found an exception is
545-raised.
546-
547- >>> print_data(ps_set.setsIncludingSource('this-will-fail'))
548- Traceback (most recent call last):
549- ...
550- NoSuchSourcePackageName: No such source package: 'this-will-fail'.
551-
552- >>> store.commit()
553-
554-
555-== Various errors ==
556-
557-Here's what happens if we try to add something that is not a source package
558-name or package set:
559-
560- >>> mozilla_ps.add('This will fail'.split())
561- Traceback (most recent call last):
562- ...
563- AssertionError: Not all data was handled.
564-
565-Likewise for removal:
566-
567- >>> mozilla_ps.remove(range(10))
568- Traceback (most recent call last):
569- ...
570- AssertionError: Not all data was handled.
571-
572-An attempt to add cycles to the package set graph also results in a failure:
573-
574- >>> mozilla_ps.add((umbrella_ps,))
575- Traceback (most recent call last):
576- ...
577- InternalError: Package set umbrella already includes mozilla.
578- Adding (mozilla -> umbrella) would introduce a cycle in the
579- package set graph (DAG).
580- <BLANKLINE>
581-
582- >>> store.rollback()
583-
584-== Amending package sets ==
585-
586-There are some methods that will enable the caller to add and delete
587-package sets. They currently require launchpad.Edit permission to
588-use, which enforces the user to be an admin or a member of the "techboard"
589-(Ubuntu Technical Board) team.
590-
591- >>> from zope.security.checker import canAccess
592- >>> from zope.security.proxy import removeSecurityProxy
593- >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
594- >>> from lp.registry.interfaces.person import IPersonSet
595- >>> from lp.registry.interfaces.teammembership import (
596- ... TeamMembershipStatus)
597- >>> restricted_methods = ('new',)
598- >>> techboard = getUtility(ILaunchpadCelebrities).ubuntu_techboard
599- >>> techboard = removeSecurityProxy(techboard)
600-
601-Ordinary users have no access:
602-
603- >>> login('js@example.com')
604- >>> canAccess(ps_factory, 'new')
605- False
606-
607-Admins have access:
608-
609- >>> login("foo.bar@canonical.com")
610- >>> canAccess(ps_factory, 'new')
611- True
612-
613-Now add "test@canonical.com" to the techboard team and log in as him.
614-
615- >>> ignored = techboard.addMember(
616- ... person2, reviewer=person2, status=TeamMembershipStatus.APPROVED,
617- ... force_team_add=True)
618- >>> ignored = login_person(person2)
619-
620-Create a new package set.
621-
622- >>> kde_ps = ps_factory.new(
623- ... u'kde', u'Contains all KDE packages', person2)
624-
625-
626-== Package sets and permissions ==
627-
628-As it stands package sets will first be used for governing source package
629-uploads i.e. in conjunction with `ArchivePermission` data.
630-
631- >>> from lp.soyuz.enums import ArchivePermissionType
632- >>> from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
633- >>> ap_set = getUtility(IArchivePermissionSet)
634-
635-So, let's assign upload permissions for the 'mozilla' package set to our
636-happy hacker.
637-
638- >>> def print_permission(result_set):
639- ... for perm in result_set.order_by(
640- ... 'person, permission, packageset, explicit'):
641- ... person = perm.person.name
642- ... pset = perm.packageset.name
643- ... permission = perm.permission.name
644- ... archive = perm.archive.name
645- ... if perm.explicit == True:
646- ... ptype = 'explicit'
647- ... else:
648- ... ptype = 'implicit'
649- ... print(
650- ... '%10s %10s: %12s -> %16s (%s)'
651- ... % (archive, person, pset, permission, ptype))
652-
653-Introduce a copy archive that will be used to disambiguate archive
654-permissions.
655-
656- >>> from lp.soyuz.enums import ArchivePurpose
657- >>> from lp.soyuz.interfaces.archive import IArchiveSet
658- >>> rebuild_archive = getUtility(IArchiveSet).new(
659- ... owner=person1, purpose=ArchivePurpose.COPY,
660- ... distribution=ubuntu, name='copy-archive',
661- ... enabled=False, require_virtualized=False)
662-
663-Since we just created the copy archive there will be no permissions
664-associated with it.
665-
666- >>> permissions = ap_set.packagesetsForUploader(rebuild_archive, person1)
667- >>> print_permission(permissions)
668-
669-Next we set up a permission for the Ubuntu main archive.
670-
671- >>> ubuntu_archive = ubuntu.main_archive
672- >>> ignore_this = ap_set.newPackagesetUploader(
673- ... ubuntu_archive, person1, mozilla_ps)
674-
675-Now we see that person 'hacker' has upload permissions to the 'mozilla'
676-package set.
677-
678- >>> permissions = ap_set.packagesetsForUploader(
679- ... ubuntu_archive, person1)
680- >>> print_permission(permissions)
681- primary hacker: mozilla -> UPLOAD (implicit)
682-
683-The generic checkAuthenticated() method works as well.
684-
685- >>> permissions = ap_set.checkAuthenticated(
686- ... person1, ubuntu_archive, ArchivePermissionType.UPLOAD,
687- ... mozilla_ps)
688- >>> permission = permissions[0]
689-
690- >>> print permission.person.name
691- hacker
692- >>> print permission.package_set_name
693- mozilla
694- >>> print permission.permission.name
695- UPLOAD
696- >>> permission.explicit
697- False
698-
699-Since the permission above was granted for 'hacker' on the main Ubuntu
700-archive the copy archive still has no permissions associated with it.
701-
702- >>> nothing = ap_set.packagesetsForUploader(rebuild_archive, person1)
703- >>> print_permission(nothing)
704-
705-'juergen' is now getting granted upload permissions for the same package set
706-but for the copy archive.
707-
708- >>> ignore_this = ap_set.newPackagesetUploader(
709- ... rebuild_archive, person2, mozilla_ps)
710-
711- >>> print_permission(ap_set.uploadersForPackageset(
712- ... rebuild_archive, mozilla_ps))
713- copy-archive juergen: mozilla -> UPLOAD (implicit)
714-
715-Now 'hacker' may upload packages associated with the 'mozilla' package set
716-in the Ubuntu main archive ..
717-
718- >>> ap_set.isSourceUploadAllowed(
719- ... ubuntu_archive, 'mozilla-firefox', person1)
720- True
721-
722-but not in the copy archive.
723-
724- >>> ap_set.isSourceUploadAllowed(
725- ... rebuild_archive, 'mozilla-firefox', person1)
726- False
727-
728-Conversely 'juergen' is entitled to uploading 'mozilla' packages in the copy
729-archive ..
730-
731- >>> ap_set.isSourceUploadAllowed(
732- ... rebuild_archive, 'mozilla-firefox', person2)
733- True
734-
735-but not in the Ubuntu main archive.
736-
737- >>> ap_set.isSourceUploadAllowed(
738- ... ubuntu_archive, 'mozilla-firefox', person2)
739- False
740-
741-The 'package_set_name' property allows easy access to the package set
742-name.
743-
744- >>> [archp] = permissions
745- >>> archp.package_set_name
746- u'mozilla'
747-
748-'hacker' is also listed as one of the 'mozilla' uploaders. Please note that
749-the upload privilege to the same package set but in a different archive does
750-not show in the listing below since we only want to see permissions that
751-apply to the Ubuntu archive.
752-
753- >>> ignore_this = ap_set.newPackagesetUploader(
754- ... rebuild_archive, person1, mozilla_ps)
755-
756- >>> print_permission(ap_set.uploadersForPackageset(
757- ... ubuntu_archive, mozilla_ps))
758- primary hacker: mozilla -> UPLOAD (implicit)
759-
760- >>> print_permission(
761- ... ap_set.packagesetsForUploader(ubuntu_archive, person1))
762- primary hacker: mozilla -> UPLOAD (implicit)
763-
764- >>> print_permission(
765- ... ap_set.packagesetsForSourceUploader(
766- ... ubuntu_archive, 'mozilla-firefox', person1))
767- primary hacker: mozilla -> UPLOAD (implicit)
768-
769-'hacker' has upload privileges for 'mozilla' in the copy archive.
770-
771- >>> print_permission(ap_set.uploadersForPackageset(
772- ... rebuild_archive, mozilla_ps))
773- copy-archive hacker: mozilla -> UPLOAD (implicit)
774- copy-archive juergen: mozilla -> UPLOAD (implicit)
775-
776-Now we delete them..
777-
778- >>> ap_set.deletePackagesetUploader(
779- ... rebuild_archive, person1, mozilla_ps)
780-
781-.. and the 'hacker' privileges are gone.
782-
783- >>> print_permission(ap_set.uploadersForPackageset(
784- ... rebuild_archive, mozilla_ps))
785- copy-archive juergen: mozilla -> UPLOAD (implicit)
786-
787-The analogous permissions for the Ubuntu archive are unaffected by the
788-deletion.
789-
790- >>> print_permission(ap_set.uploadersForPackageset(
791- ... ubuntu_archive, mozilla_ps))
792- primary hacker: mozilla -> UPLOAD (implicit)
793-
794-Furthermore, 'hacker' will be listed as an uploader for any included
795-package set as long as the latter does not have its own permission with
796-the 'explicit' flag set.
797-
798- >>> print_data(mozilla_ps.setsIncluded())
799- 5 -> firefox
800- 6 -> thunderbird
801- 7 -> languagepack
802-
803-When we ask for the 'firefox' uploaders, 'hacker' will not be listed although
804-'mozilla' includes 'firefox'.
805-
806- >>> print_permission(ap_set.uploadersForPackageset(
807- ... ubuntu_archive, firefox_ps))
808-
809-If we ask for uploaders while considering the inclusions between package
810-sets, 'hacker' will be listed as an uploader for 'firefox' by virtue of
811-the fact that the latter is included by 'mozilla' and 'hacker' is an
812-uploader for mozilla.
813-
814- >>> print_permission(
815- ... ap_set.uploadersForPackageset(
816- ... ubuntu_archive, firefox_ps, direct_permissions=False))
817- primary hacker: mozilla -> UPLOAD (implicit)
818-
819-If we add a permission for 'firefox' things will stay the same i.e. 'hacker'
820-is still listed as an uploader. Please note the different package sets
821-('mozilla' and 'firefox') although we are looking for 'firefox' uploaders.
822-
823- >>> ignore_this = ap_set.newPackagesetUploader(
824- ... ubuntu_archive, person2, firefox_ps)
825-
826-Please note also how any permissions granted for the copy archive are
827-ignored in the listing below since we are asking for permissions applying
828-to the Ubuntu archive.
829-
830- >>> ignore_this = ap_set.newPackagesetUploader(
831- ... rebuild_archive, person2, firefox_ps)
832- >>> print_permission(
833- ... ap_set.uploadersForPackageset(
834- ... ubuntu_archive, firefox_ps, direct_permissions=False))
835- primary hacker: mozilla -> UPLOAD (implicit)
836- primary juergen: firefox -> UPLOAD (implicit)
837-
838-Once 'mozilla' stops including 'firefox', user 'hacker' is not listed as
839-an uploader for 'firefox'.
840-
841- >>> mozilla_ps.remove((firefox_ps,))
842- >>> print_data(mozilla_ps.setsIncluded())
843- 6 -> thunderbird
844- 7 -> languagepack
845-
846- >>> print_permission(
847- ... ap_set.uploadersForPackageset(
848- ... ubuntu_archive, firefox_ps, direct_permissions=False))
849- primary juergen: firefox -> UPLOAD (implicit)
850-
851-Ulploaders with explicit permissions will be listed along with the other
852-uploaders.
853-
854- >>> mark = getUtility(IPersonSet).getByName("mark")
855- >>> ignore_this = ap_set.newPackagesetUploader(
856- ... ubuntu_archive, mark, firefox_ps, True)
857- >>> print_permission(
858- ... ap_set.uploadersForPackageset(ubuntu_archive, firefox_ps))
859- primary mark: firefox -> UPLOAD (explicit)
860- primary juergen: firefox -> UPLOAD (implicit)
861-
862-Persons can have both explicit or non-explicit permissions for package sets.
863-
864- >>> ignore_this = ap_set.newPackagesetUploader(
865- ... ubuntu_archive, mark, thunderbird_ps)
866- >>> print_permission(
867- ... ap_set.packagesetsForUploader(ubuntu_archive, mark))
868- primary mark: firefox -> UPLOAD (explicit)
869- primary mark: thunderbird -> UPLOAD (implicit)
870-
871-Sometimes it's handy to know what permissions apply for a given source
872-package irrespective of the person.
873-
874- >>> print_permission(
875- ... ap_set.packagesetsForSource(ubuntu_archive, 'mozilla-firefox'))
876- primary mark: firefox -> UPLOAD (explicit)
877- primary juergen: firefox -> UPLOAD (implicit)
878-
879-If we make the 'mozilla' package set include 'firefox' again and ask the same
880-question without insisiting on direct permissions we also see the permission
881-granting 'hacker' upload privileges to 'mozilla' (since the latter is now
882-a parent package set of 'firefox' and that includes the 'mozilla-firefox'
883-source package (directly)).
884-
885- >>> mozilla_ps.add((firefox_ps,))
886- >>> print_permission(
887- ... ap_set.packagesetsForSource(
888- ... ubuntu_archive, 'mozilla-firefox', direct_permissions=False))
889- primary mark: firefox -> UPLOAD (explicit)
890- primary hacker: mozilla -> UPLOAD (implicit)
891- primary juergen: firefox -> UPLOAD (implicit)
892-
893-Attempting to create an explicit permission when a non-explicit one exists
894-already will fail.
895-
896- >>> ignore_this = ap_set.newPackagesetUploader(
897- ... ubuntu_archive, mark, thunderbird_ps, True)
898- Traceback (most recent call last):
899- ...
900- ValueError: Permission for package set 'thunderbird' already exists for
901- mark but with a different 'explicit' flag value (False).
902-
903-And the other way around:
904-
905- >>> ignore_this = ap_set.newPackagesetUploader(
906- ... ubuntu_archive, mark, firefox_ps)
907- Traceback (most recent call last):
908- ...
909- ValueError: Permission for package set 'firefox' already exists for
910- mark but with a different 'explicit' flag value (True).
911-
912-Removing package set based permissions is straight-forward.
913-
914- >>> ap_set.deletePackagesetUploader(
915- ... ubuntu_archive, mark, firefox_ps, True)
916- >>> print_permission(
917- ... ap_set.packagesetsForUploader(ubuntu_archive, mark))
918- primary mark: thunderbird -> UPLOAD (implicit)
919-
920-
921- >>> ap_set.deletePackagesetUploader(
922- ... ubuntu_archive, mark, thunderbird_ps)
923- >>> print_permission(ap_set.packagesetsForUploader(
924- ... ubuntu_archive, mark))
925-
926-An attempt to look up package set based permission by something else than
927-by a package set (name) results in a failure.
928-
929- >>> print_permission(ap_set.uploadersForPackageset(
930- ... ubuntu_archive, 12345))
931- Traceback (most recent call last):
932- ...
933- ValueError: Not a package set: int
934-
935-Create a package set based permission for a team.
936-
937- >>> techboardp = (
938- ... ap_set.newPackagesetUploader(
939- ... ubuntu_archive, techboard, kde_ps))
940-
941-An attempt to create a new permission for a 'techboard' team member
942- * for the same package set
943- * but with a conflicting 'explicit' flag value
944-will fail.
945-
946- >>> ignore_this = ap_set.newPackagesetUploader(
947- ... ubuntu_archive, person2, kde_ps, True)
948- Traceback (most recent call last):
949- ...
950- ValueError: Permission for package set 'kde' already exists for
951- techboard but with a different 'explicit' flag value (False).
952-
953-An attempt to create the same permission repeatedly should just return
954-the existing one.
955-
956- >>> sameone = (
957- ... ap_set.newPackagesetUploader(
958- ... ubuntu_archive, techboard, kde_ps))
959- >>> techboardp.id == sameone.id
960- True
961-
962-Creating a "compatible" permission for person 'juergen' succeeds although
963-there is one in place for 'techboard' already (and 'juergen' belongs to
964-the 'techboard' team).
965-
966- >>> print_permission(
967- ... ap_set.uploadersForPackageset(ubuntu_archive, kde_ps))
968- primary techboard: kde -> UPLOAD (implicit)
969-
970-
971- >>> ignore_this = ap_set.newPackagesetUploader(
972- ... ubuntu_archive, person2, kde_ps)
973-
974- >>> print_permission(
975- ... ap_set.uploadersForPackageset(ubuntu_archive, kde_ps))
976- primary techboard: kde -> UPLOAD (implicit)
977- primary juergen: kde -> UPLOAD (implicit)
978-
979-
980-== Checking package set based upload permissions ==
981-
982-The 'mozilla' package set includes the 'firefox' subset and hence the
983-'mozilla-firefox' source package name indirectly.
984-
985- >>> mozilla_ps.add((firefox_ps,))
986- >>> print_data(mozilla_ps.sourcesIncluded())
987- 1 -> mozilla-firefox
988- 16 -> mozilla
989- 18 -> thunderbird
990- 25 -> language-pack-de
991- 26 -> iceweasel
992- >>> print_data(mozilla_ps.sourcesIncluded(direct_inclusion=True))
993- 16 -> mozilla
994-
995-'hacker' is authorized to upload to the 'mozilla' package set..
996-
997- >>> print_permission(
998- ... ap_set.uploadersForPackageset(ubuntu_archive, mozilla_ps))
999- primary hacker: mozilla -> UPLOAD (implicit)
1000-
1001-.. and hence listed as a *potential* uploader of the 'mozilla-firefox'
1002-source package.
1003-
1004- >>> print_permission(
1005- ... ap_set.packagesetsForSourceUploader(
1006- ... ubuntu_archive, 'mozilla-firefox', person1))
1007- primary hacker: mozilla -> UPLOAD (implicit)
1008-
1009-'juergen' is allowed to upload to the 'firefox' package set that includes
1010-the 'mozilla-firefox' source package name directly ..
1011-
1012- >>> print_data(firefox_ps.sourcesIncluded(direct_inclusion=True))
1013- 1 -> mozilla-firefox
1014- 26 -> iceweasel
1015-
1016- >>> print_permission(
1017- ... ap_set.uploadersForPackageset(ubuntu_archive, firefox_ps))
1018- primary juergen: firefox -> UPLOAD (implicit)
1019-
1020-.. and thus also listed as a possible uploader of the 'mozilla-firefox'
1021-source package.
1022-
1023- >>> print_permission(
1024- ... ap_set.packagesetsForSourceUploader(
1025- ... ubuntu_archive, 'mozilla-firefox', person2))
1026- primary juergen: firefox -> UPLOAD (implicit)
1027-
1028-Fetching the permissions for a non-existent source package name will fail
1029-as follows:
1030-
1031- >>> print_permission(
1032- ... ap_set.packagesetsForSourceUploader(
1033- ... ubuntu_archive, 'vapour-ware', person2))
1034- Traceback (most recent call last):
1035- ...
1036- NoSuchSourcePackageName: No such source package: 'vapour-ware'.
1037-
1038-Now for the verdict: is 'hacker' allowed to upload 'mozilla-firefox'?
1039-
1040- >>> ap_set.isSourceUploadAllowed(
1041- ... ubuntu_archive, 'mozilla-firefox', person1)
1042- True
1043-
1044-How about 'juergen'?
1045-
1046- >>> ap_set.isSourceUploadAllowed(
1047- ... ubuntu_archive, 'mozilla-firefox', person2)
1048- True
1049-
1050-And the unprivileged user?
1051-
1052- >>> unprivileged = getUtility(IPersonSet).getByName("no-priv")
1053- >>> ap_set.isSourceUploadAllowed(
1054- ... ubuntu_archive, 'mozilla-firefox', unprivileged)
1055- False
1056-
1057-So, what we see is that the package set inclusion hierarchy is honored as
1058-long as there are no explicit permissions for the package sets involved.
1059-
1060-Let's
1061-
1062- * create a super-special package set
1063- * populate it with the 'mozilla-firefox' package
1064- * grant an explicit permission to the unprivileged person
1065-
1066-and see how that changes things.
1067-
1068- >>> login("foo.bar@canonical.com")
1069- >>> specialist_ps = ps_factory.new(
1070- ... u'specialists-only', u'Packages that require special care.',
1071- ... person1)
1072- >>> store.commit()
1073-
1074- >>> specialist_ps.add((firefox_spn,))
1075- >>> print_data(
1076- ... ps_set.setsIncludingSource('mozilla-firefox',
1077- ... direct_inclusion=True))
1078- 1 -> umbrella
1079- 5 -> firefox
1080- 9 -> specialists-only
1081-
1082- >>> ignore_this = ap_set.newPackagesetUploader(
1083- ... ubuntu_archive, unprivileged, specialist_ps, True)
1084-
1085-Please note that there's a package set now that includes 'mozilla-firefox'
1086-and has an explicit permission.
1087-
1088- >>> for ps in sort_by_id(ps_set.setsIncludingSource(
1089- ... 'mozilla-firefox', direct_inclusion=True)):
1090- ... print_permission(
1091- ... ap_set.uploadersForPackageset(ubuntu_archive, ps))
1092- primary juergen: firefox -> UPLOAD (implicit)
1093- primary no-priv: specialists-only -> UPLOAD (explicit)
1094-
1095-Is 'hacker' still allowed to upload 'mozilla-firefox'?
1096-
1097- >>> ap_set.isSourceUploadAllowed(
1098- ... ubuntu_archive, 'mozilla-firefox', person1)
1099- False
1100-
1101-How about 'juergen'?
1102-
1103- >>> ap_set.isSourceUploadAllowed(
1104- ... ubuntu_archive, 'mozilla-firefox', person2)
1105- False
1106-
1107-Now neither 'hacker' nor 'juergen' are allowed to upload. All non-explicit
1108-permissions are ignored in the presence of explicit ones.
1109-
1110-The 'unprivileged' person who holds an explicit permission for the
1111-'specialists-only' package set (including the 'mozilla-firefox' source)
1112-is allowed to upload.
1113-
1114- >>> ap_set.isSourceUploadAllowed(
1115- ... ubuntu_archive, 'mozilla-firefox', unprivileged)
1116- True
1117-
1118-
1119-== Methods for the Launchpad web services API ==
1120-
1121-The following methods are used to expose the package set functionality
1122-via the Launchpad web services API.
1123-
1124-Let's create a few package sets first.
1125-
1126- >>> gnome_ps = ps_factory.new(
1127- ... u'gnome', u'Contains all gnome desktop packages', person2)
1128- >>> xwin_ps = ps_factory.new(
1129- ... u'x-win', u'Contains all X windows packages', person2)
1130- >>> universe_ps = ps_factory.new(
1131- ... u'universe', u'Contains all universe packages', person2)
1132- >>> multiverse_ps = ps_factory.new(
1133- ... u'multiverse', u'Contains all multiverse packages', person2)
1134-
1135-The new package sets are added to 'umbrella' by passing their names. Please
1136-note that non-existent package sets (e.g. 'not-there') are simply ignored.
1137-
1138- >>> to_be_added = (
1139- ... u'gnome', u'x-win', u'universe', u'multiverse', u'not-there')
1140- >>> umbrella_ps.addSubsets(to_be_added)
1141- >>> print_data(umbrella_ps.setsIncluded(direct_inclusion=True))
1142- 4 -> mozilla
1143- 10 -> gnome
1144- 11 -> x-win
1145- 12 -> universe
1146- 13 -> multiverse
1147-
1148-Package subsets can be removed in a similar fashion. Non-existent sets
1149-or sets which are not (direct) subsets are ignored again.
1150-
1151- >>> to_be_removed = (u'umbrella', u'universe', u'multiverse', u'not-mine')
1152- >>> umbrella_ps.removeSubsets(to_be_removed)
1153- >>> print_data(umbrella_ps.setsIncluded(direct_inclusion=True))
1154- 4 -> mozilla
1155- 10 -> gnome
1156- 11 -> x-win
1157-
1158-Source package names can be added by merely specifying their names.
1159-
1160- >>> print_data(mozilla_ps.sourcesIncluded(direct_inclusion=True))
1161- 16 -> mozilla
1162-
1163- >>> mozilla_ps.addSources(('cdrkit', 'foobar', 'emacs'))
1164- >>> print_data(mozilla_ps.sourcesIncluded(direct_inclusion=True))
1165- 16 -> mozilla
1166- 23 -> foobar
1167- 24 -> cdrkit
1168-
1169-It also possible to remove source package names by their names.
1170-
1171- >>> mozilla_ps.removeSources(('mozilla', 'zope', 'firefox'))
1172- >>> print_data(mozilla_ps.sourcesIncluded(direct_inclusion=True))
1173- 23 -> foobar
1174- 24 -> cdrkit
1175
1176=== modified file 'lib/lp/soyuz/interfaces/packageset.py'
1177--- lib/lp/soyuz/interfaces/packageset.py 2012-05-17 16:24:42 +0000
1178+++ lib/lp/soyuz/interfaces/packageset.py 2012-09-28 14:54:21 +0000
1179@@ -22,6 +22,7 @@
1180 export_as_webservice_collection,
1181 export_as_webservice_entry,
1182 export_factory_operation,
1183+ export_operation_as,
1184 export_read_operation,
1185 export_write_operation,
1186 exported,
1187@@ -71,7 +72,7 @@
1188 description=_("The creation date/time for the package set at hand.")))
1189
1190 owner = exported(Reference(
1191- IPerson, title=_("Person"), required=True, readonly=True,
1192+ IPerson, title=_("Person"), required=True,
1193 description=_("The person who owns this package set.")))
1194
1195 name = exported(TextLine(
1196@@ -79,7 +80,7 @@
1197 required=True, constraint=name_validator))
1198
1199 description = exported(TextLine(
1200- title=_("Description"), required=True, readonly=True,
1201+ title=_("Description"), required=True,
1202 description=_("The description for the package set at hand.")))
1203
1204 distroseries = exported(Reference(
1205@@ -347,6 +348,12 @@
1206 :param names: an iterable with string package set names
1207 """
1208
1209+ @export_write_operation()
1210+ @export_operation_as('delete')
1211+ @operation_for_version('devel')
1212+ def destroySelf():
1213+ """Delete the package set."""
1214+
1215
1216 class IPackagesetRestricted(Interface):
1217 """A writeable interface for restricted attributes of package sets."""
1218
1219=== modified file 'lib/lp/soyuz/model/packageset.py'
1220--- lib/lp/soyuz/model/packageset.py 2012-06-13 11:22:35 +0000
1221+++ lib/lp/soyuz/model/packageset.py 2012-09-28 14:54:21 +0000
1222@@ -329,6 +329,16 @@
1223 Packageset.id != self.id)
1224 return _order_result_set(result_set)
1225
1226+ def destroySelf(self):
1227+ store = IStore(Packageset)
1228+ sources = store.find(
1229+ PackagesetSources,
1230+ PackagesetSources.packageset == self)
1231+ sources.remove()
1232+ store.remove(self)
1233+ if self.relatedSets().is_empty():
1234+ store.remove(self.packagesetgroup)
1235+
1236
1237 class PackagesetSet:
1238 """See `IPackagesetSet`."""
1239
1240=== modified file 'lib/lp/soyuz/stories/webservice/xx-packageset.txt'
1241--- lib/lp/soyuz/stories/webservice/xx-packageset.txt 2012-09-28 06:34:26 +0000
1242+++ lib/lp/soyuz/stories/webservice/xx-packageset.txt 2012-09-28 14:54:21 +0000
1243@@ -14,7 +14,7 @@
1244 The actual package set *functionality* is tested in much greater detail
1245 here:
1246
1247- lib/lp/soyuz/doc/packageset.txt
1248+ lb/lp/soyuz/tests/test_packageset.py
1249
1250 Please refer to the tests contained in the file above if you are really
1251 interested in package sets and the complete functionality they offer.
1252@@ -83,6 +83,37 @@
1253 HTTP/1.1 404 Not Found
1254 ...
1255
1256+Let's create another set.
1257+
1258+ >>> response = webservice.named_post(
1259+ ... '/package-sets', 'new', {},
1260+ ... name=u'shortlived', description=u'An ephemeral packageset',
1261+ ... owner=name12['self_link'])
1262+ >>> print response
1263+ HTTP/1.1 201 Created
1264+ ...
1265+
1266+We can modify it, and even give it away.
1267+
1268+ >>> from simplejson import dumps
1269+ >>> name16 = webservice.get("/~name16").jsonBody()
1270+ >>> patch = {u'name': 'renamed',
1271+ ... u'description': u'Repurposed packageset',
1272+ ... u'owner_link': name16['self_link']}
1273+ >>> response = webservice.patch(
1274+ ... '/package-sets/hoary/shortlived', 'application/json', dumps(patch))
1275+ >>> print response
1276+ HTTP/1.1 301 Moved Permanently
1277+ ...
1278+
1279+And then delete it.
1280+
1281+ >>> response = webservice.named_post(
1282+ ... '/package-sets/hoary/renamed', 'delete', {}, api_version='devel')
1283+ >>> print response
1284+ HTTP/1.1 200 Ok
1285+ ...
1286+
1287 Populate the 'umbrella' package set with source packages.
1288
1289 >>> from lp.services.database.interfaces import (
1290
1291=== modified file 'lib/lp/soyuz/tests/test_packageset.py'
1292--- lib/lp/soyuz/tests/test_packageset.py 2012-01-01 02:58:52 +0000
1293+++ lib/lp/soyuz/tests/test_packageset.py 2012-09-28 14:54:21 +0000
1294@@ -4,21 +4,41 @@
1295 """Test Packageset features."""
1296
1297 from zope.component import getUtility
1298+from zope.security.interfaces import Unauthorized
1299
1300+from lp.app.errors import NotFoundError
1301+from lp.registry.errors import NoSuchSourcePackageName
1302 from lp.registry.interfaces.distribution import IDistributionSet
1303 from lp.registry.interfaces.series import SeriesStatus
1304+from lp.services.database.lpstorm import IStore
1305+from lp.soyuz.enums import ArchivePermissionType
1306+from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
1307 from lp.soyuz.interfaces.packageset import (
1308 DuplicatePackagesetName,
1309 IPackagesetSet,
1310- )
1311-from lp.testing import TestCaseWithFactory
1312-from lp.testing.layers import ZopelessDatabaseLayer
1313+ NoSuchPackageSet,
1314+ )
1315+from lp.soyuz.model.packagesetgroup import PackagesetGroup
1316+from lp.testing import (
1317+ TestCaseWithFactory,
1318+ admin_logged_in,
1319+ celebrity_logged_in,
1320+ person_logged_in,
1321+ )
1322+from lp.testing.layers import (
1323+ DatabaseFunctionalLayer,
1324+ ZopelessDatabaseLayer,
1325+ )
1326
1327
1328 class TestPackagesetSet(TestCaseWithFactory):
1329
1330 layer = ZopelessDatabaseLayer
1331
1332+ def setUp(self):
1333+ super(TestPackagesetSet, self).setUp()
1334+ self.ps_set = getUtility(IPackagesetSet)
1335+
1336 def getUbuntu(self):
1337 """Get the Ubuntu `Distribution`."""
1338 return getUtility(IDistributionSet).getByName('ubuntu')
1339@@ -32,7 +52,7 @@
1340 def test_new_defaults_to_current_distroseries(self):
1341 # If the distroseries is not provided, the current development
1342 # distroseries will be assumed.
1343- packageset = getUtility(IPackagesetSet).new(
1344+ packageset = self.ps_set.new(
1345 self.factory.getUniqueUnicode(), self.factory.getUniqueUnicode(),
1346 self.factory.makePerson())
1347 self.failUnlessEqual(
1348@@ -41,7 +61,7 @@
1349 def test_new_with_specified_distroseries(self):
1350 # A distroseries can be provided when creating a package set.
1351 experimental_series = self.makeExperimentalSeries()
1352- packageset = getUtility(IPackagesetSet).new(
1353+ packageset = self.ps_set.new(
1354 self.factory.getUniqueUnicode(), self.factory.getUniqueUnicode(),
1355 self.factory.makePerson(), distroseries=experimental_series)
1356 self.failUnlessEqual(experimental_series, packageset.distroseries)
1357@@ -51,7 +71,7 @@
1358 # group with the same owner.
1359 owner = self.factory.makePerson()
1360 experimental_series = self.makeExperimentalSeries()
1361- packageset = getUtility(IPackagesetSet).new(
1362+ packageset = self.ps_set.new(
1363 self.factory.getUniqueUnicode(), self.factory.getUniqueUnicode(),
1364 owner, distroseries=experimental_series)
1365 self.failUnlessEqual(owner, packageset.packagesetgroup.owner)
1366@@ -63,7 +83,7 @@
1367 name = self.factory.getUniqueUnicode()
1368 self.factory.makePackageset(name, distroseries=distroseries)
1369 self.assertRaises(
1370- DuplicatePackagesetName, getUtility(IPackagesetSet).new,
1371+ DuplicatePackagesetName, self.ps_set.new,
1372 name, self.factory.getUniqueUnicode(), self.factory.makePerson(),
1373 distroseries=distroseries)
1374
1375@@ -72,7 +92,7 @@
1376 # series is no problem.
1377 name = self.factory.getUniqueUnicode()
1378 packageset1 = self.factory.makePackageset(name)
1379- packageset2 = getUtility(IPackagesetSet).new(
1380+ packageset2 = self.ps_set.new(
1381 name, self.factory.getUniqueUnicode(), self.factory.makePerson(),
1382 distroseries=self.factory.makeDistroSeries())
1383 self.assertEqual(packageset1.name, packageset2.name)
1384@@ -97,7 +117,7 @@
1385 self.factory.makePackageset(
1386 name, distroseries=self.makeExperimentalSeries(),
1387 related_set=pset1)
1388- self.assertEqual(pset1, getUtility(IPackagesetSet).getByName(name))
1389+ self.assertEqual(pset1, self.ps_set.getByName(name))
1390
1391 def test_get_by_name_in_specified_distroseries(self):
1392 # IPackagesetSet.getByName() will return the package set in the
1393@@ -107,7 +127,7 @@
1394 pset1 = self.factory.makePackageset(name)
1395 pset2 = self.factory.makePackageset(
1396 name, distroseries=experimental_series, related_set=pset1)
1397- pset_found = getUtility(IPackagesetSet).getByName(
1398+ pset_found = self.ps_set.getByName(
1399 name, distroseries=experimental_series)
1400 self.assertEqual(pset2, pset_found)
1401
1402@@ -120,8 +140,7 @@
1403 distroseries=self.makeExperimentalSeries())
1404 self.assertContentEqual(
1405 package_sets_for_current_ubuntu,
1406- getUtility(IPackagesetSet).getBySeries(
1407- self.getUbuntu().currentseries))
1408+ self.ps_set.getBySeries(self.getUbuntu().currentseries))
1409
1410 def test_getForPackages_returns_packagesets(self):
1411 # getForPackages finds package sets for given source package
1412@@ -133,7 +152,7 @@
1413 packageset.addSources([package.name])
1414 self.assertEqual(
1415 {package.id: [packageset]},
1416- getUtility(IPackagesetSet).getForPackages(series, [package.id]))
1417+ self.ps_set.getForPackages(series, [package.id]))
1418
1419 def test_getForPackages_filters_by_distroseries(self):
1420 # getForPackages does not return packagesets for different
1421@@ -144,9 +163,7 @@
1422 package = self.factory.makeSourcePackageName()
1423 packageset.addSources([package.name])
1424 self.assertEqual(
1425- {},
1426- getUtility(IPackagesetSet).getForPackages(
1427- other_series, [package.id]))
1428+ {}, self.ps_set.getForPackages(other_series, [package.id]))
1429
1430 def test_getForPackages_filters_by_sourcepackagename(self):
1431 # getForPackages does not return packagesets for different
1432@@ -157,9 +174,106 @@
1433 other_package = self.factory.makeSourcePackageName()
1434 packageset.addSources([package.name])
1435 self.assertEqual(
1436- {},
1437- getUtility(IPackagesetSet).getForPackages(
1438- series, [other_package.id]))
1439+ {}, self.ps_set.getForPackages(series, [other_package.id]))
1440+
1441+ def test_getByOwner(self):
1442+ # Sets can be looked up by owner
1443+ person = self.factory.makePerson()
1444+ self.factory.makePackageset(owner=person)
1445+ self.assertEqual(self.ps_set.getByOwner(person).count(), 1)
1446+
1447+ def test_dict_access(self):
1448+ # The packagesetset acts as a dictionary
1449+ packageset = self.factory.makePackageset()
1450+ self.assertEqual(self.ps_set[packageset.name], packageset)
1451+
1452+ def test_list(self):
1453+ # get returns the first N (N=50 by default) package sets sorted by name
1454+ # for iterating packagesets over the web services API
1455+ psets = [self.factory.makePackageset() for i in range(5)]
1456+ psets.sort(key=lambda p: p.name)
1457+
1458+ self.assertEqual(list(self.ps_set.get()), psets)
1459+
1460+ def buildSimpleHierarchy(self, series=None):
1461+ parent = self.factory.makePackageset(distroseries=series)
1462+ child = self.factory.makePackageset(distroseries=series)
1463+ package = self.factory.makeSourcePackageName()
1464+ parent.add((child,))
1465+ child.add((package,))
1466+ return parent, child, package
1467+
1468+ def test_sets_including_source(self):
1469+ # Returns the list of sets including a source package
1470+ parent, child, package = self.buildSimpleHierarchy()
1471+ self.assertEqual(
1472+ sorted(self.ps_set.setsIncludingSource(package)),
1473+ sorted((parent, child)))
1474+
1475+ # And can be limited to direct inclusion
1476+ result = self.ps_set.setsIncludingSource(
1477+ package, direct_inclusion=True)
1478+ self.assertEqual(list(result), [child])
1479+
1480+ def test_sets_including_source_same_series(self):
1481+ # setsIncludingSource by default searches the current series, but a
1482+ # series can be specified
1483+ series = self.factory.makeDistroSeries()
1484+ parent, child, package = self.buildSimpleHierarchy(series)
1485+ result = self.ps_set.setsIncludingSource(
1486+ package, distroseries=series)
1487+ self.assertEqual(sorted(result), sorted([parent, child]))
1488+
1489+ def test_sets_including_source_different_series(self):
1490+ # searches are limited to one series
1491+ parent, child, package = self.buildSimpleHierarchy()
1492+ series = self.factory.makeDistroSeries()
1493+ result = self.ps_set.setsIncludingSource(
1494+ package, distroseries=series)
1495+ self.assertTrue(result.is_empty())
1496+
1497+ def test_sets_including_source_by_name(self):
1498+ # Returns the list osf sets including a source package
1499+ parent, child, package = self.buildSimpleHierarchy()
1500+ self.assertEqual(
1501+ sorted(self.ps_set.setsIncludingSource(package.name)),
1502+ sorted([parent, child]))
1503+
1504+ def test_sets_including_source_unknown_name(self):
1505+ # A non-existent package name will throw an exception
1506+ self.assertRaises(
1507+ NoSuchSourcePackageName,
1508+ self.ps_set.setsIncludingSource, 'this-will-fail')
1509+
1510+
1511+class TestPackagesetSetPermissions(TestCaseWithFactory):
1512+
1513+ layer = DatabaseFunctionalLayer
1514+
1515+ def setUp(self):
1516+ super(TestPackagesetSetPermissions, self).setUp()
1517+ self.ps_set = getUtility(IPackagesetSet)
1518+
1519+ def test_create_packageset_as_user(self):
1520+ # Normal users can't create packagesets
1521+ with person_logged_in(self.factory.makePerson()):
1522+ self.assertRaises(Unauthorized, getattr, self.ps_set, 'new')
1523+
1524+ def test_create_packagset_as_techboard(self):
1525+ # Ubuntu techboard members can create packagesets
1526+ with celebrity_logged_in('ubuntu_techboard'):
1527+ self.ps_set.new(
1528+ self.factory.getUniqueUnicode(),
1529+ self.factory.getUniqueUnicode(),
1530+ self.factory.makePerson())
1531+
1532+ def test_create_packagset_as_admin(self):
1533+ # Admins can create packagesets
1534+ with admin_logged_in():
1535+ self.ps_set.new(
1536+ self.factory.getUniqueUnicode(),
1537+ self.factory.getUniqueUnicode(),
1538+ self.factory.makePerson())
1539
1540
1541 class TestPackageset(TestCaseWithFactory):
1542@@ -226,3 +340,687 @@
1543 # Unsurprisingly, the unrelated package set is not associated with any
1544 # other package set.
1545 self.failUnlessEqual(pset3.relatedSets().count(), 0)
1546+
1547+ def test_destroy(self):
1548+ pset = self.packageset_set.new(
1549+ u'kernel', u'Contains all OS kernel packages', self.person1)
1550+ pset.destroySelf()
1551+ self.assertRaises(NoSuchPackageSet, self.packageset_set.getByName,
1552+ u'kernel')
1553+
1554+ # Did we clean up the single packagesetgroup?
1555+ store = IStore(PackagesetGroup)
1556+ result_set = store.find(PackagesetGroup)
1557+ self.assertTrue(result_set.is_empty())
1558+
1559+ def test_destroy_with_ancestor(self):
1560+ ancestor = self.packageset_set.new(
1561+ u'kernel', u'Contains all OS kernel packages', self.person1)
1562+ pset = self.packageset_set.new(
1563+ u'kernel', u'Contains all OS kernel packages', self.person1,
1564+ distroseries=self.distroseries_experimental, related_set=ancestor)
1565+ pset.destroySelf()
1566+ self.assertRaises(
1567+ NoSuchPackageSet, self.packageset_set.getByName,
1568+ u'kernel', distroseries=self.distroseries_experimental)
1569+
1570+ def test_destroy_with_packages(self):
1571+ pset = self.packageset_set.new(
1572+ u'kernel', u'Contains all OS kernel packages', self.person1)
1573+ package = self.factory.makeSourcePackageName()
1574+ pset.addSources([package.name])
1575+
1576+ pset.destroySelf()
1577+ self.assertRaises(NoSuchPackageSet, self.packageset_set.getByName,
1578+ u'kernel')
1579+
1580+ def test_destroy_child(self):
1581+ parent = self.packageset_set.new(
1582+ u'core', u'Contains all the important packages', self.person1)
1583+ child = self.packageset_set.new(
1584+ u'kernel', u'Contains all OS kernel packages', self.person1)
1585+ parent.add((child,))
1586+
1587+ child.destroySelf()
1588+ self.assertRaises(NoSuchPackageSet, self.packageset_set.getByName,
1589+ u'kernel')
1590+ self.assertTrue(parent.setsIncluded(direct_inclusion=True).is_empty())
1591+
1592+ def test_destroy_parent(self):
1593+ parent = self.packageset_set.new(
1594+ u'core', u'Contains all the important packages', self.person1)
1595+ child = self.packageset_set.new(
1596+ u'kernel', u'Contains all OS kernel packages', self.person1)
1597+ parent.add((child,))
1598+
1599+ parent.destroySelf()
1600+ self.assertRaises(NoSuchPackageSet, self.packageset_set.getByName,
1601+ u'core')
1602+ self.assertTrue(child.setsIncludedBy(direct_inclusion=True).is_empty())
1603+
1604+ def test_destroy_intermidate(self):
1605+ # Destroying an intermediate packageset severs the indirect inclusion
1606+ parent = self.factory.makePackageset()
1607+ child = self.factory.makePackageset()
1608+ grandchild = self.factory.makePackageset()
1609+ parent.add((child,))
1610+ child.add((grandchild,))
1611+ self.assertEqual(parent.setsIncluded().count(), 2)
1612+
1613+ child.destroySelf()
1614+ self.assertRaises(NoSuchPackageSet, self.packageset_set.getByName,
1615+ child.name)
1616+ self.assertTrue(parent.setsIncluded().is_empty())
1617+
1618+ def buildSet(self, size=5):
1619+ packageset = self.factory.makePackageset()
1620+ packages = [self.factory.makeSourcePackageName() for i in range(size)]
1621+ packageset.add(packages)
1622+ return packageset, packages
1623+
1624+ def test_sources_included(self):
1625+ # Lists the source packages included in a set
1626+ packageset, packages = self.buildSet()
1627+ self.assertEqual(
1628+ sorted(packageset.sourcesIncluded()), sorted(packages))
1629+
1630+ def test_get_sources_included(self):
1631+ # Lists the names of source packages included in a set
1632+ packageset, packages = self.buildSet()
1633+ self.assertEqual(
1634+ sorted(packageset.getSourcesIncluded()),
1635+ sorted(p.name for p in packages))
1636+
1637+ def test_sources_included_indirect(self):
1638+ # sourcesIncluded traverses the set tree, by default
1639+ packageset1, packages1 = self.buildSet()
1640+ packageset2, packages2 = self.buildSet()
1641+ packageset1.add((packageset2,))
1642+ self.assertEqual(
1643+ sorted(packageset1.sourcesIncluded()),
1644+ sorted(packages1 + packages2))
1645+
1646+ # direct_inclusion disables traversal
1647+ self.assertEqual(
1648+ sorted(packageset1.sourcesIncluded(direct_inclusion=True)),
1649+ sorted(packages1))
1650+
1651+ def test_sources_multiply_included(self):
1652+ # Source packages included in multiple packagesets in a tree are only
1653+ # listed once.
1654+ packageset1, packages1 = self.buildSet(5)
1655+ packageset2, packages2 = self.buildSet(5)
1656+ packageset1.add(packages2[:2])
1657+ packageset1.add((packageset2,))
1658+ self.assertEqual(
1659+ sorted(packageset1.sourcesIncluded(direct_inclusion=True)),
1660+ sorted(packages1 + packages2[:2]))
1661+ self.assertEqual(
1662+ sorted(packageset1.sourcesIncluded()),
1663+ sorted(packages1 + packages2))
1664+
1665+ def test_add_already_included_sources(self):
1666+ # Adding source packages to a package set repeatedly has no effect
1667+ packageset, packages = self.buildSet()
1668+ packageset.add(packages)
1669+ self.assertEqual(
1670+ sorted(packageset.sourcesIncluded()), sorted(packages))
1671+
1672+ def test_remove_sources(self):
1673+ # Source packages can be removed from a set
1674+ packageset, packages = self.buildSet(5)
1675+ packageset.remove(packages[:2])
1676+ self.assertEqual(
1677+ sorted(packageset.sourcesIncluded()), sorted(packages[2:]))
1678+
1679+ def test_remove_non_preset_sources(self):
1680+ # Trying to remove source packages that are *not* in the set, has no
1681+ # effect.
1682+ packageset, packages = self.buildSet()
1683+ packageset.remove([self.factory.makeSourcePackageName()])
1684+ self.assertTrue(sorted(packageset.sourcesIncluded()), sorted(packages))
1685+
1686+ def test_sets_included(self):
1687+ # Returns the sets included in a set
1688+ parent = self.factory.makePackageset()
1689+ child = self.factory.makePackageset()
1690+ parent.add((child,))
1691+ grandchild = self.factory.makePackageset()
1692+ child.add((grandchild,))
1693+ self.assertEqual(
1694+ sorted(parent.setsIncluded()), sorted([child, grandchild]))
1695+ self.assertEqual(
1696+ list(parent.setsIncluded(direct_inclusion=True)), [child])
1697+
1698+ def test_sets_included_multipath(self):
1699+ # A set can be included by multiple paths, but will only appear once in
1700+ # setsIncluded
1701+ parent = self.factory.makePackageset()
1702+ child = self.factory.makePackageset()
1703+ child2 = self.factory.makePackageset()
1704+ parent.add((child, child2))
1705+ grandchild = self.factory.makePackageset()
1706+ child.add((grandchild,))
1707+ child2.add((grandchild,))
1708+ self.assertEqual(
1709+ sorted(parent.setsIncluded()), sorted([child, child2, grandchild]))
1710+
1711+ def test_sets_included_by(self):
1712+ # Returns the set of sets including a set
1713+ parent = self.factory.makePackageset()
1714+ child = self.factory.makePackageset()
1715+ parent.add((child,))
1716+ grandchild = self.factory.makePackageset()
1717+ child.add((grandchild,))
1718+ self.assertEqual(
1719+ sorted(grandchild.setsIncludedBy()), sorted([child, parent]))
1720+ self.assertEqual(
1721+ list(grandchild.setsIncludedBy(direct_inclusion=True)), [child])
1722+
1723+ def test_remove_subset(self):
1724+ # A set can be removed from another set
1725+ parent = self.factory.makePackageset()
1726+ child = self.factory.makePackageset()
1727+ child2 = self.factory.makePackageset()
1728+ parent.add((child, child2))
1729+ self.assertEqual(
1730+ sorted(parent.setsIncluded()), sorted([child, child2]))
1731+ parent.remove((child,))
1732+ self.assertEqual(list(parent.setsIncluded()), [child2])
1733+
1734+ def test_remove_indirect_subset(self):
1735+ # Removing indirect successors has no effect.
1736+ parent = self.factory.makePackageset()
1737+ child = self.factory.makePackageset()
1738+ grandchild = self.factory.makePackageset()
1739+ parent.add((child,))
1740+ child.add((grandchild,))
1741+ self.assertEqual(
1742+ sorted(parent.setsIncluded()), sorted([child, grandchild]))
1743+ parent.remove((grandchild,))
1744+ self.assertEqual(
1745+ sorted(parent.setsIncluded()), sorted([child, grandchild]))
1746+
1747+ def test_sources_shared_by(self):
1748+ # Lists the source packages shared between two packagesets
1749+ pset1, pkgs1 = self.buildSet(5)
1750+ pset2, pkgs2 = self.buildSet(5)
1751+ self.assertTrue(pset1.sourcesSharedBy(pset2).is_empty())
1752+
1753+ pset1.add(pkgs2[:2])
1754+ pset2.add(pkgs1[:2])
1755+ self.assertEqual(
1756+ sorted(pset1.sourcesSharedBy(pset2)),
1757+ sorted(pkgs1[:2] + pkgs2[:2]))
1758+
1759+ def test_get_sources_shared_by(self):
1760+ # List the names of source packages shared between two packagesets
1761+ pset1, pkgs1 = self.buildSet(5)
1762+ pset2, pkgs2 = self.buildSet(5)
1763+ self.assertEqual(pset1.getSourcesSharedBy(pset2), [])
1764+
1765+ pset1.add(pkgs2[:2])
1766+ pset2.add(pkgs1[:2])
1767+ self.assertEqual(
1768+ sorted(pset1.getSourcesSharedBy(pset2)),
1769+ sorted(p.name for p in (pkgs1[:2] + pkgs2[:2])))
1770+
1771+ def test_sources_shared_by_subset(self):
1772+ # sourcesSharedBy takes subsets into account, unless told not to
1773+ pset1, pkgs1 = self.buildSet()
1774+ pset2, pkgs2 = self.buildSet()
1775+ self.assertTrue(pset1.sourcesSharedBy(pset2).is_empty())
1776+
1777+ pset1.add((pset2,))
1778+ self.assertEqual(sorted(pset1.sourcesSharedBy(pset2)), sorted(pkgs2))
1779+ self.assertTrue(
1780+ pset1.sourcesSharedBy(pset2, direct_inclusion=True).is_empty())
1781+
1782+ def test_sources_shared_by_symmetric(self):
1783+ # sourcesSharedBy is symmetric
1784+ pset1, pkgs1 = self.buildSet(5)
1785+ pset2, pkgs2 = self.buildSet(5)
1786+ pset3, pkgs3 = self.buildSet(5)
1787+ self.assertTrue(pset1.sourcesSharedBy(pset2).is_empty())
1788+
1789+ pset1.add(pkgs2[:2] + pkgs3)
1790+ pset2.add(pkgs1[:2] + [pset3])
1791+ self.assertEqual(
1792+ sorted(pset1.sourcesSharedBy(pset2)),
1793+ sorted(pkgs1[:2] + pkgs2[:2] + pkgs3))
1794+ self.assertEqual(
1795+ sorted(pset1.sourcesSharedBy(pset2)),
1796+ sorted(pset2.sourcesSharedBy(pset1)))
1797+
1798+ def test_sources_not_shared_by(self):
1799+ # Lists source packages in the first set, but not the second
1800+ pset1, pkgs1 = self.buildSet(5)
1801+ pset2, pkgs2 = self.buildSet(5)
1802+ self.assertEqual(
1803+ sorted(pset1.sourcesNotSharedBy(pset2)), sorted(pkgs1))
1804+ pset1.add(pkgs2[:2])
1805+ pset2.add(pkgs1[:2])
1806+ self.assertEqual(
1807+ sorted(pset1.sourcesNotSharedBy(pset2)), sorted(pkgs1[2:]))
1808+
1809+ def test_get_sources_not_shared_by(self):
1810+ # List the names of source packages in the first set, but not the
1811+ # second
1812+ pset1, pkgs1 = self.buildSet(5)
1813+ pset2, pkgs2 = self.buildSet(5)
1814+ self.assertEqual(
1815+ sorted(pset1.getSourcesNotSharedBy(pset2)),
1816+ sorted(p.name for p in pkgs1))
1817+
1818+ pset1.add(pkgs2[:2])
1819+ pset2.add(pkgs1[:2])
1820+ self.assertEqual(
1821+ sorted(pset1.getSourcesNotSharedBy(pset2)),
1822+ sorted(p.name for p in pkgs1[2:]))
1823+
1824+ def test_sources_not_shared_by_subset(self):
1825+ # sourcesNotSharedBy takes subsets into account, unless told not to
1826+ pset1, pkgs1 = self.buildSet()
1827+ pset2, pkgs2 = self.buildSet()
1828+ self.assertTrue(sorted(pset1.sourcesNotSharedBy(pset2)), sorted(pkgs1))
1829+
1830+ pset2.add((pset1,))
1831+ self.assertTrue(pset1.sourcesNotSharedBy(pset2).is_empty())
1832+ self.assertTrue(
1833+ sorted(pset1.sourcesNotSharedBy(pset2, direct_inclusion=True)),
1834+ sorted(pkgs1))
1835+
1836+ def test_add_unknown_name(self):
1837+ # Adding an unknown package name will raise an error
1838+ pset = self.factory.makePackageset()
1839+ self.assertRaises(
1840+ AssertionError, pset.add, [self.factory.getUniqueUnicode()])
1841+
1842+ def test_remove_unknown_name(self):
1843+ # Removing an unknown package name will raise an error
1844+ pset = self.factory.makePackageset()
1845+ self.assertRaises(
1846+ AssertionError, pset.remove, [self.factory.getUniqueUnicode()])
1847+
1848+ def test_add_cycle(self):
1849+ # Adding cycles to the graph will raise an error
1850+ parent = self.factory.makePackageset()
1851+ child = self.factory.makePackageset()
1852+ parent.add((child,))
1853+ self.assertRaises(Exception, child.add, (parent,))
1854+
1855+ def test_add_indirect_cycle(self):
1856+ # Adding indirect cycles to the graph will raise an error
1857+ parent = self.factory.makePackageset()
1858+ child = self.factory.makePackageset()
1859+ grandchild = self.factory.makePackageset()
1860+ parent.add((child,))
1861+ child.add((grandchild,))
1862+ self.assertRaises(Exception, grandchild.add, (parent,))
1863+
1864+
1865+class TestPackagesetPermissions(TestCaseWithFactory):
1866+
1867+ layer = DatabaseFunctionalLayer
1868+
1869+ def setUp(self):
1870+ super(TestPackagesetPermissions, self).setUp()
1871+ self.person = self.factory.makePerson()
1872+ self.person2 = self.factory.makePerson()
1873+ self.packageset = self.factory.makePackageset(owner=self.person)
1874+ self.packageset2 = self.factory.makePackageset(owner=self.person)
1875+ self.package = self.factory.makeSourcePackageName()
1876+
1877+ def test_user_modify_packageset(self):
1878+ # Normal users may not modify packagesets
1879+ with person_logged_in(self.person2):
1880+ self.assertRaises(
1881+ Unauthorized, setattr, self.packageset, 'name', u'renamed')
1882+ self.assertRaises(
1883+ Unauthorized, setattr, self.packageset, 'description',
1884+ u'Re-described')
1885+ self.assertRaises(
1886+ Unauthorized, setattr, self.packageset, 'owner', self.person2)
1887+ self.assertRaises(
1888+ Unauthorized, getattr, self.packageset, 'add')
1889+ self.assertRaises(
1890+ Unauthorized, getattr, self.packageset, 'remove')
1891+ self.assertRaises(
1892+ Unauthorized, getattr, self.packageset, 'addSources')
1893+ self.assertRaises(
1894+ Unauthorized, getattr, self.packageset, 'removeSources')
1895+ self.assertRaises(
1896+ Unauthorized, getattr, self.packageset, 'addSubsets')
1897+ self.assertRaises(
1898+ Unauthorized, getattr, self.packageset, 'removeSubsets')
1899+
1900+ def modifyPackageset(self):
1901+ self.packageset.name = u'renamed'
1902+ self.packageset.description = u'Re-described'
1903+ self.packageset.add((self.package,))
1904+ self.packageset.remove((self.package,))
1905+ self.packageset.addSources((self.package.name,))
1906+ self.packageset.removeSources((self.package.name,))
1907+ self.packageset.add((self.packageset2,))
1908+ self.packageset.remove((self.packageset2,))
1909+ self.packageset.addSubsets((self.packageset2.name,))
1910+ self.packageset.removeSubsets((self.packageset2.name,))
1911+ self.packageset.owner = self.person2
1912+
1913+ def test_owner_modify_packageset(self):
1914+ # Packageset owners can modify their packagesets
1915+ with person_logged_in(self.person):
1916+ self.modifyPackageset()
1917+
1918+ def test_admin_modify_packageset(self):
1919+ # Admins can modify packagesets
1920+ with admin_logged_in():
1921+ self.modifyPackageset()
1922+
1923+
1924+class TestArchivePermissionSet(TestCaseWithFactory):
1925+
1926+ layer = ZopelessDatabaseLayer
1927+
1928+ def setUp(self):
1929+ super(TestArchivePermissionSet, self).setUp()
1930+ self.ap_set = getUtility(IArchivePermissionSet)
1931+ self.archive = self.factory.makeArchive()
1932+ self.packageset = self.factory.makePackageset()
1933+ self.person = self.factory.makePerson()
1934+
1935+ def test_packagesets_for_uploader_empty(self):
1936+ # A new archive will have no upload permissions
1937+ self.assertTrue(
1938+ self.ap_set.packagesetsForUploader(
1939+ self.archive, self.person).is_empty())
1940+
1941+ def test_new_packageset_uploader_simple(self):
1942+ # newPackagesetUploader grants upload rights to a packagset
1943+ permission = self.ap_set.newPackagesetUploader(
1944+ self.archive, self.person, self.packageset)
1945+
1946+ self.assertEqual(permission.archive, self.archive)
1947+ self.assertEqual(permission.person, self.person)
1948+ self.assertEqual(permission.packageset, self.packageset)
1949+ self.assertEqual(permission.permission, ArchivePermissionType.UPLOAD)
1950+ self.assertFalse(permission.explicit)
1951+ # Convenience property:
1952+ self.assertEqual(permission.package_set_name, self.packageset.name)
1953+
1954+ def test_new_packageset_uploader_repeated(self):
1955+ # Creating the same permission repeatedly should re-use the existing
1956+ # permission.
1957+ permission1 = self.ap_set.newPackagesetUploader(
1958+ self.archive, self.person, self.packageset)
1959+ permission2 = self.ap_set.newPackagesetUploader(
1960+ self.archive, self.person, self.packageset)
1961+ self.assertEqual(permission1.id, permission2.id)
1962+
1963+ def test_new_packageset_uploader_repeated_explicit(self):
1964+ # Attempting to create an explicit permission when a non-explicit one
1965+ # exists already will fail.
1966+ self.ap_set.newPackagesetUploader(
1967+ self.archive, self.person, self.packageset)
1968+ self.assertRaises(ValueError, self.ap_set.newPackagesetUploader,
1969+ self.archive, self.person, self.packageset, True)
1970+
1971+ def test_new_packageset_uploader_repeated_implicit(self):
1972+ # Attempting to create an implicit permission when an explicit one
1973+ # exists already will fail.
1974+ self.ap_set.newPackagesetUploader(
1975+ self.archive, self.person, self.packageset, True)
1976+ self.assertRaises(ValueError, self.ap_set.newPackagesetUploader,
1977+ self.archive, self.person, self.packageset)
1978+
1979+ def test_new_packageset_uploader_teammember(self):
1980+ # If a team member already has upload rights through a team, they can
1981+ # be granted again, individually
1982+ team = self.factory.makeTeam(self.person)
1983+ self.ap_set.newPackagesetUploader(
1984+ self.archive, team, self.packageset)
1985+ self.ap_set.newPackagesetUploader(
1986+ self.archive, self.person, self.packageset)
1987+
1988+ # Unless the explicit flag conflicts
1989+ self.assertRaises(ValueError, self.ap_set.newPackagesetUploader,
1990+ self.archive, self.person, self.packageset, True)
1991+
1992+ def test_packagesets_for_uploader(self):
1993+ # packagesetsForUploader returns the packageset upload archive
1994+ # permissions granted to a person
1995+ self.ap_set.newPackagesetUploader(
1996+ self.archive, self.person, self.packageset)
1997+
1998+ permission = self.ap_set.packagesetsForUploader(
1999+ self.archive, self.person).one()
2000+ self.assertEqual(permission.archive, self.archive)
2001+ self.assertEqual(permission.person, self.person)
2002+ self.assertEqual(permission.packageset, self.packageset)
2003+ self.assertEqual(permission.permission, ArchivePermissionType.UPLOAD)
2004+ self.assertFalse(permission.explicit)
2005+
2006+ def test_packagesets_for_source_uploader(self):
2007+ # packagesetsForSourceUploader returns the packageset upload archive
2008+ # permissions granted to a person affecting a given package
2009+ self.ap_set.newPackagesetUploader(
2010+ self.archive, self.person, self.packageset)
2011+ package = self.factory.makeSourcePackageName()
2012+ self.packageset.add((package,))
2013+
2014+ permission = self.ap_set.packagesetsForSourceUploader(
2015+ self.archive, package, self.person).one()
2016+ self.assertEqual(permission.archive, self.archive)
2017+ self.assertEqual(permission.person, self.person)
2018+ self.assertEqual(permission.packageset, self.packageset)
2019+ self.assertEqual(permission.permission, ArchivePermissionType.UPLOAD)
2020+ self.assertFalse(permission.explicit)
2021+
2022+ def test_packagesets_for_source_uploader_by_name(self):
2023+ # packagesetsForSourceUploader can take a package name
2024+ self.ap_set.newPackagesetUploader(
2025+ self.archive, self.person, self.packageset)
2026+ package = self.factory.makeSourcePackageName()
2027+ self.packageset.add((package,))
2028+
2029+ self.assertFalse(self.ap_set.packagesetsForSourceUploader(
2030+ self.archive, package.name, self.person).is_empty())
2031+
2032+ # and will raise an exception if the name is invalid
2033+ self.assertRaises(
2034+ NoSuchSourcePackageName, self.ap_set.packagesetsForSourceUploader,
2035+ self.archive, self.factory.getUniqueUnicode(), self.person)
2036+
2037+ def test_packagesets_for_source(self):
2038+ # packagesetsForSource returns the packageset upload archive
2039+ # permissions affecting a given package
2040+ self.ap_set.newPackagesetUploader(
2041+ self.archive, self.person, self.packageset)
2042+ package = self.factory.makeSourcePackageName()
2043+ self.packageset.add((package,))
2044+
2045+ permission = self.ap_set.packagesetsForSource(
2046+ self.archive, package).one()
2047+ self.assertEqual(permission.archive, self.archive)
2048+ self.assertEqual(permission.person, self.person)
2049+ self.assertEqual(permission.packageset, self.packageset)
2050+ self.assertEqual(permission.permission, ArchivePermissionType.UPLOAD)
2051+ self.assertFalse(permission.explicit)
2052+
2053+ def test_uploaders_for_packageset(self):
2054+ # uploadersForPackageset returns the people with upload rigts for a
2055+ # packageset in a given archive
2056+ self.ap_set.newPackagesetUploader(
2057+ self.archive, self.person, self.packageset)
2058+
2059+ permission = self.ap_set.uploadersForPackageset(
2060+ self.archive, self.packageset).one()
2061+ self.assertEqual(permission.archive, self.archive)
2062+ self.assertEqual(permission.person, self.person)
2063+ self.assertEqual(permission.packageset, self.packageset)
2064+ self.assertEqual(permission.permission, ArchivePermissionType.UPLOAD)
2065+ self.assertFalse(permission.explicit)
2066+
2067+ def test_uploaders_for_packageset_subpackagesets(self):
2068+ # archive permissions apply to children of a packageset, unless they
2069+ # have their own permissions with the "explicit" flag set
2070+ child = self.factory.makePackageset()
2071+ self.packageset.add((child,))
2072+ self.ap_set.newPackagesetUploader(
2073+ self.archive, self.person, self.packageset)
2074+
2075+ # uploadersForPackageset will not list them:
2076+ self.assertTrue(
2077+ self.ap_set.uploadersForPackageset(self.archive, child).is_empty())
2078+
2079+ # unless told to:
2080+ self.assertFalse(
2081+ self.ap_set.uploadersForPackageset(
2082+ self.archive, child, direct_permissions=False).is_empty())
2083+
2084+ def test_uploaders_for_packageset_explicit(self):
2085+ # people can have both explicit and implicit upload rights to a
2086+ # packageset
2087+ child = self.factory.makePackageset()
2088+ self.packageset.add((child,))
2089+ implicit_parent = self.ap_set.newPackagesetUploader(
2090+ self.archive, self.person, self.packageset)
2091+ explicit_child = self.ap_set.newPackagesetUploader(
2092+ self.archive, self.person, child, True)
2093+
2094+ self.assertEqual(
2095+ sorted(self.ap_set.uploadersForPackageset(
2096+ self.archive, child, direct_permissions=False)),
2097+ sorted((implicit_parent, explicit_child)))
2098+
2099+ def test_uploaders_for_packageset_subpackagesets_removed(self):
2100+ # archive permissions cease to apply to removed child packagesets
2101+ child = self.factory.makePackageset()
2102+ self.packageset.add((child,))
2103+ self.ap_set.newPackagesetUploader(
2104+ self.archive, self.person, self.packageset)
2105+ self.assertFalse(
2106+ self.ap_set.uploadersForPackageset(
2107+ self.archive, child, direct_permissions=False).is_empty())
2108+
2109+ self.packageset.remove((child,))
2110+ self.assertTrue(
2111+ self.ap_set.uploadersForPackageset(
2112+ self.archive, child, direct_permissions=False).is_empty())
2113+
2114+ def test_uploaders_for_packageset_by_name(self):
2115+ # a packageset name that doesn't exist will throw an error
2116+ self.ap_set.newPackagesetUploader(
2117+ self.archive, self.person, self.packageset)
2118+ # A correct name will give us a result:
2119+ self.assertFalse(self.ap_set.uploadersForPackageset(
2120+ self.archive, self.packageset.name).is_empty())
2121+ # An incorrect one will raise an exception
2122+ self.assertRaises(
2123+ NotFoundError, self.ap_set.uploadersForPackageset,
2124+ self.archive, self.factory.getUniqueUnicode())
2125+ # An incorrect type will raise a ValueError
2126+ self.assertRaises(
2127+ ValueError, self.ap_set.uploadersForPackageset,
2128+ self.archive, 42)
2129+
2130+ def test_archive_permission_per_archive(self):
2131+ # archive permissions are limited to an archive
2132+ archive2 = self.factory.makeArchive()
2133+ self.ap_set.newPackagesetUploader(
2134+ self.archive, self.person, self.packageset)
2135+ self.assertTrue(
2136+ self.ap_set.packagesetsForUploader(
2137+ archive2, self.person).is_empty())
2138+
2139+ def test_check_authenticated_packageset(self):
2140+ # checkAuthenticated is a generic way to look up archive permissions
2141+ self.ap_set.newPackagesetUploader(
2142+ self.archive, self.person, self.packageset)
2143+
2144+ permissions = self.ap_set.checkAuthenticated(
2145+ self.person, self.archive, ArchivePermissionType.UPLOAD,
2146+ self.packageset)
2147+
2148+ self.assertEqual(permissions.count(), 1)
2149+ permission = permissions[0]
2150+
2151+ self.assertEqual(permission.archive, self.archive)
2152+ self.assertEqual(permission.person, self.person)
2153+ self.assertEqual(permission.packageset, self.packageset)
2154+ self.assertEqual(permission.permission, ArchivePermissionType.UPLOAD)
2155+ self.assertFalse(permission.explicit)
2156+
2157+ def test_is_source_upload_allowed(self):
2158+ # isSourceUploadAllowed indicates whether a user has any archive
2159+ # permissinos granting them upload access to a specific source package
2160+ # (excepting component permissions)
2161+ self.ap_set.newPackagesetUploader(
2162+ self.archive, self.person, self.packageset)
2163+ package = self.factory.makeSourcePackageName()
2164+ self.packageset.add((package,))
2165+
2166+ self.assertTrue(self.ap_set.isSourceUploadAllowed(
2167+ self.archive, package, self.person))
2168+
2169+ def test_is_source_upload_allowed_denied(self):
2170+ # isSourceUploadAllowed should return false when a user has no
2171+ # packageset/PPU permission granting upload rights
2172+ self.ap_set.newPackagesetUploader(
2173+ self.archive, self.person, self.packageset)
2174+ package = self.factory.makeSourcePackageName()
2175+
2176+ self.assertFalse(self.ap_set.isSourceUploadAllowed(
2177+ self.archive, package, self.person))
2178+
2179+ def test_explicit_packageset_upload_rights(self):
2180+ # If a package is covered by a packageset with explicit upload rights,
2181+ # they disable all implicit upload rights to that package through other
2182+ # packagesets.
2183+ self.ap_set.newPackagesetUploader(
2184+ self.archive, self.person, self.packageset)
2185+ package = self.factory.makeSourcePackageName()
2186+ package2 = self.factory.makeSourcePackageName()
2187+ self.packageset.add((package, package2))
2188+
2189+ self.assertTrue(self.ap_set.isSourceUploadAllowed(
2190+ self.archive, package, self.person))
2191+ self.assertTrue(self.ap_set.isSourceUploadAllowed(
2192+ self.archive, package2, self.person))
2193+
2194+ # Create a packageset with explicit rights to package
2195+ special_person = self.factory.makePerson()
2196+ special_packageset = self.factory.makePackageset()
2197+ special_packageset.add((package,))
2198+ self.ap_set.newPackagesetUploader(
2199+ self.archive, special_person, special_packageset, True)
2200+
2201+ self.assertFalse(self.ap_set.isSourceUploadAllowed(
2202+ self.archive, package, self.person))
2203+ self.assertTrue(self.ap_set.isSourceUploadAllowed(
2204+ self.archive, package2, self.person))
2205+ self.assertTrue(self.ap_set.isSourceUploadAllowed(
2206+ self.archive, package, special_person))
2207+ self.assertFalse(self.ap_set.isSourceUploadAllowed(
2208+ self.archive, package2, special_person))
2209+
2210+ def test_delete_packageset_uploader(self):
2211+ # deletePackagesetUploader removes upload rights
2212+ self.ap_set.newPackagesetUploader(
2213+ self.archive, self.person, self.packageset)
2214+ self.assertFalse(
2215+ self.ap_set.packagesetsForUploader(
2216+ self.archive, self.person).is_empty())
2217+
2218+ self.ap_set.deletePackagesetUploader(
2219+ self.archive, self.person, self.packageset)
2220+ self.assertTrue(
2221+ self.ap_set.packagesetsForUploader(
2222+ self.archive, self.person).is_empty())
2223+
2224+ def test_delete_packageset(self):
2225+ # Packagesets can't be deleted as long as they have uploaders
2226+ self.ap_set.newPackagesetUploader(
2227+ self.archive, self.person, self.packageset)
2228+
2229+ self.assertRaises(Exception, self.packageset.destroySelf)