Merge lp:~adeuring/launchpad/bug-511269 into lp:launchpad
- bug-511269
- Merge into devel
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 |
Related bugs: |
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.
Commit message
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 UserTeamsPartic
public teams, while we should allow users to set private teams as
assignees too, so I wrote a new class AllUserTeamsPar
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.
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
userCanSetAnyAs
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
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 (AllUserTeamsPa
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 BugTaskTableRow
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-
./bin/test -vvt bugtask.txt
./bin/test -vvt test_user_
= 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
lib/canonical
lib/canonical
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
== JSLint notices ==
jslint: No problem found in '/home/
jslint: 1 file to lint.
== Pylint notices ==
lib/canonical/
510: [E1002, AssigneeDisplay
538: [E1002, DBItemDisplayWi
lib/lp/
1204: [E1002, BugTaskEditView
1301: [E1002, BugTaskEditView
lib/lp/
29: [E1002, TestBugTasksAnd
218: [E1002, TestBugTaskEdit
304: [E1002, TestBugTaskEdit
lib/lp/
1238: [C0322, BugTaskSearchPa
search_
^
lib/lp/
32: [E1002, TestBugTaskDelt
475: [E1002, TestBugTaskHard
507: [E1002, TestBugTaskPerm
Graham Binns (gmb) : Posted in a previous version of this proposal | # |
Abel Deuring (adeuring) wrote : | # |
Graham Binns (gmb) : | # |
Preview Diff
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('<', '<') |
131 | - value = value.replace('>', '>') |
132 | - value = value.replace('&', '&') |
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&atid=493974&file_id=147710&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&atid=493974&file_id=42&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;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&aid=1278591&group_id=60374&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, & "')) |
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, & "')) |
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…) |
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)) |
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' launchpad/ scripts/ sftracker. py 2009-10-26 18:40:04 +0000 launchpad/ scripts/ sftracker. py 1970-01-01 00:00:00 +0000 effbot. org/zone/ sandbox- sourceforge. htm cElementTree as ET ElementTree as ET database. constants import UTC_NOW launchpad. interfaces import ( rities, ILibraryFileAli asSet, IMessageSet, IPersonSet, ationale) getLogger( 'canonical. launchpad. scripts. sftracker' ) 'US/Pacific' ) 'UTC') datestr) : datestr, datetime( year, month, day, hour, minute) TZ.localize( dt).astimezone( UTC) name(name) : r'[^a-z0- 9\+\.\- ]+', '-', name.lower())
--- lib/canonical/
+++ lib/canonical/
@@ -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://
-"""
-
-# 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.
-except ImportError:
- try:
- import cElementTree as ET
- except ImportError:
- import elementtree.
-
-from zope.component import getUtility
-from zope.contenttype import guess_content_type
-
-from canonical.
-from canonical.
- BugAttachmentType, BugTaskImportance, BugTaskStatus, CreateBugParams,
- IBugActivitySet, IBugAttachmentSet, IBugSet, IEmailAddressSet,
- ILaunchpadCeleb
- NotFoundError, PersonCreationR
-
-logger = logging.
-
-# when accessed anonymously, Sourceforge returns dates in this time zone:
-SOURCEFORGE_TZ = pytz.timezone(
-UTC = pytz.timezone(
-
-
-def parse_date(
- if datestr in ['', 'No updates since submission']:
- return None
- year, month, day, hour, minute = time.strptime(
- '%Y-%m-%d %H:%M')[:5]
- dt = datetime.
- return SOURCEFORGE_
-
-
-def sanitise_
- """Sanitise a string to pass the valid_name() constraint"""
- name = re.sub(
- if not name[0].isalnum():
- name = 'x' + name
- while name.endswith('-'):
- name = name[:-1]
- return name
...