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
=== modified file 'lib/lp/app/browser/tests/test_vocabulary.py'
--- lib/lp/app/browser/tests/test_vocabulary.py 2011-08-05 04:51:58 +0000
+++ lib/lp/app/browser/tests/test_vocabulary.py 2011-08-04 15:47:10 +0000
@@ -1,4 +1,3 @@
1<<<<<<< TREE
2# Copyright 2011 Canonical Ltd. This software is licensed under the1# Copyright 2011 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
43
@@ -271,271 +270,3 @@
271 self.assertEqual(6, result['total_size'])270 self.assertEqual(6, result['total_size'])
272 self.assertEqual(1, len(result['entries']))271 self.assertEqual(1, len(result['entries']))
273 self.assertEqual('pting-2', result['entries'][0]['value'])272 self.assertEqual('pting-2', result['entries'][0]['value'])
274=======
275# Copyright 2011 Canonical Ltd. This software is licensed under the
276# GNU Affero General Public License version 3 (see the file LICENSE).
277
278"""Test vocabulary adapters."""
279
280__metaclass__ = type
281
282from datetime import datetime
283from urllib import urlencode
284
285import pytz
286import simplejson
287
288from zope.app.form.interfaces import MissingInputError
289from zope.component import (
290 getSiteManager,
291 getUtility,
292 )
293from zope.interface import implements
294from zope.schema.interfaces import IVocabularyFactory
295from zope.schema.vocabulary import SimpleTerm
296from zope.security.proxy import removeSecurityProxy
297
298
299from canonical.launchpad.interfaces.launchpad import ILaunchpadRoot
300from canonical.launchpad.webapp.vocabulary import (
301 CountableIterator,
302 IHugeVocabulary,
303 )
304from canonical.testing.layers import DatabaseFunctionalLayer
305from lp.app.browser.vocabulary import (
306 IPickerEntry,
307 MAX_DESCRIPTION_LENGTH,
308 )
309from lp.app.errors import UnexpectedFormData
310from lp.registry.interfaces.irc import IIrcIDSet
311from lp.services.features.testing import FeatureFixture
312from lp.testing import (
313 login_person,
314 TestCaseWithFactory,
315 )
316from lp.testing.views import create_view
317
318
319class PersonPickerEntryAdapterTestCase(TestCaseWithFactory):
320
321 layer = DatabaseFunctionalLayer
322
323 def test_person_to_pickerentry(self):
324 # IPerson can be adpated to IPickerEntry.
325 person = self.factory.makePerson()
326 adapter = IPickerEntry(person)
327 self.assertTrue(IPickerEntry.providedBy(adapter))
328
329 def test_PersonPickerEntryAdapter_email_anonymous(self):
330 # Anonymous users cannot see entry email addresses.
331 person = self.factory.makePerson(email='snarf@eg.dom')
332 entry = IPickerEntry(person).getPickerEntry(None)
333 self.assertEqual('<email address hidden>', entry.description)
334
335 def test_PersonPickerEntryAdapter_visible_email_logged_in(self):
336 # Logged in users can see visible email addresses.
337 observer = self.factory.makePerson()
338 login_person(observer)
339 person = self.factory.makePerson(email='snarf@eg.dom')
340 entry = IPickerEntry(person).getPickerEntry(None)
341 self.assertEqual('snarf@eg.dom', entry.description)
342
343 def test_PersonPickerEntryAdapter_hidden_email_logged_in(self):
344 # Logged in users cannot see hidden email addresses.
345 person = self.factory.makePerson(email='snarf@eg.dom')
346 login_person(person)
347 person.hide_email_addresses = True
348 observer = self.factory.makePerson()
349 login_person(observer)
350 entry = IPickerEntry(person).getPickerEntry(None)
351 self.assertEqual('<email address hidden>', entry.description)
352
353 def test_PersonPickerEntryAdapter_no_email_logged_in(self):
354 # Teams without email address have no desriptions.
355 team = self.factory.makeTeam()
356 observer = self.factory.makePerson()
357 login_person(observer)
358 entry = IPickerEntry(team).getPickerEntry(None)
359 self.assertEqual(None, entry.description)
360
361 def test_PersonPickerEntryAdapter_logged_in(self):
362 # Logged in users can see visible email addresses.
363 observer = self.factory.makePerson()
364 login_person(observer)
365 person = self.factory.makePerson(
366 email='snarf@eg.dom', name='snarf')
367 entry = IPickerEntry(person).getPickerEntry(None)
368 self.assertEqual('sprite person', entry.css)
369 self.assertEqual('sprite new-window', entry.link_css)
370
371 def test_PersonPickerEntryAdapter_enhanced_picker_enabled_user(self):
372 # The enhanced person picker provides more information for users.
373 person = self.factory.makePerson(email='snarf@eg.dom', name='snarf')
374 creation_date = datetime(
375 2005, 01, 30, 0, 0, 0, 0, pytz.timezone('UTC'))
376 removeSecurityProxy(person).datecreated = creation_date
377 getUtility(IIrcIDSet).new(person, 'eg.dom', 'snarf')
378 getUtility(IIrcIDSet).new(person, 'ex.dom', 'pting')
379 entry = IPickerEntry(person).getPickerEntry(
380 None, enhanced_picker_enabled=True)
381 self.assertEqual('http://launchpad.dev/~snarf', entry.alt_title_link)
382 self.assertEqual(
383 ['snarf on eg.dom, pting on ex.dom', 'Member since 2005-01-30'],
384 entry.details)
385
386 def test_PersonPickerEntryAdapter_enhanced_picker_enabled_team(self):
387 # The enhanced person picker provides more information for teams.
388 team = self.factory.makeTeam(email='fnord@eg.dom', name='fnord')
389 entry = IPickerEntry(team).getPickerEntry(
390 None, enhanced_picker_enabled=True)
391 self.assertEqual('http://launchpad.dev/~fnord', entry.alt_title_link)
392 self.assertEqual(['Team members: 1'], entry.details)
393
394 def test_PersonPickerEntryAdapter_enhanced_picker_enabled_badges(self):
395 # The enhanced person picker provides affilliation information.
396 person = self.factory.makePerson(email='snarf@eg.dom', name='snarf')
397 project = self.factory.makeProduct(name='fnord', owner=person)
398 bugtask = self.factory.makeBugTask(target=project)
399 entry = IPickerEntry(person).getPickerEntry(
400 bugtask, enhanced_picker_enabled=True)
401 self.assertEqual(1, len(entry.badges))
402 self.assertEqual('/@@/product-badge', entry.badges[0]['url'])
403 self.assertEqual('Affiliated with Fnord', entry.badges[0]['alt'])
404
405
406class TestPersonVocabulary:
407 implements(IHugeVocabulary)
408 test_persons = []
409
410 @classmethod
411 def setTestData(cls, person_list):
412 cls.test_persons = person_list
413
414 def __init__(self, context):
415 self.context = context
416
417 def toTerm(self, person):
418 return SimpleTerm(person, person.name, person.displayname)
419
420 def searchForTerms(self, query=None):
421 found = [
422 person for person in self.test_persons if query in person.name]
423 return CountableIterator(len(found), found, self.toTerm)
424
425
426class HugeVocabularyJSONViewTestCase(TestCaseWithFactory):
427
428 layer = DatabaseFunctionalLayer
429
430 def setUp(self):
431 super(HugeVocabularyJSONViewTestCase, self).setUp()
432 test_persons = []
433 for name in range(1, 7):
434 test_persons.append(
435 self.factory.makePerson(name='pting-%s' % name))
436 TestPersonVocabulary.setTestData(test_persons)
437 getSiteManager().registerUtility(
438 TestPersonVocabulary, IVocabularyFactory, 'TestPerson')
439 self.addCleanup(
440 getSiteManager().unregisterUtility,
441 TestPersonVocabulary, IVocabularyFactory, 'TestPerson')
442 self.addCleanup(TestPersonVocabulary.setTestData, [])
443
444 @staticmethod
445 def create_vocabulary_view(form):
446 context = getUtility(ILaunchpadRoot)
447 query_string = urlencode(form)
448 return create_view(
449 context, '+huge-vocabulary', form=form, query_string=query_string)
450
451 def test_name_field_missing_error(self):
452 view = self.create_vocabulary_view({})
453 self.assertRaisesWithContent(
454 MissingInputError, "('name', '', None)", view.__call__)
455
456 def test_search_text_field_missing_error(self):
457 view = self.create_vocabulary_view({'name': 'TestPerson'})
458 self.assertRaisesWithContent(
459 MissingInputError, "('search_text', '', None)", view.__call__)
460
461 def test_vocabulary_name_unknown_error(self):
462 form = dict(name='snarf', search_text='pting')
463 view = self.create_vocabulary_view(form)
464 self.assertRaisesWithContent(
465 UnexpectedFormData, "Unknown vocabulary 'snarf'", view.__call__)
466
467 def test_json_entries(self):
468 # The results are JSON encoded.
469 feature_flag = {'disclosure.picker_enhancements.enabled': 'on'}
470 flags = FeatureFixture(feature_flag)
471 flags.setUp()
472 self.addCleanup(flags.cleanUp)
473 team = self.factory.makeTeam(name='pting-team')
474 TestPersonVocabulary.test_persons.append(team)
475 form = dict(name='TestPerson', search_text='pting-team')
476 view = self.create_vocabulary_view(form)
477 result = simplejson.loads(view())
478 expected = {
479 "alt_title": team.name,
480 "alt_title_link": "http://launchpad.dev/~%s" % team.name,
481 "api_uri": "/~%s" % team.name,
482 "css": "sprite team",
483 "details": ['Team members: 1'],
484 "link_css": "sprite new-window",
485 "metadata": "team",
486 "title": team.displayname,
487 "value": team.name
488 }
489 self.assertTrue('entries' in result)
490 self.assertContentEqual(
491 expected.items(), result['entries'][0].items())
492
493 def test_max_description_size(self):
494 # Descriptions over 120 characters are truncated and ellipsised.
495 email = 'pting-' * 19 + '@example.dom'
496 person = self.factory.makePerson(name='pting-n', email=email)
497 TestPersonVocabulary.test_persons.append(person)
498 # Login to gain permission to know the email address that used
499 # for the description
500 login_person(person)
501 form = dict(name='TestPerson', search_text='pting-n')
502 view = self.create_vocabulary_view(form)
503 result = simplejson.loads(view())
504 expected = (email[:MAX_DESCRIPTION_LENGTH - 3] + '...')
505 self.assertEqual(
506 'pting-n', result['entries'][0]['value'])
507 self.assertEqual(
508 expected, result['entries'][0]['description'])
509
510 def test_default_batch_size(self):
511 # The results are batched.
512 form = dict(name='TestPerson', search_text='pting')
513 view = self.create_vocabulary_view(form)
514 result = simplejson.loads(view())
515 total_size = result['total_size']
516 entries = len(result['entries'])
517 self.assertTrue(
518 total_size > entries,
519 'total_size: %d is less than entries: %d' % (total_size, entries))
520
521 def test_batch_size(self):
522 # A The batch size can be specified with the batch param.
523 form = dict(
524 name='TestPerson', search_text='pting',
525 start='0', batch='1')
526 view = self.create_vocabulary_view(form)
527 result = simplejson.loads(view())
528 self.assertEqual(6, result['total_size'])
529 self.assertEqual(1, len(result['entries']))
530
531 def test_start_offset(self):
532 # The offset of the batch is specified with the start param.
533 form = dict(
534 name='TestPerson', search_text='pting',
535 start='1', batch='1')
536 view = self.create_vocabulary_view(form)
537 result = simplejson.loads(view())
538 self.assertEqual(6, result['total_size'])
539 self.assertEqual(1, len(result['entries']))
540 self.assertEqual('pting-2', result['entries'][0]['value'])
541>>>>>>> MERGE-SOURCE
542273
=== modified file 'lib/lp/app/browser/vocabulary.py'
--- lib/lp/app/browser/vocabulary.py 2011-08-05 04:51:58 +0000
+++ lib/lp/app/browser/vocabulary.py 2011-08-04 15:18:04 +0000
@@ -161,7 +161,6 @@
161 # We will linkify the person's name so it can be clicked to open161 # We will linkify the person's name so it can be clicked to open
162 # the page for that person.162 # the page for that person.
163 extra.alt_title_link = canonical_url(person, rootsite='mainsite')163 extra.alt_title_link = canonical_url(person, rootsite='mainsite')
164 extra.details = []
165 # We will display the person's irc nick(s) after their email164 # We will display the person's irc nick(s) after their email
166 # address in the description text.165 # address in the description text.
167 irc_nicks = None166 irc_nicks = None
@@ -169,7 +168,6 @@
169 irc_nicks = ", ".join(168 irc_nicks = ", ".join(
170 [IRCNicknameFormatterAPI(ircid).displayname()169 [IRCNicknameFormatterAPI(ircid).displayname()
171 for ircid in person.ircnicknames])170 for ircid in person.ircnicknames])
172<<<<<<< TREE
173 if irc_nicks and not picker_expander_enabled:171 if irc_nicks and not picker_expander_enabled:
174 if extra.description:172 if extra.description:
175 extra.description = ("%s (%s)" %173 extra.description = ("%s (%s)" %
@@ -186,17 +184,6 @@
186 extra.details.append(184 extra.details.append(
187 'Member since %s' % DateTimeFormatterAPI(185 'Member since %s' % DateTimeFormatterAPI(
188 person.datecreated).date())186 person.datecreated).date())
189=======
190 if irc_nicks:
191 extra.details.append(irc_nicks)
192 if person.is_team:
193 extra.details.append(
194 'Team members: %s' % person.all_member_count)
195 else:
196 extra.details.append(
197 'Member since %s' % DateTimeFormatterAPI(
198 person.datecreated).date())
199>>>>>>> MERGE-SOURCE
200187
201 return extra188 return extra
202189
203190
=== modified file 'lib/lp/app/javascript/picker/picker.js'
--- lib/lp/app/javascript/picker/picker.js 2011-08-05 04:51:58 +0000
+++ lib/lp/app/javascript/picker/picker.js 2011-08-05 04:51:59 +0000
@@ -380,40 +380,11 @@
380 data.title, data.title_link, data.link_css);380 data.title, data.title_link, data.link_css);
381 li_title.appendChild(title);381 li_title.appendChild(title);
382 if (data.alt_title) {382 if (data.alt_title) {
383<<<<<<< TREE
384 if (!data.details) {
385 // XXX sinzui 2011-08-04: Remove this block when expanders
386 // are released.
387 var alt_link = null;
388 if (data.alt_title_link) {
389 alt_link =Y.Node.create('<a></a>')
390 .addClass(data.link_css)
391 .addClass('discreet');
392 alt_link.set('text', " Details...")
393 .set('href', data.alt_title_link);
394 Y.on('click', function(e) {
395 e.halt();
396 window.open(data.alt_title_link);
397 }, alt_link);
398 }
399 }
400
401=======
402>>>>>>> MERGE-SOURCE
403 li_title.appendChild('&nbsp;(');383 li_title.appendChild('&nbsp;(');
404 var alt_title_node = Y.Node.create('<span></span>')384 var alt_title_node = Y.Node.create('<span></span>')
405 .set('text', data.alt_title);385 .set('text', data.alt_title);
406 li_title.appendChild(alt_title_node);386 li_title.appendChild(alt_title_node);
407 li_title.appendChild(')');387 li_title.appendChild(')');
408<<<<<<< TREE
409 if (alt_link !== null) {
410 // XXX sinzui 2011-08-04: Remove this block when expanders
411 // are released.
412 li_title.appendChild(Y.Node.create('&nbsp;&nbsp;'));
413 li_title.appendChild(alt_link);
414 }
415=======
416>>>>>>> MERGE-SOURCE
417 }388 }
418 return li_title;389 return li_title;
419 },390 },
@@ -458,48 +429,6 @@
458 },429 },
459430
460 /**431 /**
461<<<<<<< TREE
462 * Render a node containing the optional details part of the picker entry.
463 * @param data a json data object with the details to render
464 */
465 _renderDetailsUI: function(data) {
466 if (!data.details) {
467 return null;
468 }
469 var details_node = Y.Node.create('<div></div>')
470 .addClass('sprite')
471 .addClass(C_RESULT_DESCRIPTION);
472 if (Y.Lang.isArray(data.details)) {
473 var data_node = Y.Node.create('<div></div>');
474 var escaped_details = [];
475 Y.Array.each(data.details, function(detail, i) {
476 escaped_details.push(Y.Escape.html(detail));
477 });
478 data_node.append(Y.Node.create(escaped_details.join('<br />')));
479 details_node.append(data_node);
480 }
481 var links = [];
482 links.push(Y.Node.create(
483 '<a class="sprite yes save" href="#"></a>')
484 .set('text', 'Select ' + data.title));
485 links[0].on('click', function (e, value) {
486 this.fire(SAVE, value);
487 }, this, data);
488 links.push(this._text_or_link(
489 'View details', data.alt_title_link, data.link_css));
490 var link_list = Y.Node.create('<ul></ul>')
491 .addClass('horizontal');
492 Y.Array.each(links, function(link, i) {
493 var li = Y.Node.create('<li></li>');
494 li.append(link);
495 link_list.append(li);
496 });
497 details_node.append(link_list);
498 return details_node;
499 },
500
501 /**
502=======
503 * Render a node containing the optional details part of the picker entry.432 * Render a node containing the optional details part of the picker entry.
504 * @param data a json data object with the details to render433 * @param data a json data object with the details to render
505 */434 */
@@ -540,7 +469,6 @@
540 },469 },
541470
542 /**471 /**
543>>>>>>> MERGE-SOURCE
544 * Update the UI based on the results attribute.472 * Update the UI based on the results attribute.
545 *473 *
546 * @method _syncResultsUI474 * @method _syncResultsUI
547475
=== modified file 'lib/lp/app/javascript/picker/tests/test_picker.js'
--- lib/lp/app/javascript/picker/tests/test_picker.js 2011-08-05 04:51:58 +0000
+++ lib/lp/app/javascript/picker/tests/test_picker.js 2011-08-05 04:51:59 +0000
@@ -148,8 +148,7 @@
148 alt_title: 'Joe Again <foo></foo>',148 alt_title: 'Joe Again <foo></foo>',
149 title_link: 'http://somewhere.com',149 title_link: 'http://somewhere.com',
150 alt_title_link: 'http://somewhereelse.com',150 alt_title_link: 'http://somewhereelse.com',
151 link_css: 'cool-style',151 link_css: 'cool-style'
152 details: ['Member since 2007'],
153 }152 }
154 ]);153 ]);
155154
@@ -179,83 +178,6 @@
179 Assert.areEqual('Joe Again <foo></foo>', alt_text_node.get('text'));178 Assert.areEqual('Joe Again <foo></foo>', alt_text_node.get('text'));
180 },179 },
181180
182<<<<<<< TREE
183 test_details: function () {
184 // The details of the li is the content node of the expander.
185 this.picker.render();
186 this.picker.set('results', [
187 {
188 css: 'yui3-blah-blue',
189 value: 'jschmo',
190 title: 'Joe Schmo',
191 description: 'joe@example.com',
192 details: ['joe on irc.freenode.net', 'Member since 2007'],
193 alt_title_link: '/~jschmo'
194 }
195 ]);
196 var bb = this.picker.get('boundingBox');
197 var li = bb.one('.yui3-picker-results li');
198 var details = li.expander.content_node;
199 Assert.areEqual(
200 'joe on irc.freenode.net<br>Member since 2007',
201 details.one('div').getContent());
202 Assert.areEqual(
203 'Select Joe Schmo', details.one('ul li:first-child').get('text'));
204 Assert.areEqual(
205 'View details', details.one('ul li:last-child').get('text'));
206 },
207
208 test_details_escaping: function () {
209 // The content of details is escaped.
210 this.picker.render();
211 this.picker.set('results', [
212 {
213 css: 'yui3-blah-blue',
214 value: 'jschmo',
215 title: 'Joe <Schmo>',
216 description: 'joe@example.com',
217 details: ['<joe> on irc.freenode.net', 'f<nor>d maintainer'],
218 alt_title_link: '/~jschmo'
219 }
220 ]);
221 var bb = this.picker.get('boundingBox');
222 var li = bb.one('.yui3-picker-results li');
223 var details = li.expander.content_node;
224 Assert.areEqual(
225 '&lt;joe&gt; on irc.freenode.net<br>f&lt;nor&gt;d maintainer',
226 details.one('div').getContent());
227 Assert.areEqual(
228 'Select Joe &lt;Schmo&gt;',
229 details.one('ul li:first-child a').getContent('text'));
230 },
231
232 test_details_save_link: function () {
233 // The select link the li's details saves the selection.
234 this.picker.render();
235 this.picker.set('results', [
236 {
237 css: 'yui3-blah-blue',
238 value: 'jschmo',
239 title: 'Joe Schmo',
240 description: 'joe@example.com',
241 alt_title_link: 'http://somewhereelse.com',
242 link_css: 'cool-style',
243 details: ['Member since 2007'],
244 }
245 ]);
246 var bb = this.picker.get('boundingBox');
247 var link_node = bb.one('a.save');
248 Assert.areEqual('Select Joe Schmo', link_node.get('text'));
249 Assert.isTrue(link_node.get('href').indexOf(window.location) === 0);
250 var selected_value = null;
251 this.picker.subscribe('save', function(e) {
252 selected_value = e.details[0].value;
253 }, this);
254 simulate(bb, 'a.save', 'click');
255 Assert.areEqual('jschmo', selected_value);
256 },
257
258=======
259 test_details: function () {181 test_details: function () {
260 // The details of the li is the content node of the expander.182 // The details of the li is the content node of the expander.
261 this.picker.render();183 this.picker.render();
@@ -327,7 +249,6 @@
327 Assert.areEqual('jschmo', selected_value);249 Assert.areEqual('jschmo', selected_value);
328 },250 },
329251
330>>>>>>> MERGE-SOURCE
331 test_title_badges: function () {252 test_title_badges: function () {
332 this.picker.render();253 this.picker.render();
333 var badge_info = [254 var badge_info = [
334255
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2011-08-02 05:35:39 +0000
+++ lib/lp/registry/configure.zcml 2011-08-05 04:51:59 +0000
@@ -885,9 +885,27 @@
885 <adapter885 <adapter
886 factory="lp.registry.model.pillaraffiliation.BugTaskPillarAffiliation"886 factory="lp.registry.model.pillaraffiliation.BugTaskPillarAffiliation"
887 />887 />
888888 <adapter
889 <adapter889 factory="lp.registry.model.pillaraffiliation.QuestionPillarAffiliation"
890 factory="lp.registry.model.pillaraffiliation.PillarAffiliation"890 />
891 <adapter
892 factory="lp.registry.model.pillaraffiliation.SpecificationPillarAffiliation"
893 />
894 <adapter
895 for="lp.registry.interfaces.distribution.IDistribution"
896 factory="lp.registry.model.pillaraffiliation.PillarAffiliation"
897 />
898 <adapter
899 for="lp.registry.interfaces.distroseries.IDistroSeries"
900 factory="lp.registry.model.pillaraffiliation.DistroSeriesPillarAffiliation"
901 />
902 <adapter
903 for="lp.registry.interfaces.product.IProduct"
904 factory="lp.registry.model.pillaraffiliation.PillarAffiliation"
905 />
906 <adapter
907 for="lp.registry.interfaces.productseries.IProductSeries"
908 factory="lp.registry.model.pillaraffiliation.ProductSeriesPillarAffiliation"
891 />909 />
892910
893 <!-- Using911 <!-- Using
894912
=== modified file 'lib/lp/registry/model/pillaraffiliation.py'
--- lib/lp/registry/model/pillaraffiliation.py 2011-08-05 04:51:58 +0000
+++ lib/lp/registry/model/pillaraffiliation.py 2011-08-05 04:51:59 +0000
@@ -29,15 +29,14 @@
29 )29 )
3030
31from canonical.launchpad.interfaces.launchpad import IHasIcon31from canonical.launchpad.interfaces.launchpad import IHasIcon
32from lp.answers.interfaces.question import IQuestion
33from lp.blueprints.interfaces.specification import ISpecification
32from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor34from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
33from lp.bugs.interfaces.bugtask import IBugTask35from lp.bugs.interfaces.bugtask import IBugTask
34<<<<<<< TREE
35from lp.registry.interfaces.distribution import IDistribution
36=======
37from lp.bugs.interfaces.securitycontact import IHasSecurityContact36from lp.bugs.interfaces.securitycontact import IHasSecurityContact
38from lp.registry.interfaces.distribution import IDistribution37from lp.registry.interfaces.distribution import IDistribution
39from lp.registry.interfaces.role import IHasAppointedDriver38from lp.registry.interfaces.distroseries import IDistroSeries
40>>>>>>> MERGE-SOURCE39from lp.registry.interfaces.productseries import IProductSeries
4140
4241
43class IHasAffiliation(Interface):42class IHasAffiliation(Interface):
@@ -58,7 +57,9 @@
58class PillarAffiliation(object):57class PillarAffiliation(object):
59 """Default affiliation adapter.58 """Default affiliation adapter.
6059
61 No affiliation is returned.60 Subclasses may need to override getPillar() in order to provide the pillar
61 entity for which affiliation is to be determined. The default is just to
62 use the context object directly.
62 """63 """
6364
64 implements(IHasAffiliation)65 implements(IHasAffiliation)
@@ -66,61 +67,108 @@
66 def __init__(self, context):67 def __init__(self, context):
67 self.context = context68 self.context = context
6869
69 def getAffiliationBadge(self, person):70 def getPillar(self):
70 return None71 return self.context
7172
7273 def _getAffiliationDetails(self, person, pillar):
73# XXX: wallyworld 2011-05-24 bug=81692: TODO Work is required to determine74 """ Return the affiliation information for a person, if any.
74# exactly what is required in terms of figuring out affiliation..75
7576 A person is affiliated with a pillar if they are in the list of
76@adapter(IBugTask)77 drivers or are the maintainer.
77class BugTaskPillarAffiliation(PillarAffiliation):78 """
78 """An affiliation adapter for bug tasks."""
79
80 def getAffiliationBadge(self, person):
81 pillar = self.context.pillar
82 bug_supervisor = None
83 security_contact = None
84 driver = None
85 if IHasAppointedDriver.providedBy(pillar):
86 driver = pillar.driver
87 if IHasSecurityContact.providedBy(pillar):
88 security_contact = pillar.security_contact
89 if IHasBugSupervisor.providedBy(pillar):
90 bug_supervisor = pillar.bug_supervisor
91
92 affiliated = None
93 if person.inTeam(pillar.owner):79 if person.inTeam(pillar.owner):
94 affiliated = 'maintainer'80 return pillar.displayname, 'maintainer'
95 elif person.inTeam(driver):81 for driver in pillar.drivers:
96 affiliated = 'driver'82 if person.inTeam(driver):
97 elif person.inTeam(bug_supervisor):83 return pillar.displayname, 'driver'
98 affiliated = 'bug supervisor'84 return None
99 elif person.inTeam(security_contact):
100 affiliated = 'security contact'
10185
102 if not affiliated:86 def getAffiliationBadge(self, person):
87 """ Return the affiliation badge details for a person given a context.
88 """
89 pillar = self.getPillar()
90 affiliation_details = self._getAffiliationDetails(person, pillar)
91 if not affiliation_details:
103 return None92 return None
10493
105 def getIconUrl(context, default_url):94 def getIconUrl(context, pillar, default_url):
106 if IHasIcon.providedBy(context) and context.icon is not None:95 if IHasIcon.providedBy(context) and context.icon is not None:
107 icon_url = context.icon.getURL()96 icon_url = context.icon.getURL()
108 return icon_url97 return icon_url
98 if IHasIcon.providedBy(pillar) and pillar.icon is not None:
99 icon_url = context.icon.getURL()
100 return icon_url
109 return default_url101 return default_url
110<<<<<<< TREE102
111103 alt_text = "%s %s" % affiliation_details
112 alt_text = "Affiliated with %s" % pillar.displayname104 if IDistribution.providedBy(pillar):
113 if IDistribution.providedBy(pillar):105 default_icon_url = "/@@/distribution-badge"
114 icon_url = getIconUrl(pillar, "/@@/distribution-badge")106 else:
115 else:107 default_icon_url = "/@@/product-badge"
116 icon_url = getIconUrl(pillar, "/@@/product-badge")108 icon_url = getIconUrl(self.context, pillar, default_icon_url)
117 return BadgeDetails(icon_url, alt_text)109 return BadgeDetails(icon_url, alt_text)
118=======110
119111
120 alt_text = "%s %s" % (pillar.displayname, affiliated)112@adapter(IBugTask)
121 if IDistribution.providedBy(pillar):113class BugTaskPillarAffiliation(PillarAffiliation):
122 icon_url = getIconUrl(pillar, "/@@/distribution-badge")114 """An affiliation adapter for bug tasks."""
123 else:115 def getPillar(self):
124 icon_url = getIconUrl(pillar, "/@@/product-badge")116 return self.context.pillar
125 return BadgeDetails(icon_url, alt_text)117
126>>>>>>> MERGE-SOURCE118 def _getAffiliationDetails(self, person, pillar):
119 """ A person is affiliated with a bugtask based on (in order):
120 - owner of bugtask pillar
121 - driver of bugtask pillar
122 - bug supervisor of bugtask pillar
123 - security contact of bugtask pillar
124 """
125 result = super(BugTaskPillarAffiliation, self)._getAffiliationDetails(
126 person, pillar)
127 if result is not None:
128 return result
129 if person.inTeam(pillar.bug_supervisor):
130 return pillar.displayname, 'bug supervisor'
131 if person.inTeam(pillar.security_contact):
132 return pillar.displayname, 'security contact'
133
134
135@adapter(IDistroSeries)
136class DistroSeriesPillarAffiliation(PillarAffiliation):
137 """An affiliation adapter for distroseries."""
138 def getPillar(self):
139 return self.context.distribution
140
141
142@adapter(IProductSeries)
143class ProductSeriesPillarAffiliation(PillarAffiliation):
144 """An affiliation adapter for productseries."""
145 def getPillar(self):
146 return self.context.product
147
148
149@adapter(ISpecification)
150class SpecificationPillarAffiliation(PillarAffiliation):
151 """An affiliation adapter for blueprints."""
152 def getPillar(self):
153 return (self.context.target)
154
155
156@adapter(IQuestion)
157class QuestionPillarAffiliation(PillarAffiliation):
158 """An affiliation adapter for questions."""
159 def getPillar(self):
160 return self.context.product or self.context.distribution
161
162 def _getAffiliationDetails(self, person, pillar):
163 """ A person is affiliated with a question based on (in order):
164 - answer contact for question target
165 - owner of question target
166 - driver of question target
167 """
168 target = self.context.target
169 answer_contacts = target.answer_contacts
170 for answer_contact in answer_contacts:
171 if person.inTeam(answer_contact):
172 return target.displayname, 'answer contact'
173 return super(QuestionPillarAffiliation, self)._getAffiliationDetails(
174 person, pillar)
127175
=== modified file 'lib/lp/registry/tests/test_pillaraffiliation.py'
--- lib/lp/registry/tests/test_pillaraffiliation.py 2011-08-05 04:51:58 +0000
+++ lib/lp/registry/tests/test_pillaraffiliation.py 2011-08-05 04:51:59 +0000
@@ -7,112 +7,173 @@
77
8from storm.store import Store8from storm.store import Store
9from testtools.matchers import Equals9from testtools.matchers import Equals
10from zope.component import getUtility
11from zope.security.proxy import removeSecurityProxy
1012
11from canonical.testing.layers import DatabaseFunctionalLayer13from canonical.testing.layers import DatabaseFunctionalLayer
12from lp.registry.model.pillaraffiliation import IHasAffiliation14from lp.registry.model.pillaraffiliation import IHasAffiliation
13from lp.testing import StormStatementRecorder, TestCaseWithFactory15from lp.services.worlddata.interfaces.language import ILanguageSet
16from lp.testing import (
17 person_logged_in,
18 StormStatementRecorder,
19 TestCaseWithFactory,
20 )
14from lp.testing.matchers import HasQueryCount21from lp.testing.matchers import HasQueryCount
1522
1623
17class TestBugTaskPillarAffiliation(TestCaseWithFactory):24class TestPillarAffiliation(TestCaseWithFactory):
1825
19 layer = DatabaseFunctionalLayer26 layer = DatabaseFunctionalLayer
2027
21 def _check_affiliated_with_distro(self, person, distro, role):28 def _check_affiliated_with_distro(self, person, distro, role):
22 bugtask = self.factory.makeBugTask(target=distro)29 badge = IHasAffiliation(distro).getAffiliationBadge(person)
23 badge = IHasAffiliation(bugtask).getAffiliationBadge(person)
24 self.assertEqual(30 self.assertEqual(
25 badge, ("/@@/distribution-badge", "Pting %s" % role))31 ("/@@/distribution-badge", "Pting %s" % role), badge)
2632
27 def test_bugtask_distro_owner_affiliation(self):33 def test_distro_owner_affiliation(self):
28 # A person who owns a bugtask distro is affiliated.34 # A person who owns a distro is affiliated.
29 person = self.factory.makePerson()35 person = self.factory.makePerson()
30<<<<<<< TREE
31 distro = self.factory.makeDistribution(owner=person, name='pting')
32 bugtask = self.factory.makeBugTask(target=distro)
33=======
34 distro = self.factory.makeDistribution(owner=person, name='pting')36 distro = self.factory.makeDistribution(owner=person, name='pting')
35 self._check_affiliated_with_distro(person, distro, 'maintainer')37 self._check_affiliated_with_distro(person, distro, 'maintainer')
3638
37 def test_bugtask_distro_security_contact_affiliation(self):39 def test_distro_driver_affiliation(self):
38 # A person who is the security contact for a bugtask distro is40 # A person who is a distro driver is affiliated.
39 # affiliated.41 person = self.factory.makePerson()
42 distro = self.factory.makeDistribution(driver=person, name='pting')
43 self._check_affiliated_with_distro(person, distro, 'driver')
44
45 def test_distro_team_driver_affiliation(self):
46 # A person who is a member of the distro driver team is affiliated.
47 person = self.factory.makePerson()
48 team = self.factory.makeTeam(members=[person])
49 distro = self.factory.makeDistribution(driver=team, name='pting')
50 self._check_affiliated_with_distro(person, distro, 'driver')
51
52 def test_no_distro_security_contact_affiliation(self):
53 # A person who is the security contact for a distro is not affiliated
54 # for simple distro affiliation checks.
55 person = self.factory.makePerson()
56 distro = self.factory.makeDistribution(security_contact=person)
57 self.assertIs(
58 None, IHasAffiliation(distro).getAffiliationBadge(person))
59
60 def test_no_distro_bug_supervisor_affiliation(self):
61 # A person who is the bug supervisor for a distro is not affiliated
62 # for simple distro affiliation checks.
63 person = self.factory.makePerson()
64 distro = self.factory.makeDistribution(bug_supervisor=person)
65 self.assertIs(
66 None, IHasAffiliation(distro).getAffiliationBadge(person))
67
68 def _check_affiliated_with_product(self, person, product, role):
69 badge = IHasAffiliation(product).getAffiliationBadge(person)
70 self.assertEqual(
71 ("/@@/product-badge", "Pting %s" % role), badge)
72
73 def test_product_driver_affiliation(self):
74 # A person who is the driver for a product is affiliated.
75 person = self.factory.makePerson()
76 product = self.factory.makeProduct(driver=person, name='pting')
77 self._check_affiliated_with_product(person, product, 'driver')
78
79 def test_product_team_driver_affiliation(self):
80 # A person who is a member of the product driver team is affiliated.
81 person = self.factory.makePerson()
82 team = self.factory.makeTeam(members=[person])
83 product = self.factory.makeProduct(driver=team, name='pting')
84 self._check_affiliated_with_product(person, product, 'driver')
85
86 def test_product_group_driver_affiliation(self):
87 # A person who is the driver for a product's group is affiliated.
88 person = self.factory.makePerson()
89 project = self.factory.makeProject(driver=person)
90 product = self.factory.makeProduct(project=project, name='pting')
91 self._check_affiliated_with_product(person, product, 'driver')
92
93 def test_no_product_security_contact_affiliation(self):
94 # A person who is the security contact for a product is is not
95 # affiliated for simple product affiliation checks.
96 person = self.factory.makePerson()
97 product = self.factory.makeProduct(security_contact=person)
98 self.assertIs(
99 None, IHasAffiliation(product).getAffiliationBadge(person))
100
101 def test_no_product_bug_supervisor_affiliation(self):
102 # A person who is the bug supervisor for a product is is not
103 # affiliated for simple product affiliation checks.
104 person = self.factory.makePerson()
105 product = self.factory.makeProduct(bug_supervisor=person)
106 self.assertIs(
107 None, IHasAffiliation(product).getAffiliationBadge(person))
108
109 def test_product_owner_affiliation(self):
110 # A person who owns a product is affiliated.
111 person = self.factory.makePerson()
112 product = self.factory.makeProduct(owner=person, name='pting')
113 self._check_affiliated_with_product(person, product, 'maintainer')
114
115
116class TestBugTaskPillarAffiliation(TestCaseWithFactory):
117
118 layer = DatabaseFunctionalLayer
119
120 def test_correct_pillar_is_used(self):
121 bugtask = self.factory.makeBugTask()
122 badge = IHasAffiliation(bugtask)
123 self.assertEqual(bugtask.pillar, badge.getPillar())
124
125 def _check_affiliated_with_distro(self, person, target, role):
126 bugtask = self.factory.makeBugTask(target=target)
127 badge = IHasAffiliation(bugtask).getAffiliationBadge(person)
128 self.assertEqual(
129 ("/@@/distribution-badge", "Pting %s" % role), badge)
130
131 def _check_affiliated_with_product(self, person, target, role):
132 bugtask = self.factory.makeBugTask(target=target)
133 badge = IHasAffiliation(bugtask).getAffiliationBadge(person)
134 self.assertEqual(
135 ("/@@/product-badge", "Pting %s" % role), badge)
136
137 def test_distro_security_contact_affiliation(self):
138 # A person who is the security contact for a distro is affiliated.
40 person = self.factory.makePerson()139 person = self.factory.makePerson()
41 distro = self.factory.makeDistribution(140 distro = self.factory.makeDistribution(
42 security_contact=person, name='pting')141 security_contact=person, name='pting')
43 self._check_affiliated_with_distro(person, distro, 'security contact')142 self._check_affiliated_with_distro(person, distro, 'security contact')
44143
45 def test_bugtask_distro_bug_supervisor_affiliation(self):144 def test_distro_bug_supervisor_affiliation(self):
46 # A person who is the bug supervisor for a bugtask distro is145 # A person who is the bug supervisor for a distro is affiliated.
47 # affiliated.
48 person = self.factory.makePerson()146 person = self.factory.makePerson()
49 distro = self.factory.makeDistribution(147 distro = self.factory.makeDistribution(
50 bug_supervisor=person, name='pting')148 bug_supervisor=person, name='pting')
51 self._check_affiliated_with_distro(person, distro, 'bug supervisor')149 self._check_affiliated_with_distro(person, distro, 'bug supervisor')
52150
53 def _check_affiliated_with_product(self, person, product, role):151 def test_product_security_contact_affiliation(self):
54 bugtask = self.factory.makeBugTask(target=product)152 # A person who is the security contact for a distro is affiliated.
55>>>>>>> MERGE-SOURCE
56 badge = IHasAffiliation(bugtask).getAffiliationBadge(person)
57 self.assertEqual(
58<<<<<<< TREE
59 badge, ("/@@/distribution-badge", "Affiliated with Pting"))
60
61 def test_bugtask_product_affiliation(self):
62=======
63 badge, ("/@@/product-badge", "Pting %s" % role))
64
65 def test_bugtask_product_driver_affiliation(self):
66 # A person who is the driver for a bugtask product is affiliated.
67 person = self.factory.makePerson()
68 product = self.factory.makeProduct(driver=person, name='pting')
69 self._check_affiliated_with_product(person, product, 'driver')
70
71 def test_bugtask_product_security_contact_affiliation(self):
72 # A person who is the security contact for a bugtask product is
73 # affiliated.
74 person = self.factory.makePerson()153 person = self.factory.makePerson()
75 product = self.factory.makeProduct(154 product = self.factory.makeProduct(
76 security_contact=person, name='pting')155 security_contact=person, name='pting')
77 self._check_affiliated_with_product(156 self._check_affiliated_with_product(person, product, 'security contact')
78 person, product, 'security contact')
79157
80 def test_bugtask_product_bug_supervisor_affiliation(self):158 def test_product_bug_supervisor_affiliation(self):
81 # A person who is the bug supervisor for a bugtask product is159 # A person who is the bug supervisor for a distro is affiliated.
82 # affiliated.
83 person = self.factory.makePerson()160 person = self.factory.makePerson()
84 product = self.factory.makeProduct(161 product = self.factory.makeProduct(
85 bug_supervisor=person, name='pting')162 bug_supervisor=person, name='pting')
86 self._check_affiliated_with_product(person, product, 'bug supervisor')163 self._check_affiliated_with_product(person, product, 'bug supervisor')
87164
88 def test_bugtask_product_owner_affiliation(self):165 def test_product_affiliation_query_count(self):
89>>>>>>> MERGE-SOURCE
90 # A person who owns a bugtask product is affiliated.
91 person = self.factory.makePerson()
92<<<<<<< TREE
93 product = self.factory.makeProduct(owner=person, name='pting')
94=======
95 product = self.factory.makeProduct(owner=person, name='pting')
96 self._check_affiliated_with_product(person, product, 'maintainer')
97
98 def test_bugtask_product_affiliation_query_count(self):
99 # Only 4 queries are expected, selects from:166 # Only 4 queries are expected, selects from:
100 # - Bug, BugTask, Product, Person167 # - Bug, BugTask, Product, Person
101 person = self.factory.makePerson()168 person = self.factory.makePerson()
102 product = self.factory.makeProduct(owner=person, name='pting')169 product = self.factory.makeProduct(owner=person, name='pting')
103>>>>>>> MERGE-SOURCE
104 bugtask = self.factory.makeBugTask(target=product)170 bugtask = self.factory.makeBugTask(target=product)
105<<<<<<< TREE
106 badge = IHasAffiliation(bugtask).getAffiliationBadge(person)
107 self.assertEqual(
108 badge, ("/@@/product-badge", "Affiliated with Pting"))
109=======
110 Store.of(bugtask).invalidate()171 Store.of(bugtask).invalidate()
111 with StormStatementRecorder() as recorder:172 with StormStatementRecorder() as recorder:
112 IHasAffiliation(bugtask).getAffiliationBadge(person)173 IHasAffiliation(bugtask).getAffiliationBadge(person)
113 self.assertThat(recorder, HasQueryCount(Equals(4)))174 self.assertThat(recorder, HasQueryCount(Equals(4)))
114175
115 def test_bugtask_distro_affiliation_query_count(self):176 def test_distro_affiliation_query_count(self):
116 # Only 4 queries are expected, selects from:177 # Only 4 queries are expected, selects from:
117 # - Bug, BugTask, Distribution, Person178 # - Bug, BugTask, Distribution, Person
118 person = self.factory.makePerson()179 person = self.factory.makePerson()
@@ -122,4 +183,225 @@
122 with StormStatementRecorder() as recorder:183 with StormStatementRecorder() as recorder:
123 IHasAffiliation(bugtask).getAffiliationBadge(person)184 IHasAffiliation(bugtask).getAffiliationBadge(person)
124 self.assertThat(recorder, HasQueryCount(Equals(4)))185 self.assertThat(recorder, HasQueryCount(Equals(4)))
125>>>>>>> MERGE-SOURCE186
187
188class TestDistroSeriesPillarAffiliation(TestCaseWithFactory):
189
190 layer = DatabaseFunctionalLayer
191
192 def test_correct_pillar_is_used(self):
193 series = self.factory.makeDistroSeries()
194 badge = IHasAffiliation(series)
195 self.assertEqual(series.distribution, badge.getPillar())
196
197 def test_driver_affiliation(self):
198 # A person who is the driver for a distroseries is affiliated.
199 # Here, the affiliation is with the distribution of the series.
200 owner = self.factory.makePerson()
201 driver = self.factory.makePerson()
202 distribution = self.factory.makeDistribution(
203 owner=owner, driver=driver, name='pting')
204 distroseries = self.factory.makeDistroSeries(
205 registrant=driver, distribution=distribution)
206 badge = IHasAffiliation(distroseries).getAffiliationBadge(driver)
207 self.assertEqual(
208 ("/@@/distribution-badge", "Pting driver"), badge)
209
210 def test_distro_driver_affiliation(self):
211 # A person who is the driver for a distroseries' distro is affiliated.
212 # Here, the affiliation is with the distribution of the series.
213 owner = self.factory.makePerson()
214 driver = self.factory.makePerson()
215 distribution = self.factory.makeDistribution(
216 owner=owner, driver=driver, name='pting')
217 distroseries = self.factory.makeDistroSeries(
218 registrant=owner, distribution=distribution)
219 badge = IHasAffiliation(distroseries).getAffiliationBadge(driver)
220 self.assertEqual(
221 ("/@@/distribution-badge", "Pting driver"), badge)
222
223
224class TestProductSeriesPillarAffiliation(TestCaseWithFactory):
225
226 layer = DatabaseFunctionalLayer
227
228 def test_correct_pillar_is_used(self):
229 series = self.factory.makeProductSeries()
230 badge = IHasAffiliation(series)
231 self.assertEqual(series.product, badge.getPillar())
232
233 def test_driver_affiliation(self):
234 # A person who is the driver for a productseries is affiliated.
235 # Here, the affiliation is with the product.
236 owner = self.factory.makePerson()
237 driver = self.factory.makePerson()
238 product = self.factory.makeProduct(
239 owner=owner, driver=driver, name='pting')
240 productseries = self.factory.makeProductSeries(
241 owner=driver, product=product)
242 badge = IHasAffiliation(productseries).getAffiliationBadge(driver)
243 self.assertEqual(
244 ("/@@/product-badge", "Pting driver"), badge)
245
246 def test_product_driver_affiliation(self):
247 # A person who is the driver for a productseries' product is
248 # affiliated. Here, the affiliation is with the product.
249 owner = self.factory.makePerson()
250 driver = self.factory.makePerson()
251 product = self.factory.makeProduct(
252 owner=owner, driver=driver, name='pting')
253 productseries = self.factory.makeProductSeries(
254 owner=owner, product=product)
255 badge = IHasAffiliation(productseries).getAffiliationBadge(driver)
256 self.assertEqual(
257 ("/@@/product-badge", "Pting driver"), badge)
258
259 def test_product_group_driver_affiliation(self):
260 # A person who is the driver for a productseries' product's group is
261 # affiliated. Here, the affiliation is with the product.
262 owner = self.factory.makePerson()
263 driver = self.factory.makePerson()
264 project = self.factory.makeProject(driver=driver)
265 product = self.factory.makeProduct(
266 owner=owner, project=project, name='pting')
267 productseries = self.factory.makeProductSeries(
268 owner=owner, product=product)
269 badge = IHasAffiliation(productseries).getAffiliationBadge(driver)
270 self.assertEqual(
271 ("/@@/product-badge", "Pting driver"), badge)
272
273
274class TestQuestionPillarAffiliation(TestCaseWithFactory):
275
276 layer = DatabaseFunctionalLayer
277
278 def test_correct_pillar_is_used_for_product(self):
279 product = self.factory.makeProduct()
280 question = self.factory.makeQuestion(target=product)
281 badge = IHasAffiliation(question)
282 self.assertEqual(question.product, badge.getPillar())
283
284 def test_correct_pillar_is_used_for_distribution(self):
285 distribution = self.factory.makeDistribution()
286 question = self.factory.makeQuestion(target=distribution)
287 badge = IHasAffiliation(question)
288 self.assertEqual(question.distribution, badge.getPillar())
289
290 def test_correct_pillar_is_used_for_distro_sourcepackage(self):
291 distribution = self.factory.makeDistribution()
292 distro_sourcepackage = self.factory.makeDistributionSourcePackage(
293 distribution=distribution)
294 owner = self.factory.makePerson()
295 question = self.factory.makeQuestion(
296 target=distro_sourcepackage, owner=owner)
297 badge = IHasAffiliation(question)
298 self.assertEqual(distribution, badge.getPillar())
299
300 def test_answer_contact_affiliation_for_distro(self):
301 # A person is affiliated if they are an answer contact for a distro
302 # target. Even if they also own the distro, the answer contact
303 # affiliation takes precedence.
304 answer_contact = self.factory.makePerson()
305 english = getUtility(ILanguageSet)['en']
306 answer_contact.addLanguage(english)
307 distro = self.factory.makeDistribution(owner=answer_contact)
308 with person_logged_in(answer_contact):
309 distro.addAnswerContact(answer_contact, answer_contact)
310 question = self.factory.makeQuestion(target=distro)
311 badge = IHasAffiliation(question).getAffiliationBadge(answer_contact)
312 self.assertEqual(
313 ("/@@/distribution-badge", "%s answer contact" %
314 distro.displayname), badge)
315
316 def test_answer_contact_affiliation_for_distro_sourcepackage(self):
317 # A person is affiliated if they are an answer contact for a dsp
318 # target. Even if they also own the distro, the answer contact
319 # affiliation takes precedence.
320 answer_contact = self.factory.makePerson()
321 english = getUtility(ILanguageSet)['en']
322 answer_contact.addLanguage(english)
323 distribution = self.factory.makeDistribution(owner=answer_contact)
324 distro_sourcepackage = self.factory.makeDistributionSourcePackage(
325 distribution=distribution)
326 with person_logged_in(answer_contact):
327 distro_sourcepackage.addAnswerContact(
328 answer_contact, answer_contact)
329 question = self.factory.makeQuestion(
330 target=distro_sourcepackage, owner=answer_contact)
331 badge = IHasAffiliation(question).getAffiliationBadge(answer_contact)
332 self.assertEqual(
333 ("/@@/distribution-badge", "%s answer contact" %
334 distro_sourcepackage.displayname), badge)
335
336 def test_answer_contact_affiliation_for_product(self):
337 # A person is affiliated if they are an answer contact for a product
338 # target. Even if they also own the product, the answer contact
339 # affiliation takes precedence.
340 answer_contact = self.factory.makePerson()
341 english = getUtility(ILanguageSet)['en']
342 answer_contact.addLanguage(english)
343 product = self.factory.makeProduct(owner=answer_contact)
344 with person_logged_in(answer_contact):
345 product.addAnswerContact(answer_contact, answer_contact)
346 question = self.factory.makeQuestion(target=product)
347 badge = IHasAffiliation(question).getAffiliationBadge(answer_contact)
348 self.assertEqual(
349 ("/@@/product-badge", "%s answer contact" %
350 product.displayname), badge)
351
352 def test_product_affiliation(self):
353 # A person is affiliated if they are affiliated with the product.
354 person = self.factory.makePerson()
355 product = self.factory.makeProduct(owner=person)
356 question = self.factory.makeQuestion(target=product)
357 badge = IHasAffiliation(question).getAffiliationBadge(person)
358 self.assertEqual(
359 ("/@@/product-badge", "%s maintainer" %
360 product.displayname), badge)
361
362 def test_distribution_affiliation(self):
363 # A person is affiliated if they are affiliated with the distribution.
364 person = self.factory.makePerson()
365 distro = self.factory.makeDistribution(owner=person)
366 question = self.factory.makeQuestion(target=distro)
367 badge = IHasAffiliation(question).getAffiliationBadge(person)
368 self.assertEqual(
369 ("/@@/distribution-badge", "%s maintainer" %
370 distro.displayname), badge)
371
372
373class TestSpecificationPillarAffiliation(TestCaseWithFactory):
374
375 layer = DatabaseFunctionalLayer
376
377 def test_correct_pillar_is_used_for_product(self):
378 product = self.factory.makeProduct()
379 specification = self.factory.makeSpecification(product=product)
380 badge = IHasAffiliation(specification)
381 self.assertEqual(specification.product, badge.getPillar())
382
383 def test_correct_pillar_is_used_for_distribution(self):
384 distro = self.factory.makeDistribution()
385 specification = self.factory.makeSpecification(distribution=distro)
386 badge = IHasAffiliation(specification)
387 self.assertEqual(specification.distribution, badge.getPillar())
388
389 def test_product_affiliation(self):
390 # A person is affiliated if they are affiliated with the pillar.
391 person = self.factory.makePerson()
392 product = self.factory.makeProduct(owner=person)
393 specification = self.factory.makeSpecification(product=product)
394 badge = IHasAffiliation(specification).getAffiliationBadge(person)
395 self.assertEqual(
396 ("/@@/product-badge", "%s maintainer" %
397 product.displayname), badge)
398
399 def test_distribution_affiliation(self):
400 # A person is affiliated if they are affiliated with the distribution.
401 person = self.factory.makePerson()
402 distro = self.factory.makeDistribution(owner=person)
403 specification = self.factory.makeSpecification(distribution=distro)
404 badge = IHasAffiliation(specification).getAffiliationBadge(person)
405 self.assertEqual(
406 ("/@@/distribution-badge", "%s maintainer" %
407 distro.displayname), badge)
126408
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2011-08-05 04:51:58 +0000
+++ lib/lp/testing/factory.py 2011-08-05 04:51:59 +0000
@@ -1025,7 +1025,7 @@
1025 return ProxyFactory(series)1025 return ProxyFactory(series)
10261026
1027 def makeProject(self, name=None, displayname=None, title=None,1027 def makeProject(self, name=None, displayname=None, title=None,
1028 homepageurl=None, summary=None, owner=None,1028 homepageurl=None, summary=None, owner=None, driver=None,
1029 description=None):1029 description=None):
1030 """Create and return a new, arbitrary ProjectGroup."""1030 """Create and return a new, arbitrary ProjectGroup."""
1031 if owner is None:1031 if owner is None:
@@ -1040,7 +1040,7 @@
1040 description = self.getUniqueString('description')1040 description = self.getUniqueString('description')
1041 if title is None:1041 if title is None:
1042 title = self.getUniqueString('title')1042 title = self.getUniqueString('title')
1043 return getUtility(IProjectGroupSet).new(1043 project = getUtility(IProjectGroupSet).new(
1044 name=name,1044 name=name,
1045 displayname=displayname,1045 displayname=displayname,
1046 title=title,1046 title=title,
@@ -1048,6 +1048,9 @@
1048 summary=summary,1048 summary=summary,
1049 description=description,1049 description=description,
1050 owner=owner)1050 owner=owner)
1051 if driver is not None:
1052 removeSecurityProxy(project).driver = driver
1053 return project
10511054
1052 def makeSprint(self, title=None, name=None):1055 def makeSprint(self, title=None, name=None):
1053 """Make a sprint."""1056 """Make a sprint."""
@@ -2326,7 +2329,7 @@
23262329
2327 def makeDistribution(self, name=None, displayname=None, owner=None,2330 def makeDistribution(self, name=None, displayname=None, owner=None,
2328 registrant=None, members=None, title=None,2331 registrant=None, members=None, title=None,
2329 aliases=None, bug_supervisor=None,2332 aliases=None, bug_supervisor=None, driver=None,
2330 security_contact=None, publish_root_dir=None,2333 security_contact=None, publish_root_dir=None,
2331 publish_base_url=None, publish_copy_base_url=None,2334 publish_base_url=None, publish_copy_base_url=None,
2332 no_pubconf=False):2335 no_pubconf=False):
@@ -2352,6 +2355,8 @@
2352 naked_distro = removeSecurityProxy(distro)2355 naked_distro = removeSecurityProxy(distro)
2353 if aliases is not None:2356 if aliases is not None:
2354 naked_distro.setAliases(aliases)2357 naked_distro.setAliases(aliases)
2358 if driver is not None:
2359 naked_distro.driver = driver
2355 if bug_supervisor is not None:2360 if bug_supervisor is not None:
2356 naked_distro.bug_supervisor = bug_supervisor2361 naked_distro.bug_supervisor = bug_supervisor
2357 if security_contact is not None:2362 if security_contact is not None: