Merge lp:~adeuring/launchpad/bug-511269 into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 10943
Proposed branch: lp:~adeuring/launchpad/bug-511269
Merge into: lp:launchpad
Diff against target: 1950 lines (+679/-892)
20 files modified
lib/canonical/launchpad/javascript/bugs/bugtask-index.js (+18/-4)
lib/canonical/launchpad/scripts/sftracker.py (+0/-448)
lib/canonical/launchpad/scripts/tests/test_sftracker.py (+0/-347)
lib/canonical/launchpad/templates/bugtask-assignee-widget.pt (+2/-2)
lib/canonical/widgets/bugtask.py (+23/-0)
lib/lp/bugs/browser/bugtask.py (+20/-2)
lib/lp/bugs/browser/tests/test_bugtask.py (+30/-0)
lib/lp/bugs/configure.zcml (+3/-1)
lib/lp/bugs/doc/bugtask.txt (+74/-0)
lib/lp/bugs/interfaces/bugtask.py (+25/-0)
lib/lp/bugs/model/bugtask.py (+45/-2)
lib/lp/bugs/scripts/bugzilla.py (+12/-7)
lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt (+87/-4)
lib/lp/bugs/tests/bugs-emailinterface.txt (+8/-7)
lib/lp/bugs/tests/test_bugtask.py (+226/-2)
lib/lp/coop/answersbugs/tests/notifications-linked-bug.txt (+2/-0)
lib/lp/registry/tests/test_user_vocabularies.py (+69/-2)
lib/lp/registry/vocabularies.py (+22/-0)
lib/lp/registry/vocabularies.zcml (+13/-0)
scripts/sourceforge-import.py (+0/-64)
To merge this branch: bzr merge lp:~adeuring/launchpad/bug-511269
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+26176@code.launchpad.net

This proposal supersedes a proposal from 2010-05-25.

Description of the change

This branches fixes bug 511269: "only bug supervisor should be able to
assign bugs to other people".

As discussed in the bug report, I limited the right to set anybody as
a bug task assign not only to the bug supoervisor of a project or
distribution, but allowed that also for the project owner and the driver
of project/distro and of serieses for Launchpad admins.

Regular users can now assign only themselves and their teams; similary,
they can unassign only themselves and their teams.

Implementation details:

I needed a vocabulary that enumerates all teams where a user is a member.
This vocabulary must implement IHugeVocabulary because the server-side
machinery accessed by Javascript popups to select an assignee call a
method searchForTerms() which is only defined for IHugeVocabulary. Also,
the existing vocabulary UserTeamsParticipationVocabulary contains only
public teams, while we should allow users to set private teams as
assignees too, so I wrote a new class AllUserTeamsParticipationVocabulary.

The test for this class looks a bit ugly because of bug 583502.

The "core change" is a
I added a permission check in BugTask.transitionToAssignee() which
implements the rules for different as described above. Since there are
also some UI changes where we need to know if a given user can assign
anybody or not, I moved the actual check to two new methods
userCanSetAnyAssignee() and userCanUnassign() which are also called by some
browser class methods.

We want to show only those assignee related options a user can acutally
set or which show the current status.

Thus I changed the template l/c/l/templates/bugtask-assignee-widget.pt
so that the "unassign" radio button is only shown if there is at present
no assignee or if the current user can unassign the current assignee.

Similary, the input field to set any assignee is only shown for persons
who can set any assignee and for those regular users that are a member
of at least one team.

The input validation for the assignee field now must check against different
vocabularies for different users (AllUserTeamsParticipationVocabulary for
regular users; ValidPersonOrTeam for users with "superpowers"); this required
a change in the class BugTaskEditView.

Similary, we want to display only settable options via Javascript, so
I extended to configuration data for the YUI widget to select a user
in class BugTaskTableRowView.

Finally, I modified the YUI widget to set the assignee so that only those
options are shown which the user can actually set. Windmill tests for
these changes will follow in another branch -- this one is already longer
than recommended...

./bin/test -vvt test_bugtask
./bin/test -vvt xx-change-assignee.txt
./bin/test -vvt bugtask.txt
./bin/test -vvt test_user_vocabularies

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/canonical/launchpad/javascript/bugs/bugtask-index.js
  lib/canonical/launchpad/templates/bugtask-assignee-widget.pt
  lib/canonical/widgets/bugtask.py
  lib/lp/bugs/configure.zcml
  lib/lp/bugs/browser/bugtask.py
  lib/lp/bugs/browser/tests/test_bugtask.py
  lib/lp/bugs/interfaces/bugtask.py
  lib/lp/bugs/model/bugtask.py
  lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt
  lib/lp/bugs/tests/test_bugtask.py
  lib/lp/registry/vocabularies.py
  lib/lp/registry/vocabularies.zcml
  lib/lp/registry/tests/test_user_vocabularies.py

== JSLint notices ==
jslint: No problem found in '/home/abel/canonical/lp-branches/bug-511269/lib/canonical/launchpad/javascript/bugs/bugtask-index.js'.

jslint: 1 file to lint.

== Pylint notices ==

lib/canonical/widgets/bugtask.py
    510: [E1002, AssigneeDisplayWidget.__init__] Use super on an old style class
    538: [E1002, DBItemDisplayWidget.__init__] Use super on an old style class

lib/lp/bugs/browser/bugtask.py
    1204: [E1002, BugTaskEditView.initialize] Use super on an old style class
    1301: [E1002, BugTaskEditView.setUpFields] Use super on an old style class

lib/lp/bugs/browser/tests/test_bugtask.py
    29: [E1002, TestBugTasksAndNominationsView.setUp] Use super on an old style class
    218: [E1002, TestBugTaskEditViewStatusField.setUp] Use super on an old style class
    304: [E1002, TestBugTaskEditViewAssigneeField.setUp] Use super on an old style class

lib/lp/bugs/interfaces/bugtask.py
    1238: [C0322, BugTaskSearchParams.fromSearchForm] Operator not preceded by a space
    search_params.linked_branches=linked_branches
    ^

lib/lp/bugs/tests/test_bugtask.py
    32: [E1002, TestBugTaskDelta.setUp] Use super on an old style class
    475: [E1002, TestBugTaskHardwareSearch.setUp] Use super on an old style class
    507: [E1002, TestBugTaskPermissionsToSetAssigneeBase.setUp] Use super on an old style class

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) : Posted in a previous version of this proposal
review: Approve (code)
Revision history for this message
Abel Deuring (adeuring) wrote :
Download full text (42.4 KiB)

sorry, I clicked "resubmit" too early. I ahd to fix several test failures; most of them qre quite obvious: The branch limits most people to (un)assign only themselves; several tests assumed that everybody can assigned anybody else. So, most changes just ensure that this new constraint is fulfilled.

In addition, I added the celebrity bug_importer to the group of "persons" which can set any assignee.

Finally, I removed the module sftracker, because a test of it failed too. Since it is no longer used, I removed it instead of trying to figure out how to best fix the test...

the incremental diff since the last review:

=== removed file 'lib/canonical/launchpad/scripts/sftracker.py'
--- lib/canonical/launchpad/scripts/sftracker.py 2009-10-26 18:40:04 +0000
+++ lib/canonical/launchpad/scripts/sftracker.py 1970-01-01 00:00:00 +0000
@@ -1,448 +0,0 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Sourceforge.net Tracker import logic.
-
-This code relies on the output of Frederik Lundh's Sourceforge tracker
-screen-scraping tools:
-
- http://effbot.org/zone/sandbox-sourceforge.htm
-"""
-
-# XXX: jamesh 2007-01-10:
-# It would be good to change this code so that it generates an XML
-# dump suitable for use with the bug-import.py script. This would
-# reduce the number of bug importers we need to manage.
-
-__metaclass__ = type
-
-__all__ = [
- 'Tracker',
- 'TrackerImporter'
- ]
-
-from cStringIO import StringIO
-import datetime
-import logging
-import os
-import re
-import time
-
-import pytz
-
-from storm.store import Store
-
-# use cElementTree if it is available ...
-try:
- import xml.etree.cElementTree as ET
-except ImportError:
- try:
- import cElementTree as ET
- except ImportError:
- import elementtree.ElementTree as ET
-
-from zope.component import getUtility
-from zope.contenttype import guess_content_type
-
-from canonical.database.constants import UTC_NOW
-from canonical.launchpad.interfaces import (
- BugAttachmentType, BugTaskImportance, BugTaskStatus, CreateBugParams,
- IBugActivitySet, IBugAttachmentSet, IBugSet, IEmailAddressSet,
- ILaunchpadCelebrities, ILibraryFileAliasSet, IMessageSet, IPersonSet,
- NotFoundError, PersonCreationRationale)
-
-logger = logging.getLogger('canonical.launchpad.scripts.sftracker')
-
-# when accessed anonymously, Sourceforge returns dates in this time zone:
-SOURCEFORGE_TZ = pytz.timezone('US/Pacific')
-UTC = pytz.timezone('UTC')
-
-
-def parse_date(datestr):
- if datestr in ['', 'No updates since submission']:
- return None
- year, month, day, hour, minute = time.strptime(datestr,
- '%Y-%m-%d %H:%M')[:5]
- dt = datetime.datetime(year, month, day, hour, minute)
- return SOURCEFORGE_TZ.localize(dt).astimezone(UTC)
-
-
-def sanitise_name(name):
- """Sanitise a string to pass the valid_name() constraint"""
- name = re.sub(r'[^a-z0-9\+\.\-]+', '-', name.lower())
- if not name[0].isalnum():
- name = 'x' + name
- while name.endswith('-'):
- name = name[:-1]
- return name
...

Revision history for this message
Graham Binns (gmb) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/javascript/bugs/bugtask-index.js'
2--- lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2010-04-29 17:49:19 +0000
3+++ lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2010-06-04 09:33:28 +0000
4@@ -1446,17 +1446,31 @@
5 milestone_choice_edit.render();
6 }
7 if (Y.Lang.isValue(assignee_content)) {
8+ var step_title =
9+ (conf.assignee_vocabulary == 'ValidAssignee') ?
10+ "Search for people or teams" :
11+ "Select a team of which you are a member";
12 var assignee_picker = Y.lp.picker.addPickerPatcher(
13- 'ValidAssignee',
14+ conf.assignee_vocabulary,
15 conf.bugtask_path,
16 "assignee_link",
17 assignee_content.get('id'),
18- true,
19- true,
20- {"step_title": "Search for people or teams",
21+ conf.user_can_unassign,
22+ true,
23+ {"step_title": step_title,
24 "header": "Change assignee",
25 "remove_button_text": "Remove assignee",
26 "null_display_value": "Unassigned"});
27+ // Ordinary users can select only themselves and their teams.
28+ // Do not show the team selection, if a user is not a member
29+ // of any team,
30+ if (conf.hide_assignee_team_selection) {
31+ content_box = assignee_picker.get('contentBox');
32+ search_box = content_box.one('.yui-picker-search-box');
33+ search_box.setStyle('display', 'none');
34+ step_title = content_box.one('.contains-steptitle');
35+ step_title.setStyle('display', 'none');
36+ }
37 assignee_picker.render();
38 }
39 };
40
41=== removed file 'lib/canonical/launchpad/scripts/sftracker.py'
42--- lib/canonical/launchpad/scripts/sftracker.py 2009-10-26 18:40:04 +0000
43+++ lib/canonical/launchpad/scripts/sftracker.py 1970-01-01 00:00:00 +0000
44@@ -1,448 +0,0 @@
45-# Copyright 2009 Canonical Ltd. This software is licensed under the
46-# GNU Affero General Public License version 3 (see the file LICENSE).
47-
48-"""Sourceforge.net Tracker import logic.
49-
50-This code relies on the output of Frederik Lundh's Sourceforge tracker
51-screen-scraping tools:
52-
53- http://effbot.org/zone/sandbox-sourceforge.htm
54-"""
55-
56-# XXX: jamesh 2007-01-10:
57-# It would be good to change this code so that it generates an XML
58-# dump suitable for use with the bug-import.py script. This would
59-# reduce the number of bug importers we need to manage.
60-
61-__metaclass__ = type
62-
63-__all__ = [
64- 'Tracker',
65- 'TrackerImporter'
66- ]
67-
68-from cStringIO import StringIO
69-import datetime
70-import logging
71-import os
72-import re
73-import time
74-
75-import pytz
76-
77-from storm.store import Store
78-
79-# use cElementTree if it is available ...
80-try:
81- import xml.etree.cElementTree as ET
82-except ImportError:
83- try:
84- import cElementTree as ET
85- except ImportError:
86- import elementtree.ElementTree as ET
87-
88-from zope.component import getUtility
89-from zope.contenttype import guess_content_type
90-
91-from canonical.database.constants import UTC_NOW
92-from canonical.launchpad.interfaces import (
93- BugAttachmentType, BugTaskImportance, BugTaskStatus, CreateBugParams,
94- IBugActivitySet, IBugAttachmentSet, IBugSet, IEmailAddressSet,
95- ILaunchpadCelebrities, ILibraryFileAliasSet, IMessageSet, IPersonSet,
96- NotFoundError, PersonCreationRationale)
97-
98-logger = logging.getLogger('canonical.launchpad.scripts.sftracker')
99-
100-# when accessed anonymously, Sourceforge returns dates in this time zone:
101-SOURCEFORGE_TZ = pytz.timezone('US/Pacific')
102-UTC = pytz.timezone('UTC')
103-
104-
105-def parse_date(datestr):
106- if datestr in ['', 'No updates since submission']:
107- return None
108- year, month, day, hour, minute = time.strptime(datestr,
109- '%Y-%m-%d %H:%M')[:5]
110- dt = datetime.datetime(year, month, day, hour, minute)
111- return SOURCEFORGE_TZ.localize(dt).astimezone(UTC)
112-
113-
114-def sanitise_name(name):
115- """Sanitise a string to pass the valid_name() constraint"""
116- name = re.sub(r'[^a-z0-9\+\.\-]+', '-', name.lower())
117- if not name[0].isalnum():
118- name = 'x' + name
119- while name.endswith('-'):
120- name = name[:-1]
121- return name
122-
123-
124-def gettext(elem):
125- if elem is not None:
126- value = elem.text.strip()
127- # exported data contains escaped HTML entities.
128- value = value.replace('"', '"')
129- value = value.replace(''', '\'')
130- value = value.replace('&lt;', '<')
131- value = value.replace('&gt;', '>')
132- value = value.replace('&amp;', '&')
133- return value
134- else:
135- return ''
136-
137-
138-class TrackerAttachment:
139- """An attachment associated with a SF tracker item"""
140-
141- def __init__(self, attachment_node):
142- self.file_id = attachment_node.get('file_id')
143- self._content_type = gettext(attachment_node.find('content_type'))
144- self.filename = gettext(attachment_node.find('title'))
145- if not self.filename:
146- self.filename = 'untitled'
147- self.title = gettext(attachment_node.find('description'))
148- if not self.title:
149- self.title = self.filename
150- self.date = parse_date(gettext(attachment_node.find('date')))
151- self.sender = gettext(attachment_node.find('sender'))
152- self.data = gettext(attachment_node.find('data')).decode('base-64')
153-
154- @property
155- def is_patch(self):
156- """True if this attachment is a patch
157-
158- As the sourceforge tracker does not differentiate between
159- patches and other attachments, we need to use heuristics to
160- differentiate.
161- """
162- return (self.filename.endswith('patch') or
163- self.filename.endswith('diff'))
164-
165- @property
166- def content_type(self):
167- # always treat patches as text/plain
168- if self.is_patch:
169- return 'text/plain'
170-
171- # if we have no content type, or it is application/octet-stream,
172- # sniff the content type
173- if (self._content_type is None or
174- self._content_type.startswith('application/octet-stream')):
175- content_type, encoding = guess_content_type(
176- name=self.filename, body=self.data)
177- return content_type
178-
179- # otherwise, trust SourceForge.
180- return self._content_type
181-
182-
183-class TrackerItem:
184- """An SF tracker item"""
185-
186- def __init__(self, item_node, summary_node):
187- self.url = 'http://sourceforge.net' + gettext(
188- summary_node.find('link'))
189- self.item_id = item_node.get('id')
190- self.datecreated = parse_date(gettext(
191- item_node.find('date_submitted')))
192- self.date_last_updated = parse_date(gettext(
193- item_node.find('date_last_updated')))
194- self.title = gettext(item_node.find('summary'))
195- self.description = gettext(item_node.find('description'))
196- self.category = gettext(item_node.find('category'))
197- self.group = gettext(item_node.find('group'))
198- self.priority = gettext(item_node.find('priority'))
199- self.resolution = gettext(item_node.find('resolution'))
200- self.status = gettext(item_node.find('status'))
201- # We get these two from the summary file because it contains user IDs
202- self.reporter = gettext(summary_node.find('submitted_by'))
203- self.assignee = gettext(summary_node.find('assigned_to'))
204- # initial comment:
205- self.comments = [(self.datecreated, self.reporter, self.description)]
206- # remaining comments ...
207- for comment_node in item_node.findall('comment'):
208- dt = parse_date(gettext(comment_node.find('date')))
209- sender = gettext(comment_node.find('sender'))
210- description = gettext(comment_node.find('description'))
211- # remove recognised headers from description
212- lines = description.splitlines(True)
213- while lines and (lines[0].startswith('Date:') or
214- lines[0].startswith('Sender:') or
215- lines[0].startswith('Logged In:') or
216- lines[0].startswith('user_id=')
217- or lines[0].isspace()):
218- del lines[0]
219- description = ''.join(lines)
220- self.comments.append((dt, sender, description))
221- # attachments
222- self.attachments = [TrackerAttachment(node)
223- for node in item_node.findall('attachment')]
224-
225- @property
226- def lp_importance(self):
227- """The Launchpad importance value for this item"""
228- try:
229- priority = int(self.priority)
230- except ValueError:
231- return BugTaskImportance.UNTRIAGED
232- # make priority >= 9 CRITICAL
233- if priority >= 9:
234- return BugTaskImportance.CRITICAL
235- elif priority >= 7:
236- return BugTaskImportance.HIGH
237- elif priority >= 4:
238- return BugTaskImportance.MEDIUM
239- else:
240- return BugTaskImportance.LOW
241-
242- @property
243- def lp_status(self):
244- if self.status == 'Open':
245- if self.resolution == 'Accepted' or self.assignee != 'nobody':
246- return BugTaskStatus.CONFIRMED
247- else:
248- return BugTaskStatus.NEW
249- elif self.status == 'Closed':
250- if self.resolution in ['Accepted', 'Fixed', 'None']:
251- return BugTaskStatus.FIXRELEASED
252- else:
253- return BugTaskStatus.INVALID
254- elif self.status == 'Deleted':
255- # "Duplicate" bugs are marked deleted. INVALID is the
256- # best fit for this.
257- return BugTaskStatus.INVALID
258- elif self.status == 'Pending':
259- if self.resolution in ['Fixed', 'None']:
260- return BugTaskStatus.FIXCOMMITTED
261- else:
262- return BugTaskStatus.INPROGRESS
263- raise AssertionError('Unhandled item status: (%s, %s)'
264- % (self.status, self.resolution))
265-
266-
267-class Tracker:
268- """An SF tracker"""
269-
270- def __init__(self, dumpfile, dumpdir=None):
271- """Create a Tracker instance.
272-
273- Dumpfile is a dump of the tracker as generated by xml-export.py
274- Dumpdir contains the individual tracker item XML files.
275- """
276- self.data = ET.parse(dumpfile).getroot()
277- if dumpdir is None:
278- self.dumpdir = os.path.join(os.path.dirname(dumpfile),
279- self.data.get('id'))
280- else:
281- self.dumpdir = dumpdir
282-
283- def __iter__(self):
284- """Yield TrackerItem instances for the bugs in this tracker."""
285- for item_node in self.data.findall('item'):
286- # open the summary file
287- item_id = item_node.get('id')
288- summary_file = os.path.join(self.dumpdir, 'item-%s.xml' % item_id)
289- summary_node = ET.parse(summary_file)
290- yield TrackerItem(item_node, summary_node)
291-
292-
293-class TrackerImporter:
294- """Helper class for importing SF tracker items into Launchpad"""
295-
296- def __init__(self, product, verify_users=False):
297- self.product = product
298- self.verify_users = verify_users
299- self._person_id_cache = {}
300- self.bug_importer = getUtility(ILaunchpadCelebrities).bug_importer
301-
302- def get_person(self, sf_userid):
303- """Get the Launchpad user corresponding to the given SF user ID"""
304- if sf_userid in [None, '', 'nobody']:
305- return None
306-
307- email = '%s@users.sourceforge.net' % sf_userid
308-
309- launchpad_id = self._person_id_cache.get(sf_userid)
310- if launchpad_id is not None:
311- person = getUtility(IPersonSet).get(launchpad_id)
312- if person is not None and person.merged is not None:
313- person = None
314- else:
315- person = None
316-
317- if person is None:
318- person = getUtility(IPersonSet).getByEmail(email)
319- if person is None:
320- logger.debug('creating person for %s' % email)
321- person = getUtility(IPersonSet).ensurePerson(
322- email=email, displayname=None,
323- rationale=PersonCreationRationale.BUGIMPORT,
324- comment='when importing bugs for %s from SourceForge.net'
325- % self.product.displayname)
326- self._person_id_cache[sf_userid] = person.id
327-
328- # if we are auto-verifying new accounts, make sure the person
329- # has a preferred email
330- if self.verify_users and person.preferredemail is None:
331- emailaddr = getUtility(IEmailAddressSet).getByEmail(email)
332- assert emailaddr is not None
333- person.setPreferredEmail(emailaddr)
334-
335- return person
336-
337- def getMilestone(self, name):
338- if name in ['None', '', None]:
339- return None
340-
341- # turn milestone into a Launchpad name
342- name = sanitise_name(name)
343-
344- milestone = self.product.getMilestone(name)
345- if milestone is not None:
346- return milestone
347-
348- # pick a series to attach the milestone. Pick 'trunk' or
349- # 'main' if they exist. Otherwise pick the first.
350- # pylint: disable-msg=W0631
351- for series in self.product.series:
352- if series.name in ['trunk', 'main']:
353- break
354- else:
355- series = self.product.series[0]
356-
357- milestone = series.newMilestone(name)
358- Store.of(milestone).flush()
359- return milestone
360-
361- def createMessage(self, subject, date, userid, text):
362- """Create an IMessage for a particular comment."""
363- if not text.strip():
364- text = '<empty comment>'
365- owner = self.get_person(userid)
366- if owner is None:
367- owner = self.bug_importer
368- return getUtility(IMessageSet).fromText(subject, text, owner, date)
369-
370- def importTrackerItem(self, item):
371- """Import an SF tracker item into Launchpad.
372-
373- We identify SF tracker items by setting their nick name to
374- 'sf1234' where the SF item id was 1234. If such a bug already
375- exists, the import is skipped.
376- """
377- logger.info('Handling Sourceforge tracker item #%s', item.item_id)
378-
379- nickname = 'sf%s' % item.item_id
380- try:
381- bug = getUtility(IBugSet).getByNameOrID(nickname)
382- except NotFoundError:
383- bug = None
384-
385- if bug is not None:
386- logger.info('Sourceforge bug %s has already been imported as #%d',
387- item.item_id, bug.id)
388- return bug
389-
390- comments_by_date_and_user = {}
391- comments = list(item.comments)
392-
393- # The first comment is used as the bug description, so we pop
394- # it off the list.
395- date, userid, text = comments.pop(0)
396- # Add a link back to the original SourceForge bug report:
397- text = text + '\n\n[' + item.url + ']'
398- msg = self.createMessage(item.title, date, userid, text)
399- comments_by_date_and_user[(date, userid)] = msg
400-
401- owner = self.get_person(item.reporter)
402- # LP bugs can't have no reporter ...
403- if owner is None:
404- owner = self.bug_importer
405-
406- bug = self.product.createBug(CreateBugParams(
407- msg=msg,
408- datecreated=item.datecreated,
409- title=item.title,
410- owner=owner))
411- bug.name = nickname
412- bugtask = bug.bugtasks[0]
413- logger.info('Creating Launchpad bug #%d', bug.id)
414-
415- # attach comments and create CVE links.
416- bug.findCvesInText(text, bug.owner)
417- for (date, userid, text) in comments:
418- msg = self.createMessage(bug.followup_subject(), date,
419- userid, text)
420- bug.linkMessage(msg)
421- comments_by_date_and_user[(date, userid)] = msg
422-
423- # set up bug task
424- bugtask.datecreated = item.datecreated
425- bugtask.transitionToImportance(item.lp_importance, self.bug_importer)
426- bugtask.transitionToStatus(item.lp_status, self.bug_importer)
427- bugtask.transitionToAssignee(self.get_person(item.assignee))
428-
429- # Convert the category to a tag name
430- if item.category not in ['None', '', None]:
431- bug.tags = [sanitise_name(item.category)]
432-
433- # Convert group to a milestone
434- bugtask.milestone = self.getMilestone(item.group)
435-
436- # Convert attachments
437- for attachment in item.attachments:
438- if attachment.is_patch:
439- attach_type = BugAttachmentType.PATCH
440- else:
441- attach_type = BugAttachmentType.UNSPECIFIED
442-
443- # do we already have the message for this bug?
444- msg = comments_by_date_and_user.get((attachment.date,
445- attachment.sender))
446- if msg is None:
447- msg = self.createMessage(
448- attachment.title,
449- attachment.date or UTC_NOW,
450- attachment.sender,
451- 'Other attachments')
452- bug.linkMessage(msg)
453- comments_by_date_and_user[(attachment.date,
454- attachment.sender)] = msg
455-
456- # upload the attachment and add to the bug.
457- filealias = getUtility(ILibraryFileAliasSet).create(
458- name=attachment.filename,
459- size=len(attachment.data),
460- file=StringIO(attachment.data),
461- contentType=attachment.content_type)
462-
463- getUtility(IBugAttachmentSet).create(
464- bug=bug,
465- filealias=filealias,
466- attach_type=attach_type,
467- title=attachment.title,
468- message=msg)
469-
470- # Make a note of the import in the activity log:
471- getUtility(IBugActivitySet).new(
472- bug=bug.id,
473- datechanged=UTC_NOW,
474- person=self.bug_importer,
475- whatchanged='bug',
476- message='Imported SF tracker item #%s' % item.item_id)
477-
478- return bug
479-
480- def importTracker(self, ztm, tracker):
481- """Import bugs from the given tracker"""
482- for item in tracker:
483- ztm.begin()
484- try:
485- self.importTrackerItem(item)
486- except (SystemExit, KeyboardInterrupt):
487- raise
488- except:
489- logger.exception('Could not import item #%s', item.item_id)
490- ztm.abort()
491- else:
492- ztm.commit()
493
494=== removed file 'lib/canonical/launchpad/scripts/tests/test_sftracker.py'
495--- lib/canonical/launchpad/scripts/tests/test_sftracker.py 2009-06-25 05:30:52 +0000
496+++ lib/canonical/launchpad/scripts/tests/test_sftracker.py 1970-01-01 00:00:00 +0000
497@@ -1,347 +0,0 @@
498-# Copyright 2009 Canonical Ltd. This software is licensed under the
499-# GNU Affero General Public License version 3 (see the file LICENSE).
500-
501-__metaclass__ = type
502-
503-from cStringIO import StringIO
504-import datetime
505-import unittest
506-
507-import pytz
508-import transaction
509-from zope.component import getUtility
510-from canonical.launchpad.interfaces import (
511- BugAttachmentType, BugTaskImportance, BugTaskStatus, IEmailAddressSet,
512- ILaunchpadCelebrities, IPersonSet, IProductSet, PersonCreationRationale)
513-from canonical.launchpad.scripts import sftracker
514-
515-from canonical.testing import LaunchpadZopelessLayer
516-
517-item_data = r"""
518-<item id="1278591">
519- <assigned_to>Thomas Ries</assigned_to>
520- <attachment file_id="147710">
521- <content_disposition>attachment; filename=siproxd.patch</content_disposition>
522- <content_length>1327</content_length>
523- <content_type>application/octet-stream</content_type>
524- <date>2005-09-01 02:35</date>
525- <description>Patch to include Proxy-Authenticate in response</description>
526- <etag>"jpd--1645707516.1327"</etag>
527- <link>/tracker/download.php?group_id=60374&amp;atid=493974&amp;file_id=147710&amp;aid=1278591</link>
528- <sender>nobody</sender>
529- <title>siproxd.patch</title>
530- <data encoding="base64">
531-LS0tIGF1dGguYy5vcmlnCTIwMDUtMDEtMDggMTE6MDU6MTIuMDAwMDAwMDAwICswMTAwCisrKyBh
532-dXRoLmMJMjAwNS0wOS0wMSAxMToyNjowOC4wMDAwMDAwMDAgKzAyMDAKQEAgLTkxLDcgKzkxLDcg
533-QEAKICAqCVNUU19TVUNDRVNTCiAgKglTVFNfRkFJTFVSRQogICovCi1pbnQgYXV0aF9pbmNsdWRl
534-X2F1dGhycShzaXBfdGlja2V0X3QgKnRpY2tldCkgeworaW50IGF1dGhfaW5jbHVkZV9hdXRocnEo
535-b3NpcF9tZXNzYWdlX3QgKnNpcG1zZykgewogICAgb3NpcF9wcm94eV9hdXRoZW50aWNhdGVfdCAq
536-cF9hdXRoOwogICAgY2hhciAqcmVhbG09TlVMTDsKIApAQCAtMTEyLDcgKzExMiw3IEBACg==
537-</data>
538- </attachment>
539- <attachment file_id="42">
540- <content_disposition>attachment; filename=hello.txt</content_disposition>
541- <content_length>12</content_length>
542- <content_type>application/octet-stream; extra crap at end</content_type>
543- <date>2005-10-01 08:14</date>
544- <description>A non-patch attachment</description>
545- <link>/tracker/download.php?group_id=60374&amp;atid=493974&amp;file_id=42&amp;aid=1278591</link>
546- <sender>tries</sender>
547- <title>hello.txt</title>
548- <data encoding="base64">
549-SGVsbG8gV29ybGQK
550-</data>
551- </attachment>
552- <category>General</category>
553- <closed_by>tries</closed_by>
554- <comment>
555- <date>2005-10-01 08:14</date>
556- <description>Date: 2005-10-01 08:14
557-Sender: tries
558-Logged In: YES
559-user_id=438614
560-
561-Thanks, &amp;amp; &amp;amp;quot;
562-I applied the included patch. Will be available in version
563-0.5.12 or use the "daily snapshot" where is is
564-included.
565-
566-/Thomas</description>
567- <sender>tries</sender>
568- <sender_user_id>438614</sender_user_id>
569- </comment><date_closed>2005-10-01 08:14</date_closed>
570- <date_last_updated>2005-10-01 08:14</date_last_updated>
571- <date_submitted>2005-09-01 02:35</date_submitted>
572- <description>When siproxd is used with authentication (eg.
573-proxy_auth_pwfile defined) it does not set
574-'Proxy-Authenticate' header in 407 code response.
575-Looking into code we can see that funtion
576-'auth_include_authrq' is used against 'ticket' whereas
577-we send 'response' back to the client. Modyfying code
578-to use response instead ticket solves the problem (see
579-attached patch) Add a Comment:</description>
580- <group>siproxd-0.5.x</group>
581- <item_id>1278591</item_id>
582- <last_updated_by>tries - Comment added</last_updated_by>
583- <number_of_attachments>1</number_of_attachments>
584- <number_of_comments>1</number_of_comments>
585- <priority>5</priority>
586- <resolution>Fixed</resolution>
587- <status>Closed</status>
588- <submitted_by>Nobody/Anonymous - nobody</submitted_by>
589- <summary>Proxy-Authenticate header not included in response</summary>
590- <title>Proxy-Authenticate header not included in response</title>
591- </item>
592-"""
593-summary_data = r"""
594-<item id="1278591"><assigned_to>tries</assigned_to><description>Proxy-Authenticate header not included in response</description><link>/tracker/index.php?func=detail&amp;aid=1278591&amp;group_id=60374&amp;atid=493974</link><priority>5</priority><status>Closed</status><submitted_by>nobody</submitted_by><timestamp>* 2005-09-01 02:35</timestamp><tracker>493974</tracker></item>
595-"""
596-
597-UTC = pytz.timezone('UTC')
598-
599-
600-class TrackerItemLoaderTestCase(unittest.TestCase):
601-
602- def test_parse_tracker_item(self):
603- item_node = sftracker.ET.parse(StringIO(item_data)).getroot()
604- summary_node = sftracker.ET.parse(StringIO(summary_data)).getroot()
605- item = sftracker.TrackerItem(item_node, summary_node)
606-
607- self.assertEqual(item.url,
608- 'http://sourceforge.net/tracker/index.php?'
609- 'func=detail&aid=1278591&group_id=60374&atid=493974')
610- self.assertEqual(item.item_id, '1278591')
611- self.assertEqual(item.reporter, 'nobody')
612- self.assertEqual(item.assignee, 'tries')
613- self.assertEqual(item.datecreated,
614- datetime.datetime(2005, 9, 1, 9, 35, tzinfo=UTC))
615- self.assertEqual(item.title,
616- 'Proxy-Authenticate header not included in response')
617- self.assertEqual(item.category, 'General')
618- self.assertEqual(item.group, 'siproxd-0.5.x')
619- self.assertEqual(item.priority, '5')
620- self.assertEqual(item.status, 'Closed')
621- self.assertEqual(item.resolution, 'Fixed')
622- self.assertTrue(item.description.startswith(
623- 'When siproxd is used with authentication'))
624-
625- self.assertEqual(len(item.comments), 2)
626- self.assertEqual(item.comments[0][0],
627- datetime.datetime(2005, 9, 1, 9, 35, tzinfo=UTC))
628- self.assertEqual(item.comments[0][1], 'nobody')
629- self.assertTrue(item.comments[0][2].startswith(
630- 'When siproxd is used with authentication'))
631- self.assertEqual(item.comments[1][0],
632- datetime.datetime(2005, 10, 1, 15, 14, tzinfo=UTC))
633- self.assertEqual(item.comments[1][1], 'tries')
634- self.assertTrue(item.comments[1][2].startswith('Thanks, & &quot;'))
635-
636- self.assertEqual(len(item.attachments), 2)
637- self.assertEqual(item.attachments[0].filename, 'siproxd.patch')
638- self.assertEqual(item.attachments[0].title,
639- 'Patch to include Proxy-Authenticate in response')
640- self.assertEqual(item.attachments[0].sender, 'nobody')
641- self.assertEqual(item.attachments[0].date,
642- datetime.datetime(2005, 9, 1, 9, 35, tzinfo=UTC))
643- self.assertEqual(item.attachments[0].is_patch, True)
644- self.assertTrue(item.attachments[0].data.startswith(
645- '--- auth.c.orig\t2005-01-08 11:05:12.000000000 +0100\n'))
646-
647- self.assertEqual(item.attachments[1].filename, 'hello.txt')
648- self.assertEqual(item.attachments[1].is_patch, False)
649- self.assertEqual(item.attachments[1].content_type, 'text/plain')
650- self.assertEqual(item.attachments[1].data, 'Hello World\n')
651-
652- self.assertEqual(item.lp_status, BugTaskStatus.FIXRELEASED)
653- self.assertEqual(item.lp_importance, BugTaskImportance.MEDIUM)
654-
655-
656-class SanitiseNameTestCase(unittest.TestCase):
657-
658- def test_sanitise_name(self):
659- self.assertEqual(sftracker.sanitise_name('foobar'), 'foobar')
660- self.assertEqual(sftracker.sanitise_name('Python 2.4'), 'python-2.4')
661- self.assertEqual(sftracker.sanitise_name('Core (C Code)'),
662- 'core-c-code')
663- self.assertEqual(sftracker.sanitise_name('python-2.4'), 'python-2.4')
664- self.assertEqual(sftracker.sanitise_name('1.0'), '1.0')
665- self.assertEqual(sftracker.sanitise_name('+42'), 'x+42')
666-
667-
668-class PersonMappingTestCase(unittest.TestCase):
669-
670- layer = LaunchpadZopelessLayer
671-
672- def test_create_person(self):
673- # Test that person creation works
674- person = getUtility(IPersonSet).getByEmail('foo@users.sourceforge.net')
675- self.assertEqual(person, None)
676-
677- product = getUtility(IProductSet).getByName('netapplet')
678- importer = sftracker.TrackerImporter(product)
679- person = importer.get_person('foo')
680- # Changes were just made to two different Stores, so commit
681- # to make the changes visible to the subsequent tests.
682- transaction.commit()
683- self.assertNotEqual(person, None)
684- self.assertEqual(person.guessedemails.count(), 1)
685- self.assertEqual(person.guessedemails[0].email,
686- 'foo@users.sourceforge.net')
687- self.assertEqual(person.creation_rationale,
688- PersonCreationRationale.BUGIMPORT)
689- self.assertEqual(person.creation_comment,
690- 'when importing bugs for NetApplet from SourceForge.net')
691-
692- def test_find_existing_person(self):
693- person = getUtility(IPersonSet).getByEmail('foo@users.sourceforge.net')
694- self.assertEqual(person, None)
695- person, email = getUtility(IPersonSet).createPersonAndEmail(
696- email='foo@users.sourceforge.net',
697- rationale=PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)
698- self.assertNotEqual(person, None)
699-
700- product = getUtility(IProductSet).getByName('netapplet')
701- importer = sftracker.TrackerImporter(product)
702- self.assertEqual(importer.get_person('foo'), person)
703-
704- def test_nobody_person(self):
705- # Test that TrackerImporter.get_person() returns None where appropriate
706- product = getUtility(IProductSet).getByName('netapplet')
707- importer = sftracker.TrackerImporter(product)
708- self.assertEqual(importer.get_person(None), None)
709- self.assertEqual(importer.get_person(''), None)
710- self.assertEqual(importer.get_person('nobody'), None)
711-
712- def test_verify_new_person(self):
713- product = getUtility(IProductSet).getByName('netapplet')
714- importer = sftracker.TrackerImporter(product, verify_users=True)
715- person = importer.get_person('foo')
716- self.assertNotEqual(person, None)
717- self.assertNotEqual(person.preferredemail, None)
718- self.assertEqual(person.preferredemail.email,
719- 'foo@users.sourceforge.net')
720- self.assertEqual(person.creation_rationale,
721- PersonCreationRationale.BUGIMPORT)
722- self.assertEqual(person.creation_comment,
723- 'when importing bugs for NetApplet from SourceForge.net')
724-
725- def test_verify_existing_person(self):
726- person = getUtility(IPersonSet).ensurePerson(
727- 'foo@users.sourceforge.net', None,
728- PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)
729- self.assertEqual(person.preferredemail, None)
730-
731- product = getUtility(IProductSet).getByName('netapplet')
732- importer = sftracker.TrackerImporter(product, verify_users=True)
733- person = importer.get_person('foo')
734- self.assertNotEqual(person.preferredemail, None)
735- self.assertEqual(person.preferredemail.email,
736- 'foo@users.sourceforge.net')
737-
738- def test_verify_doesnt_clobber_preferred_email(self):
739- person = getUtility(IPersonSet).ensurePerson(
740- 'foo@users.sourceforge.net', None,
741- PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)
742- transaction.commit()
743- self.failIf(person.account is None, 'Person must have an account.')
744- email = getUtility(IEmailAddressSet).new(
745- 'foo@example.com', person, account=person.account)
746- person.setPreferredEmail(email)
747- transaction.commit()
748- self.assertEqual(person.preferredemail.email, 'foo@example.com')
749-
750- product = getUtility(IProductSet).getByName('netapplet')
751- importer = sftracker.TrackerImporter(product, verify_users=True)
752- person = importer.get_person('foo')
753- transaction.commit()
754- self.assertNotEqual(person.preferredemail, None)
755- self.assertEqual(person.preferredemail.email, 'foo@example.com')
756-
757-
758-class TrackerItemImporterTestCase(unittest.TestCase):
759-
760- layer = LaunchpadZopelessLayer
761-
762- def test_import_item(self):
763- item_node = sftracker.ET.parse(StringIO(item_data)).getroot()
764- summary_node = sftracker.ET.parse(StringIO(summary_data)).getroot()
765- item = sftracker.TrackerItem(item_node, summary_node)
766-
767- # import against some product.
768- product = getUtility(IProductSet).getByName('netapplet')
769- importer = sftracker.TrackerImporter(product)
770- bug = importer.importTrackerItem(item)
771- # Creating a user makes changes to two different Stores, so we have
772- # to commit to make these changes visible (or we have to pull this
773- # information from the correct stores, which is a more tedious fix).
774- transaction.commit()
775- bugtask = bug.bugtasks[0]
776-
777- self.assertEqual(bug.name, 'sf1278591')
778- # bugs submitted anonymously map to the bug importer
779- self.assertEqual(bug.owner,
780- getUtility(ILaunchpadCelebrities).bug_importer)
781- self.assertEqual(bug.title,
782- 'Proxy-Authenticate header not included in response')
783- self.assertEqual(bug.datecreated,
784- datetime.datetime(2005, 9, 1, 9, 35, tzinfo=UTC))
785- self.assertEqual(bug.tags, ['general'])
786-
787- self.assertEqual(bugtask.product, product)
788- self.assertNotEqual(bugtask.assignee, None)
789- self.assertEqual(bugtask.assignee.guessedemails[0].email,
790- 'tries@users.sourceforge.net')
791- self.assertEqual(bugtask.importance, BugTaskImportance.MEDIUM)
792- self.assertEqual(bugtask.status, BugTaskStatus.FIXRELEASED)
793- self.assertNotEqual(bugtask.milestone, None)
794- self.assertEqual(bugtask.milestone.name, 'siproxd-0.5.x')
795-
796- self.assertEqual(bug.messages.count(), 2)
797- comment1, comment2 = bug.messages
798- self.assertEqual(comment1.owner,
799- getUtility(ILaunchpadCelebrities).bug_importer)
800- self.assertEqual(comment1.datecreated,
801- datetime.datetime(2005, 9, 1, 9, 35, tzinfo=UTC))
802- self.assertTrue(comment1.text_contents.startswith(
803- 'When siproxd is used with authentication'))
804-
805- self.assertEqual(comment2.owner.guessedemails[0].email,
806- 'tries@users.sourceforge.net')
807- self.assertEqual(comment2.datecreated,
808- datetime.datetime(2005, 10, 1, 15, 14, tzinfo=UTC))
809- self.assertTrue(comment2.text_contents.startswith('Thanks, & &quot;'))
810-
811- self.assertEqual(comment1.bugattachments.count(), 1)
812- attachment = comment1.bugattachments[0]
813- self.assertEqual(attachment.bug, bug)
814- self.assertEqual(attachment.type, BugAttachmentType.PATCH)
815- self.assertEqual(attachment.title,
816- 'Patch to include Proxy-Authenticate in response')
817- self.assertEqual(attachment.libraryfile.filename, 'siproxd.patch')
818- self.assertEqual(attachment.libraryfile.mimetype, 'text/plain')
819-
820- self.assertEqual(comment2.bugattachments.count(), 1)
821- attachment = comment2.bugattachments[0]
822- self.assertEqual(attachment.bug, bug)
823- self.assertEqual(attachment.type, BugAttachmentType.UNSPECIFIED)
824- self.assertEqual(attachment.libraryfile.filename, 'hello.txt')
825- self.assertEqual(attachment.libraryfile.mimetype, 'text/plain')
826-
827- self.assertEqual(bug.activity.count(), 2)
828-
829- # Activity record for bug creation.
830- self.assertEqual(bug.activity[0].person,
831- getUtility(ILaunchpadCelebrities).bug_importer)
832- self.assertEqual(bug.activity[0].whatchanged, 'bug')
833- self.assertEqual(bug.activity[0].message, 'added bug')
834-
835- # Activity record for importing.
836- self.assertEqual(bug.activity[1].person,
837- getUtility(ILaunchpadCelebrities).bug_importer)
838- self.assertEqual(bug.activity[1].whatchanged, 'bug')
839- self.assertEqual(bug.activity[1].message,
840- 'Imported SF tracker item #1278591')
841-
842-
843-def test_suite():
844- return unittest.TestLoader().loadTestsFromName(__name__)
845
846=== modified file 'lib/canonical/launchpad/templates/bugtask-assignee-widget.pt'
847--- lib/canonical/launchpad/templates/bugtask-assignee-widget.pt 2009-07-17 17:59:07 +0000
848+++ lib/canonical/launchpad/templates/bugtask-assignee-widget.pt 2010-06-04 09:33:28 +0000
849@@ -4,7 +4,7 @@
850 define="widget_name string:${view/name}.option">
851
852 <table>
853- <tr>
854+ <tr tal:condition="view/showUnassignOption">
855 <td style="padding: 0 2px 2px 0">
856 <input type="radio"
857 tal:attributes="name widget_name;
858@@ -56,7 +56,7 @@
859 </tr>
860 </tal:assigned_to_another_user>
861 </tal:assigned>
862- <tr>
863+ <tr tal:condition="view/showPersonChooserWidget">
864 <td style="padding: 2px 2px 0 0;">
865 <input type="radio"
866 tal:attributes="name widget_name;
867
868=== modified file 'lib/canonical/widgets/bugtask.py'
869--- lib/canonical/widgets/bugtask.py 2009-07-29 03:29:24 +0000
870+++ lib/canonical/widgets/bugtask.py 2010-06-04 09:33:28 +0000
871@@ -224,6 +224,29 @@
872 else:
873 return self.assigned_to
874
875+ def showUnassignOption(self):
876+ """Should the "unassign bugtask" option be shown?
877+
878+ To avoid user confusion, we show this option only if the user
879+ can set the bug task assignee to None or if there is currently
880+ no assignee set.
881+ """
882+ user = getUtility(ILaunchBag).user
883+ context = self.context.context
884+ return context.userCanUnassign(user) or context.assignee is None
885+
886+ def showPersonChooserWidget(self):
887+ """Should the person chooser widget bw shown?
888+
889+ The person chooser is shown only if the user can assign at least
890+ one other person or team in addition to himself.
891+ """
892+ user = getUtility(ILaunchBag).user
893+ context = self.context.context
894+ return user is not None and (
895+ context.userCanSetAnyAssignee(user) or
896+ user.teams_participated_in.count() > 0)
897+
898
899 class BugWatchEditForm(Interface):
900 """Form field definition for the bug watch widget.
901
902=== modified file 'lib/lp/bugs/browser/bugtask.py'
903--- lib/lp/bugs/browser/bugtask.py 2010-05-21 15:19:20 +0000
904+++ lib/lp/bugs/browser/bugtask.py 2010-06-04 09:33:28 +0000
905@@ -1177,6 +1177,14 @@
906 return '_'.join(parts)
907
908
909+def get_assignee_vocabulary(context):
910+ """The vocabulary of bug task assignees the current user can set."""
911+ if context.userCanSetAnyAssignee(getUtility(ILaunchBag).user):
912+ return 'ValidAssignee'
913+ else:
914+ return 'AllUserTeamsParticipation'
915+
916+
917 class BugTaskBugWatchMixin:
918 """A mixin to be used where a BugTask view displays BugWatch data."""
919
920@@ -1441,7 +1449,8 @@
921 self.form_fields = self.form_fields.omit('assignee')
922 self.form_fields += formlib.form.Fields(ParticipatingPersonChoice(
923 __name__='assignee', title=_('Assigned to'), required=False,
924- vocabulary='ValidAssignee', readonly=False))
925+ vocabulary=get_assignee_vocabulary(self.context),
926+ readonly=False))
927 self.form_fields['assignee'].custom_widget = CustomWidgetFactory(
928 BugTaskAssigneeWidget)
929
930@@ -3497,11 +3506,21 @@
931
932 def js_config(self):
933 """Configuration for the JS widgets on the row, JSON-serialized."""
934+ assignee_vocabulary = get_assignee_vocabulary(self.context)
935+ # Display the search field only if the user can set any person
936+ # or team
937+ user = getUtility(ILaunchBag).user
938+ hide_assignee_team_selection = (
939+ not self.context.userCanSetAnyAssignee(user) and
940+ (user is None or user.teams_participated_in.count() == 0))
941 return dumps({
942 'row_id': 'tasksummary%s' % self.context.id,
943 'bugtask_path': '/'.join(
944 [''] + canonical_url(self.context).split('/')[3:]),
945 'prefix': get_prefix(self.context),
946+ 'assignee_vocabulary': assignee_vocabulary,
947+ 'hide_assignee_team_selection': hide_assignee_team_selection,
948+ 'user_can_unassign': self.context.userCanUnassign(user),
949 'target_is_product': IProduct.providedBy(self.context.target),
950 'status_widget_items': self.status_widget_items,
951 'status_value': self.context.status.title,
952@@ -3899,4 +3918,3 @@
953 @property
954 def text(self):
955 return self.context.bug.displayname
956-
957
958=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
959--- lib/lp/bugs/browser/tests/test_bugtask.py 2010-04-15 13:26:33 +0000
960+++ lib/lp/bugs/browser/tests/test_bugtask.py 2010-06-04 09:33:28 +0000
961@@ -297,10 +297,40 @@
962 self.getWidgetOptionTitles(view.form_fields['status']))
963
964
965+class TestBugTaskEditViewAssigneeField(TestCaseWithFactory):
966+
967+ layer = LaunchpadFunctionalLayer
968+
969+ def setUp(self):
970+ super(TestBugTaskEditViewAssigneeField, self).setUp()
971+ self.bugtask = self.factory.makeBug().default_bugtask
972+
973+ def test_assignee_field_vocabulary_regular_user(self):
974+ # For regular users, the assignee vocabulary is
975+ # AllUserTeamsParticipation.
976+ login('test@canonical.com')
977+ view = BugTaskEditView(self.bugtask, LaunchpadTestRequest())
978+ view.initialize()
979+ self.assertEqual(
980+ 'AllUserTeamsParticipation',
981+ view.form_fields['assignee'].field.vocabularyName)
982+
983+ def test_assignee_field_vocabulary_privileged_user(self):
984+ # Privileged users, like the bug task target owner, can
985+ # assign anybody.
986+ login_person(self.bugtask.target.owner)
987+ view = BugTaskEditView(self.bugtask, LaunchpadTestRequest())
988+ view.initialize()
989+ self.assertEqual(
990+ 'ValidAssignee',
991+ view.form_fields['assignee'].field.vocabularyName)
992+
993+
994 def test_suite():
995 suite = unittest.TestSuite()
996 suite.addTest(unittest.makeSuite(TestBugTasksAndNominationsView))
997 suite.addTest(unittest.makeSuite(TestBugTaskEditViewStatusField))
998+ suite.addTest(unittest.makeSuite(TestBugTaskEditViewAssigneeField))
999 suite.addTest(DocTestSuite(bugtask))
1000 suite.addTest(LayeredDocFileSuite(
1001 'bugtask-target-link-titles.txt', setUp=setUp, tearDown=tearDown,
1002
1003=== modified file 'lib/lp/bugs/configure.zcml'
1004--- lib/lp/bugs/configure.zcml 2010-05-26 15:24:23 +0000
1005+++ lib/lp/bugs/configure.zcml 2010-06-04 09:33:28 +0000
1006@@ -193,7 +193,9 @@
1007 isSubscribed
1008 getPackageComponent
1009 userCanEditImportance
1010- userCanEditMilestone"/>
1011+ userCanEditMilestone
1012+ userCanSetAnyAssignee
1013+ userCanUnassign"/>
1014 <require
1015 permission="launchpad.View"
1016 attributes="
1017
1018=== modified file 'lib/lp/bugs/doc/bugtask.txt'
1019--- lib/lp/bugs/doc/bugtask.txt 2010-04-01 03:46:44 +0000
1020+++ lib/lp/bugs/doc/bugtask.txt 2010-06-04 09:33:28 +0000
1021@@ -564,6 +564,80 @@
1022 ... devel_focus_alsa_utils_task.date_assigned)
1023 True
1024
1025+Ordinary users can assign themselves and teams they are a member of.
1026+
1027+ >>> login('test@canonical.com')
1028+ >>> devel_focus_alsa_utils_task.transitionToAssignee(None)
1029+ Traceback (most recent call last):
1030+ ...
1031+ UserCannotEditBugTaskAssignee: Regular users can assign and unassign
1032+ only themselves and their teams. Only project onwers, bug supervisors,
1033+ drivers and release managers can assign others.
1034+
1035+ >>> devel_focus_alsa_utils_task.transitionToAssignee(sample_person)
1036+ >>> print generic_alsa_utils_task.assignee.displayname
1037+ Sample Person
1038+ >>> login('no-priv@canonical.com')
1039+ >>> devel_focus_alsa_utils_task.transitionToAssignee(no_priv)
1040+ >>> print devel_focus_alsa_utils_task.assignee.displayname
1041+ No Privileges Person
1042+ >>> devel_focus_alsa_utils_task.transitionToAssignee(None)
1043+ >>> print generic_alsa_utils_task.assignee
1044+ None
1045+
1046+ >>> warty_security_team = getUtility(IPersonSet).getByName('name20')
1047+ >>> no_priv.inTeam(warty_security_team)
1048+ False
1049+ >>> devel_focus_alsa_utils_task.transitionToAssignee(warty_security_team)
1050+ Traceback (most recent call last):
1051+ ...
1052+ UserCannotEditBugTaskAssignee: Regular users can assign and unassign
1053+ only themselves and their teams. Only project onwers, bug supervisors,
1054+ drivers and release managers can assign others.
1055+
1056+ >>> login('test@canonical.com')
1057+ >>> sample_person.inTeam(warty_security_team)
1058+ True
1059+ >>> devel_focus_alsa_utils_task.transitionToAssignee(warty_security_team)
1060+ >>> print devel_focus_alsa_utils_task.assignee.displayname
1061+ Warty Security Team
1062+
1063+These persons can assign anybody: Project owners...
1064+
1065+ >>> from lp.testing import login_person
1066+ >>> login_person(devel_focus_alsa_utils_task.pillar.owner)
1067+ >>> devel_focus_alsa_utils_task.transitionToAssignee(no_priv)
1068+ >>> print generic_alsa_utils_task.assignee.displayname
1069+ No Privileges Person
1070+
1071+...drivers...
1072+
1073+ >>> devel_focus_alsa_utils_task.pillar.driver = sample_person
1074+ >>> login('test@canonical.com')
1075+ >>> devel_focus_alsa_utils_task.transitionToAssignee(None)
1076+ >>> print devel_focus_alsa_utils_task.assignee
1077+ None
1078+ >>> devel_focus_alsa_utils_task.transitionToAssignee(mark)
1079+ >>> print devel_focus_alsa_utils_task.assignee.displayname
1080+ Mark Shuttleworth
1081+
1082+...bug supervisors...
1083+
1084+ >>> login_person(devel_focus_alsa_utils_task.pillar.owner)
1085+ >>> devel_focus_alsa_utils_task.pillar.setBugSupervisor(
1086+ ... sample_person, devel_focus_alsa_utils_task.pillar.owner)
1087+ >>> devel_focus_alsa_utils_task.pillar.driver = None
1088+ >>> login('test@canonical.com')
1089+ >>> devel_focus_alsa_utils_task.transitionToAssignee(no_priv)
1090+ >>> print devel_focus_alsa_utils_task.assignee.displayname
1091+ No Privileges Person
1092+
1093+ ...and Launchpad admins.
1094+
1095+ >>> login('foo.bar@canonical.com')
1096+ >>> devel_focus_alsa_utils_task.transitionToAssignee(sample_person)
1097+
1098+
1099 3. Importance
1100
1101 >>> print generic_netapplet_task.importance.title
1102
1103=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
1104--- lib/lp/bugs/interfaces/bugtask.py 2010-05-18 17:50:25 +0000
1105+++ lib/lp/bugs/interfaces/bugtask.py 2010-06-04 09:33:28 +0000
1106@@ -39,6 +39,7 @@
1107 'IUpstreamProductBugTaskSearch',
1108 'RESOLVED_BUGTASK_STATUSES',
1109 'UNRESOLVED_BUGTASK_STATUSES',
1110+ 'UserCannotEditBugTaskAssignee',
1111 'UserCannotEditBugTaskImportance',
1112 'UserCannotEditBugTaskMilestone',
1113 'UserCannotEditBugTaskStatus',
1114@@ -365,6 +366,15 @@
1115 webservice_error(401) # HTTP Error: 'Unauthorised'
1116
1117
1118+class UserCannotEditBugTaskAssignee(Unauthorized):
1119+ """User not permitted to change bugtask assignees.
1120+
1121+ Raised when a user with insufficient prilieges tries to set
1122+ the assignee of a bug task.
1123+ """
1124+ webservice_error(401) # HTTP Error: 'Unauthorised'
1125+
1126+
1127 class IllegalTarget(Exception):
1128 """Exception raised when trying to set an illegal bug task target."""
1129 webservice_error(400) #Bad request.
1130@@ -661,6 +671,21 @@
1131 See `canTransitionToStatus` for more details.
1132 """
1133
1134+ def userCanSetAnyAssignee(user):
1135+ """Check if the current user can set anybody sa a bugtask assignee.
1136+
1137+ Return True for project owner, project drivers, series drivers,
1138+ bug supervisors and Launchpad admins; return False for other users.
1139+ """
1140+
1141+ def userCanUnassign(user):
1142+ """Check if the current user can set assignee to None.
1143+
1144+ Project owner, project drivers, series drivers, bug supervisors
1145+ and Launchpad admins can do this always; other users can do this
1146+ only if they or their reams are the assignee.
1147+ """
1148+
1149 @mutator_for(assignee)
1150 @operation_parameters(
1151 assignee=copy_field(assignee))
1152
1153=== modified file 'lib/lp/bugs/model/bugtask.py'
1154--- lib/lp/bugs/model/bugtask.py 2010-04-17 11:58:39 +0000
1155+++ lib/lp/bugs/model/bugtask.py 2010-06-04 09:33:28 +0000
1156@@ -51,6 +51,7 @@
1157 from canonical.database.enumcol import EnumCol
1158
1159 from canonical.launchpad.interfaces.lpstorm import IStore
1160+from canonical.launchpad.webapp.interfaces import ILaunchBag
1161
1162 from lp.registry.model.pillar import pillar_sort_key
1163 from canonical.launchpad.helpers import shortlist
1164@@ -64,8 +65,8 @@
1165 IDistroBugTask, IDistroSeriesBugTask, INullBugTask, IProductSeriesBugTask,
1166 IUpstreamBugTask, IllegalRelatedBugTasksParams, IllegalTarget,
1167 RESOLVED_BUGTASK_STATUSES, UNRESOLVED_BUGTASK_STATUSES,
1168- UserCannotEditBugTaskImportance, UserCannotEditBugTaskMilestone,
1169- UserCannotEditBugTaskStatus)
1170+ UserCannotEditBugTaskAssignee, UserCannotEditBugTaskImportance,
1171+ UserCannotEditBugTaskMilestone, UserCannotEditBugTaskStatus)
1172 from lp.bugs.model.bugsubscription import BugSubscription
1173 from lp.registry.interfaces.distribution import (
1174 IDistribution, IDistributionSet)
1175@@ -945,12 +946,54 @@
1176 if new_status < BugTaskStatus.FIXRELEASED:
1177 self.date_fix_released = None
1178
1179+ def userCanSetAnyAssignee(self, user):
1180+ """See `IBugTask`."""
1181+ celebrities = getUtility(ILaunchpadCelebrities)
1182+ return user is not None and (
1183+ user.inTeam(self.pillar.bug_supervisor) or
1184+ user.inTeam(self.pillar.owner) or
1185+ user.inTeam(self.pillar.driver) or
1186+ (self.distroseries is not None and
1187+ user.inTeam(self.distroseries.driver)) or
1188+ (self.productseries is not None and
1189+ user.inTeam(self.productseries.driver)) or
1190+ user.inTeam(celebrities.admin)
1191+ or user == celebrities.bug_importer)
1192+
1193+ def userCanUnassign(self, user):
1194+ """True if user can set the assignee to None.
1195+
1196+ This option not shown for regular users unless they or their teams
1197+ are the assignees. Project owners, drivers, bug supervisors and
1198+ Launchpad admins can always unassign.
1199+ """
1200+ return user is not None and (
1201+ user.inTeam(self.assignee) or self.userCanSetAnyAssignee(user))
1202+
1203+ def canTransitionToAssignee(self, assignee):
1204+ """See `IBugTask`."""
1205+ # All users can assign and unassign themselves and their teams,
1206+ # but only project owners, bug supervisors, project/distribution
1207+ # drivers and Launchpad admins can assign others.
1208+ user = getUtility(ILaunchBag).user
1209+ return (
1210+ user is not None and (
1211+ user.inTeam(assignee) or
1212+ (assignee is None and self.userCanUnassign(user)) or
1213+ self.userCanSetAnyAssignee(user)))
1214+
1215 def transitionToAssignee(self, assignee):
1216 """See `IBugTask`."""
1217 if assignee == self.assignee:
1218 # No change to the assignee, so nothing to do.
1219 return
1220
1221+ if not self.canTransitionToAssignee(assignee):
1222+ raise UserCannotEditBugTaskAssignee(
1223+ 'Regular users can assign and unassign only themselves and '
1224+ 'their teams. Only project onwers, bug supervisors, drivers '
1225+ 'and release managers can assign others.')
1226+
1227 now = datetime.datetime.now(pytz.UTC)
1228 if self.assignee and not assignee:
1229 # The assignee is being cleared, so clear the date_assigned
1230
1231=== modified file 'lib/lp/bugs/scripts/bugzilla.py'
1232--- lib/lp/bugs/scripts/bugzilla.py 2009-10-26 18:40:04 +0000
1233+++ lib/lp/bugs/scripts/bugzilla.py 2010-06-04 09:33:28 +0000
1234@@ -36,7 +36,8 @@
1235 from canonical.launchpad.interfaces.message import IMessageSet
1236 from canonical.launchpad.webapp.interfaces import NotFoundError
1237 from lp.bugs.interfaces.bug import CreateBugParams, IBugSet
1238-from lp.bugs.interfaces.bugattachment import BugAttachmentType, IBugAttachmentSet
1239+from lp.bugs.interfaces.bugattachment import (
1240+ BugAttachmentType, IBugAttachmentSet)
1241 from lp.bugs.interfaces.bugtask import (
1242 BugTaskImportance, BugTaskStatus, IBugTaskSet)
1243 from lp.bugs.interfaces.bugwatch import IBugWatchSet
1244@@ -213,26 +214,30 @@
1245 @property
1246 def ccs(self):
1247 """Return the IDs of people CC'd to this bug"""
1248- if self._ccs is not None: return self._ccs
1249+ if self._ccs is not None:
1250+ return self._ccs
1251 self._ccs = self.backend.getBugCcs(self.bug_id)
1252 return self._ccs
1253
1254 @property
1255 def comments(self):
1256 """Return the comments attached to this bug"""
1257- if self._comments is not None: return self._comments
1258+ if self._comments is not None:
1259+ return self._comments
1260 self._comments = self.backend.getBugComments(self.bug_id)
1261 return self._comments
1262
1263 @property
1264 def attachments(self):
1265 """Return the attachments for this bug"""
1266- if self._attachments is not None: return self._attachments
1267+ if self._attachments is not None:
1268+ return self._attachments
1269 self._attachments = self.backend.getBugAttachments(self.bug_id)
1270 return self._attachments
1271
1272 def mapSeverity(self, bugtask):
1273- """Set a Launchpad bug task's importance based on this bug's severity."""
1274+ """Set a Launchpad bug task's importance based on this bug's severity.
1275+ """
1276 bug_importer = getUtility(ILaunchpadCelebrities).bug_importer
1277 importance_map = {
1278 'blocker': BugTaskImportance.CRITICAL,
1279@@ -289,8 +294,8 @@
1280 bugzilla_status += ', component=%s' % self.component
1281
1282 if bugtask.statusexplanation:
1283- bugtask.statusexplanation = '%s (%s)' % (bugtask.statusexplanation,
1284- bugzilla_status)
1285+ bugtask.statusexplanation = '%s (%s)' % (
1286+ bugtask.statusexplanation, bugzilla_status)
1287 else:
1288 bugtask.statusexplanation = bugzilla_status
1289
1290
1291=== modified file 'lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt'
1292--- lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt 2009-06-12 16:36:02 +0000
1293+++ lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt 2010-06-04 09:33:28 +0000
1294@@ -79,13 +79,96 @@
1295 >>> user_browser.open("http://bugs.launchpad.dev/jokosher/+bug/11")
1296 >>> assignee_control = user_browser.getControl(
1297 ... name="jokosher.assignee.option", index=0)
1298- >>> assignee_control.value = ["jokosher.assignee.assign_to"]
1299- >>> assign_to_control = user_browser.getControl(
1300- ... name="jokosher.assignee", index=0)
1301- >>> assign_to_control.value = "no-priv"
1302+ >>> assignee_control.value = ["jokosher.assignee.assign_to_me"]
1303 >>> user_browser.getControl("Save Changes", index=0).click()
1304 >>> print user_browser.url
1305 http://bugs.launchpad.dev/jokosher/+bug/11
1306 >>> print first_tag_by_class(
1307 ... user_browser.contents, 'warning message')
1308 None
1309+
1310+
1311+== Bug task assignment by regular users ==
1312+
1313+Regular users can only set themselves and their teams as assignees.
1314+To avoid any confusion, the option to assign somebody else is only
1315+shown if the user has sufficient privileges to assign anybody or if
1316+the user is a member of at least one team. no-priv is no a member of
1317+any team and hence does no see the option to asign somebody else.
1318+
1319+ >>> no_priv.teams_participated_in.count()
1320+ 0
1321+ >>> user_browser.open("http://bugs.launchpad.dev/jokosher/+bug/11")
1322+ >>> assignee_control = user_browser.getControl(
1323+ ... name="jokosher.assignee.option", index=0)
1324+ >>> assignee_control.value = ["jokosher.assignee.assign_to"]
1325+ Traceback (most recent call last):
1326+ ...
1327+ ItemNotFoundError: insufficient items with name
1328+ 'jokosher.assignee.assign_to'
1329+ >>> user_browser.getControl(name="jokosher.assignee", index=0)
1330+ Traceback (most recent call last):
1331+ ...
1332+ LookupError: name 'jokosher.assignee'
1333+
1334+Once no_priv is a member of a team, the option is shown.
1335+
1336+ >>> login('no-priv@canonical.com')
1337+ >>> no_privs_team_name = factory.makeTeam(owner=no_priv).name
1338+ >>> logout()
1339+ >>> user_browser.open("http://bugs.launchpad.dev/jokosher/+bug/11")
1340+ >>> assignee_control = user_browser.getControl(
1341+ ... name="jokosher.assignee.option", index=0)
1342+ >>> assignee_control.value = ["jokosher.assignee.assign_to"]
1343+ >>> assign_to_control = user_browser.getControl(
1344+ ... name="jokosher.assignee", index=0)
1345+ >>> assign_to_control.value = no_privs_team_name
1346+ >>> user_browser.getControl("Save Changes", index=0).click()
1347+ >>> print_errors(user_browser.contents)
1348+
1349+But if he treis to set other persons or teams, he gets an error message.
1350+
1351+ >>> user_browser.open("http://bugs.launchpad.dev/jokosher/+bug/11")
1352+ >>> assignee_control = user_browser.getControl(
1353+ ... name="jokosher.assignee.option", index=0)
1354+ >>> assignee_control.value = ["jokosher.assignee.assign_to"]
1355+ >>> assign_to_control = user_browser.getControl(
1356+ ... name="jokosher.assignee", index=0)
1357+ >>> assign_to_control.value = "name12"
1358+ >>> user_browser.getControl("Save Changes", index=0).click()
1359+ >>> print_errors(user_browser.contents)
1360+ There is 1 error in the data you entered. Please fix it and try again.
1361+ (Find&hellip;)
1362+ Constraint not satisfied
1363+
1364+The unassign option is only shown if the current assignee is the user
1365+or one of his teams...
1366+
1367+ >>> user_browser.open("http://bugs.launchpad.dev/jokosher/+bug/11")
1368+ >>> assignee_control = user_browser.getControl(
1369+ ... name="jokosher.assignee.option", index=0)
1370+ >>> assignee_control.value = ["jokosher.assignee.assign_to_nobody"]
1371+ >>> user_browser.getControl("Save Changes", index=0).click()
1372+
1373+...or if there is no assignee.
1374+
1375+ >>> assignee_control = user_browser.getControl(
1376+ ... name="jokosher.assignee.option", index=0)
1377+ >>> assignee_control.value = ["jokosher.assignee.assign_to_nobody"]
1378+
1379+If the bugtask is assigned to anybody else, the unassign option is not
1380+shown.
1381+
1382+ >>> admin_browser.open("http://bugs.launchpad.dev/jokosher/+bug/11")
1383+ >>> assignee_control = admin_browser.getControl(
1384+ ... name="jokosher.assignee.option", index=0)
1385+ >>> assignee_control.value = ["jokosher.assignee.assign_to_me"]
1386+ >>> admin_browser.getControl("Save Changes", index=0).click()
1387+ >>> user_browser.open("http://bugs.launchpad.dev/jokosher/+bug/11")
1388+ >>> assignee_control = user_browser.getControl(
1389+ ... name="jokosher.assignee.option", index=0)
1390+ >>> assignee_control.value = ["jokosher.assignee.assign_to_nobody"]
1391+ Traceback (most recent call last):
1392+ ...
1393+ ItemNotFoundError: insufficient items with name
1394+ 'jokosher.assignee.assign_to_nobody'
1395
1396=== modified file 'lib/lp/bugs/tests/bugs-emailinterface.txt'
1397--- lib/lp/bugs/tests/bugs-emailinterface.txt 2010-04-22 18:53:09 +0000
1398+++ lib/lp/bugs/tests/bugs-emailinterface.txt 2010-06-04 09:33:28 +0000
1399@@ -1643,13 +1643,14 @@
1400 >>> login('kreutzm@itp.uni-hannover.de')
1401
1402 >>> submit_commands(
1403- ... bug_one, 'status confirmed', 'assignee no-priv@canonical.com')
1404+ ... bug_one, 'status confirmed',
1405+ ... 'assignee kreutzm@itp.uni-hannover.de')
1406 >>> for bugtask in bug_one.bugtasks:
1407 ... print '%s: %s, assigned to %s' % (
1408 ... bugtask.bugtargetdisplayname, bugtask.status.title,
1409 ... getattr(bugtask.assignee, 'displayname', 'no one'))
1410 Mozilla Firefox: Confirmed, assigned to Sample Person
1411- mozilla-firefox (Ubuntu): Confirmed, assigned to No Privileges Person
1412+ mozilla-firefox (Ubuntu): Confirmed, assigned to Helge Kreutzmann
1413 mozilla-firefox (Debian): Confirmed, assigned to no one
1414
1415 >>> pending_notifications = BugNotification.select(orderBy='-id', limit=2)
1416@@ -1658,7 +1659,7 @@
1417 ... print bug_notification.message.text_contents
1418 1
1419 ** Changed in: mozilla-firefox (Ubuntu)
1420- Assignee: (unassigned) => No Privileges Person (no-priv)
1421+ Assignee: (unassigned) => Helge Kreutzmann (kreutzm)
1422 1
1423 ** Changed in: mozilla-firefox (Ubuntu)
1424 Status: New => Confirmed
1425@@ -1687,7 +1688,7 @@
1426 ... print bug_notification.message.text_contents
1427 1
1428 ** Changed in: mozilla-firefox (Ubuntu)
1429- Assignee: No Privileges Person (no-priv) => Sample Person (name12)
1430+ Assignee: Helge Kreutzmann (kreutzm) => Sample Person (name12)
1431 1
1432 ** Changed in: mozilla-firefox (Ubuntu)
1433 Status: Confirmed => New
1434@@ -2036,16 +2037,16 @@
1435 None
1436
1437 We send another email, creating a new task (for the package in ubuntu)
1438-and assigning the bug to `warty-gnome`.
1439+and assigning the bug to `landscape-developers`.
1440
1441 >>> submit_commands(ff_bug,
1442- ... 'affects ubuntu/mozilla-firefox', 'assignee warty-gnome')
1443+ ... 'affects ubuntu/mozilla-firefox', 'assignee landscape-developers')
1444
1445 The email was handled correctly - A new bugtask was added and assigned
1446 to the specified team.
1447
1448 >>> print ff_bug.bugtasks[-1].assignee.name
1449- warty-gnome
1450+ landscape-developers
1451
1452
1453 == Recovering from errors ==
1454
1455=== modified file 'lib/lp/bugs/tests/test_bugtask.py'
1456--- lib/lp/bugs/tests/test_bugtask.py 2010-02-02 17:12:29 +0000
1457+++ lib/lp/bugs/tests/test_bugtask.py 2010-06-04 09:33:28 +0000
1458@@ -11,8 +11,8 @@
1459
1460 from lazr.lifecycle.snapshot import Snapshot
1461
1462-from canonical.launchpad.ftests import login
1463 from lp.hardwaredb.interfaces.hwdb import HWBus, IHWDeviceSet
1464+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
1465 from canonical.launchpad.searchbuilder import all, any
1466 from canonical.testing import LaunchpadFunctionalLayer, LaunchpadZopelessLayer
1467
1468@@ -20,7 +20,10 @@
1469 BugTaskImportance, BugTaskSearchParams, BugTaskStatus)
1470 from lp.bugs.model.bugtask import build_tag_search_clause
1471 from lp.registry.interfaces.distribution import IDistributionSet
1472-from lp.testing import TestCase, normalize_whitespace, TestCaseWithFactory
1473+from lp.registry.interfaces.person import IPersonSet
1474+from lp.testing import (
1475+ ANONYMOUS, TestCase, TestCaseWithFactory, login, login_person, logout,
1476+ normalize_whitespace)
1477
1478
1479 class TestBugTaskDelta(TestCaseWithFactory):
1480@@ -498,10 +501,231 @@
1481 [bugtask.bug.id for bugtask in bugtasks])
1482
1483
1484+class TestBugTaskPermissionsToSetAssigneeBase(TestCaseWithFactory):
1485+
1486+ layer = LaunchpadFunctionalLayer
1487+
1488+ def setUp(self):
1489+ """Crate the test setup.
1490+
1491+ We need
1492+ - bug task targets (a product and a product series, or
1493+ a distribution and distoseries, see classes derived from
1494+ this one)
1495+ - persons and team with special roles: product and distribution,
1496+ owners, bug supervisors, drivers
1497+ - bug tasks for the targets
1498+ """
1499+ super(TestBugTaskPermissionsToSetAssigneeBase, self).setUp()
1500+ self.target_owner_member = self.factory.makePerson()
1501+ self.target_owner_team = self.factory.makeTeam(
1502+ owner=self.target_owner_member)
1503+ self.regular_user = self.factory.makePerson()
1504+
1505+ login_person(self.target_owner_member)
1506+ self.makeTarget()
1507+
1508+ self.supervisor_team = self.factory.makeTeam(
1509+ owner=self.target_owner_member)
1510+ self.supervisor_member = self.factory.makePerson()
1511+ self.supervisor_team.addMember(
1512+ self.supervisor_member, self.target_owner_member)
1513+ self.target.setBugSupervisor(
1514+ self.supervisor_team, self.target_owner_member)
1515+
1516+ self.driver_team = self.factory.makeTeam(
1517+ owner=self.target_owner_member)
1518+ self.driver_member = self.factory.makePerson()
1519+ self.driver_team.addMember(
1520+ self.driver_member, self.target_owner_member)
1521+ self.target.driver = self.driver_team
1522+
1523+ self.series_driver_team = self.factory.makeTeam(
1524+ owner=self.target_owner_member)
1525+ self.series_driver_member = self.factory.makePerson()
1526+ self.series_driver_team.addMember(
1527+ self.series_driver_member, self.target_owner_member)
1528+ self.series.driver = self.series_driver_team
1529+
1530+ self.series_bugtask = self.factory.makeBugTask(target=self.series)
1531+ self.series_bugtask.transitionToAssignee(self.regular_user)
1532+ bug = self.series_bugtask.bug
1533+ # If factory.makeBugTask() is called with a series target, it
1534+ # creates automatically another bug task for the main target.
1535+ self.target_bugtask = bug.getBugTask(self.target)
1536+ self.target_bugtask.transitionToAssignee(self.regular_user)
1537+ logout()
1538+
1539+ def test_userCanSetAnyAssignee_anonymous_user(self):
1540+ # Anonymous users cannot set anybody as an assignee.
1541+ login(ANONYMOUS)
1542+ self.assertFalse(self.target_bugtask.userCanSetAnyAssignee(None))
1543+ self.assertFalse(self.series_bugtask.userCanSetAnyAssignee(None))
1544+
1545+ def test_userCanUnassign_anonymous_user(self):
1546+ # Anonymous users cannot unassign anyone.
1547+ login(ANONYMOUS)
1548+ self.assertFalse(self.target_bugtask.userCanUnassign(None))
1549+ self.assertFalse(self.series_bugtask.userCanUnassign(None))
1550+
1551+ def test_userCanSetAnyAssignee_regular_user(self):
1552+ # Ordinary users cannot set others as an assignee.
1553+ login_person(self.regular_user)
1554+ self.assertFalse(
1555+ self.target_bugtask.userCanSetAnyAssignee(self.regular_user))
1556+ self.assertFalse(
1557+ self.series_bugtask.userCanSetAnyAssignee(self.regular_user))
1558+
1559+ def test_userCanUnassign_regular_user(self):
1560+ # Ordinary users can unassign themselves...
1561+ login_person(self.regular_user)
1562+ self.assertEqual(self.target_bugtask.assignee, self.regular_user)
1563+ self.assertEqual(self.series_bugtask.assignee, self.regular_user)
1564+ self.assertTrue(
1565+ self.target_bugtask.userCanUnassign(self.regular_user))
1566+ self.assertTrue(
1567+ self.series_bugtask.userCanUnassign(self.regular_user))
1568+ # ...but not other assignees.
1569+ login_person(self.target_owner_member)
1570+ other_user = self.factory.makePerson()
1571+ self.series_bugtask.transitionToAssignee(other_user)
1572+ self.target_bugtask.transitionToAssignee(other_user)
1573+ login_person(self.regular_user)
1574+ self.assertFalse(
1575+ self.target_bugtask.userCanUnassign(self.regular_user))
1576+ self.assertFalse(
1577+ self.series_bugtask.userCanUnassign(self.regular_user))
1578+
1579+ def test_userCanSetAnyAssignee_target_owner(self):
1580+ # The bug task target owner can assign anybody.
1581+ login_person(self.target_owner_member)
1582+ self.assertTrue(
1583+ self.target_bugtask.userCanSetAnyAssignee(self.target.owner))
1584+ self.assertTrue(
1585+ self.series_bugtask.userCanSetAnyAssignee(self.target.owner))
1586+
1587+ def test_userCanUnassign_target_owner(self):
1588+ # The target owner can unassign anybody.
1589+ login_person(self.target_owner_member)
1590+ self.assertTrue(
1591+ self.target_bugtask.userCanUnassign(self.target_owner_member))
1592+ self.assertTrue(
1593+ self.series_bugtask.userCanUnassign(self.target_owner_member))
1594+
1595+ def test_userCanSetAnyAssignee_bug_supervisor(self):
1596+ # A bug supervisor can assign anybody.
1597+ login_person(self.supervisor_member)
1598+ self.assertTrue(
1599+ self.target_bugtask.userCanSetAnyAssignee(self.supervisor_member))
1600+ self.assertTrue(
1601+ self.series_bugtask.userCanSetAnyAssignee(self.supervisor_member))
1602+
1603+ def test_userCanUnassign_bug_supervisor(self):
1604+ # A bug supervisor can unassign anybody.
1605+ login_person(self.supervisor_member)
1606+ self.assertTrue(
1607+ self.target_bugtask.userCanUnassign(self.supervisor_member))
1608+ self.assertTrue(
1609+ self.series_bugtask.userCanUnassign(self.supervisor_member))
1610+
1611+ def test_userCanSetAnyAssignee_driver(self):
1612+ # A project driver can assign anybody.
1613+ login_person(self.driver_member)
1614+ self.assertTrue(
1615+ self.target_bugtask.userCanSetAnyAssignee(self.driver_member))
1616+ self.assertTrue(
1617+ self.series_bugtask.userCanSetAnyAssignee(self.driver_member))
1618+
1619+ def test_userCanUnassign_driver(self):
1620+ # A project driver can unassign anybody.
1621+ login_person(self.driver_member)
1622+ self.assertTrue(
1623+ self.target_bugtask.userCanUnassign(self.driver_member))
1624+ self.assertTrue(
1625+ self.series_bugtask.userCanUnassign(self.driver_member))
1626+
1627+ def test_userCanSetAnyAssignee_series_driver(self):
1628+ # A series driver can assign anybody to series bug tasks.
1629+ login_person(self.driver_member)
1630+ self.assertTrue(
1631+ self.series_bugtask.userCanSetAnyAssignee(
1632+ self.series_driver_member))
1633+ # But he cannot assign anybody to bug tasks of the main target.
1634+ self.assertFalse(
1635+ self.target_bugtask.userCanSetAnyAssignee(
1636+ self.series_driver_member))
1637+
1638+ def test_userCanUnassign_series_driver(self):
1639+ # The target owner can unassign anybody from series bug tasks...
1640+ login_person(self.series_driver_member)
1641+ self.assertTrue(
1642+ self.series_bugtask.userCanUnassign(self.series_driver_member))
1643+ # ...but not from tasks of the main product/distribution.
1644+ self.assertFalse(
1645+ self.target_bugtask.userCanUnassign(self.series_driver_member))
1646+
1647+ def test_userCanSetAnyAssignee_launchpad_admins(self):
1648+ # Launchpad admins can assign anybody.
1649+ login_person(self.target_owner_member)
1650+ foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@canonical.com')
1651+ login_person(foo_bar)
1652+ self.assertTrue(self.target_bugtask.userCanSetAnyAssignee(foo_bar))
1653+ self.assertTrue(self.series_bugtask.userCanSetAnyAssignee(foo_bar))
1654+
1655+ def test_userCanUnassign_launchpad_admins(self):
1656+ # Launchpad admins can unassign anybody.
1657+ login_person(self.target_owner_member)
1658+ foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@canonical.com')
1659+ login_person(foo_bar)
1660+ self.assertTrue(self.target_bugtask.userCanUnassign(foo_bar))
1661+ self.assertTrue(self.series_bugtask.userCanUnassign(foo_bar))
1662+
1663+ def test_userCanSetAnyAssignee_bug_importer(self):
1664+ # The bug importer celebrity can assign anybody.
1665+ login_person(self.target_owner_member)
1666+ bug_importer = getUtility(ILaunchpadCelebrities).bug_importer
1667+ login_person(bug_importer)
1668+ self.assertTrue(
1669+ self.target_bugtask.userCanSetAnyAssignee(bug_importer))
1670+ self.assertTrue(
1671+ self.series_bugtask.userCanSetAnyAssignee(bug_importer))
1672+
1673+ def test_userCanUnassign_launchpad_bug_importer(self):
1674+ # The bug importer celebrity can unassign anybody.
1675+ login_person(self.target_owner_member)
1676+ bug_importer = getUtility(ILaunchpadCelebrities).bug_importer
1677+ login_person(bug_importer)
1678+ self.assertTrue(self.target_bugtask.userCanUnassign(bug_importer))
1679+ self.assertTrue(self.series_bugtask.userCanUnassign(bug_importer))
1680+
1681+
1682+class TestProductBugTaskPermissionsToSetAssignee(
1683+ TestBugTaskPermissionsToSetAssigneeBase):
1684+
1685+ def makeTarget(self):
1686+ """Create a product and a product series."""
1687+ self.target = self.factory.makeProduct(owner=self.target_owner_team)
1688+ self.series = self.factory.makeProductSeries(self.target)
1689+
1690+
1691+class TestDistributionBugTaskPermissionsToSetAssignee(
1692+ TestBugTaskPermissionsToSetAssigneeBase):
1693+
1694+ def makeTarget(self):
1695+ """Create a distribution and a distroseries."""
1696+ self.target = self.factory.makeDistribution(
1697+ owner=self.target_owner_team)
1698+ self.series = self.factory.makeDistroSeries(self.target)
1699+
1700+
1701 def test_suite():
1702 suite = unittest.TestSuite()
1703 suite.addTest(unittest.makeSuite(TestBugTaskDelta))
1704 suite.addTest(unittest.makeSuite(TestBugTaskTagSearchClauses))
1705 suite.addTest(unittest.makeSuite(TestBugTaskHardwareSearch))
1706+ suite.addTest(unittest.makeSuite(
1707+ TestProductBugTaskPermissionsToSetAssignee))
1708+ suite.addTest(unittest.makeSuite(
1709+ TestDistributionBugTaskPermissionsToSetAssignee))
1710 suite.addTest(DocTestSuite('lp.bugs.model.bugtask'))
1711 return suite
1712
1713=== modified file 'lib/lp/coop/answersbugs/tests/notifications-linked-bug.txt'
1714--- lib/lp/coop/answersbugs/tests/notifications-linked-bug.txt 2009-05-12 08:11:06 +0000
1715+++ lib/lp/coop/answersbugs/tests/notifications-linked-bug.txt 2010-06-04 09:33:28 +0000
1716@@ -49,7 +49,9 @@
1717
1718 Only a change in status triggers a notification.
1719
1720+ >>> from lp.testing import login_person
1721 >>> sample_person = getUtility(IPersonSet).getByEmail('test@canonical.com')
1722+ >>> login_person(sample_person)
1723 >>> original_bugtask = Snapshot(bugtask, providing=providedBy(bugtask))
1724 >>> bugtask.transitionToAssignee(sample_person)
1725 >>> notify(ObjectModifiedEvent(
1726
1727=== modified file 'lib/lp/registry/tests/test_user_vocabularies.py'
1728--- lib/lp/registry/tests/test_user_vocabularies.py 2009-06-25 04:06:00 +0000
1729+++ lib/lp/registry/tests/test_user_vocabularies.py 2010-06-04 09:33:28 +0000
1730@@ -7,12 +7,16 @@
1731
1732 from unittest import TestLoader
1733
1734+from zope.component import getUtility
1735 from zope.schema.vocabulary import getVocabularyRegistry
1736
1737+from canonical.launchpad.ftests import ANONYMOUS, login, login_person
1738+from canonical.launchpad.webapp.interfaces import (
1739+ IStoreSelector, MAIN_STORE, DEFAULT_FLAVOR)
1740+from canonical.testing import LaunchpadFunctionalLayer
1741+from lp.registry.model.person import Person
1742 from lp.registry.interfaces.person import PersonVisibility
1743-from canonical.launchpad.ftests import login, login_person
1744 from lp.testing import TestCaseWithFactory
1745-from canonical.testing import LaunchpadFunctionalLayer
1746
1747
1748 class TestUserTeamsParticipationPlusSelfVocabulary(TestCaseWithFactory):
1749@@ -72,5 +76,68 @@
1750 self.assertEqual([user, alpha, bravo], self._vocabTermValues())
1751
1752
1753+class TestAllUserTeamsParticipationVocabulary(TestCaseWithFactory):
1754+ """AllUserTeamsParticipation contains all teams joined by a user.
1755+
1756+ This includes private teams.
1757+ """
1758+
1759+ layer = LaunchpadFunctionalLayer
1760+
1761+ def _vocabTermValues(self):
1762+ """Return the token values for the vocab."""
1763+ # XXX Abel Deuring 2010-05-21, bug 583502: We cannot simply iterate
1764+ # over the items of AllUserTeamsPariticipationVocabulary as in
1765+ # class TestUserTeamsParticipationPlusSelfVocabulary.
1766+ # So we iterate over all Person records and return all terms
1767+ # returned by vocabulary.searchForTerms(person.name)
1768+ vocabulary_registry = getVocabularyRegistry()
1769+ vocab = vocabulary_registry.get(None, 'AllUserTeamsParticipation')
1770+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
1771+ result = []
1772+ for person in store.find(Person):
1773+ result.extend(
1774+ term.value for term in vocab.searchForTerms(person.name))
1775+ return result
1776+
1777+ def test_user_no_team(self):
1778+ user = self.factory.makePerson()
1779+ login_person(user)
1780+ self.assertEqual([], self._vocabTermValues())
1781+
1782+ def test_user_is_team_owner(self):
1783+ user = self.factory.makePerson()
1784+ login_person(user)
1785+ team = self.factory.makeTeam(owner=user)
1786+ self.assertEqual([team], self._vocabTermValues())
1787+
1788+ def test_user_in_two_teams(self):
1789+ user = self.factory.makePerson()
1790+ login_person(user)
1791+ team1 = self.factory.makeTeam()
1792+ user.join(team1)
1793+ team2 = self.factory.makeTeam()
1794+ user.join(team2)
1795+ self.assertEqual(set([team1, team2]), set(self._vocabTermValues()))
1796+
1797+ def test_user_in_private_teams(self):
1798+ # Private teams are included in the vocabulary.
1799+ user = self.factory.makePerson()
1800+ team_owner = self.factory.makePerson()
1801+ login_person(team_owner)
1802+ team = self.factory.makeTeam(owner=team_owner)
1803+ team.addMember(person=user, reviewer=team_owner)
1804+ # Launchpad admin rights are needed to set private membership.
1805+ login('foo.bar@canonical.com')
1806+ team.visibility = PersonVisibility.PRIVATE
1807+ login_person(user)
1808+ self.assertEqual([team], self._vocabTermValues())
1809+
1810+ def test_teams_of_anonymous(self):
1811+ # AllUserTeamsPariticipationVocabulary is empty for anoymous users.
1812+ login(ANONYMOUS)
1813+ self.assertEqual([], self._vocabTermValues())
1814+
1815+
1816 def test_suite():
1817 return TestLoader().loadTestsFromName(__name__)
1818
1819=== modified file 'lib/lp/registry/vocabularies.py'
1820--- lib/lp/registry/vocabularies.py 2010-05-27 01:46:06 +0000
1821+++ lib/lp/registry/vocabularies.py 2010-06-04 09:33:28 +0000
1822@@ -26,6 +26,7 @@
1823 __all__ = [
1824 'ActiveMailingListVocabulary',
1825 'AdminMergeablePersonVocabulary',
1826+ 'AllUserTeamsParticipationVocabulary',
1827 'CommercialProjectsVocabulary',
1828 'DistributionOrProductOrProjectGroupVocabulary',
1829 'DistributionOrProductVocabulary',
1830@@ -757,6 +758,27 @@
1831 ValidPersonOrTeamVocabulary.__init__(self, context)
1832
1833
1834+class AllUserTeamsParticipationVocabulary(ValidTeamVocabulary):
1835+ """The set of teams where the current user is a member.
1836+
1837+ Other than UserTeamsParticipationVocabulary, this vocabulary includes
1838+ private teams.
1839+ """
1840+
1841+ displayname = 'Select a Team of which you are a member'
1842+
1843+ def __init__(self, context):
1844+ super(AllUserTeamsParticipationVocabulary, self).__init__(context)
1845+ user = getUtility(ILaunchBag).user
1846+ if user is None:
1847+ self.extra_clause = False
1848+ else:
1849+ self.extra_clause = AND(
1850+ super(AllUserTeamsParticipationVocabulary, self).extra_clause,
1851+ TeamParticipation.person == user.id,
1852+ TeamParticipation.team == Person.id)
1853+
1854+
1855 class PersonActiveMembershipVocabulary:
1856 """All the teams the person is an active member of."""
1857
1858
1859=== modified file 'lib/lp/registry/vocabularies.zcml'
1860--- lib/lp/registry/vocabularies.zcml 2010-04-19 08:11:52 +0000
1861+++ lib/lp/registry/vocabularies.zcml 2010-06-04 09:33:28 +0000
1862@@ -256,6 +256,19 @@
1863
1864
1865 <securedutility
1866+ name="AllUserTeamsParticipation"
1867+ component="lp.registry.vocabularies.AllUserTeamsParticipationVocabulary"
1868+ provides="zope.schema.interfaces.IVocabularyFactory"
1869+ >
1870+ <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
1871+ </securedutility>
1872+
1873+ <class class="lp.registry.vocabularies.AllUserTeamsParticipationVocabulary">
1874+ <allow interface="canonical.launchpad.webapp.vocabulary.IHugeVocabulary"/>
1875+ </class>
1876+
1877+
1878+ <securedutility
1879 name="ActiveMailingList"
1880 component="lp.registry.vocabularies.ActiveMailingListVocabulary"
1881 provides="zope.schema.interfaces.IVocabularyFactory"
1882
1883=== removed file 'scripts/sourceforge-import.py'
1884--- scripts/sourceforge-import.py 2010-04-22 17:30:35 +0000
1885+++ scripts/sourceforge-import.py 1970-01-01 00:00:00 +0000
1886@@ -1,64 +0,0 @@
1887-#!/usr/bin/python2.5 -S
1888-#
1889-# Copyright 2009 Canonical Ltd. This software is licensed under the
1890-# GNU Affero General Public License version 3 (see the file LICENSE).
1891-
1892-# pylint: disable-msg=W0403
1893-import _pythonpath
1894-
1895-import logging
1896-import optparse
1897-import sys
1898-
1899-from zope.component import getUtility
1900-from canonical.config import config
1901-from canonical.lp import initZopeless
1902-from canonical.launchpad.interfaces import IProductSet
1903-from canonical.launchpad.scripts import (
1904- execute_zcml_for_scripts, logger_options, logger)
1905-from canonical.launchpad.webapp.interaction import setupInteractionByEmail
1906-
1907-from canonical.launchpad.scripts.sftracker import Tracker, TrackerImporter
1908-
1909-def main(argv):
1910- parser = optparse.OptionParser(description="This script imports bugs "
1911- "from Sourceforge into Launchpad.")
1912-
1913- parser.add_option('--product', metavar='PRODUCT', action='store',
1914- help='The product to associate bugs with',
1915- type='string', dest='product', default=None)
1916- parser.add_option('--dumpfile', metavar='XML', action='store',
1917- help='The XML tracker data dump',
1918- type='string', dest='dumpfile', default=None)
1919- parser.add_option('--dumpdir', metavar='DIR', action='store',
1920- help='The directory with the dumped tracker data',
1921- type='string', dest='dumpdir', default=None)
1922- parser.add_option('--verify-users', dest='verify_users',
1923- help='Should created users have verified emails?',
1924- action='store_true', default=False)
1925-
1926- logger_options(parser, logging.INFO)
1927-
1928- options, args = parser.parse_args(argv[1:])
1929- logger(options, 'canonical.launchpad.scripts.sftracker')
1930-
1931- # don't send email
1932- send_email_data = """
1933- [zopeless]
1934- send_email: False
1935- """
1936- config.push('send_email_data', send_email_data)
1937-
1938- execute_zcml_for_scripts()
1939- ztm = initZopeless()
1940- setupInteractionByEmail('bug-importer@launchpad.net')
1941-
1942- product = getUtility(IProductSet).getByName(options.product)
1943- tracker = Tracker(options.dumpfile, options.dumpdir)
1944- importer = TrackerImporter(product, options.verify_users)
1945-
1946- importer.importTracker(ztm, tracker)
1947- config.pop('send_email_data')
1948-
1949-if __name__ == '__main__':
1950- sys.exit(main(sys.argv))