Merge lp:~wallyworld/launchpad/additional-affiliation-types into lp:launchpad

Proposed by Ian Booth
Status: Superseded
Proposed branch: lp:~wallyworld/launchpad/additional-affiliation-types
Merge into: lp:launchpad
Prerequisite: lp:~wallyworld/launchpad/improve-personpicker-bugtaskaffiliation-798764
Diff against target: 1271 lines (+478/-558)
8 files modified
lib/lp/app/browser/tests/test_vocabulary.py (+0/-269)
lib/lp/app/browser/vocabulary.py (+0/-13)
lib/lp/app/javascript/picker/picker.js (+0/-72)
lib/lp/app/javascript/picker/tests/test_picker.js (+1/-80)
lib/lp/registry/configure.zcml (+21/-3)
lib/lp/registry/model/pillaraffiliation.py (+104/-56)
lib/lp/registry/tests/test_pillaraffiliation.py (+344/-62)
lib/lp/testing/factory.py (+8/-3)
To merge this branch: bzr merge lp:~wallyworld/launchpad/additional-affiliation-types
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Needs Information
Review via email: mp+70442@code.launchpad.net

This proposal has been superseded by a proposal from 2011-08-05.

Commit message

Add to the affiliation adaptor to provide affiliation for other entity types like Question, DistroSeries, ProductSeries, Specification

Description of the change

This branch adds to the affiliation adaptor to provide affiliation for other entity types:
- Specification
- Question
- Distribution
- DistroSeries
- ProductSeries
- Product

== Implementation ==

Add extra affiliation adaptors for the additional context types. Checks are done for:
owner
driver
security contact
bug supervisor

Some contexts have pillars eg BugTask has a product or distribution; a distroseries has a distribution. The context is checked first. If the context doesn't match on the given attribute (eg owner), then the pillar is checked. If there is no match on owner, then driver is checked etc.

I think this mp also covers the intent of bug 81692, although that bug talks about additional checks eg registrant. Do we consider the work done here sufficient to address that bug?

== Tests ==

Add a bunch of tests to test_pillaraffiliation

== Lint ==

Linting changed files:
  lib/lp/registry/configure.zcml
  lib/lp/registry/model/pillaraffiliation.py
  lib/lp/registry/tests/test_pillaraffiliation.py

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :
Download full text (9.7 KiB)

> === modified file 'lib/lp/registry/model/pillaraffiliation.py'
> --- lib/lp/registry/model/pillaraffiliation.py 2011-08-04 14:31:56 +0000
> +++ lib/lp/registry/model/pillaraffiliation.py 2011-08-04 14:32:17 +0000
...
> + def getPillar(self):
> + return self.context
> +
> + def getAffiliationBadge(self, person):
> + """ Return the affiliation information for a person given a context.
> +
> + The context is a Distribution, Product etc and is associated with a
> + pillar. Checks are done to see if the person is associated with the
> + context first (owner, driver etc) and if not, then the pillar.
> + """
> + pillar = self.getPillar()
> +
> + def checkAffiliation(capability, attribute, role):
> + # Check the affiliation defined by the specified capability on the
> + # context and then pillar. Capability is IHasOwner etc. WHatever
> + # matches first (context or pillar) is used for the display name.
> + affiliated_entity = None
> + capabilityProvidedBy = getattr(capability, 'providedBy')
> + if capabilityProvidedBy(self.context):
> + if person.inTeam(getattr(self.context, attribute)):
> + affiliated_entity = self.context.displayname
> + if (affiliated_entity is None and capabilityProvidedBy(pillar)):
> + if person.inTeam(getattr(pillar, attribute)):
> + affiliated_entity = pillar.displayname
> + if affiliated_entity is None:
> + return None
> + return affiliated_entity, role

I do not see why this is an inner function. This could be simpler too if
we decide that all we care about is product or distribution. We know how to
check owner, drivers, and other roles. The other kinds of items I
see returned, notably for specification and question are probably wrong.
We do not need the interface checks if we are certain we are getting a
distro or product.

I have some doubts about the universality of these checks. I think
owner and driver are universal. bug_supervisor and security contact are
only useful in bugs and branches cases that deal with privacy and security.
eg assigning a branch reviewer, bug assignee, subscriber.

Questions might care more about answer contacts which are more likely to
be assigned. Consider Ubuntu. The owners and drivers are rarely assigned.
when I assign a question, I care about the answer contact for the package
first, then the contacts for Ubuntu.

Specifications care about the people who are assignees, drafters, approvers,
and need feedback. In most cases these people are owners and drivers. There
is a rare case for the approver who is for the series goal...

The series is the problem. Maybe a separate branch becuase it is a rule
that may not prove very important. The series driver (release manager) is
the most important person for bugs that affect a series or blueprints with
a series goal. Those driver are assumed to be a subset of owner or drivers
who have the authority to define the work for a series. I happen to know
that This is not an issue for Ubuntu or any of our major pro...

Read more...

review: Needs Information (code)
Revision history for this message
Ian Booth (wallyworld) wrote :
Download full text (9.8 KiB)

Thanks for the review. Clearly I'm missing some knowledge.

On 05/08/11 05:22, Curtis Hovey wrote:
> Review: Needs Information code
>> === modified file 'lib/lp/registry/model/pillaraffiliation.py'
>> --- lib/lp/registry/model/pillaraffiliation.py 2011-08-04 14:31:56 +0000
>> +++ lib/lp/registry/model/pillaraffiliation.py 2011-08-04 14:32:17 +0000
> ...
>> + def getPillar(self):
>> + return self.context
>> +
>> + def getAffiliationBadge(self, person):
>> + """ Return the affiliation information for a person given a context.
>> +
>> + The context is a Distribution, Product etc and is associated with a
>> + pillar. Checks are done to see if the person is associated with the
>> + context first (owner, driver etc) and if not, then the pillar.
>> + """
>> + pillar = self.getPillar()
>> +
>> + def checkAffiliation(capability, attribute, role):
>> + # Check the affiliation defined by the specified capability on the
>> + # context and then pillar. Capability is IHasOwner etc. WHatever
>> + # matches first (context or pillar) is used for the display name.
>> + affiliated_entity = None
>> + capabilityProvidedBy = getattr(capability, 'providedBy')
>> + if capabilityProvidedBy(self.context):
>> + if person.inTeam(getattr(self.context, attribute)):
>> + affiliated_entity = self.context.displayname
>> + if (affiliated_entity is None and capabilityProvidedBy(pillar)):
>> + if person.inTeam(getattr(pillar, attribute)):
>> + affiliated_entity = pillar.displayname
>> + if affiliated_entity is None:
>> + return None
>> + return affiliated_entity, role
>
> I do not see why this is an inner function. This could be simpler too if

Me either.

> we decide that all we care about is product or distribution. We know how to
> check owner, drivers, and other roles. The other kinds of items I
> see returned, notably for specification and question are probably wrong.
> We do not need the interface checks if we are certain we are getting a
> distro or product.
>

Will we always be getting a distro or product though? If I have a
Specification, wouldn't I want to possibly first see if the person in
question is affiliated with the specification itself and then check the
affiliation with the target (product/distro) only if the specification
check turned up empty?

> I have some doubts about the universality of these checks. I think
> owner and driver are universal. bug_supervisor and security contact are
> only useful in bugs and branches cases that deal with privacy and security.
> eg assigning a branch reviewer, bug assignee, subscriber.
>

This suggests we need another piece of context information to properly
determine the affiliation - the name of the attribute that is being
updated with the person, not just the person alone. So instead of saying
"we are associating person fred with bugtask 4 in some capacity" we are
saying "we are associating person fred with bugtask 4 as an assignee"
and that this distinction possibly affect...

13603. By Ian Booth

Rework affiliation checks and reimplement question adaptor

13604. By Ian Booth

Reimplement specification adaptor

13605. By Ian Booth

Reimplement distro/product affiliation adaptors and fix tests

13606. By Ian Booth

Add extra tests for distroseries and productseries

13607. By Ian Booth

Merge from trunk

Unmerged revisions

13607. By Ian Booth

Merge from trunk

13606. By Ian Booth

Add extra tests for distroseries and productseries

13605. By Ian Booth

Reimplement distro/product affiliation adaptors and fix tests

13604. By Ian Booth

Reimplement specification adaptor

13603. By Ian Booth

Rework affiliation checks and reimplement question adaptor

13602. By Ian Booth

Lint

13601. By Ian Booth

Lint

13600. By Ian Booth

Implement affiliation for other entity types

13599. By Ian Booth

Merge from trunk

13598. By Ian Booth

Improve affiliatin text

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/browser/tests/test_vocabulary.py'
2--- lib/lp/app/browser/tests/test_vocabulary.py 2011-08-05 04:51:58 +0000
3+++ lib/lp/app/browser/tests/test_vocabulary.py 2011-08-04 15:47:10 +0000
4@@ -1,4 +1,3 @@
5-<<<<<<< TREE
6 # Copyright 2011 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9@@ -271,271 +270,3 @@
10 self.assertEqual(6, result['total_size'])
11 self.assertEqual(1, len(result['entries']))
12 self.assertEqual('pting-2', result['entries'][0]['value'])
13-=======
14-# Copyright 2011 Canonical Ltd. This software is licensed under the
15-# GNU Affero General Public License version 3 (see the file LICENSE).
16-
17-"""Test vocabulary adapters."""
18-
19-__metaclass__ = type
20-
21-from datetime import datetime
22-from urllib import urlencode
23-
24-import pytz
25-import simplejson
26-
27-from zope.app.form.interfaces import MissingInputError
28-from zope.component import (
29- getSiteManager,
30- getUtility,
31- )
32-from zope.interface import implements
33-from zope.schema.interfaces import IVocabularyFactory
34-from zope.schema.vocabulary import SimpleTerm
35-from zope.security.proxy import removeSecurityProxy
36-
37-
38-from canonical.launchpad.interfaces.launchpad import ILaunchpadRoot
39-from canonical.launchpad.webapp.vocabulary import (
40- CountableIterator,
41- IHugeVocabulary,
42- )
43-from canonical.testing.layers import DatabaseFunctionalLayer
44-from lp.app.browser.vocabulary import (
45- IPickerEntry,
46- MAX_DESCRIPTION_LENGTH,
47- )
48-from lp.app.errors import UnexpectedFormData
49-from lp.registry.interfaces.irc import IIrcIDSet
50-from lp.services.features.testing import FeatureFixture
51-from lp.testing import (
52- login_person,
53- TestCaseWithFactory,
54- )
55-from lp.testing.views import create_view
56-
57-
58-class PersonPickerEntryAdapterTestCase(TestCaseWithFactory):
59-
60- layer = DatabaseFunctionalLayer
61-
62- def test_person_to_pickerentry(self):
63- # IPerson can be adpated to IPickerEntry.
64- person = self.factory.makePerson()
65- adapter = IPickerEntry(person)
66- self.assertTrue(IPickerEntry.providedBy(adapter))
67-
68- def test_PersonPickerEntryAdapter_email_anonymous(self):
69- # Anonymous users cannot see entry email addresses.
70- person = self.factory.makePerson(email='snarf@eg.dom')
71- entry = IPickerEntry(person).getPickerEntry(None)
72- self.assertEqual('<email address hidden>', entry.description)
73-
74- def test_PersonPickerEntryAdapter_visible_email_logged_in(self):
75- # Logged in users can see visible email addresses.
76- observer = self.factory.makePerson()
77- login_person(observer)
78- person = self.factory.makePerson(email='snarf@eg.dom')
79- entry = IPickerEntry(person).getPickerEntry(None)
80- self.assertEqual('snarf@eg.dom', entry.description)
81-
82- def test_PersonPickerEntryAdapter_hidden_email_logged_in(self):
83- # Logged in users cannot see hidden email addresses.
84- person = self.factory.makePerson(email='snarf@eg.dom')
85- login_person(person)
86- person.hide_email_addresses = True
87- observer = self.factory.makePerson()
88- login_person(observer)
89- entry = IPickerEntry(person).getPickerEntry(None)
90- self.assertEqual('<email address hidden>', entry.description)
91-
92- def test_PersonPickerEntryAdapter_no_email_logged_in(self):
93- # Teams without email address have no desriptions.
94- team = self.factory.makeTeam()
95- observer = self.factory.makePerson()
96- login_person(observer)
97- entry = IPickerEntry(team).getPickerEntry(None)
98- self.assertEqual(None, entry.description)
99-
100- def test_PersonPickerEntryAdapter_logged_in(self):
101- # Logged in users can see visible email addresses.
102- observer = self.factory.makePerson()
103- login_person(observer)
104- person = self.factory.makePerson(
105- email='snarf@eg.dom', name='snarf')
106- entry = IPickerEntry(person).getPickerEntry(None)
107- self.assertEqual('sprite person', entry.css)
108- self.assertEqual('sprite new-window', entry.link_css)
109-
110- def test_PersonPickerEntryAdapter_enhanced_picker_enabled_user(self):
111- # The enhanced person picker provides more information for users.
112- person = self.factory.makePerson(email='snarf@eg.dom', name='snarf')
113- creation_date = datetime(
114- 2005, 01, 30, 0, 0, 0, 0, pytz.timezone('UTC'))
115- removeSecurityProxy(person).datecreated = creation_date
116- getUtility(IIrcIDSet).new(person, 'eg.dom', 'snarf')
117- getUtility(IIrcIDSet).new(person, 'ex.dom', 'pting')
118- entry = IPickerEntry(person).getPickerEntry(
119- None, enhanced_picker_enabled=True)
120- self.assertEqual('http://launchpad.dev/~snarf', entry.alt_title_link)
121- self.assertEqual(
122- ['snarf on eg.dom, pting on ex.dom', 'Member since 2005-01-30'],
123- entry.details)
124-
125- def test_PersonPickerEntryAdapter_enhanced_picker_enabled_team(self):
126- # The enhanced person picker provides more information for teams.
127- team = self.factory.makeTeam(email='fnord@eg.dom', name='fnord')
128- entry = IPickerEntry(team).getPickerEntry(
129- None, enhanced_picker_enabled=True)
130- self.assertEqual('http://launchpad.dev/~fnord', entry.alt_title_link)
131- self.assertEqual(['Team members: 1'], entry.details)
132-
133- def test_PersonPickerEntryAdapter_enhanced_picker_enabled_badges(self):
134- # The enhanced person picker provides affilliation information.
135- person = self.factory.makePerson(email='snarf@eg.dom', name='snarf')
136- project = self.factory.makeProduct(name='fnord', owner=person)
137- bugtask = self.factory.makeBugTask(target=project)
138- entry = IPickerEntry(person).getPickerEntry(
139- bugtask, enhanced_picker_enabled=True)
140- self.assertEqual(1, len(entry.badges))
141- self.assertEqual('/@@/product-badge', entry.badges[0]['url'])
142- self.assertEqual('Affiliated with Fnord', entry.badges[0]['alt'])
143-
144-
145-class TestPersonVocabulary:
146- implements(IHugeVocabulary)
147- test_persons = []
148-
149- @classmethod
150- def setTestData(cls, person_list):
151- cls.test_persons = person_list
152-
153- def __init__(self, context):
154- self.context = context
155-
156- def toTerm(self, person):
157- return SimpleTerm(person, person.name, person.displayname)
158-
159- def searchForTerms(self, query=None):
160- found = [
161- person for person in self.test_persons if query in person.name]
162- return CountableIterator(len(found), found, self.toTerm)
163-
164-
165-class HugeVocabularyJSONViewTestCase(TestCaseWithFactory):
166-
167- layer = DatabaseFunctionalLayer
168-
169- def setUp(self):
170- super(HugeVocabularyJSONViewTestCase, self).setUp()
171- test_persons = []
172- for name in range(1, 7):
173- test_persons.append(
174- self.factory.makePerson(name='pting-%s' % name))
175- TestPersonVocabulary.setTestData(test_persons)
176- getSiteManager().registerUtility(
177- TestPersonVocabulary, IVocabularyFactory, 'TestPerson')
178- self.addCleanup(
179- getSiteManager().unregisterUtility,
180- TestPersonVocabulary, IVocabularyFactory, 'TestPerson')
181- self.addCleanup(TestPersonVocabulary.setTestData, [])
182-
183- @staticmethod
184- def create_vocabulary_view(form):
185- context = getUtility(ILaunchpadRoot)
186- query_string = urlencode(form)
187- return create_view(
188- context, '+huge-vocabulary', form=form, query_string=query_string)
189-
190- def test_name_field_missing_error(self):
191- view = self.create_vocabulary_view({})
192- self.assertRaisesWithContent(
193- MissingInputError, "('name', '', None)", view.__call__)
194-
195- def test_search_text_field_missing_error(self):
196- view = self.create_vocabulary_view({'name': 'TestPerson'})
197- self.assertRaisesWithContent(
198- MissingInputError, "('search_text', '', None)", view.__call__)
199-
200- def test_vocabulary_name_unknown_error(self):
201- form = dict(name='snarf', search_text='pting')
202- view = self.create_vocabulary_view(form)
203- self.assertRaisesWithContent(
204- UnexpectedFormData, "Unknown vocabulary 'snarf'", view.__call__)
205-
206- def test_json_entries(self):
207- # The results are JSON encoded.
208- feature_flag = {'disclosure.picker_enhancements.enabled': 'on'}
209- flags = FeatureFixture(feature_flag)
210- flags.setUp()
211- self.addCleanup(flags.cleanUp)
212- team = self.factory.makeTeam(name='pting-team')
213- TestPersonVocabulary.test_persons.append(team)
214- form = dict(name='TestPerson', search_text='pting-team')
215- view = self.create_vocabulary_view(form)
216- result = simplejson.loads(view())
217- expected = {
218- "alt_title": team.name,
219- "alt_title_link": "http://launchpad.dev/~%s" % team.name,
220- "api_uri": "/~%s" % team.name,
221- "css": "sprite team",
222- "details": ['Team members: 1'],
223- "link_css": "sprite new-window",
224- "metadata": "team",
225- "title": team.displayname,
226- "value": team.name
227- }
228- self.assertTrue('entries' in result)
229- self.assertContentEqual(
230- expected.items(), result['entries'][0].items())
231-
232- def test_max_description_size(self):
233- # Descriptions over 120 characters are truncated and ellipsised.
234- email = 'pting-' * 19 + '@example.dom'
235- person = self.factory.makePerson(name='pting-n', email=email)
236- TestPersonVocabulary.test_persons.append(person)
237- # Login to gain permission to know the email address that used
238- # for the description
239- login_person(person)
240- form = dict(name='TestPerson', search_text='pting-n')
241- view = self.create_vocabulary_view(form)
242- result = simplejson.loads(view())
243- expected = (email[:MAX_DESCRIPTION_LENGTH - 3] + '...')
244- self.assertEqual(
245- 'pting-n', result['entries'][0]['value'])
246- self.assertEqual(
247- expected, result['entries'][0]['description'])
248-
249- def test_default_batch_size(self):
250- # The results are batched.
251- form = dict(name='TestPerson', search_text='pting')
252- view = self.create_vocabulary_view(form)
253- result = simplejson.loads(view())
254- total_size = result['total_size']
255- entries = len(result['entries'])
256- self.assertTrue(
257- total_size > entries,
258- 'total_size: %d is less than entries: %d' % (total_size, entries))
259-
260- def test_batch_size(self):
261- # A The batch size can be specified with the batch param.
262- form = dict(
263- name='TestPerson', search_text='pting',
264- start='0', batch='1')
265- view = self.create_vocabulary_view(form)
266- result = simplejson.loads(view())
267- self.assertEqual(6, result['total_size'])
268- self.assertEqual(1, len(result['entries']))
269-
270- def test_start_offset(self):
271- # The offset of the batch is specified with the start param.
272- form = dict(
273- name='TestPerson', search_text='pting',
274- start='1', batch='1')
275- view = self.create_vocabulary_view(form)
276- result = simplejson.loads(view())
277- self.assertEqual(6, result['total_size'])
278- self.assertEqual(1, len(result['entries']))
279- self.assertEqual('pting-2', result['entries'][0]['value'])
280->>>>>>> MERGE-SOURCE
281
282=== modified file 'lib/lp/app/browser/vocabulary.py'
283--- lib/lp/app/browser/vocabulary.py 2011-08-05 04:51:58 +0000
284+++ lib/lp/app/browser/vocabulary.py 2011-08-04 15:18:04 +0000
285@@ -161,7 +161,6 @@
286 # We will linkify the person's name so it can be clicked to open
287 # the page for that person.
288 extra.alt_title_link = canonical_url(person, rootsite='mainsite')
289- extra.details = []
290 # We will display the person's irc nick(s) after their email
291 # address in the description text.
292 irc_nicks = None
293@@ -169,7 +168,6 @@
294 irc_nicks = ", ".join(
295 [IRCNicknameFormatterAPI(ircid).displayname()
296 for ircid in person.ircnicknames])
297-<<<<<<< TREE
298 if irc_nicks and not picker_expander_enabled:
299 if extra.description:
300 extra.description = ("%s (%s)" %
301@@ -186,17 +184,6 @@
302 extra.details.append(
303 'Member since %s' % DateTimeFormatterAPI(
304 person.datecreated).date())
305-=======
306- if irc_nicks:
307- extra.details.append(irc_nicks)
308- if person.is_team:
309- extra.details.append(
310- 'Team members: %s' % person.all_member_count)
311- else:
312- extra.details.append(
313- 'Member since %s' % DateTimeFormatterAPI(
314- person.datecreated).date())
315->>>>>>> MERGE-SOURCE
316
317 return extra
318
319
320=== modified file 'lib/lp/app/javascript/picker/picker.js'
321--- lib/lp/app/javascript/picker/picker.js 2011-08-05 04:51:58 +0000
322+++ lib/lp/app/javascript/picker/picker.js 2011-08-05 04:51:59 +0000
323@@ -380,40 +380,11 @@
324 data.title, data.title_link, data.link_css);
325 li_title.appendChild(title);
326 if (data.alt_title) {
327-<<<<<<< TREE
328- if (!data.details) {
329- // XXX sinzui 2011-08-04: Remove this block when expanders
330- // are released.
331- var alt_link = null;
332- if (data.alt_title_link) {
333- alt_link =Y.Node.create('<a></a>')
334- .addClass(data.link_css)
335- .addClass('discreet');
336- alt_link.set('text', " Details...")
337- .set('href', data.alt_title_link);
338- Y.on('click', function(e) {
339- e.halt();
340- window.open(data.alt_title_link);
341- }, alt_link);
342- }
343- }
344-
345-=======
346->>>>>>> MERGE-SOURCE
347 li_title.appendChild('&nbsp;(');
348 var alt_title_node = Y.Node.create('<span></span>')
349 .set('text', data.alt_title);
350 li_title.appendChild(alt_title_node);
351 li_title.appendChild(')');
352-<<<<<<< TREE
353- if (alt_link !== null) {
354- // XXX sinzui 2011-08-04: Remove this block when expanders
355- // are released.
356- li_title.appendChild(Y.Node.create('&nbsp;&nbsp;'));
357- li_title.appendChild(alt_link);
358- }
359-=======
360->>>>>>> MERGE-SOURCE
361 }
362 return li_title;
363 },
364@@ -458,48 +429,6 @@
365 },
366
367 /**
368-<<<<<<< TREE
369- * Render a node containing the optional details part of the picker entry.
370- * @param data a json data object with the details to render
371- */
372- _renderDetailsUI: function(data) {
373- if (!data.details) {
374- return null;
375- }
376- var details_node = Y.Node.create('<div></div>')
377- .addClass('sprite')
378- .addClass(C_RESULT_DESCRIPTION);
379- if (Y.Lang.isArray(data.details)) {
380- var data_node = Y.Node.create('<div></div>');
381- var escaped_details = [];
382- Y.Array.each(data.details, function(detail, i) {
383- escaped_details.push(Y.Escape.html(detail));
384- });
385- data_node.append(Y.Node.create(escaped_details.join('<br />')));
386- details_node.append(data_node);
387- }
388- var links = [];
389- links.push(Y.Node.create(
390- '<a class="sprite yes save" href="#"></a>')
391- .set('text', 'Select ' + data.title));
392- links[0].on('click', function (e, value) {
393- this.fire(SAVE, value);
394- }, this, data);
395- links.push(this._text_or_link(
396- 'View details', data.alt_title_link, data.link_css));
397- var link_list = Y.Node.create('<ul></ul>')
398- .addClass('horizontal');
399- Y.Array.each(links, function(link, i) {
400- var li = Y.Node.create('<li></li>');
401- li.append(link);
402- link_list.append(li);
403- });
404- details_node.append(link_list);
405- return details_node;
406- },
407-
408- /**
409-=======
410 * Render a node containing the optional details part of the picker entry.
411 * @param data a json data object with the details to render
412 */
413@@ -540,7 +469,6 @@
414 },
415
416 /**
417->>>>>>> MERGE-SOURCE
418 * Update the UI based on the results attribute.
419 *
420 * @method _syncResultsUI
421
422=== modified file 'lib/lp/app/javascript/picker/tests/test_picker.js'
423--- lib/lp/app/javascript/picker/tests/test_picker.js 2011-08-05 04:51:58 +0000
424+++ lib/lp/app/javascript/picker/tests/test_picker.js 2011-08-05 04:51:59 +0000
425@@ -148,8 +148,7 @@
426 alt_title: 'Joe Again <foo></foo>',
427 title_link: 'http://somewhere.com',
428 alt_title_link: 'http://somewhereelse.com',
429- link_css: 'cool-style',
430- details: ['Member since 2007'],
431+ link_css: 'cool-style'
432 }
433 ]);
434
435@@ -179,83 +178,6 @@
436 Assert.areEqual('Joe Again <foo></foo>', alt_text_node.get('text'));
437 },
438
439-<<<<<<< TREE
440- test_details: function () {
441- // The details of the li is the content node of the expander.
442- this.picker.render();
443- this.picker.set('results', [
444- {
445- css: 'yui3-blah-blue',
446- value: 'jschmo',
447- title: 'Joe Schmo',
448- description: 'joe@example.com',
449- details: ['joe on irc.freenode.net', 'Member since 2007'],
450- alt_title_link: '/~jschmo'
451- }
452- ]);
453- var bb = this.picker.get('boundingBox');
454- var li = bb.one('.yui3-picker-results li');
455- var details = li.expander.content_node;
456- Assert.areEqual(
457- 'joe on irc.freenode.net<br>Member since 2007',
458- details.one('div').getContent());
459- Assert.areEqual(
460- 'Select Joe Schmo', details.one('ul li:first-child').get('text'));
461- Assert.areEqual(
462- 'View details', details.one('ul li:last-child').get('text'));
463- },
464-
465- test_details_escaping: function () {
466- // The content of details is escaped.
467- this.picker.render();
468- this.picker.set('results', [
469- {
470- css: 'yui3-blah-blue',
471- value: 'jschmo',
472- title: 'Joe <Schmo>',
473- description: 'joe@example.com',
474- details: ['<joe> on irc.freenode.net', 'f<nor>d maintainer'],
475- alt_title_link: '/~jschmo'
476- }
477- ]);
478- var bb = this.picker.get('boundingBox');
479- var li = bb.one('.yui3-picker-results li');
480- var details = li.expander.content_node;
481- Assert.areEqual(
482- '&lt;joe&gt; on irc.freenode.net<br>f&lt;nor&gt;d maintainer',
483- details.one('div').getContent());
484- Assert.areEqual(
485- 'Select Joe &lt;Schmo&gt;',
486- details.one('ul li:first-child a').getContent('text'));
487- },
488-
489- test_details_save_link: function () {
490- // The select link the li's details saves the selection.
491- this.picker.render();
492- this.picker.set('results', [
493- {
494- css: 'yui3-blah-blue',
495- value: 'jschmo',
496- title: 'Joe Schmo',
497- description: 'joe@example.com',
498- alt_title_link: 'http://somewhereelse.com',
499- link_css: 'cool-style',
500- details: ['Member since 2007'],
501- }
502- ]);
503- var bb = this.picker.get('boundingBox');
504- var link_node = bb.one('a.save');
505- Assert.areEqual('Select Joe Schmo', link_node.get('text'));
506- Assert.isTrue(link_node.get('href').indexOf(window.location) === 0);
507- var selected_value = null;
508- this.picker.subscribe('save', function(e) {
509- selected_value = e.details[0].value;
510- }, this);
511- simulate(bb, 'a.save', 'click');
512- Assert.areEqual('jschmo', selected_value);
513- },
514-
515-=======
516 test_details: function () {
517 // The details of the li is the content node of the expander.
518 this.picker.render();
519@@ -327,7 +249,6 @@
520 Assert.areEqual('jschmo', selected_value);
521 },
522
523->>>>>>> MERGE-SOURCE
524 test_title_badges: function () {
525 this.picker.render();
526 var badge_info = [
527
528=== modified file 'lib/lp/registry/configure.zcml'
529--- lib/lp/registry/configure.zcml 2011-08-02 05:35:39 +0000
530+++ lib/lp/registry/configure.zcml 2011-08-05 04:51:59 +0000
531@@ -885,9 +885,27 @@
532 <adapter
533 factory="lp.registry.model.pillaraffiliation.BugTaskPillarAffiliation"
534 />
535-
536- <adapter
537- factory="lp.registry.model.pillaraffiliation.PillarAffiliation"
538+ <adapter
539+ factory="lp.registry.model.pillaraffiliation.QuestionPillarAffiliation"
540+ />
541+ <adapter
542+ factory="lp.registry.model.pillaraffiliation.SpecificationPillarAffiliation"
543+ />
544+ <adapter
545+ for="lp.registry.interfaces.distribution.IDistribution"
546+ factory="lp.registry.model.pillaraffiliation.PillarAffiliation"
547+ />
548+ <adapter
549+ for="lp.registry.interfaces.distroseries.IDistroSeries"
550+ factory="lp.registry.model.pillaraffiliation.DistroSeriesPillarAffiliation"
551+ />
552+ <adapter
553+ for="lp.registry.interfaces.product.IProduct"
554+ factory="lp.registry.model.pillaraffiliation.PillarAffiliation"
555+ />
556+ <adapter
557+ for="lp.registry.interfaces.productseries.IProductSeries"
558+ factory="lp.registry.model.pillaraffiliation.ProductSeriesPillarAffiliation"
559 />
560
561 <!-- Using
562
563=== modified file 'lib/lp/registry/model/pillaraffiliation.py'
564--- lib/lp/registry/model/pillaraffiliation.py 2011-08-05 04:51:58 +0000
565+++ lib/lp/registry/model/pillaraffiliation.py 2011-08-05 04:51:59 +0000
566@@ -29,15 +29,14 @@
567 )
568
569 from canonical.launchpad.interfaces.launchpad import IHasIcon
570+from lp.answers.interfaces.question import IQuestion
571+from lp.blueprints.interfaces.specification import ISpecification
572 from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
573 from lp.bugs.interfaces.bugtask import IBugTask
574-<<<<<<< TREE
575-from lp.registry.interfaces.distribution import IDistribution
576-=======
577 from lp.bugs.interfaces.securitycontact import IHasSecurityContact
578 from lp.registry.interfaces.distribution import IDistribution
579-from lp.registry.interfaces.role import IHasAppointedDriver
580->>>>>>> MERGE-SOURCE
581+from lp.registry.interfaces.distroseries import IDistroSeries
582+from lp.registry.interfaces.productseries import IProductSeries
583
584
585 class IHasAffiliation(Interface):
586@@ -58,7 +57,9 @@
587 class PillarAffiliation(object):
588 """Default affiliation adapter.
589
590- No affiliation is returned.
591+ Subclasses may need to override getPillar() in order to provide the pillar
592+ entity for which affiliation is to be determined. The default is just to
593+ use the context object directly.
594 """
595
596 implements(IHasAffiliation)
597@@ -66,61 +67,108 @@
598 def __init__(self, context):
599 self.context = context
600
601- def getAffiliationBadge(self, person):
602- return None
603-
604-
605-# XXX: wallyworld 2011-05-24 bug=81692: TODO Work is required to determine
606-# exactly what is required in terms of figuring out affiliation..
607-
608-@adapter(IBugTask)
609-class BugTaskPillarAffiliation(PillarAffiliation):
610- """An affiliation adapter for bug tasks."""
611-
612- def getAffiliationBadge(self, person):
613- pillar = self.context.pillar
614- bug_supervisor = None
615- security_contact = None
616- driver = None
617- if IHasAppointedDriver.providedBy(pillar):
618- driver = pillar.driver
619- if IHasSecurityContact.providedBy(pillar):
620- security_contact = pillar.security_contact
621- if IHasBugSupervisor.providedBy(pillar):
622- bug_supervisor = pillar.bug_supervisor
623-
624- affiliated = None
625+ def getPillar(self):
626+ return self.context
627+
628+ def _getAffiliationDetails(self, person, pillar):
629+ """ Return the affiliation information for a person, if any.
630+
631+ A person is affiliated with a pillar if they are in the list of
632+ drivers or are the maintainer.
633+ """
634 if person.inTeam(pillar.owner):
635- affiliated = 'maintainer'
636- elif person.inTeam(driver):
637- affiliated = 'driver'
638- elif person.inTeam(bug_supervisor):
639- affiliated = 'bug supervisor'
640- elif person.inTeam(security_contact):
641- affiliated = 'security contact'
642+ return pillar.displayname, 'maintainer'
643+ for driver in pillar.drivers:
644+ if person.inTeam(driver):
645+ return pillar.displayname, 'driver'
646+ return None
647
648- if not affiliated:
649+ def getAffiliationBadge(self, person):
650+ """ Return the affiliation badge details for a person given a context.
651+ """
652+ pillar = self.getPillar()
653+ affiliation_details = self._getAffiliationDetails(person, pillar)
654+ if not affiliation_details:
655 return None
656
657- def getIconUrl(context, default_url):
658+ def getIconUrl(context, pillar, default_url):
659 if IHasIcon.providedBy(context) and context.icon is not None:
660 icon_url = context.icon.getURL()
661 return icon_url
662+ if IHasIcon.providedBy(pillar) and pillar.icon is not None:
663+ icon_url = context.icon.getURL()
664+ return icon_url
665 return default_url
666-<<<<<<< TREE
667-
668- alt_text = "Affiliated with %s" % pillar.displayname
669- if IDistribution.providedBy(pillar):
670- icon_url = getIconUrl(pillar, "/@@/distribution-badge")
671- else:
672- icon_url = getIconUrl(pillar, "/@@/product-badge")
673- return BadgeDetails(icon_url, alt_text)
674-=======
675-
676- alt_text = "%s %s" % (pillar.displayname, affiliated)
677- if IDistribution.providedBy(pillar):
678- icon_url = getIconUrl(pillar, "/@@/distribution-badge")
679- else:
680- icon_url = getIconUrl(pillar, "/@@/product-badge")
681- return BadgeDetails(icon_url, alt_text)
682->>>>>>> MERGE-SOURCE
683+
684+ alt_text = "%s %s" % affiliation_details
685+ if IDistribution.providedBy(pillar):
686+ default_icon_url = "/@@/distribution-badge"
687+ else:
688+ default_icon_url = "/@@/product-badge"
689+ icon_url = getIconUrl(self.context, pillar, default_icon_url)
690+ return BadgeDetails(icon_url, alt_text)
691+
692+
693+@adapter(IBugTask)
694+class BugTaskPillarAffiliation(PillarAffiliation):
695+ """An affiliation adapter for bug tasks."""
696+ def getPillar(self):
697+ return self.context.pillar
698+
699+ def _getAffiliationDetails(self, person, pillar):
700+ """ A person is affiliated with a bugtask based on (in order):
701+ - owner of bugtask pillar
702+ - driver of bugtask pillar
703+ - bug supervisor of bugtask pillar
704+ - security contact of bugtask pillar
705+ """
706+ result = super(BugTaskPillarAffiliation, self)._getAffiliationDetails(
707+ person, pillar)
708+ if result is not None:
709+ return result
710+ if person.inTeam(pillar.bug_supervisor):
711+ return pillar.displayname, 'bug supervisor'
712+ if person.inTeam(pillar.security_contact):
713+ return pillar.displayname, 'security contact'
714+
715+
716+@adapter(IDistroSeries)
717+class DistroSeriesPillarAffiliation(PillarAffiliation):
718+ """An affiliation adapter for distroseries."""
719+ def getPillar(self):
720+ return self.context.distribution
721+
722+
723+@adapter(IProductSeries)
724+class ProductSeriesPillarAffiliation(PillarAffiliation):
725+ """An affiliation adapter for productseries."""
726+ def getPillar(self):
727+ return self.context.product
728+
729+
730+@adapter(ISpecification)
731+class SpecificationPillarAffiliation(PillarAffiliation):
732+ """An affiliation adapter for blueprints."""
733+ def getPillar(self):
734+ return (self.context.target)
735+
736+
737+@adapter(IQuestion)
738+class QuestionPillarAffiliation(PillarAffiliation):
739+ """An affiliation adapter for questions."""
740+ def getPillar(self):
741+ return self.context.product or self.context.distribution
742+
743+ def _getAffiliationDetails(self, person, pillar):
744+ """ A person is affiliated with a question based on (in order):
745+ - answer contact for question target
746+ - owner of question target
747+ - driver of question target
748+ """
749+ target = self.context.target
750+ answer_contacts = target.answer_contacts
751+ for answer_contact in answer_contacts:
752+ if person.inTeam(answer_contact):
753+ return target.displayname, 'answer contact'
754+ return super(QuestionPillarAffiliation, self)._getAffiliationDetails(
755+ person, pillar)
756
757=== modified file 'lib/lp/registry/tests/test_pillaraffiliation.py'
758--- lib/lp/registry/tests/test_pillaraffiliation.py 2011-08-05 04:51:58 +0000
759+++ lib/lp/registry/tests/test_pillaraffiliation.py 2011-08-05 04:51:59 +0000
760@@ -7,112 +7,173 @@
761
762 from storm.store import Store
763 from testtools.matchers import Equals
764+from zope.component import getUtility
765+from zope.security.proxy import removeSecurityProxy
766
767 from canonical.testing.layers import DatabaseFunctionalLayer
768 from lp.registry.model.pillaraffiliation import IHasAffiliation
769-from lp.testing import StormStatementRecorder, TestCaseWithFactory
770+from lp.services.worlddata.interfaces.language import ILanguageSet
771+from lp.testing import (
772+ person_logged_in,
773+ StormStatementRecorder,
774+ TestCaseWithFactory,
775+ )
776 from lp.testing.matchers import HasQueryCount
777
778
779-class TestBugTaskPillarAffiliation(TestCaseWithFactory):
780+class TestPillarAffiliation(TestCaseWithFactory):
781
782 layer = DatabaseFunctionalLayer
783
784 def _check_affiliated_with_distro(self, person, distro, role):
785- bugtask = self.factory.makeBugTask(target=distro)
786- badge = IHasAffiliation(bugtask).getAffiliationBadge(person)
787+ badge = IHasAffiliation(distro).getAffiliationBadge(person)
788 self.assertEqual(
789- badge, ("/@@/distribution-badge", "Pting %s" % role))
790+ ("/@@/distribution-badge", "Pting %s" % role), badge)
791
792- def test_bugtask_distro_owner_affiliation(self):
793- # A person who owns a bugtask distro is affiliated.
794+ def test_distro_owner_affiliation(self):
795+ # A person who owns a distro is affiliated.
796 person = self.factory.makePerson()
797-<<<<<<< TREE
798- distro = self.factory.makeDistribution(owner=person, name='pting')
799- bugtask = self.factory.makeBugTask(target=distro)
800-=======
801 distro = self.factory.makeDistribution(owner=person, name='pting')
802 self._check_affiliated_with_distro(person, distro, 'maintainer')
803
804- def test_bugtask_distro_security_contact_affiliation(self):
805- # A person who is the security contact for a bugtask distro is
806- # affiliated.
807+ def test_distro_driver_affiliation(self):
808+ # A person who is a distro driver is affiliated.
809+ person = self.factory.makePerson()
810+ distro = self.factory.makeDistribution(driver=person, name='pting')
811+ self._check_affiliated_with_distro(person, distro, 'driver')
812+
813+ def test_distro_team_driver_affiliation(self):
814+ # A person who is a member of the distro driver team is affiliated.
815+ person = self.factory.makePerson()
816+ team = self.factory.makeTeam(members=[person])
817+ distro = self.factory.makeDistribution(driver=team, name='pting')
818+ self._check_affiliated_with_distro(person, distro, 'driver')
819+
820+ def test_no_distro_security_contact_affiliation(self):
821+ # A person who is the security contact for a distro is not affiliated
822+ # for simple distro affiliation checks.
823+ person = self.factory.makePerson()
824+ distro = self.factory.makeDistribution(security_contact=person)
825+ self.assertIs(
826+ None, IHasAffiliation(distro).getAffiliationBadge(person))
827+
828+ def test_no_distro_bug_supervisor_affiliation(self):
829+ # A person who is the bug supervisor for a distro is not affiliated
830+ # for simple distro affiliation checks.
831+ person = self.factory.makePerson()
832+ distro = self.factory.makeDistribution(bug_supervisor=person)
833+ self.assertIs(
834+ None, IHasAffiliation(distro).getAffiliationBadge(person))
835+
836+ def _check_affiliated_with_product(self, person, product, role):
837+ badge = IHasAffiliation(product).getAffiliationBadge(person)
838+ self.assertEqual(
839+ ("/@@/product-badge", "Pting %s" % role), badge)
840+
841+ def test_product_driver_affiliation(self):
842+ # A person who is the driver for a product is affiliated.
843+ person = self.factory.makePerson()
844+ product = self.factory.makeProduct(driver=person, name='pting')
845+ self._check_affiliated_with_product(person, product, 'driver')
846+
847+ def test_product_team_driver_affiliation(self):
848+ # A person who is a member of the product driver team is affiliated.
849+ person = self.factory.makePerson()
850+ team = self.factory.makeTeam(members=[person])
851+ product = self.factory.makeProduct(driver=team, name='pting')
852+ self._check_affiliated_with_product(person, product, 'driver')
853+
854+ def test_product_group_driver_affiliation(self):
855+ # A person who is the driver for a product's group is affiliated.
856+ person = self.factory.makePerson()
857+ project = self.factory.makeProject(driver=person)
858+ product = self.factory.makeProduct(project=project, name='pting')
859+ self._check_affiliated_with_product(person, product, 'driver')
860+
861+ def test_no_product_security_contact_affiliation(self):
862+ # A person who is the security contact for a product is is not
863+ # affiliated for simple product affiliation checks.
864+ person = self.factory.makePerson()
865+ product = self.factory.makeProduct(security_contact=person)
866+ self.assertIs(
867+ None, IHasAffiliation(product).getAffiliationBadge(person))
868+
869+ def test_no_product_bug_supervisor_affiliation(self):
870+ # A person who is the bug supervisor for a product is is not
871+ # affiliated for simple product affiliation checks.
872+ person = self.factory.makePerson()
873+ product = self.factory.makeProduct(bug_supervisor=person)
874+ self.assertIs(
875+ None, IHasAffiliation(product).getAffiliationBadge(person))
876+
877+ def test_product_owner_affiliation(self):
878+ # A person who owns a product is affiliated.
879+ person = self.factory.makePerson()
880+ product = self.factory.makeProduct(owner=person, name='pting')
881+ self._check_affiliated_with_product(person, product, 'maintainer')
882+
883+
884+class TestBugTaskPillarAffiliation(TestCaseWithFactory):
885+
886+ layer = DatabaseFunctionalLayer
887+
888+ def test_correct_pillar_is_used(self):
889+ bugtask = self.factory.makeBugTask()
890+ badge = IHasAffiliation(bugtask)
891+ self.assertEqual(bugtask.pillar, badge.getPillar())
892+
893+ def _check_affiliated_with_distro(self, person, target, role):
894+ bugtask = self.factory.makeBugTask(target=target)
895+ badge = IHasAffiliation(bugtask).getAffiliationBadge(person)
896+ self.assertEqual(
897+ ("/@@/distribution-badge", "Pting %s" % role), badge)
898+
899+ def _check_affiliated_with_product(self, person, target, role):
900+ bugtask = self.factory.makeBugTask(target=target)
901+ badge = IHasAffiliation(bugtask).getAffiliationBadge(person)
902+ self.assertEqual(
903+ ("/@@/product-badge", "Pting %s" % role), badge)
904+
905+ def test_distro_security_contact_affiliation(self):
906+ # A person who is the security contact for a distro is affiliated.
907 person = self.factory.makePerson()
908 distro = self.factory.makeDistribution(
909 security_contact=person, name='pting')
910 self._check_affiliated_with_distro(person, distro, 'security contact')
911
912- def test_bugtask_distro_bug_supervisor_affiliation(self):
913- # A person who is the bug supervisor for a bugtask distro is
914- # affiliated.
915+ def test_distro_bug_supervisor_affiliation(self):
916+ # A person who is the bug supervisor for a distro is affiliated.
917 person = self.factory.makePerson()
918 distro = self.factory.makeDistribution(
919 bug_supervisor=person, name='pting')
920 self._check_affiliated_with_distro(person, distro, 'bug supervisor')
921
922- def _check_affiliated_with_product(self, person, product, role):
923- bugtask = self.factory.makeBugTask(target=product)
924->>>>>>> MERGE-SOURCE
925- badge = IHasAffiliation(bugtask).getAffiliationBadge(person)
926- self.assertEqual(
927-<<<<<<< TREE
928- badge, ("/@@/distribution-badge", "Affiliated with Pting"))
929-
930- def test_bugtask_product_affiliation(self):
931-=======
932- badge, ("/@@/product-badge", "Pting %s" % role))
933-
934- def test_bugtask_product_driver_affiliation(self):
935- # A person who is the driver for a bugtask product is affiliated.
936- person = self.factory.makePerson()
937- product = self.factory.makeProduct(driver=person, name='pting')
938- self._check_affiliated_with_product(person, product, 'driver')
939-
940- def test_bugtask_product_security_contact_affiliation(self):
941- # A person who is the security contact for a bugtask product is
942- # affiliated.
943+ def test_product_security_contact_affiliation(self):
944+ # A person who is the security contact for a distro is affiliated.
945 person = self.factory.makePerson()
946 product = self.factory.makeProduct(
947 security_contact=person, name='pting')
948- self._check_affiliated_with_product(
949- person, product, 'security contact')
950+ self._check_affiliated_with_product(person, product, 'security contact')
951
952- def test_bugtask_product_bug_supervisor_affiliation(self):
953- # A person who is the bug supervisor for a bugtask product is
954- # affiliated.
955+ def test_product_bug_supervisor_affiliation(self):
956+ # A person who is the bug supervisor for a distro is affiliated.
957 person = self.factory.makePerson()
958 product = self.factory.makeProduct(
959 bug_supervisor=person, name='pting')
960 self._check_affiliated_with_product(person, product, 'bug supervisor')
961
962- def test_bugtask_product_owner_affiliation(self):
963->>>>>>> MERGE-SOURCE
964- # A person who owns a bugtask product is affiliated.
965- person = self.factory.makePerson()
966-<<<<<<< TREE
967- product = self.factory.makeProduct(owner=person, name='pting')
968-=======
969- product = self.factory.makeProduct(owner=person, name='pting')
970- self._check_affiliated_with_product(person, product, 'maintainer')
971-
972- def test_bugtask_product_affiliation_query_count(self):
973+ def test_product_affiliation_query_count(self):
974 # Only 4 queries are expected, selects from:
975 # - Bug, BugTask, Product, Person
976 person = self.factory.makePerson()
977 product = self.factory.makeProduct(owner=person, name='pting')
978->>>>>>> MERGE-SOURCE
979 bugtask = self.factory.makeBugTask(target=product)
980-<<<<<<< TREE
981- badge = IHasAffiliation(bugtask).getAffiliationBadge(person)
982- self.assertEqual(
983- badge, ("/@@/product-badge", "Affiliated with Pting"))
984-=======
985 Store.of(bugtask).invalidate()
986 with StormStatementRecorder() as recorder:
987 IHasAffiliation(bugtask).getAffiliationBadge(person)
988 self.assertThat(recorder, HasQueryCount(Equals(4)))
989
990- def test_bugtask_distro_affiliation_query_count(self):
991+ def test_distro_affiliation_query_count(self):
992 # Only 4 queries are expected, selects from:
993 # - Bug, BugTask, Distribution, Person
994 person = self.factory.makePerson()
995@@ -122,4 +183,225 @@
996 with StormStatementRecorder() as recorder:
997 IHasAffiliation(bugtask).getAffiliationBadge(person)
998 self.assertThat(recorder, HasQueryCount(Equals(4)))
999->>>>>>> MERGE-SOURCE
1000+
1001+
1002+class TestDistroSeriesPillarAffiliation(TestCaseWithFactory):
1003+
1004+ layer = DatabaseFunctionalLayer
1005+
1006+ def test_correct_pillar_is_used(self):
1007+ series = self.factory.makeDistroSeries()
1008+ badge = IHasAffiliation(series)
1009+ self.assertEqual(series.distribution, badge.getPillar())
1010+
1011+ def test_driver_affiliation(self):
1012+ # A person who is the driver for a distroseries is affiliated.
1013+ # Here, the affiliation is with the distribution of the series.
1014+ owner = self.factory.makePerson()
1015+ driver = self.factory.makePerson()
1016+ distribution = self.factory.makeDistribution(
1017+ owner=owner, driver=driver, name='pting')
1018+ distroseries = self.factory.makeDistroSeries(
1019+ registrant=driver, distribution=distribution)
1020+ badge = IHasAffiliation(distroseries).getAffiliationBadge(driver)
1021+ self.assertEqual(
1022+ ("/@@/distribution-badge", "Pting driver"), badge)
1023+
1024+ def test_distro_driver_affiliation(self):
1025+ # A person who is the driver for a distroseries' distro is affiliated.
1026+ # Here, the affiliation is with the distribution of the series.
1027+ owner = self.factory.makePerson()
1028+ driver = self.factory.makePerson()
1029+ distribution = self.factory.makeDistribution(
1030+ owner=owner, driver=driver, name='pting')
1031+ distroseries = self.factory.makeDistroSeries(
1032+ registrant=owner, distribution=distribution)
1033+ badge = IHasAffiliation(distroseries).getAffiliationBadge(driver)
1034+ self.assertEqual(
1035+ ("/@@/distribution-badge", "Pting driver"), badge)
1036+
1037+
1038+class TestProductSeriesPillarAffiliation(TestCaseWithFactory):
1039+
1040+ layer = DatabaseFunctionalLayer
1041+
1042+ def test_correct_pillar_is_used(self):
1043+ series = self.factory.makeProductSeries()
1044+ badge = IHasAffiliation(series)
1045+ self.assertEqual(series.product, badge.getPillar())
1046+
1047+ def test_driver_affiliation(self):
1048+ # A person who is the driver for a productseries is affiliated.
1049+ # Here, the affiliation is with the product.
1050+ owner = self.factory.makePerson()
1051+ driver = self.factory.makePerson()
1052+ product = self.factory.makeProduct(
1053+ owner=owner, driver=driver, name='pting')
1054+ productseries = self.factory.makeProductSeries(
1055+ owner=driver, product=product)
1056+ badge = IHasAffiliation(productseries).getAffiliationBadge(driver)
1057+ self.assertEqual(
1058+ ("/@@/product-badge", "Pting driver"), badge)
1059+
1060+ def test_product_driver_affiliation(self):
1061+ # A person who is the driver for a productseries' product is
1062+ # affiliated. Here, the affiliation is with the product.
1063+ owner = self.factory.makePerson()
1064+ driver = self.factory.makePerson()
1065+ product = self.factory.makeProduct(
1066+ owner=owner, driver=driver, name='pting')
1067+ productseries = self.factory.makeProductSeries(
1068+ owner=owner, product=product)
1069+ badge = IHasAffiliation(productseries).getAffiliationBadge(driver)
1070+ self.assertEqual(
1071+ ("/@@/product-badge", "Pting driver"), badge)
1072+
1073+ def test_product_group_driver_affiliation(self):
1074+ # A person who is the driver for a productseries' product's group is
1075+ # affiliated. Here, the affiliation is with the product.
1076+ owner = self.factory.makePerson()
1077+ driver = self.factory.makePerson()
1078+ project = self.factory.makeProject(driver=driver)
1079+ product = self.factory.makeProduct(
1080+ owner=owner, project=project, name='pting')
1081+ productseries = self.factory.makeProductSeries(
1082+ owner=owner, product=product)
1083+ badge = IHasAffiliation(productseries).getAffiliationBadge(driver)
1084+ self.assertEqual(
1085+ ("/@@/product-badge", "Pting driver"), badge)
1086+
1087+
1088+class TestQuestionPillarAffiliation(TestCaseWithFactory):
1089+
1090+ layer = DatabaseFunctionalLayer
1091+
1092+ def test_correct_pillar_is_used_for_product(self):
1093+ product = self.factory.makeProduct()
1094+ question = self.factory.makeQuestion(target=product)
1095+ badge = IHasAffiliation(question)
1096+ self.assertEqual(question.product, badge.getPillar())
1097+
1098+ def test_correct_pillar_is_used_for_distribution(self):
1099+ distribution = self.factory.makeDistribution()
1100+ question = self.factory.makeQuestion(target=distribution)
1101+ badge = IHasAffiliation(question)
1102+ self.assertEqual(question.distribution, badge.getPillar())
1103+
1104+ def test_correct_pillar_is_used_for_distro_sourcepackage(self):
1105+ distribution = self.factory.makeDistribution()
1106+ distro_sourcepackage = self.factory.makeDistributionSourcePackage(
1107+ distribution=distribution)
1108+ owner = self.factory.makePerson()
1109+ question = self.factory.makeQuestion(
1110+ target=distro_sourcepackage, owner=owner)
1111+ badge = IHasAffiliation(question)
1112+ self.assertEqual(distribution, badge.getPillar())
1113+
1114+ def test_answer_contact_affiliation_for_distro(self):
1115+ # A person is affiliated if they are an answer contact for a distro
1116+ # target. Even if they also own the distro, the answer contact
1117+ # affiliation takes precedence.
1118+ answer_contact = self.factory.makePerson()
1119+ english = getUtility(ILanguageSet)['en']
1120+ answer_contact.addLanguage(english)
1121+ distro = self.factory.makeDistribution(owner=answer_contact)
1122+ with person_logged_in(answer_contact):
1123+ distro.addAnswerContact(answer_contact, answer_contact)
1124+ question = self.factory.makeQuestion(target=distro)
1125+ badge = IHasAffiliation(question).getAffiliationBadge(answer_contact)
1126+ self.assertEqual(
1127+ ("/@@/distribution-badge", "%s answer contact" %
1128+ distro.displayname), badge)
1129+
1130+ def test_answer_contact_affiliation_for_distro_sourcepackage(self):
1131+ # A person is affiliated if they are an answer contact for a dsp
1132+ # target. Even if they also own the distro, the answer contact
1133+ # affiliation takes precedence.
1134+ answer_contact = self.factory.makePerson()
1135+ english = getUtility(ILanguageSet)['en']
1136+ answer_contact.addLanguage(english)
1137+ distribution = self.factory.makeDistribution(owner=answer_contact)
1138+ distro_sourcepackage = self.factory.makeDistributionSourcePackage(
1139+ distribution=distribution)
1140+ with person_logged_in(answer_contact):
1141+ distro_sourcepackage.addAnswerContact(
1142+ answer_contact, answer_contact)
1143+ question = self.factory.makeQuestion(
1144+ target=distro_sourcepackage, owner=answer_contact)
1145+ badge = IHasAffiliation(question).getAffiliationBadge(answer_contact)
1146+ self.assertEqual(
1147+ ("/@@/distribution-badge", "%s answer contact" %
1148+ distro_sourcepackage.displayname), badge)
1149+
1150+ def test_answer_contact_affiliation_for_product(self):
1151+ # A person is affiliated if they are an answer contact for a product
1152+ # target. Even if they also own the product, the answer contact
1153+ # affiliation takes precedence.
1154+ answer_contact = self.factory.makePerson()
1155+ english = getUtility(ILanguageSet)['en']
1156+ answer_contact.addLanguage(english)
1157+ product = self.factory.makeProduct(owner=answer_contact)
1158+ with person_logged_in(answer_contact):
1159+ product.addAnswerContact(answer_contact, answer_contact)
1160+ question = self.factory.makeQuestion(target=product)
1161+ badge = IHasAffiliation(question).getAffiliationBadge(answer_contact)
1162+ self.assertEqual(
1163+ ("/@@/product-badge", "%s answer contact" %
1164+ product.displayname), badge)
1165+
1166+ def test_product_affiliation(self):
1167+ # A person is affiliated if they are affiliated with the product.
1168+ person = self.factory.makePerson()
1169+ product = self.factory.makeProduct(owner=person)
1170+ question = self.factory.makeQuestion(target=product)
1171+ badge = IHasAffiliation(question).getAffiliationBadge(person)
1172+ self.assertEqual(
1173+ ("/@@/product-badge", "%s maintainer" %
1174+ product.displayname), badge)
1175+
1176+ def test_distribution_affiliation(self):
1177+ # A person is affiliated if they are affiliated with the distribution.
1178+ person = self.factory.makePerson()
1179+ distro = self.factory.makeDistribution(owner=person)
1180+ question = self.factory.makeQuestion(target=distro)
1181+ badge = IHasAffiliation(question).getAffiliationBadge(person)
1182+ self.assertEqual(
1183+ ("/@@/distribution-badge", "%s maintainer" %
1184+ distro.displayname), badge)
1185+
1186+
1187+class TestSpecificationPillarAffiliation(TestCaseWithFactory):
1188+
1189+ layer = DatabaseFunctionalLayer
1190+
1191+ def test_correct_pillar_is_used_for_product(self):
1192+ product = self.factory.makeProduct()
1193+ specification = self.factory.makeSpecification(product=product)
1194+ badge = IHasAffiliation(specification)
1195+ self.assertEqual(specification.product, badge.getPillar())
1196+
1197+ def test_correct_pillar_is_used_for_distribution(self):
1198+ distro = self.factory.makeDistribution()
1199+ specification = self.factory.makeSpecification(distribution=distro)
1200+ badge = IHasAffiliation(specification)
1201+ self.assertEqual(specification.distribution, badge.getPillar())
1202+
1203+ def test_product_affiliation(self):
1204+ # A person is affiliated if they are affiliated with the pillar.
1205+ person = self.factory.makePerson()
1206+ product = self.factory.makeProduct(owner=person)
1207+ specification = self.factory.makeSpecification(product=product)
1208+ badge = IHasAffiliation(specification).getAffiliationBadge(person)
1209+ self.assertEqual(
1210+ ("/@@/product-badge", "%s maintainer" %
1211+ product.displayname), badge)
1212+
1213+ def test_distribution_affiliation(self):
1214+ # A person is affiliated if they are affiliated with the distribution.
1215+ person = self.factory.makePerson()
1216+ distro = self.factory.makeDistribution(owner=person)
1217+ specification = self.factory.makeSpecification(distribution=distro)
1218+ badge = IHasAffiliation(specification).getAffiliationBadge(person)
1219+ self.assertEqual(
1220+ ("/@@/distribution-badge", "%s maintainer" %
1221+ distro.displayname), badge)
1222
1223=== modified file 'lib/lp/testing/factory.py'
1224--- lib/lp/testing/factory.py 2011-08-05 04:51:58 +0000
1225+++ lib/lp/testing/factory.py 2011-08-05 04:51:59 +0000
1226@@ -1025,7 +1025,7 @@
1227 return ProxyFactory(series)
1228
1229 def makeProject(self, name=None, displayname=None, title=None,
1230- homepageurl=None, summary=None, owner=None,
1231+ homepageurl=None, summary=None, owner=None, driver=None,
1232 description=None):
1233 """Create and return a new, arbitrary ProjectGroup."""
1234 if owner is None:
1235@@ -1040,7 +1040,7 @@
1236 description = self.getUniqueString('description')
1237 if title is None:
1238 title = self.getUniqueString('title')
1239- return getUtility(IProjectGroupSet).new(
1240+ project = getUtility(IProjectGroupSet).new(
1241 name=name,
1242 displayname=displayname,
1243 title=title,
1244@@ -1048,6 +1048,9 @@
1245 summary=summary,
1246 description=description,
1247 owner=owner)
1248+ if driver is not None:
1249+ removeSecurityProxy(project).driver = driver
1250+ return project
1251
1252 def makeSprint(self, title=None, name=None):
1253 """Make a sprint."""
1254@@ -2326,7 +2329,7 @@
1255
1256 def makeDistribution(self, name=None, displayname=None, owner=None,
1257 registrant=None, members=None, title=None,
1258- aliases=None, bug_supervisor=None,
1259+ aliases=None, bug_supervisor=None, driver=None,
1260 security_contact=None, publish_root_dir=None,
1261 publish_base_url=None, publish_copy_base_url=None,
1262 no_pubconf=False):
1263@@ -2352,6 +2355,8 @@
1264 naked_distro = removeSecurityProxy(distro)
1265 if aliases is not None:
1266 naked_distro.setAliases(aliases)
1267+ if driver is not None:
1268+ naked_distro.driver = driver
1269 if bug_supervisor is not None:
1270 naked_distro.bug_supervisor = bug_supervisor
1271 if security_contact is not None: