Merge lp:~wallyworld/launchpad/additional-affiliation-types into lp:launchpad
- additional-affiliation-types
- Merge into devel
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 | ||||||||||||
Related bugs: |
|
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_pillaraffi
== Lint ==
Linting changed files:
lib/lp/
lib/lp/
lib/lp/
Ian Booth (wallyworld) wrote : | # |
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/
>> --- lib/lp/
>> +++ lib/lp/
> ...
>> + def getPillar(self):
>> + return self.context
>> +
>> + def getAffiliationB
>> + """ 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 checkAffiliatio
>> + # 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
>> + capabilityProvi
>> + if capabilityProvi
>> + if person.
>> + affiliated_entity = self.context.
>> + if (affiliated_entity is None and capabilityProvi
>> + if person.
>> + 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
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(' ('); |
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(' ')); |
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 | - '<joe> on irc.freenode.net<br>f<nor>d maintainer', |
483 | - details.one('div').getContent()); |
484 | - Assert.areEqual( |
485 | - 'Select Joe <Schmo>', |
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: |
> === modified file 'lib/lp/ registry/ model/pillaraff iliation. py' registry/ model/pillaraff iliation. py 2011-08-04 14:31:56 +0000 registry/ model/pillaraff iliation. py 2011-08-04 14:32:17 +0000 adge(self, person): n(capability, attribute, role): dedBy = getattr(capability, 'providedBy') dedBy(self. context) : inTeam( getattr( self.context, attribute)): displayname dedBy(pillar) ): inTeam( getattr( pillar, attribute)):
> --- lib/lp/
> +++ lib/lp/
...
> + def getPillar(self):
> + return self.context
> +
> + def getAffiliationB
> + """ 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 checkAffiliatio
> + # 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
> + capabilityProvi
> + if capabilityProvi
> + if person.
> + affiliated_entity = self.context.
> + if (affiliated_entity is None and capabilityProvi
> + if person.
> + 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...