Merge lp:~adeuring/launchpad/bug-598484 into lp:launchpad/db-devel
- bug-598484
- Merge into db-devel
Proposed by
Abel Deuring
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 9499 | ||||
Proposed branch: | lp:~adeuring/launchpad/bug-598484 | ||||
Merge into: | lp:launchpad/db-devel | ||||
Diff against target: |
1885 lines (+924/-272) 38 files modified
.bzrignore (+1/-0) Makefile (+2/-3) lib/canonical/launchpad/icing/style-3-0.css.in (+14/-0) lib/canonical/launchpad/mailnotification.py (+18/-178) lib/canonical/launchpad/security.py (+8/-1) lib/canonical/launchpad/webapp/launchpadform.py (+6/-1) lib/canonical/widgets/popup.py (+50/-0) lib/canonical/widgets/product.py (+4/-4) lib/lp/app/templates/base-layout-macros.pt (+2/-0) lib/lp/bugs/browser/bug.py (+2/-1) lib/lp/bugs/browser/bugtracker.py (+2/-2) lib/lp/bugs/browser/configure.zcml (+6/-0) lib/lp/bugs/doc/bugnotification-email.txt (+1/-1) lib/lp/bugs/interfaces/bugtarget.py (+9/-3) lib/lp/bugs/interfaces/bugtask.py (+11/-3) lib/lp/bugs/interfaces/bugtracker.py (+3/-2) lib/lp/bugs/javascript/bugtracker_overlay.js (+131/-0) lib/lp/bugs/mail/bugnotificationbuilder.py (+187/-0) lib/lp/bugs/model/bugtarget.py (+2/-1) lib/lp/bugs/model/bugtask.py (+5/-0) lib/lp/bugs/scripts/bugnotification.py (+3/-2) lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt (+8/-0) lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt (+8/-4) lib/lp/bugs/stories/webservice/xx-bug.txt (+10/-0) lib/lp/bugs/tests/test_bugtask.py (+84/-15) lib/lp/registry/javascript/milestoneoverlay.js (+2/-2) lib/lp/registry/windmill/tests/test_add_bugtracker.py (+100/-0) lib/lp/registry/windmill/tests/test_add_milestone.py (+2/-4) lib/lp/soyuz/browser/archive.py (+17/-1) lib/lp/soyuz/browser/tests/test_archive_packages.py (+101/-0) lib/lp/soyuz/doc/archiveauthtoken.txt (+1/-8) lib/lp/soyuz/interfaces/archive.py (+28/-19) lib/lp/soyuz/model/archive.py (+20/-16) lib/lp/soyuz/stories/webservice/xx-archive.txt (+10/-0) lib/lp/soyuz/tests/test_archive.py (+24/-0) lib/lp/soyuz/tests/test_archive_privacy.py (+40/-0) utilities/lp-deps.py (+1/-0) utilities/qa-ready (+1/-1) |
||||
To merge this branch: | bzr merge lp:~adeuring/launchpad/bug-598484 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Abel Deuring (community) | Disapprove | ||
Review via email:
|
Commit message
Description of the change
This branch fixes bug 598484 which I caused in my branch for bug 583385. I simply forgot that IHasBugs was not only implemented by IBugTarget and IPerson, but also by IProjectGroup.
So I added another ZCML directive that defines the currently missing page again for project groups.
test: ./bin/test -t xx-bug-
no lint
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2010-05-27 07:12:50 +0000 |
3 | +++ .bzrignore 2010-06-25 13:39:35 +0000 |
4 | @@ -68,3 +68,4 @@ |
5 | lp.sfood |
6 | apidocs |
7 | twistd.pid |
8 | +lib/canonical/launchpad/apidoc |
9 | |
10 | === modified file 'Makefile' |
11 | --- Makefile 2010-06-15 01:52:28 +0000 |
12 | +++ Makefile 2010-06-25 13:39:35 +0000 |
13 | @@ -61,8 +61,7 @@ |
14 | $(API_INDEX): $(BZR_VERSION_INFO) |
15 | mkdir -p $(APIDOC_DIR).tmp |
16 | LPCONFIG=$(LPCONFIG) $(PY) ./utilities/create-lp-wadl-and-apidoc.py "$(WADL_TEMPLATE)" |
17 | - mv $(APIDOC_DIR).tmp/* $(APIDOC_DIR) |
18 | - rmdir $(APIDOC_DIR).tmp |
19 | + mv $(APIDOC_DIR).tmp $(APIDOC_DIR) |
20 | |
21 | apidoc: compile $(API_INDEX) |
22 | |
23 | @@ -340,7 +339,7 @@ |
24 | $(RM) -r lib/mailman |
25 | $(RM) -rf lib/canonical/launchpad/icing/build/* |
26 | $(RM) -r $(CODEHOSTING_ROOT) |
27 | - $(RM) $(APIDOC_DIR)/wadl*.xml $(APIDOC_DIR)/*.html |
28 | + $(RM) -rf $(APIDOC_DIR) |
29 | $(RM) -rf $(APIDOC_DIR).tmp |
30 | $(RM) $(BZR_VERSION_INFO) |
31 | $(RM) +config-overrides.zcml |
32 | |
33 | === removed directory 'lib/canonical/launchpad/apidoc' |
34 | === modified file 'lib/canonical/launchpad/icing/style-3-0.css.in' |
35 | --- lib/canonical/launchpad/icing/style-3-0.css.in 2010-06-17 00:39:03 +0000 |
36 | +++ lib/canonical/launchpad/icing/style-3-0.css.in 2010-06-25 13:39:35 +0000 |
37 | @@ -126,6 +126,20 @@ |
38 | Universal presentation |
39 | Block elements. |
40 | */ |
41 | +/* XXX EdwinGrubbs 2010-06-18 bug=570354 |
42 | + * The PrettyOverlay css uses static values for the width, but |
43 | + * the overlay needs to stretch for forms with wide input fields. |
44 | + */ |
45 | +.yui-pretty-overlay { |
46 | + width: auto !important; |
47 | + min-width: 402px; |
48 | + } |
49 | + |
50 | +.yui-pretty-overlay #yui-pretty-overlay-modal { |
51 | + width: auto !important; |
52 | + min-width: 340px; |
53 | + } |
54 | + |
55 | html, body { |
56 | font-family: "dejavu sans", "bitstream vera sans", verdana, sans-serif; |
57 | font-size: 93%; |
58 | |
59 | === renamed directory 'lib/lp/bugs/javascript' => 'lib/canonical/launchpad/javascript/bugs' |
60 | === modified file 'lib/canonical/launchpad/mailnotification.py' |
61 | --- lib/canonical/launchpad/mailnotification.py 2010-06-23 21:24:13 +0000 |
62 | +++ lib/canonical/launchpad/mailnotification.py 2010-06-25 13:39:35 +0000 |
63 | @@ -16,157 +16,50 @@ |
64 | from email.MIMEText import MIMEText |
65 | from email.MIMEMultipart import MIMEMultipart |
66 | from email.MIMEMessage import MIMEMessage |
67 | -from email.Utils import formataddr, formatdate, make_msgid |
68 | +from email.Utils import formataddr, make_msgid |
69 | |
70 | import re |
71 | -import rfc822 |
72 | |
73 | from zope.component import getAdapter, getUtility |
74 | -from zope.interface import implements |
75 | |
76 | from canonical.config import config |
77 | from canonical.database.sqlbase import block_implicit_flushes |
78 | -from lp.bugs.adapters.bugdelta import BugDelta |
79 | -from lp.bugs.adapters.bugchange import ( |
80 | - BugDuplicateChange, get_bug_changes, BugTaskAssigneeChange) |
81 | from canonical.launchpad.helpers import ( |
82 | - get_contact_email_addresses, get_email_template, shortlist) |
83 | + get_contact_email_addresses, get_email_template) |
84 | from canonical.launchpad.interfaces import ( |
85 | - IEmailAddressSet, IHeldMessageDetails, ILaunchpadCelebrities, |
86 | - IPerson, IPersonSet, ISpecification, IStructuralSubscriptionTarget, |
87 | - ITeamMembershipSet, IUpstreamBugTask, TeamMembershipStatus) |
88 | -from lp.bugs.interfaces.bugchange import IBugChange |
89 | + IHeldMessageDetails, IPerson, IPersonSet, ISpecification, |
90 | + IStructuralSubscriptionTarget, ITeamMembershipSet, IUpstreamBugTask, |
91 | + TeamMembershipStatus) |
92 | from canonical.launchpad.interfaces.launchpad import ILaunchpadRoot |
93 | from canonical.launchpad.interfaces.message import ( |
94 | IDirectEmailAuthorization, QuotaReachedError) |
95 | -from lp.registry.interfaces.structuralsubscription import ( |
96 | - BugNotificationLevel) |
97 | from canonical.launchpad.mail import ( |
98 | sendmail, simple_sendmail, simple_sendmail_from_person, format_address) |
99 | -from lp.services.mail.mailwrapper import MailWrapper |
100 | from canonical.launchpad.webapp.publisher import canonical_url |
101 | from canonical.launchpad.webapp.url import urlappend |
102 | |
103 | +from lp.bugs.adapters.bugdelta import BugDelta |
104 | +from lp.bugs.adapters.bugchange import ( |
105 | + BugDuplicateChange, get_bug_changes, BugTaskAssigneeChange) |
106 | +from lp.bugs.interfaces.bugchange import IBugChange |
107 | +from lp.bugs.mail.bugnotificationbuilder import get_bugmail_error_address |
108 | +from lp.registry.interfaces.structuralsubscription import ( |
109 | + BugNotificationLevel) |
110 | +from lp.services.mail.mailwrapper import MailWrapper |
111 | + |
112 | # XXX 2010-06-16 gmb bug=594985 |
113 | # This shouldn't be here, but if we take it out lots of things cry, |
114 | # which is sad. |
115 | from lp.services.mail.notificationrecipientset import ( |
116 | NotificationRecipientSet) |
117 | |
118 | +from lp.bugs.mail.bugnotificationbuilder import ( |
119 | + BugNotificationBuilder) |
120 | from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients |
121 | |
122 | CC = "CC" |
123 | |
124 | |
125 | -def format_rfc2822_date(date): |
126 | - """Formats a date according to RFC2822's desires.""" |
127 | - return formatdate(rfc822.mktime_tz(date.utctimetuple() + (0, ))) |
128 | - |
129 | - |
130 | -class BugNotificationBuilder: |
131 | - """Constructs a MIMEText message for a bug notification. |
132 | - |
133 | - Takes a bug and a set of headers and returns a new MIMEText |
134 | - object. Common and expensive to calculate headers are cached |
135 | - up-front. |
136 | - """ |
137 | - |
138 | - def __init__(self, bug): |
139 | - self.bug = bug |
140 | - |
141 | - # Pre-calculate common headers. |
142 | - self.common_headers = [ |
143 | - ('Reply-To', get_bugmail_replyto_address(bug)), |
144 | - ('Sender', config.canonical.bounce_address), |
145 | - ] |
146 | - |
147 | - # X-Launchpad-Bug |
148 | - self.common_headers.extend( |
149 | - ('X-Launchpad-Bug', bugtask.asEmailHeaderValue()) |
150 | - for bugtask in bug.bugtasks) |
151 | - |
152 | - # X-Launchpad-Bug-Tags |
153 | - if len(bug.tags) > 0: |
154 | - self.common_headers.append( |
155 | - ('X-Launchpad-Bug-Tags', ' '.join(bug.tags))) |
156 | - |
157 | - # Add the X-Launchpad-Bug-Private header. This is a simple |
158 | - # yes/no value denoting privacy for the bug. |
159 | - if bug.private: |
160 | - self.common_headers.append( |
161 | - ('X-Launchpad-Bug-Private', 'yes')) |
162 | - else: |
163 | - self.common_headers.append( |
164 | - ('X-Launchpad-Bug-Private', 'no')) |
165 | - |
166 | - # Add the X-Launchpad-Bug-Security-Vulnerability header to |
167 | - # denote security for this bug. This follows the same form as |
168 | - # the -Bug-Private header. |
169 | - if bug.security_related: |
170 | - self.common_headers.append( |
171 | - ('X-Launchpad-Bug-Security-Vulnerability', 'yes')) |
172 | - else: |
173 | - self.common_headers.append( |
174 | - ('X-Launchpad-Bug-Security-Vulnerability', 'no')) |
175 | - |
176 | - # Add the -Bug-Commenters header, a space-separated list of |
177 | - # distinct IDs of people who have commented on the bug. The |
178 | - # list is sorted to aid testing. |
179 | - commenters = set(message.owner.name for message in bug.messages) |
180 | - self.common_headers.append( |
181 | - ('X-Launchpad-Bug-Commenters', ' '.join(sorted(commenters)))) |
182 | - |
183 | - # Add the -Bug-Reporter header to identify the owner of the bug |
184 | - # and the original bug task for filtering |
185 | - self.common_headers.append( |
186 | - ('X-Launchpad-Bug-Reporter', |
187 | - '%s (%s)' % ( bug.owner.displayname, bug.owner.name ))) |
188 | - |
189 | - def build(self, from_address, to_address, body, subject, email_date, |
190 | - rationale=None, references=None, message_id=None): |
191 | - """Construct the notification. |
192 | - |
193 | - :param from_address: The From address of the notification. |
194 | - :param to_address: The To address for the notification. |
195 | - :param body: The body text of the notification. |
196 | - :type body: unicode |
197 | - :param subject: The Subject of the notification. |
198 | - :param email_date: The Date for the notification. |
199 | - :param rationale: The rationale for why the recipient is |
200 | - receiving this notification. |
201 | - :param references: A value for the References header. |
202 | - :param message_id: A value for the Message-ID header. |
203 | - |
204 | - :return: An `email.MIMEText.MIMEText` object. |
205 | - """ |
206 | - message = MIMEText(body.encode('utf8'), 'plain', 'utf8') |
207 | - message['Date'] = format_rfc2822_date(email_date) |
208 | - message['From'] = from_address |
209 | - message['To'] = to_address |
210 | - |
211 | - # Add the common headers. |
212 | - for header in self.common_headers: |
213 | - message.add_header(*header) |
214 | - |
215 | - if references is not None: |
216 | - message['References'] = ' '.join(references) |
217 | - if message_id is not None: |
218 | - message['Message-Id'] = message_id |
219 | - |
220 | - subject_prefix = "[Bug %d]" % self.bug.id |
221 | - if subject is None: |
222 | - message['Subject'] = subject_prefix |
223 | - elif subject_prefix in subject: |
224 | - message['Subject'] = subject |
225 | - else: |
226 | - message['Subject'] = "%s %s" % (subject_prefix, subject) |
227 | - |
228 | - if rationale is not None: |
229 | - message.add_header('X-Launchpad-Message-Rationale', rationale) |
230 | - |
231 | - return message |
232 | - |
233 | - |
234 | def _send_bug_details_to_new_bug_subscribers( |
235 | bug, previous_subscribers, current_subscribers, subscribed_by=None, |
236 | event_creator=None): |
237 | @@ -234,65 +127,12 @@ |
238 | if (bugtask_before_modification.product != |
239 | bugtask_after_modification.product): |
240 | new_product = bugtask_after_modification.product |
241 | - if bugtask_before_modification.bug.security_related and new_product.security_contact: |
242 | + if (bugtask_before_modification.bug.security_related and |
243 | + new_product.security_contact): |
244 | bugtask_after_modification.bug.subscribe( |
245 | new_product.security_contact, IPerson(event.user)) |
246 | |
247 | |
248 | -def get_bugmail_from_address(person, bug): |
249 | - """Returns the right From: address to use for a bug notification.""" |
250 | - if person == getUtility(ILaunchpadCelebrities).janitor: |
251 | - return format_address( |
252 | - 'Launchpad Bug Tracker', |
253 | - "%s@%s" % (bug.id, config.launchpad.bugs_domain)) |
254 | - |
255 | - if person.hide_email_addresses: |
256 | - return format_address( |
257 | - person.displayname, |
258 | - "%s@%s" % (bug.id, config.launchpad.bugs_domain)) |
259 | - |
260 | - if person.preferredemail is not None: |
261 | - return format_address(person.displayname, person.preferredemail.email) |
262 | - |
263 | - # XXX: Bjorn Tillenius 2006-04-05: |
264 | - # The person doesn't have a preferred email set, but he |
265 | - # added a comment (either via the email UI, or because he was |
266 | - # imported as a deaf reporter). It shouldn't be possible to use the |
267 | - # email UI if you don't have a preferred email set, but work around |
268 | - # it for now by trying hard to find the right email address to use. |
269 | - email_addresses = shortlist( |
270 | - getUtility(IEmailAddressSet).getByPerson(person)) |
271 | - if not email_addresses: |
272 | - # XXX: Bjorn Tillenius 2006-05-21 bug=33427: |
273 | - # A user should always have at least one email address, |
274 | - # but due to bug #33427, this isn't always the case. |
275 | - return format_address(person.displayname, |
276 | - "%s@%s" % (bug.id, config.launchpad.bugs_domain)) |
277 | - |
278 | - # At this point we have no validated emails to use: if any of the |
279 | - # person's emails had been validated the preferredemail would be |
280 | - # set. Since we have no idea of which email address is best to use, |
281 | - # we choose the first one. |
282 | - return format_address(person.displayname, email_addresses[0].email) |
283 | - |
284 | - |
285 | -def get_bugmail_replyto_address(bug): |
286 | - """Return an appropriate bugmail Reply-To address. |
287 | - |
288 | - :bug: the IBug. |
289 | - |
290 | - :user: an IPerson whose name will appear in the From address, e.g.: |
291 | - |
292 | - From: Foo Bar via Malone <123@bugs...> |
293 | - """ |
294 | - return u"Bug %d <%s@%s>" % (bug.id, bug.id, config.launchpad.bugs_domain) |
295 | - |
296 | - |
297 | -def get_bugmail_error_address(): |
298 | - """Return a suitable From address for a bug transaction error email.""" |
299 | - return config.malone.bugmail_error_from_address |
300 | - |
301 | - |
302 | def send_process_error_notification(to_address, subject, error_msg, |
303 | original_msg, failing_command=None): |
304 | """Send a mail about an error occurring while using the email interface. |
305 | |
306 | === modified file 'lib/canonical/launchpad/security.py' |
307 | --- lib/canonical/launchpad/security.py 2010-06-16 18:49:47 +0000 |
308 | +++ lib/canonical/launchpad/security.py 2010-06-25 13:39:35 +0000 |
309 | @@ -19,7 +19,7 @@ |
310 | IArchivePermissionSet) |
311 | from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken |
312 | from lp.soyuz.interfaces.archivesubscriber import ( |
313 | - IArchiveSubscriber, IPersonalArchiveSubscription) |
314 | + IArchiveSubscriber, IArchiveSubscriberSet, IPersonalArchiveSubscription) |
315 | from lp.code.interfaces.branch import ( |
316 | IBranch, user_has_special_branch_access) |
317 | from lp.code.interfaces.branchmergeproposal import ( |
318 | @@ -2040,6 +2040,13 @@ |
319 | if self.obj.is_ppa and self.obj.checkArchivePermission(user.person): |
320 | return True |
321 | |
322 | + # Subscribers can view private PPAs. |
323 | + if self.obj.is_ppa and self.obj.private: |
324 | + archive_subs = getUtility(IArchiveSubscriberSet).getBySubscriber( |
325 | + user.person, self.obj).any() |
326 | + if archive_subs: |
327 | + return True |
328 | + |
329 | return False |
330 | |
331 | def checkUnauthenticated(self): |
332 | |
333 | === modified file 'lib/canonical/launchpad/webapp/launchpadform.py' |
334 | --- lib/canonical/launchpad/webapp/launchpadform.py 2010-03-15 16:58:49 +0000 |
335 | +++ lib/canonical/launchpad/webapp/launchpadform.py 2010-06-25 13:39:35 +0000 |
336 | @@ -16,6 +16,7 @@ |
337 | ] |
338 | |
339 | import transaction |
340 | + |
341 | from zope.interface import classImplements, providedBy |
342 | from zope.interface.advice import addClassAdvisor |
343 | from zope.event import notify |
344 | @@ -243,7 +244,7 @@ |
345 | self.errors.append(cleanmsg) |
346 | |
347 | @staticmethod |
348 | - def validate_none(self, action, data): |
349 | + def validate_none(form, action, data): |
350 | """Do not do any validation. |
351 | |
352 | This is to be used in subclasses that have actions in which no |
353 | @@ -473,6 +474,10 @@ |
354 | if referrer is None: |
355 | # "referer" is misspelled in the HTTP specification. |
356 | referrer = self.request.getHeader('referer') |
357 | + # Windmill doesn't pass in a correct referer. |
358 | + if (referrer is not None |
359 | + and '/windmill-serv/remote.html' in referrer): |
360 | + referrer = None |
361 | else: |
362 | attribute_name = self.request.form.get('_return_attribute_name') |
363 | attribute_value = self.request.form.get('_return_attribute_value') |
364 | |
365 | === modified file 'lib/canonical/widgets/popup.py' |
366 | --- lib/canonical/widgets/popup.py 2010-01-29 10:52:58 +0000 |
367 | +++ lib/canonical/widgets/popup.py 2010-06-25 13:39:35 +0000 |
368 | @@ -180,6 +180,56 @@ |
369 | return '/people/' |
370 | |
371 | |
372 | +class BugTrackerPickerWidget(VocabularyPickerWidget): |
373 | + link_template = """ |
374 | + or (<a id="%(activator_id)s" href="/bugs/bugtrackers/+newbugtracker" |
375 | + >Register an external bug tracker…</a>) |
376 | + <script> |
377 | + LPS.use('lp.bugs.bugtracker_overlay', function(Y) { |
378 | + if (Y.UA.ie) { |
379 | + return; |
380 | + } |
381 | + Y.on('domready', function () { |
382 | + // After the success handler finishes, it calls the |
383 | + // next_step function. |
384 | + var next_step = function(bug_tracker) { |
385 | + // Fill in the text field with either the name of |
386 | + // the newly created bug tracker or the name of an |
387 | + // existing bug tracker whose base_url matches. |
388 | + var bugtracker_text_box = Y.one( |
389 | + Y.DOM.byId('field.bugtracker.bugtracker')); |
390 | + if (bugtracker_text_box !== null) { |
391 | + bugtracker_text_box.set( |
392 | + 'value', bug_tracker.get('name')); |
393 | + // It doesn't appear possible to use onChange |
394 | + // event, so the onKeyPress event is explicitely |
395 | + // fired here. |
396 | + if (bugtracker_text_box.get('onkeypress')) { |
397 | + bugtracker_text_box.get('onkeypress')(); |
398 | + } |
399 | + bugtracker_text_box.scrollIntoView(); |
400 | + } |
401 | + } |
402 | + Y.lp.bugs.bugtracker_overlay.attach_widget({ |
403 | + activate_node: Y.get('#%(activator_id)s'), |
404 | + next_step: next_step |
405 | + }); |
406 | + }); |
407 | + }); |
408 | + </script> |
409 | + """ |
410 | + |
411 | + def chooseLink(self): |
412 | + link = super(BugTrackerPickerWidget, self).chooseLink() |
413 | + link += self.link_template % dict( |
414 | + activator_id='create-bugtracker-link') |
415 | + return link |
416 | + |
417 | + @property |
418 | + def nonajax_uri(self): |
419 | + return '/bugs/bugtrackers/' |
420 | + |
421 | + |
422 | class SearchForUpstreamPopupWidget(VocabularyPickerWidget): |
423 | """A SinglePopupWidget with a custom error message. |
424 | |
425 | |
426 | === modified file 'lib/canonical/widgets/product.py' |
427 | --- lib/canonical/widgets/product.py 2010-06-16 16:56:58 +0000 |
428 | +++ lib/canonical/widgets/product.py 2010-06-25 13:39:35 +0000 |
429 | @@ -37,7 +37,7 @@ |
430 | from canonical.launchpad.webapp import canonical_url |
431 | from canonical.widgets.itemswidgets import ( |
432 | CheckBoxMatrixWidget, LaunchpadRadioWidget) |
433 | -from canonical.widgets.popup import VocabularyPickerWidget |
434 | +from canonical.widgets.popup import BugTrackerPickerWidget |
435 | from canonical.widgets.textwidgets import ( |
436 | LowerCaseTextWidget, StrippedTextWidget) |
437 | from lp.registry.interfaces.product import IProduct |
438 | @@ -57,7 +57,7 @@ |
439 | self.bugtracker = Choice( |
440 | vocabulary="WebBugTracker", |
441 | __name__='bugtracker') |
442 | - self.bugtracker_widget = CustomWidgetFactory(VocabularyPickerWidget) |
443 | + self.bugtracker_widget = CustomWidgetFactory(BugTrackerPickerWidget) |
444 | setUpWidget( |
445 | self, 'bugtracker', self.bugtracker, IInputWidget, |
446 | prefix=self.name, value=field.context.bugtracker, |
447 | @@ -82,7 +82,7 @@ |
448 | if self.upstream_email_address_widget.extra is None: |
449 | self.upstream_email_address_widget.extra = '' |
450 | self.upstream_email_address_widget.extra += ( |
451 | - ' onkeypress="selectWidget(\'%s.3\', event);"' % self.name) |
452 | + ''' onkeypress="selectWidget('%s.3', event);"\n''' % self.name) |
453 | |
454 | def _renderItem(self, index, text, value, name, cssClass, checked=False): |
455 | # This form has a custom need to render their labels separately, |
456 | @@ -192,7 +192,7 @@ |
457 | self.upstream_email_address_widget.setRenderedValue( |
458 | value.baseurl.lstrip('mailto:')) |
459 | external_bugtracker_email_text = "%s %s" % ( |
460 | - self._renderLabel("By emailing an upstream bug contact:", 3), |
461 | + self._renderLabel("By emailing an upstream bug contact:\n", 3), |
462 | self.upstream_email_address_widget()) |
463 | external_bugtracker_email_arguments = dict( |
464 | index=3, text=external_bugtracker_email_text, |
465 | |
466 | === modified file 'lib/lp/app/templates/base-layout-macros.pt' |
467 | --- lib/lp/app/templates/base-layout-macros.pt 2010-06-17 19:25:53 +0000 |
468 | +++ lib/lp/app/templates/base-layout-macros.pt 2010-06-25 13:39:35 +0000 |
469 | @@ -181,6 +181,8 @@ |
470 | <script type="text/javascript" |
471 | tal:attributes="src string:${lp_js}/lp/mapping.js"></script> |
472 | <script type="text/javascript" |
473 | + tal:attributes="src string:${lp_js}/bugs/bugtracker_overlay.js"></script> |
474 | + <script type="text/javascript" |
475 | tal:attributes="src string:${lp_js}/registry/milestoneoverlay.js"></script> |
476 | <script type="text/javascript" |
477 | tal:attributes="src string:${lp_js}/registry/milestonetable.js"></script> |
478 | |
479 | === modified file 'lib/lp/bugs/browser/bug.py' |
480 | --- lib/lp/bugs/browser/bug.py 2010-06-07 19:48:29 +0000 |
481 | +++ lib/lp/bugs/browser/bug.py 2010-06-25 13:39:35 +0000 |
482 | @@ -58,9 +58,10 @@ |
483 | from lp.bugs.interfaces.cve import ICveSet |
484 | from lp.bugs.interfaces.bugattachment import IBugAttachmentSet |
485 | from lp.bugs.interfaces.bugnomination import IBugNominationSet |
486 | +from lp.bugs.mail.bugnotificationbuilder import format_rfc2822_date |
487 | |
488 | from canonical.launchpad.mailnotification import ( |
489 | - MailWrapper, format_rfc2822_date) |
490 | + MailWrapper) |
491 | from canonical.launchpad.searchbuilder import any, greater_than |
492 | from canonical.launchpad.webapp import ( |
493 | ContextMenu, LaunchpadEditFormView, LaunchpadFormView, LaunchpadView, |
494 | |
495 | === modified file 'lib/lp/bugs/browser/bugtracker.py' |
496 | --- lib/lp/bugs/browser/bugtracker.py 2009-09-04 08:17:15 +0000 |
497 | +++ lib/lp/bugs/browser/bugtracker.py 2010-06-25 13:39:35 +0000 |
498 | @@ -82,8 +82,8 @@ |
499 | page_title = u"Register an external bug tracker" |
500 | schema = IBugTracker |
501 | label = page_title |
502 | - field_names = ['name', 'bugtrackertype', 'title', 'summary', |
503 | - 'baseurl', 'contactdetails'] |
504 | + field_names = ['bugtrackertype', 'name', 'title', 'baseurl', 'summary', |
505 | + 'contactdetails'] |
506 | |
507 | def setUpWidgets(self, context=None): |
508 | # We only show those bug tracker types for which there can be |
509 | |
510 | === modified file 'lib/lp/bugs/browser/configure.zcml' |
511 | --- lib/lp/bugs/browser/configure.zcml 2010-06-18 10:41:48 +0000 |
512 | +++ lib/lp/bugs/browser/configure.zcml 2010-06-25 13:39:35 +0000 |
513 | @@ -59,6 +59,12 @@ |
514 | name="+bugs-text" |
515 | attribute="__call__"/> |
516 | <browser:page |
517 | + for="lp.registry.interfaces.projectgroup.IProjectGroup" |
518 | + class="lp.bugs.browser.bugtask.TextualBugTaskSearchListingView" |
519 | + permission="zope.Public" |
520 | + name="+bugs-text" |
521 | + attribute="__call__"/> |
522 | + <browser:page |
523 | for="lp.bugs.interfaces.bugtarget.IHasBugs" |
524 | class="lp.bugs.browser.bugtask.BugTaskSearchListingView" |
525 | permission="zope.Public" |
526 | |
527 | === modified file 'lib/lp/bugs/doc/bugnotification-email.txt' |
528 | --- lib/lp/bugs/doc/bugnotification-email.txt 2010-06-23 21:24:13 +0000 |
529 | +++ lib/lp/bugs/doc/bugnotification-email.txt 2010-06-25 13:39:35 +0000 |
530 | @@ -424,7 +424,7 @@ |
531 | The Reply-To: and From: addresses used to send email are generated in a |
532 | pair of handy functions defined in mailnotification.py: |
533 | |
534 | - >>> from canonical.launchpad.mailnotification import ( |
535 | + >>> from lp.bugs.mail.bugnotificationbuilder import ( |
536 | ... get_bugmail_from_address, get_bugmail_replyto_address) |
537 | |
538 | The Reply-To address generation is straightforward: |
539 | |
540 | === modified file 'lib/lp/bugs/interfaces/bugtarget.py' |
541 | --- lib/lp/bugs/interfaces/bugtarget.py 2010-06-18 07:54:36 +0000 |
542 | +++ lib/lp/bugs/interfaces/bugtarget.py 2010-06-25 13:39:35 +0000 |
543 | @@ -21,7 +21,7 @@ |
544 | ] |
545 | |
546 | from zope.interface import Interface, Attribute |
547 | -from zope.schema import Bool, Choice, List, Object, Text, TextLine |
548 | +from zope.schema import Bool, Choice, Datetime, List, Object, Text, TextLine |
549 | |
550 | from canonical.launchpad import _ |
551 | from canonical.launchpad.fields import Tag |
552 | @@ -164,7 +164,13 @@ |
553 | title=( |
554 | u"Search for bugs that are linked to branches or for bugs " |
555 | "that are not linked to branches."), |
556 | - vocabulary=BugBranchSearch, required=False)) |
557 | + vocabulary=BugBranchSearch, required=False), |
558 | + modified_since=Datetime( |
559 | + title=( |
560 | + u"Search for bugs that have been modified since the given " |
561 | + "date."), |
562 | + required=False), |
563 | + ) |
564 | @operation_returns_collection_of(IBugTask) |
565 | @export_read_operation() |
566 | def searchTasks(search_params, user=None, |
567 | @@ -186,7 +192,7 @@ |
568 | hardware_owner_is_affected_by_bug=False, |
569 | hardware_owner_is_subscribed_to_bug=False, |
570 | hardware_is_linked_to_bug=False, linked_branches=None, |
571 | - structural_subscriber=None): |
572 | + structural_subscriber=None, modified_since=None): |
573 | """Search the IBugTasks reported on this entity. |
574 | |
575 | :search_params: a BugTaskSearchParams object |
576 | |
577 | === modified file 'lib/lp/bugs/interfaces/bugtask.py' |
578 | --- lib/lp/bugs/interfaces/bugtask.py 2010-06-08 14:54:22 +0000 |
579 | +++ lib/lp/bugs/interfaces/bugtask.py 2010-06-25 13:39:35 +0000 |
580 | @@ -478,6 +478,12 @@ |
581 | "Confirmed."), |
582 | readonly=True, |
583 | required=False)) |
584 | + date_incomplete = exported( |
585 | + Datetime(title=_("Date Incomplete"), |
586 | + description=_("The date on which this task was marked " |
587 | + "Incomplete."), |
588 | + readonly=True, |
589 | + required=False)) |
590 | date_inprogress = exported( |
591 | Datetime(title=_("Date In Progress"), |
592 | description=_("The date on which this task was marked " |
593 | @@ -1084,8 +1090,8 @@ |
594 | hardware_owner_is_affected_by_bug=False, |
595 | hardware_owner_is_subscribed_to_bug=False, |
596 | hardware_is_linked_to_bug=False, |
597 | - linked_branches=None, structural_subscriber=None |
598 | - ): |
599 | + linked_branches=None, structural_subscriber=None, |
600 | + modified_since=None): |
601 | |
602 | self.bug = bug |
603 | self.searchtext = searchtext |
604 | @@ -1130,6 +1136,7 @@ |
605 | self.hardware_is_linked_to_bug = hardware_is_linked_to_bug |
606 | self.linked_branches = linked_branches |
607 | self.structural_subscriber = structural_subscriber |
608 | + self.modified_since = None |
609 | |
610 | def setProduct(self, product): |
611 | """Set the upstream context on which to filter the search.""" |
612 | @@ -1203,7 +1210,7 @@ |
613 | hardware_owner_is_affected_by_bug=False, |
614 | hardware_owner_is_subscribed_to_bug=False, |
615 | hardware_is_linked_to_bug=False, linked_branches=None, |
616 | - structural_subscriber=None): |
617 | + structural_subscriber=None, modified_since=None): |
618 | """Create and return a new instance using the parameter list.""" |
619 | search_params = cls(user=user, orderby=order_by) |
620 | |
621 | @@ -1272,6 +1279,7 @@ |
622 | hardware_is_linked_to_bug) |
623 | search_params.linked_branches=linked_branches |
624 | search_params.structural_subscriber = structural_subscriber |
625 | + search_params.modified_since = modified_since |
626 | |
627 | return search_params |
628 | |
629 | |
630 | === modified file 'lib/lp/bugs/interfaces/bugtracker.py' |
631 | --- lib/lp/bugs/interfaces/bugtracker.py 2010-04-15 08:45:31 +0000 |
632 | +++ lib/lp/bugs/interfaces/bugtracker.py 2010-06-25 13:39:35 +0000 |
633 | @@ -68,7 +68,8 @@ |
634 | bugtracker = getUtility(IBugTrackerSet).queryByBaseURL(input) |
635 | if bugtracker is not None and bugtracker != self.context: |
636 | raise LaunchpadValidationError( |
637 | - "%s is already registered in Launchpad." % input) |
638 | + '%s is already registered in Launchpad as "%s" (%s).' |
639 | + % (input, bugtracker.title, bugtracker.name)) |
640 | |
641 | |
642 | class BugTrackerType(DBEnumeratedType): |
643 | @@ -183,7 +184,7 @@ |
644 | BugTrackerNameField( |
645 | title=_('Name'), |
646 | constraint=name_validator, |
647 | - description=_('An URL-friendly name for the bug tracker, ' |
648 | + description=_('A URL-friendly name for the bug tracker, ' |
649 | 'such as "mozilla-bugs".'))) |
650 | title = exported( |
651 | TextLine( |
652 | |
653 | === added directory 'lib/lp/bugs/javascript' |
654 | === added file 'lib/lp/bugs/javascript/bugtracker_overlay.js' |
655 | --- lib/lp/bugs/javascript/bugtracker_overlay.js 1970-01-01 00:00:00 +0000 |
656 | +++ lib/lp/bugs/javascript/bugtracker_overlay.js 2010-06-25 13:39:35 +0000 |
657 | @@ -0,0 +1,131 @@ |
658 | +/* Copyright 2010 Canonical Ltd. This software is licensed under the |
659 | + * GNU Affero General Public License version 3 (see the file LICENSE). |
660 | + * |
661 | + * A bugtracker form overlay that can create a bugtracker within any page. |
662 | + * |
663 | + * @namespace Y.lp.bugs.bugtracker_overlay |
664 | + * @requires dom, node, io-base, lazr.anim, lazr.formoverlay |
665 | + */ |
666 | +YUI.add('lp.bugs.bugtracker_overlay', function(Y) { |
667 | + Y.log('loading lp.bugs.bugtracker_overlay'); |
668 | + var namespace = Y.namespace('lp.bugs.bugtracker_overlay'); |
669 | + |
670 | + var bugtracker_form; |
671 | + var next_step; |
672 | + |
673 | + var save_new_bugtracker = function(data) { |
674 | + |
675 | + var parameters = { |
676 | + bug_tracker_type: data['field.bugtrackertype'][0], |
677 | + name: data['field.name'][0].toLowerCase(), |
678 | + title: data['field.title'][0], |
679 | + base_url: data['field.baseurl'][0], |
680 | + summary: data['field.summary'][0], |
681 | + contact_details: data['field.contactdetails'][0] |
682 | + }; |
683 | + |
684 | + var finish_new_bugtracker = function(entry) { |
685 | + bugtracker_form.clearError(); |
686 | + bugtracker_form.hide(); |
687 | + // Reset the HTML form inside the widget. |
688 | + bugtracker_form.get('contentBox').one('form').reset(); |
689 | + next_step(entry); |
690 | + }; |
691 | + |
692 | + var client = new LP.client.Launchpad(); |
693 | + client.named_post('/bugs/bugtrackers', 'ensureBugTracker', { |
694 | + parameters: parameters, |
695 | + on: { |
696 | + success: finish_new_bugtracker, |
697 | + failure: function (ignore, response, args) { |
698 | + var error_box = Y.one('#bugtracker-error'); |
699 | + var error_message = response.statusText + '\n\n' + |
700 | + response.responseText; |
701 | + bugtracker_form.showError(error_message); |
702 | + // XXX EdwinGrubbs 2007-06-18 bug=596025 |
703 | + // This should be done by FormOverlay.showError(). |
704 | + bugtracker_form.error_node.scrollIntoView(); |
705 | + } |
706 | + } |
707 | + }); |
708 | + }; |
709 | + |
710 | + |
711 | + var setup_bugtracker_form = function () { |
712 | + var form_submit_button = Y.Node.create( |
713 | + '<input type="submit" name="field.actions.register" ' + |
714 | + 'id="formoverlay-add-bugtracker" value="Create bug tracker"/>'); |
715 | + bugtracker_form = new Y.lazr.FormOverlay({ |
716 | + headerContent: '<h2>Create Bug Tracker</h2>', |
717 | + form_submit_button: form_submit_button, |
718 | + centered: true, |
719 | + form_submit_callback: save_new_bugtracker, |
720 | + visible: false |
721 | + }); |
722 | + bugtracker_form.loadFormContentAndRender( |
723 | + '/bugs/bugtrackers/+newbugtracker/++form++'); |
724 | + // XXX EdwinGrubbs 2010-06-18 bug=596130 |
725 | + // render() and show() will actually be called before the |
726 | + // asynchronous io call finishes, so the widget appears first |
727 | + // without any content. However, this is better than loading the |
728 | + // form every time the page loads despite the form overlay being |
729 | + // used rarely. |
730 | + bugtracker_form.render(); |
731 | + bugtracker_form.show(); |
732 | + }; |
733 | + |
734 | + var show_bugtracker_form = function(e) { |
735 | + e.preventDefault(); |
736 | + if (bugtracker_form) { |
737 | + bugtracker_form.show(); |
738 | + } else { |
739 | + // This function call is asynchronous, so we can move |
740 | + // bugtracker_form.show() below it. |
741 | + setup_bugtracker_form(); |
742 | + } |
743 | + |
744 | + // XXX EdwinGrubbs 2010-06-18 bug=596113 |
745 | + // FormOverlay calls centered(), which can cause this tall form |
746 | + // to be position where the top of the form is no longer |
747 | + // accessible. |
748 | + var bounding_box = bugtracker_form.get('boundingBox'); |
749 | + var min_top = 10; |
750 | + if (bounding_box.get('offsetTop') < min_top) { |
751 | + bounding_box.setStyle('top', min_top + 'px'); |
752 | + } |
753 | + }; |
754 | + |
755 | + /** |
756 | + * Attaches a bugtracker form overlay widget to an element. |
757 | + * |
758 | + * @method attach_widget |
759 | + * @param {Object} config Object literal of config name/value pairs. |
760 | + * activate_node is the node that shows the form |
761 | + * when it is clicked. |
762 | + * next_step is the function to be called after |
763 | + * the bugtracker is created. |
764 | + */ |
765 | + namespace.attach_widget = function(config) { |
766 | + Y.log('lp.bugs.bugtracker_overlay.attach_widget()'); |
767 | + if (Y.UA.ie) { |
768 | + return; |
769 | + } |
770 | + if (config === undefined) { |
771 | + throw new Error( |
772 | + "Missing attach_widget config for bugtracker_overlay."); |
773 | + } |
774 | + if (config.activate_node === undefined || |
775 | + config.next_step === undefined) { |
776 | + throw new Error( |
777 | + "attach_widget config for bugtracker_overlay has " + |
778 | + "undefined properties."); |
779 | + } |
780 | + next_step = config.next_step; |
781 | + Y.log('lp.bugs.bugtracker_overlay.attach_widget() setup onclick'); |
782 | + config.activate_node.addClass('js-action'); |
783 | + config.activate_node.on('click', show_bugtracker_form); |
784 | + }; |
785 | + |
786 | +}, "0.1", {"requires": [ |
787 | + "dom", "node", "io-base", "lazr.anim", "lazr.formoverlay", "lp.calendar" |
788 | + ]}); |
789 | |
790 | === added directory 'lib/lp/bugs/javascript/tests' |
791 | === added file 'lib/lp/bugs/mail/bugnotificationbuilder.py' |
792 | --- lib/lp/bugs/mail/bugnotificationbuilder.py 1970-01-01 00:00:00 +0000 |
793 | +++ lib/lp/bugs/mail/bugnotificationbuilder.py 2010-06-25 13:39:35 +0000 |
794 | @@ -0,0 +1,187 @@ |
795 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
796 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
797 | + |
798 | +"""Bug notification building code.""" |
799 | + |
800 | +__metaclass__ = type |
801 | +__all__ = [ |
802 | + 'BugNotificationBuilder', |
803 | + 'format_rfc2822_date', |
804 | + 'get_bugmail_error_address', |
805 | + 'get_bugmail_from_address', |
806 | + ] |
807 | + |
808 | +import rfc822 |
809 | +from email.MIMEText import MIMEText |
810 | +from email.Utils import formatdate |
811 | + |
812 | +from zope.component import getUtility |
813 | + |
814 | +from canonical.config import config |
815 | +from canonical.launchpad.helpers import shortlist |
816 | +from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet |
817 | +from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
818 | +from canonical.launchpad.mail import format_address |
819 | + |
820 | + |
821 | +def format_rfc2822_date(date): |
822 | + """Formats a date according to RFC2822's desires.""" |
823 | + return formatdate(rfc822.mktime_tz(date.utctimetuple() + (0, ))) |
824 | + |
825 | + |
826 | +def get_bugmail_from_address(person, bug): |
827 | + """Returns the right From: address to use for a bug notification.""" |
828 | + if person == getUtility(ILaunchpadCelebrities).janitor: |
829 | + return format_address( |
830 | + 'Launchpad Bug Tracker', |
831 | + "%s@%s" % (bug.id, config.launchpad.bugs_domain)) |
832 | + |
833 | + if person.hide_email_addresses: |
834 | + return format_address( |
835 | + person.displayname, |
836 | + "%s@%s" % (bug.id, config.launchpad.bugs_domain)) |
837 | + |
838 | + if person.preferredemail is not None: |
839 | + return format_address(person.displayname, person.preferredemail.email) |
840 | + |
841 | + # XXX: Bjorn Tillenius 2006-04-05: |
842 | + # The person doesn't have a preferred email set, but he |
843 | + # added a comment (either via the email UI, or because he was |
844 | + # imported as a deaf reporter). It shouldn't be possible to use the |
845 | + # email UI if you don't have a preferred email set, but work around |
846 | + # it for now by trying hard to find the right email address to use. |
847 | + email_addresses = shortlist( |
848 | + getUtility(IEmailAddressSet).getByPerson(person)) |
849 | + if not email_addresses: |
850 | + # XXX: Bjorn Tillenius 2006-05-21 bug=33427: |
851 | + # A user should always have at least one email address, |
852 | + # but due to bug #33427, this isn't always the case. |
853 | + return format_address(person.displayname, |
854 | + "%s@%s" % (bug.id, config.launchpad.bugs_domain)) |
855 | + |
856 | + # At this point we have no validated emails to use: if any of the |
857 | + # person's emails had been validated the preferredemail would be |
858 | + # set. Since we have no idea of which email address is best to use, |
859 | + # we choose the first one. |
860 | + return format_address(person.displayname, email_addresses[0].email) |
861 | + |
862 | + |
863 | +def get_bugmail_replyto_address(bug): |
864 | + """Return an appropriate bugmail Reply-To address. |
865 | + |
866 | + :bug: the IBug. |
867 | + |
868 | + :user: an IPerson whose name will appear in the From address, e.g.: |
869 | + |
870 | + From: Foo Bar via Malone <123@bugs...> |
871 | + """ |
872 | + return u"Bug %d <%s@%s>" % (bug.id, bug.id, config.launchpad.bugs_domain) |
873 | + |
874 | + |
875 | +def get_bugmail_error_address(): |
876 | + """Return a suitable From address for a bug transaction error email.""" |
877 | + return config.malone.bugmail_error_from_address |
878 | + |
879 | + |
880 | +class BugNotificationBuilder: |
881 | + """Constructs a MIMEText message for a bug notification. |
882 | + |
883 | + Takes a bug and a set of headers and returns a new MIMEText |
884 | + object. Common and expensive to calculate headers are cached |
885 | + up-front. |
886 | + """ |
887 | + |
888 | + def __init__(self, bug): |
889 | + self.bug = bug |
890 | + |
891 | + # Pre-calculate common headers. |
892 | + self.common_headers = [ |
893 | + ('Reply-To', get_bugmail_replyto_address(bug)), |
894 | + ('Sender', config.canonical.bounce_address), |
895 | + ] |
896 | + |
897 | + # X-Launchpad-Bug |
898 | + self.common_headers.extend( |
899 | + ('X-Launchpad-Bug', bugtask.asEmailHeaderValue()) |
900 | + for bugtask in bug.bugtasks) |
901 | + |
902 | + # X-Launchpad-Bug-Tags |
903 | + if len(bug.tags) > 0: |
904 | + self.common_headers.append( |
905 | + ('X-Launchpad-Bug-Tags', ' '.join(bug.tags))) |
906 | + |
907 | + # Add the X-Launchpad-Bug-Private header. This is a simple |
908 | + # yes/no value denoting privacy for the bug. |
909 | + if bug.private: |
910 | + self.common_headers.append( |
911 | + ('X-Launchpad-Bug-Private', 'yes')) |
912 | + else: |
913 | + self.common_headers.append( |
914 | + ('X-Launchpad-Bug-Private', 'no')) |
915 | + |
916 | + # Add the X-Launchpad-Bug-Security-Vulnerability header to |
917 | + # denote security for this bug. This follows the same form as |
918 | + # the -Bug-Private header. |
919 | + if bug.security_related: |
920 | + self.common_headers.append( |
921 | + ('X-Launchpad-Bug-Security-Vulnerability', 'yes')) |
922 | + else: |
923 | + self.common_headers.append( |
924 | + ('X-Launchpad-Bug-Security-Vulnerability', 'no')) |
925 | + |
926 | + # Add the -Bug-Commenters header, a space-separated list of |
927 | + # distinct IDs of people who have commented on the bug. The |
928 | + # list is sorted to aid testing. |
929 | + commenters = set(message.owner.name for message in bug.messages) |
930 | + self.common_headers.append( |
931 | + ('X-Launchpad-Bug-Commenters', ' '.join(sorted(commenters)))) |
932 | + |
933 | + # Add the -Bug-Reporter header to identify the owner of the bug |
934 | + # and the original bug task for filtering |
935 | + self.common_headers.append( |
936 | + ('X-Launchpad-Bug-Reporter', |
937 | + '%s (%s)' % ( bug.owner.displayname, bug.owner.name ))) |
938 | + |
939 | + def build(self, from_address, to_address, body, subject, email_date, |
940 | + rationale=None, references=None, message_id=None): |
941 | + """Construct the notification. |
942 | + |
943 | + :param from_address: The From address of the notification. |
944 | + :param to_address: The To address for the notification. |
945 | + :param body: The body text of the notification. |
946 | + :type body: unicode |
947 | + :param subject: The Subject of the notification. |
948 | + :param email_date: The Date for the notification. |
949 | + :param rationale: The rationale for why the recipient is |
950 | + receiving this notification. |
951 | + :param references: A value for the References header. |
952 | + :param message_id: A value for the Message-ID header. |
953 | + |
954 | + :return: An `email.MIMEText.MIMEText` object. |
955 | + """ |
956 | + message = MIMEText(body.encode('utf8'), 'plain', 'utf8') |
957 | + message['Date'] = format_rfc2822_date(email_date) |
958 | + message['From'] = from_address |
959 | + message['To'] = to_address |
960 | + |
961 | + # Add the common headers. |
962 | + for header in self.common_headers: |
963 | + message.add_header(*header) |
964 | + |
965 | + if references is not None: |
966 | + message['References'] = ' '.join(references) |
967 | + if message_id is not None: |
968 | + message['Message-Id'] = message_id |
969 | + |
970 | + subject_prefix = "[Bug %d]" % self.bug.id |
971 | + if subject is None: |
972 | + message['Subject'] = subject_prefix |
973 | + elif subject_prefix in subject: |
974 | + message['Subject'] = subject |
975 | + else: |
976 | + message['Subject'] = "%s %s" % (subject_prefix, subject) |
977 | + |
978 | + if rationale is not None: |
979 | + message.add_header('X-Launchpad-Message-Rationale', rationale) |
980 | + |
981 | + return message |
982 | |
983 | === modified file 'lib/lp/bugs/model/bugtarget.py' |
984 | --- lib/lp/bugs/model/bugtarget.py 2010-06-11 09:41:07 +0000 |
985 | +++ lib/lp/bugs/model/bugtarget.py 2010-06-25 13:39:35 +0000 |
986 | @@ -65,7 +65,8 @@ |
987 | hardware_owner_is_bug_reporter=None, |
988 | hardware_owner_is_affected_by_bug=False, |
989 | hardware_owner_is_subscribed_to_bug=False, |
990 | - hardware_is_linked_to_bug=False, linked_branches=None): |
991 | + hardware_is_linked_to_bug=False, linked_branches=None, |
992 | + modified_since=None): |
993 | """See `IHasBugs`.""" |
994 | if status is None: |
995 | # If no statuses are supplied, default to the |
996 | |
997 | === modified file 'lib/lp/bugs/model/bugtask.py' |
998 | --- lib/lp/bugs/model/bugtask.py 2010-06-23 22:39:15 +0000 |
999 | +++ lib/lp/bugs/model/bugtask.py 2010-06-25 13:39:35 +0000 |
1000 | @@ -1833,6 +1833,11 @@ |
1001 | # we don't need to add any clause. |
1002 | pass |
1003 | |
1004 | + if params.modified_since: |
1005 | + extra_clauses.append( |
1006 | + "Bug.date_last_updated > %s" % ( |
1007 | + sqlvalues(params.modified_since,))) |
1008 | + |
1009 | orderby_arg = self._processOrderBy(params) |
1010 | |
1011 | query = " AND ".join(extra_clauses) |
1012 | |
1013 | === modified file 'lib/lp/bugs/scripts/bugnotification.py' |
1014 | --- lib/lp/bugs/scripts/bugnotification.py 2009-11-17 17:33:28 +0000 |
1015 | +++ lib/lp/bugs/scripts/bugnotification.py 2010-06-25 13:39:35 +0000 |
1016 | @@ -16,8 +16,9 @@ |
1017 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
1018 | from lp.registry.interfaces.person import IPersonSet |
1019 | from canonical.launchpad.mailnotification import ( |
1020 | - generate_bug_add_email, MailWrapper, BugNotificationBuilder, |
1021 | - get_bugmail_from_address) |
1022 | + generate_bug_add_email, MailWrapper) |
1023 | +from lp.bugs.mail.bugnotificationbuilder import ( |
1024 | + BugNotificationBuilder, get_bugmail_from_address) |
1025 | from canonical.launchpad.scripts.logger import log |
1026 | from canonical.launchpad.webapp import canonical_url |
1027 | |
1028 | |
1029 | === modified file 'lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt' |
1030 | --- lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt 2010-06-03 21:49:47 +0000 |
1031 | +++ lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt 2010-06-25 13:39:35 +0000 |
1032 | @@ -281,6 +281,14 @@ |
1033 | >>> print anon_browser.contents |
1034 | 10 |
1035 | |
1036 | +This page is also available for project groups. |
1037 | + |
1038 | + >>> anon_browser.open('http://launchpad.dev/mozilla/+bugs-text') |
1039 | + >>> print anon_browser.contents |
1040 | + 15 |
1041 | + 5 |
1042 | + 4 |
1043 | + |
1044 | |
1045 | == Private bugs == |
1046 | |
1047 | |
1048 | === modified file 'lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt' |
1049 | --- lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt 2010-06-16 15:56:08 +0000 |
1050 | +++ lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt 2010-06-25 13:39:35 +0000 |
1051 | @@ -72,7 +72,8 @@ |
1052 | >>> for message in find_tags_by_class(user_browser.contents, 'message'): |
1053 | ... print extract_text(message) |
1054 | There is 1 error. |
1055 | - http://bugzilla.mozilla.org/ is already registered in Launchpad. |
1056 | + http://bugzilla.mozilla.org/ is already registered in Launchpad |
1057 | + as "The Mozilla.org Bug Tracker" (mozilla.org). |
1058 | |
1059 | The same happens if the requested URL is aliased to another bug |
1060 | tracker. Aliases can be edited once a bug tracker has been added, but |
1061 | @@ -94,7 +95,8 @@ |
1062 | >>> for message in find_tags_by_class(user_browser.contents, 'message'): |
1063 | ... print extract_text(message) |
1064 | There is 1 error. |
1065 | - http://alias.example.com/ is already registered in Launchpad. |
1066 | + http://alias.example.com/ is already registered in Launchpad |
1067 | + as "GnomeGBug GTracker" (gnome-bugzilla). |
1068 | |
1069 | After successfully registering the bug tracker, the user is redirected |
1070 | to the bug tracker page. |
1071 | @@ -201,7 +203,8 @@ |
1072 | >>> for message in get_feedback_messages(user_browser.contents): |
1073 | ... print message |
1074 | There is 1 error. |
1075 | - http://bugzilla.mozilla.org/ is already registered in Launchpad. |
1076 | + http://bugzilla.mozilla.org/ is already registered in Launchpad |
1077 | + as "The Mozilla.org Bug Tracker" (mozilla.org). |
1078 | |
1079 | If the user inadvertently enters an invalid URL, they are shown an |
1080 | informative error message explaining why it is invalid. |
1081 | @@ -304,7 +307,8 @@ |
1082 | >>> for message in get_feedback_messages(user_browser.contents): |
1083 | ... print message |
1084 | There is 1 error. |
1085 | - http://bugzilla.mozilla.org/ is already registered in Launchpad. |
1086 | + http://bugzilla.mozilla.org/ is already registered in Launchpad |
1087 | + as "The Mozilla.org Bug Tracker" (mozilla.org). |
1088 | |
1089 | Multiple aliases can be entered by separating URLs with whitespace. |
1090 | |
1091 | |
1092 | === modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt' |
1093 | --- lib/lp/bugs/stories/webservice/xx-bug.txt 2010-06-10 18:55:22 +0000 |
1094 | +++ lib/lp/bugs/stories/webservice/xx-bug.txt 2010-06-25 13:39:35 +0000 |
1095 | @@ -327,6 +327,7 @@ |
1096 | date_fix_committed: None |
1097 | date_fix_released: None |
1098 | date_in_progress: None |
1099 | + date_incomplete: None |
1100 | date_left_closed: None |
1101 | date_left_new: None |
1102 | date_triaged: None |
1103 | @@ -1436,6 +1437,15 @@ |
1104 | total_size: 0 |
1105 | --- |
1106 | |
1107 | +It can also be used to find bugs modified since a certain date. |
1108 | + |
1109 | + >>> pprint_collection(webservice.named_get( |
1110 | + ... '/ubuntu', 'searchTasks', |
1111 | + ... modified_since=u'2011-01-01T00:00:00+00:00').jsonBody()) |
1112 | + start: None |
1113 | + total_size: 0 |
1114 | + --- |
1115 | + |
1116 | It is possible to search for bugs targeted to a milestone within a |
1117 | project group. |
1118 | |
1119 | |
1120 | === modified file 'lib/lp/bugs/tests/test_bugtask.py' |
1121 | --- lib/lp/bugs/tests/test_bugtask.py 2010-06-21 18:09:47 +0000 |
1122 | +++ lib/lp/bugs/tests/test_bugtask.py 2010-06-25 13:39:35 +0000 |
1123 | @@ -3,6 +3,7 @@ |
1124 | |
1125 | __metaclass__ = type |
1126 | |
1127 | +from datetime import timedelta |
1128 | import unittest |
1129 | |
1130 | from zope.component import getUtility |
1131 | @@ -14,8 +15,10 @@ |
1132 | from lp.hardwaredb.interfaces.hwdb import HWBus, IHWDeviceSet |
1133 | from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities |
1134 | from canonical.launchpad.searchbuilder import all, any |
1135 | -from canonical.testing import LaunchpadFunctionalLayer, LaunchpadZopelessLayer |
1136 | +from canonical.testing import ( |
1137 | + DatabaseFunctionalLayer, LaunchpadFunctionalLayer, LaunchpadZopelessLayer) |
1138 | |
1139 | +from lp.bugs.interfaces.bugtarget import IBugTarget |
1140 | from lp.bugs.interfaces.bugtask import ( |
1141 | BugTaskImportance, BugTaskSearchParams, BugTaskStatus) |
1142 | from lp.bugs.model.bugtask import build_tag_search_clause |
1143 | @@ -28,7 +31,7 @@ |
1144 | |
1145 | class TestBugTaskDelta(TestCaseWithFactory): |
1146 | |
1147 | - layer = LaunchpadFunctionalLayer |
1148 | + layer = DatabaseFunctionalLayer |
1149 | |
1150 | def setUp(self): |
1151 | super(TestBugTaskDelta, self).setUp() |
1152 | @@ -66,7 +69,6 @@ |
1153 | |
1154 | def test_get_bugwatch_delta(self): |
1155 | # Exercise getDelta() with a change to bugwatch. |
1156 | - user = self.factory.makePerson() |
1157 | bug_task = self.factory.makeBugTask() |
1158 | bug_task_before_modification = Snapshot( |
1159 | bug_task, providing=providedBy(bug_task)) |
1160 | @@ -501,9 +503,9 @@ |
1161 | [bugtask.bug.id for bugtask in bugtasks]) |
1162 | |
1163 | |
1164 | -class TestBugTaskPermissionsToSetAssigneeBase(TestCaseWithFactory): |
1165 | +class TestBugTaskPermissionsToSetAssigneeMixin: |
1166 | |
1167 | - layer = LaunchpadFunctionalLayer |
1168 | + layer = DatabaseFunctionalLayer |
1169 | |
1170 | def setUp(self): |
1171 | """Create the test setup. |
1172 | @@ -516,7 +518,7 @@ |
1173 | owners, bug supervisors, drivers |
1174 | - bug tasks for the targets |
1175 | """ |
1176 | - super(TestBugTaskPermissionsToSetAssigneeBase, self).setUp() |
1177 | + super(TestBugTaskPermissionsToSetAssigneeMixin, self).setUp() |
1178 | self.target_owner_member = self.factory.makePerson() |
1179 | self.target_owner_team = self.factory.makeTeam( |
1180 | owner=self.target_owner_member) |
1181 | @@ -556,6 +558,14 @@ |
1182 | self.target_bugtask.transitionToAssignee(self.regular_user) |
1183 | logout() |
1184 | |
1185 | + def makeTarget(self): |
1186 | + """Create a target and a series. |
1187 | + |
1188 | + The target and series must be assigned as attributes of self: |
1189 | + 'self.target' and 'self.series'. |
1190 | + """ |
1191 | + raise NotImplementedError(self.makeTarget) |
1192 | + |
1193 | def test_userCanSetAnyAssignee_anonymous_user(self): |
1194 | # Anonymous users cannot set anybody as an assignee. |
1195 | login(ANONYMOUS) |
1196 | @@ -700,7 +710,7 @@ |
1197 | |
1198 | |
1199 | class TestProductBugTaskPermissionsToSetAssignee( |
1200 | - TestBugTaskPermissionsToSetAssigneeBase): |
1201 | + TestBugTaskPermissionsToSetAssigneeMixin, TestCaseWithFactory): |
1202 | |
1203 | def makeTarget(self): |
1204 | """Create a product and a product series.""" |
1205 | @@ -709,7 +719,7 @@ |
1206 | |
1207 | |
1208 | class TestDistributionBugTaskPermissionsToSetAssignee( |
1209 | - TestBugTaskPermissionsToSetAssigneeBase): |
1210 | + TestBugTaskPermissionsToSetAssigneeMixin, TestCaseWithFactory): |
1211 | |
1212 | def makeTarget(self): |
1213 | """Create a distribution and a distroseries.""" |
1214 | @@ -718,14 +728,73 @@ |
1215 | self.series = self.factory.makeDistroSeries(self.target) |
1216 | |
1217 | |
1218 | +class TestBugTaskSearch(TestCaseWithFactory): |
1219 | + |
1220 | + layer = DatabaseFunctionalLayer |
1221 | + |
1222 | + def login(self): |
1223 | + # Log in as an arbitrary person. |
1224 | + person = self.factory.makePerson() |
1225 | + login_person(person) |
1226 | + self.addCleanup(logout) |
1227 | + return person |
1228 | + |
1229 | + def makeBugTarget(self): |
1230 | + """Make an arbitrary bug target with no tasks on it.""" |
1231 | + return IBugTarget(self.factory.makeProduct()) |
1232 | + |
1233 | + def test_no_tasks(self): |
1234 | + # A brand new bug target has no tasks. |
1235 | + target = self.makeBugTarget() |
1236 | + self.assertEqual([], list(target.searchTasks(None))) |
1237 | + |
1238 | + def test_new_task_shows_up(self): |
1239 | + # When we create a new bugtask on the target, it shows up in |
1240 | + # searchTasks. |
1241 | + target = self.makeBugTarget() |
1242 | + self.login() |
1243 | + task = self.factory.makeBugTask(target=target) |
1244 | + self.assertEqual([task], list(target.searchTasks(None))) |
1245 | + |
1246 | + def test_modified_since_excludes_earlier_bugtasks(self): |
1247 | + # When we search for bug tasks that have been modified since a certain |
1248 | + # time, tasks for bugs that have not been modified since then are |
1249 | + # excluded. |
1250 | + target = self.makeBugTarget() |
1251 | + self.login() |
1252 | + task = self.factory.makeBugTask(target=target) |
1253 | + date = task.bug.date_last_updated + timedelta(days=1) |
1254 | + result = target.searchTasks(None, modified_since=date) |
1255 | + self.assertEqual([], list(result)) |
1256 | + |
1257 | + def test_modified_since_includes_later_bugtasks(self): |
1258 | + # When we search for bug tasks that have been modified since a certain |
1259 | + # time, tasks for bugs that have been modified since then are |
1260 | + # included. |
1261 | + target = self.makeBugTarget() |
1262 | + self.login() |
1263 | + task = self.factory.makeBugTask(target=target) |
1264 | + date = task.bug.date_last_updated - timedelta(days=1) |
1265 | + result = target.searchTasks(None, modified_since=date) |
1266 | + self.assertEqual([task], list(result)) |
1267 | + |
1268 | + def test_modified_since_includes_later_bugtasks_excludes_earlier(self): |
1269 | + # When we search for bugs that have been modified since a certain |
1270 | + # time, tasks for bugs that have been modified since then are |
1271 | + # included, tasks that have not are excluded. |
1272 | + target = self.makeBugTarget() |
1273 | + self.login() |
1274 | + task1 = self.factory.makeBugTask(target=target) |
1275 | + date = task1.bug.date_last_updated |
1276 | + task1.bug.date_last_updated -= timedelta(days=1) |
1277 | + task2 = self.factory.makeBugTask(target=target) |
1278 | + task2.bug.date_last_updated += timedelta(days=1) |
1279 | + result = target.searchTasks(None, modified_since=date) |
1280 | + self.assertEqual([task2], list(result)) |
1281 | + |
1282 | + |
1283 | def test_suite(): |
1284 | suite = unittest.TestSuite() |
1285 | - suite.addTest(unittest.makeSuite(TestBugTaskDelta)) |
1286 | - suite.addTest(unittest.makeSuite(TestBugTaskTagSearchClauses)) |
1287 | - suite.addTest(unittest.makeSuite(TestBugTaskHardwareSearch)) |
1288 | - suite.addTest(unittest.makeSuite( |
1289 | - TestProductBugTaskPermissionsToSetAssignee)) |
1290 | - suite.addTest(unittest.makeSuite( |
1291 | - TestDistributionBugTaskPermissionsToSetAssignee)) |
1292 | + suite.addTest(unittest.TestLoader().loadTestsFromName(__name__)) |
1293 | suite.addTest(DocTestSuite('lp.bugs.model.bugtask')) |
1294 | return suite |
1295 | |
1296 | === modified file 'lib/lp/registry/javascript/milestoneoverlay.js' |
1297 | --- lib/lp/registry/javascript/milestoneoverlay.js 2010-04-29 15:21:05 +0000 |
1298 | +++ lib/lp/registry/javascript/milestoneoverlay.js 2010-06-25 13:39:35 +0000 |
1299 | @@ -71,7 +71,8 @@ |
1300 | milestone_form.show(); |
1301 | }; |
1302 | |
1303 | - show_milestone_form = function(e) { |
1304 | + var show_milestone_form = function(e) { |
1305 | + e.preventDefault(); |
1306 | if (milestone_form) { |
1307 | milestone_form.show(); |
1308 | } else { |
1309 | @@ -79,7 +80,6 @@ |
1310 | // milestone_form.show() below it. |
1311 | setup_milestone_form(); |
1312 | } |
1313 | - e.preventDefault(); |
1314 | }; |
1315 | |
1316 | /** |
1317 | |
1318 | === added file 'lib/lp/registry/windmill/tests/test_add_bugtracker.py' |
1319 | --- lib/lp/registry/windmill/tests/test_add_bugtracker.py 1970-01-01 00:00:00 +0000 |
1320 | +++ lib/lp/registry/windmill/tests/test_add_bugtracker.py 2010-06-25 13:39:35 +0000 |
1321 | @@ -0,0 +1,100 @@ |
1322 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
1323 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
1324 | + |
1325 | +"""Test adding bug tracker in formoverlay.""" |
1326 | + |
1327 | +__metaclass__ = type |
1328 | +__all__ = [] |
1329 | + |
1330 | +import unittest |
1331 | + |
1332 | +from canonical.launchpad.windmill.testing import lpuser |
1333 | + |
1334 | +from lp.registry.windmill.testing import RegistryWindmillLayer |
1335 | +from lp.testing import WindmillTestCase |
1336 | + |
1337 | + |
1338 | +def test_inline_add_bugtracker(client, url, name=None, suite='bugtracker', |
1339 | + user=lpuser.FOO_BAR): |
1340 | + """Test the form overlay for adding a bugtracker. |
1341 | + |
1342 | + :param name: Name of the test. |
1343 | + :param url: Starting url. |
1344 | + :param suite: The suite in which this test is part of. |
1345 | + :param user: The user who should be logged in. |
1346 | + """ |
1347 | + bugtracker_name = u'FOObar' |
1348 | + title = u'\xdf-title-%s' % bugtracker_name |
1349 | + location = u'http://example.com/%s' % bugtracker_name |
1350 | + |
1351 | + user.ensure_login(client) |
1352 | + client.open(url=url) |
1353 | + client.waits.forPageLoad(timeout=u'20000') |
1354 | + |
1355 | + client.waits.forElement(id=u'create-bugtracker-link') |
1356 | + |
1357 | + # Click the "Create external bug tracker" link. |
1358 | + client.click(id=u'create-bugtracker-link') |
1359 | + |
1360 | + # Submit bugtracker form. |
1361 | + client.waits.forElement(id=u'field.name') |
1362 | + client.type(id='field.name', text=bugtracker_name) |
1363 | + client.type(id='field.title', text=title) |
1364 | + client.type(id='field.baseurl', text=location) |
1365 | + client.click(id=u'formoverlay-add-bugtracker') |
1366 | + |
1367 | + # Verify that the bugtracker name was entered in the text box. |
1368 | + client.waits.sleep(milliseconds='1000') |
1369 | + client.asserts.assertProperty( |
1370 | + id="field.bugtracker.bugtracker", |
1371 | + validator='value|%s' % bugtracker_name.lower()) |
1372 | + client.asserts.assertChecked(id="field.bugtracker.2") |
1373 | + |
1374 | + # Verify error message when trying to create a bugtracker with a |
1375 | + # conflicting name. |
1376 | + client.click(id=u'create-bugtracker-link') |
1377 | + client.waits.forElement(id=u'field.name') |
1378 | + client.type(id='field.name', text=bugtracker_name) |
1379 | + client.click(id=u'formoverlay-add-bugtracker') |
1380 | + client.waits.forElement( |
1381 | + xpath="//div[contains(@class, 'yui-lazr-formoverlay-errors')]/ul/li") |
1382 | + client.asserts.assertTextIn( |
1383 | + classname='yui-lazr-formoverlay-errors', |
1384 | + validator='name: %s is already in use' % bugtracker_name.lower()) |
1385 | + client.click(classname='close-button') |
1386 | + |
1387 | + # Configure bug tracker for the project. |
1388 | + client.click(id=u'field.actions.change') |
1389 | + |
1390 | + # You should now be on the project index page. |
1391 | + client.waits.forElement( |
1392 | + xpath="//a[contains(@class, 'menu-link-configure_bugtracker')]") |
1393 | + client.click( |
1394 | + xpath="//a[contains(@class, 'menu-link-configure_bugtracker')]") |
1395 | + |
1396 | + # Verify that the new bug tracker was configured for this project. |
1397 | + client.waits.forElement(id="field.bugtracker.bugtracker") |
1398 | + client.asserts.assertProperty( |
1399 | + id="field.bugtracker.bugtracker", |
1400 | + validator='value|%s' % bugtracker_name.lower()) |
1401 | + client.asserts.assertChecked(id="field.bugtracker.2") |
1402 | + |
1403 | + |
1404 | +class TestAddBugTracker(WindmillTestCase): |
1405 | + """Test form overlay widget for adding a bug tracker.""" |
1406 | + |
1407 | + # This test doesn't run well in the BugsWindmillLayer, since |
1408 | + # submitting the +configure-bugtracker form takes you back to |
1409 | + # the project index page, which is not on the bugs.launchpad.dev. |
1410 | + layer = RegistryWindmillLayer |
1411 | + suite_name = 'AddBugTracker' |
1412 | + |
1413 | + def test_adding_bugtracker_for_project(self): |
1414 | + test_inline_add_bugtracker( |
1415 | + self.client, |
1416 | + url='http://launchpad.dev:8085/bzr/+configure-bugtracker', |
1417 | + name='test_inline_add_bugtracker_for_project') |
1418 | + |
1419 | + |
1420 | +def test_suite(): |
1421 | + return unittest.TestLoader().loadTestsFromName(__name__) |
1422 | |
1423 | === modified file 'lib/lp/registry/windmill/tests/test_add_milestone.py' |
1424 | --- lib/lp/registry/windmill/tests/test_add_milestone.py 2010-02-01 18:37:00 +0000 |
1425 | +++ lib/lp/registry/windmill/tests/test_add_milestone.py 2010-06-25 13:39:35 +0000 |
1426 | @@ -1,7 +1,7 @@ |
1427 | # Copyright 2009 Canonical Ltd. This software is licensed under the |
1428 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1429 | |
1430 | -"""Test for translation import queue behaviour.""" |
1431 | +"""Test adding milestone in formoverlay.""" |
1432 | |
1433 | __metaclass__ = type |
1434 | __all__ = [] |
1435 | @@ -24,9 +24,7 @@ |
1436 | :param suite: The suite in which this test is part of. |
1437 | :param user: The user who should be logged in. |
1438 | """ |
1439 | - # Ensure that the milestone name doesn't conflict with previous |
1440 | - # test runs, and test that it correctly lowercases the name. |
1441 | - milestone_name = u'FOObar%x' % int(time.time()) |
1442 | + milestone_name = u'FOObar' |
1443 | code_name = u'code-%s' % milestone_name |
1444 | |
1445 | user.ensure_login(client) |
1446 | |
1447 | === modified file 'lib/lp/soyuz/browser/archive.py' |
1448 | --- lib/lp/soyuz/browser/archive.py 2010-06-21 19:29:34 +0000 |
1449 | +++ lib/lp/soyuz/browser/archive.py 2010-06-25 13:39:35 +0000 |
1450 | @@ -35,6 +35,7 @@ |
1451 | from zope.component import getUtility |
1452 | from zope.formlib import form |
1453 | from zope.interface import implements, Interface |
1454 | +from zope.security.interfaces import Unauthorized |
1455 | from zope.security.proxy import removeSecurityProxy |
1456 | from zope.schema import Choice, List, TextLine |
1457 | from zope.schema.interfaces import IContextSourceBinder |
1458 | @@ -426,7 +427,12 @@ |
1459 | |
1460 | def packages(self): |
1461 | text = 'View package details' |
1462 | - return Link('+packages', text, icon='info') |
1463 | + link = Link('+packages', text, icon='info') |
1464 | + # Disable the link for P3As if they don't have upload rights. |
1465 | + if self.context.private: |
1466 | + if not check_permission('launchpad.Append', self.context): |
1467 | + link.enabled = False |
1468 | + return link |
1469 | |
1470 | @enabled_with_permission('launchpad.Edit') |
1471 | def delete(self): |
1472 | @@ -500,6 +506,10 @@ |
1473 | """Common features for Archive view classes.""" |
1474 | |
1475 | @cachedproperty |
1476 | + def private(self): |
1477 | + return self.context.private |
1478 | + |
1479 | + @cachedproperty |
1480 | def has_sources(self): |
1481 | """Whether or not this PPA has any sources for the view. |
1482 | |
1483 | @@ -960,6 +970,12 @@ |
1484 | """Detailed packages view for an archive.""" |
1485 | implements(IArchivePackagesActionMenu) |
1486 | |
1487 | + def initialize(self): |
1488 | + super(ArchivePackagesView, self).initialize() |
1489 | + if self.context.private: |
1490 | + if not check_permission('launchpad.Append', self.context): |
1491 | + raise Unauthorized |
1492 | + |
1493 | @property |
1494 | def page_title(self): |
1495 | return smartquote('Packages in "%s"' % self.context.displayname) |
1496 | |
1497 | === added file 'lib/lp/soyuz/browser/tests/test_archive_packages.py' |
1498 | --- lib/lp/soyuz/browser/tests/test_archive_packages.py 1970-01-01 00:00:00 +0000 |
1499 | +++ lib/lp/soyuz/browser/tests/test_archive_packages.py 2010-06-25 13:39:35 +0000 |
1500 | @@ -0,0 +1,101 @@ |
1501 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
1502 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
1503 | + |
1504 | +# pylint: disable-msg=F0401 |
1505 | + |
1506 | +"""Unit tests for TestP3APackages.""" |
1507 | + |
1508 | +__metaclass__ = type |
1509 | +__all__ = [ |
1510 | + 'TestP3APackages', |
1511 | + 'TestPPAPackages', |
1512 | + 'test_suite', |
1513 | + ] |
1514 | + |
1515 | +import unittest |
1516 | + |
1517 | +from zope.security.interfaces import Unauthorized |
1518 | + |
1519 | +from canonical.testing import LaunchpadFunctionalLayer |
1520 | +from lp.soyuz.browser.archive import ArchiveNavigationMenu |
1521 | +from lp.testing import login, login_person, TestCaseWithFactory |
1522 | +from lp.testing.views import create_initialized_view |
1523 | + |
1524 | + |
1525 | +class TestP3APackages(TestCaseWithFactory): |
1526 | + """P3A archive pages are rendered correctly.""" |
1527 | + |
1528 | + layer = LaunchpadFunctionalLayer |
1529 | + |
1530 | + def setUp(self): |
1531 | + super(TestP3APackages, self).setUp() |
1532 | + self.private_ppa = self.factory.makeArchive(description='Foo') |
1533 | + login('admin@canonical.com') |
1534 | + self.private_ppa.buildd_secret = 'blah' |
1535 | + self.private_ppa.private = True |
1536 | + self.joe = self.factory.makePerson(name='joe') |
1537 | + self.fred = self.factory.makePerson(name='fred') |
1538 | + self.mary = self.factory.makePerson(name='mary') |
1539 | + login_person(self.private_ppa.owner) |
1540 | + self.private_ppa.newSubscription(self.joe, self.private_ppa.owner) |
1541 | + self.private_ppa.newComponentUploader(self.mary, 'main') |
1542 | + |
1543 | + def test_packages_unauthorized(self): |
1544 | + """A person with no subscription will not be able to view +packages |
1545 | + """ |
1546 | + login_person(self.fred) |
1547 | + self.assertRaises( |
1548 | + Unauthorized, create_initialized_view, self.private_ppa, |
1549 | + "+packages") |
1550 | + |
1551 | + def test_packages_unauthorized_subscriber(self): |
1552 | + """A person with a subscription will not be able to view +packages |
1553 | + """ |
1554 | + login_person(self.joe) |
1555 | + self.assertRaises( |
1556 | + Unauthorized, create_initialized_view, self.private_ppa, |
1557 | + "+packages") |
1558 | + |
1559 | + def test_packages_authorized(self): |
1560 | + """A person with launchpad.{Append,Edit} will be able to do so""" |
1561 | + login_person(self.private_ppa.owner) |
1562 | + view = create_initialized_view(self.private_ppa, "+packages") |
1563 | + menu = ArchiveNavigationMenu(view) |
1564 | + self.assertTrue(menu.packages().enabled) |
1565 | + |
1566 | + def test_packages_uploader(self): |
1567 | + """A person with launchpad.Append will also be able to do so""" |
1568 | + login_person(self.mary) |
1569 | + view = create_initialized_view(self.private_ppa, "+packages") |
1570 | + menu = ArchiveNavigationMenu(view) |
1571 | + self.assertTrue(menu.packages().enabled) |
1572 | + |
1573 | + def test_packages_link_unauthorized(self): |
1574 | + login_person(self.fred) |
1575 | + view = create_initialized_view(self.private_ppa, "+index") |
1576 | + menu = ArchiveNavigationMenu(view) |
1577 | + self.assertFalse(menu.packages().enabled) |
1578 | + |
1579 | + def test_packages_link_subscriber(self): |
1580 | + login_person(self.joe) |
1581 | + view = create_initialized_view(self.private_ppa, "+index") |
1582 | + menu = ArchiveNavigationMenu(view) |
1583 | + self.assertFalse(menu.packages().enabled) |
1584 | + |
1585 | + |
1586 | +class TestPPAPackages(TestCaseWithFactory): |
1587 | + layer = LaunchpadFunctionalLayer |
1588 | + |
1589 | + def setUp(self): |
1590 | + super(TestPPAPackages, self).setUp() |
1591 | + self.joe = self.factory.makePerson(name='joe') |
1592 | + self.ppa = self.factory.makeArchive() |
1593 | + |
1594 | + def test_ppa_packages(self): |
1595 | + login_person(self.joe) |
1596 | + view = create_initialized_view(self.ppa, "+index") |
1597 | + menu = ArchiveNavigationMenu(view) |
1598 | + self.assertTrue(menu.packages().enabled) |
1599 | + |
1600 | +def test_suite(): |
1601 | + return unittest.TestLoader().loadTestsFromName(__name__) |
1602 | |
1603 | === modified file 'lib/lp/soyuz/doc/archiveauthtoken.txt' |
1604 | --- lib/lp/soyuz/doc/archiveauthtoken.txt 2010-04-28 16:28:02 +0000 |
1605 | +++ lib/lp/soyuz/doc/archiveauthtoken.txt 2010-06-25 13:39:35 +0000 |
1606 | @@ -21,8 +21,7 @@ |
1607 | possible if there is already a valid subscription for the user for |
1608 | that archive. |
1609 | |
1610 | -First, login as joe and try to create a token for ourselves, even |
1611 | -though we do not yet have a subscription: |
1612 | +Create Brad, and his team: |
1613 | |
1614 | >>> login("admin@canonical.com") |
1615 | >>> bradsmith = factory.makePerson( |
1616 | @@ -30,12 +29,6 @@ |
1617 | ... email="brad@example.com") |
1618 | >>> teambrad = factory.makeTeam( |
1619 | ... owner=bradsmith, displayname="Team Brad", name='teambrad') |
1620 | - >>> login("brad@example.com") |
1621 | - >>> new_token = joe_private_ppa.newAuthToken(bradsmith) |
1622 | - Traceback (most recent call last): |
1623 | - ... |
1624 | - Unauthorized: You do not have a subscription for |
1625 | - PPA for Joe Smith. |
1626 | |
1627 | Create a subscription for Team Brad to joe's archive: |
1628 | |
1629 | |
1630 | === modified file 'lib/lp/soyuz/interfaces/archive.py' |
1631 | --- lib/lp/soyuz/interfaces/archive.py 2010-06-21 19:29:34 +0000 |
1632 | +++ lib/lp/soyuz/interfaces/archive.py 2010-06-25 13:39:35 +0000 |
1633 | @@ -1,4 +1,4 @@ |
1634 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
1635 | +# Copyright 2009-2010 Canonical Ltd. This software is licensed under the |
1636 | # GNU Affero General Public License version 3 (see the file LICENSE). |
1637 | |
1638 | # pylint: disable-msg=E0211,E0213 |
1639 | @@ -629,24 +629,6 @@ |
1640 | :return The new `IPackageCopyRequest` |
1641 | """ |
1642 | |
1643 | - # XXX: noodles 2009-03-02 bug=336779: This should be moved into |
1644 | - # IArchiveView once the archive permissions are updated to grant |
1645 | - # IArchiveView to archive subscribers. |
1646 | - def newAuthToken(person, token=None, date_created=None): |
1647 | - """Create a new authorisation token. |
1648 | - |
1649 | - XXX: noodles 2009-03-12 bug=341600 This method should not be exposed |
1650 | - through the API as we do not yet check that the callsite has |
1651 | - launchpad.Edit on the person. |
1652 | - |
1653 | - :param person: An IPerson whom this token is for |
1654 | - :param token: Optional unicode text to use as the token. One will be |
1655 | - generated if not given |
1656 | - :param date_created: Optional, defaults to now |
1657 | - |
1658 | - :return: A new IArchiveAuthToken |
1659 | - """ |
1660 | - |
1661 | @operation_parameters( |
1662 | person=Reference(schema=IPerson), |
1663 | # Really IPackageset, corrected in _schema_circular_imports to avoid |
1664 | @@ -1112,6 +1094,33 @@ |
1665 | :return: A dictionary of filenames and SHA1s. |
1666 | """ |
1667 | |
1668 | + def getAuthToken(person): |
1669 | + """Returns an IArchiveAuthToken for the archive in question for |
1670 | + IPerson provided. |
1671 | + |
1672 | + :return: A IArchiveAuthToken, or None if the user has none. |
1673 | + """ |
1674 | + |
1675 | + def newAuthToken(person, token=None, date_created=None): |
1676 | + """Create a new authorisation token. |
1677 | + |
1678 | + :param person: An IPerson whom this token is for |
1679 | + :param token: Optional unicode text to use as the token. One will be |
1680 | + generated if not given |
1681 | + :param date_created: Optional, defaults to now |
1682 | + |
1683 | + :return: A new IArchiveAuthToken |
1684 | + """ |
1685 | + |
1686 | + @call_with(person=REQUEST_USER) |
1687 | + @export_write_operation() |
1688 | + def getPrivateSourcesList(person): |
1689 | + """Get a text line that is suitable to be used for a sources.list |
1690 | + entry. |
1691 | + |
1692 | + It will create a new IArchiveAuthToken if one doesn't already exist. |
1693 | + """ |
1694 | + |
1695 | class IArchiveAppend(Interface): |
1696 | """Archive interface for operations restricted by append privilege.""" |
1697 | |
1698 | |
1699 | === modified file 'lib/lp/soyuz/model/archive.py' |
1700 | --- lib/lp/soyuz/model/archive.py 2010-06-21 19:29:34 +0000 |
1701 | +++ lib/lp/soyuz/model/archive.py 2010-06-25 13:39:35 +0000 |
1702 | @@ -1392,30 +1392,26 @@ |
1703 | # Perform the copy, may raise CannotCopy. |
1704 | do_copy(sources, self, series, pocket, include_binaries) |
1705 | |
1706 | + def getAuthToken(self, person): |
1707 | + """See `IArchive`.""" |
1708 | + |
1709 | + token_set = getUtility(IArchiveAuthTokenSet) |
1710 | + return token_set.getActiveTokenForArchiveAndPerson(self, person) |
1711 | + |
1712 | def newAuthToken(self, person, token=None, date_created=None): |
1713 | """See `IArchive`.""" |
1714 | |
1715 | + # Bail if the archive isn't private |
1716 | + if not self.private: |
1717 | + raise ArchiveNotPrivate("Archive must be private.") |
1718 | + |
1719 | # Tokens can only be created for individuals. |
1720 | if person.is_team: |
1721 | raise NoTokensForTeams( |
1722 | "Subscription tokens can be created for individuals only.") |
1723 | |
1724 | - # First, ensure that a current subscription exists for the |
1725 | - # person and archive: |
1726 | - # XXX: noodles 2009-03-02 bug=336779: This can be removed once |
1727 | - # newAuthToken() is moved into IArchiveView. |
1728 | - subscription_set = getUtility(IArchiveSubscriberSet) |
1729 | - subscriptions = subscription_set.getBySubscriber(person, archive=self) |
1730 | - if subscriptions.count() == 0: |
1731 | - raise Unauthorized( |
1732 | - "You do not have a subscription for %s." % self.displayname) |
1733 | - |
1734 | - # Second, ensure that the current subscription does not already |
1735 | - # have a token: |
1736 | - token_set = getUtility(IArchiveAuthTokenSet) |
1737 | - previous_token = token_set.getActiveTokenForArchiveAndPerson( |
1738 | - self, person) |
1739 | - if previous_token: |
1740 | + # Ensure that the current subscription does not already have a token |
1741 | + if self.getAuthToken(person) is not None: |
1742 | raise ArchiveSubscriptionError( |
1743 | "%s already has a token for %s." % ( |
1744 | person.displayname, self.displayname)) |
1745 | @@ -1433,6 +1429,14 @@ |
1746 | store.add(archive_auth_token) |
1747 | return archive_auth_token |
1748 | |
1749 | + def getPrivateSourcesList(self, person): |
1750 | + """See `IArchive`.""" |
1751 | + |
1752 | + token = self.getAuthToken(person) |
1753 | + if token is None: |
1754 | + token = self.newAuthToken(person) |
1755 | + return token.archive_url |
1756 | + |
1757 | def newSubscription(self, subscriber, registrant, date_expires=None, |
1758 | description=None): |
1759 | """See `IArchive`.""" |
1760 | |
1761 | === modified file 'lib/lp/soyuz/stories/webservice/xx-archive.txt' |
1762 | --- lib/lp/soyuz/stories/webservice/xx-archive.txt 2010-06-14 14:16:13 +0000 |
1763 | +++ lib/lp/soyuz/stories/webservice/xx-archive.txt 2010-06-25 13:39:35 +0000 |
1764 | @@ -890,6 +890,16 @@ |
1765 | >>> print response.getHeader('Location') |
1766 | http://.../~cprov/+archive/p3a/+subscriptions/mark |
1767 | |
1768 | +We can print the sources.list entry for the archive, which will include an |
1769 | +AuthToken: |
1770 | + |
1771 | + >>> sources_response = webservice.named_post( |
1772 | + ... cprov_private_ppa['self_link'], 'getPrivateSourcesList') |
1773 | + >>> print sources_response |
1774 | + HTTP/1.1 200 Ok |
1775 | + ... |
1776 | + "http://salgado:...@private-ppa.launchpad.dev/cprov/p3a/ubuntu" |
1777 | + |
1778 | We publish a subset of the IArchiveSubscriber attributes. |
1779 | |
1780 | >>> new_subscription = cprov_webservice.get( |
1781 | |
1782 | === modified file 'lib/lp/soyuz/tests/test_archive.py' |
1783 | --- lib/lp/soyuz/tests/test_archive.py 2010-06-16 18:47:46 +0000 |
1784 | +++ lib/lp/soyuz/tests/test_archive.py 2010-06-25 13:39:35 +0000 |
1785 | @@ -790,6 +790,30 @@ |
1786 | self.archive, self.arm).count()) |
1787 | self.assertFalse(self.archive.arm_builds_allowed) |
1788 | |
1789 | +class TestArchiveTokens(TestCaseWithFactory): |
1790 | + layer = LaunchpadZopelessLayer |
1791 | + |
1792 | + def setUp(self): |
1793 | + super(TestArchiveTokens, self).setUp() |
1794 | + owner = self.factory.makePerson() |
1795 | + self.private_ppa = self.factory.makeArchive(owner=owner) |
1796 | + self.private_ppa.buildd_secret = 'blah' |
1797 | + self.private_ppa.private = True |
1798 | + self.joe = self.factory.makePerson(name='joe') |
1799 | + self.private_ppa.newSubscription(self.joe, owner) |
1800 | + |
1801 | + def test_getAuthToken_with_no_token(self): |
1802 | + token = self.private_ppa.getAuthToken(self.joe) |
1803 | + self.assertEqual(token, None) |
1804 | + |
1805 | + def test_getAuthToken_with_token(self): |
1806 | + token = self.private_ppa.newAuthToken(self.joe) |
1807 | + self.assertEqual(self.private_ppa.getAuthToken(self.joe), token) |
1808 | + |
1809 | + def test_getPrivateSourcesList(self): |
1810 | + url = self.private_ppa.getPrivateSourcesList(self.joe) |
1811 | + token = self.private_ppa.getAuthToken(self.joe) |
1812 | + self.assertEqual(token.archive_url, url) |
1813 | |
1814 | class TestArchivePrivacySwitching(TestCaseWithFactory): |
1815 | |
1816 | |
1817 | === added file 'lib/lp/soyuz/tests/test_archive_privacy.py' |
1818 | --- lib/lp/soyuz/tests/test_archive_privacy.py 1970-01-01 00:00:00 +0000 |
1819 | +++ lib/lp/soyuz/tests/test_archive_privacy.py 2010-06-25 13:39:35 +0000 |
1820 | @@ -0,0 +1,40 @@ |
1821 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
1822 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
1823 | + |
1824 | +"""Test Archive privacy features.""" |
1825 | + |
1826 | +from zope.component import getUtility |
1827 | +from zope.security.interfaces import Unauthorized |
1828 | +from lp.soyuz.interfaces.archive import IArchiveSet |
1829 | + |
1830 | +from canonical.testing import LaunchpadFunctionalLayer |
1831 | +from lp.testing import login, login_person, TestCaseWithFactory |
1832 | + |
1833 | + |
1834 | +class TestArchivePrivacy(TestCaseWithFactory): |
1835 | + layer = LaunchpadFunctionalLayer |
1836 | + |
1837 | + def setUp(self): |
1838 | + super(TestArchivePrivacy, self).setUp() |
1839 | + self.private_ppa = self.factory.makeArchive(description='Foo') |
1840 | + login('admin@canonical.com') |
1841 | + self.private_ppa.buildd_secret = 'blah' |
1842 | + self.private_ppa.private = True |
1843 | + self.joe = self.factory.makePerson(name='joe') |
1844 | + self.fred = self.factory.makePerson(name='fred') |
1845 | + login_person(self.private_ppa.owner) |
1846 | + self.private_ppa.newSubscription(self.joe, self.private_ppa.owner) |
1847 | + |
1848 | + def _getDescription(self, p3a): |
1849 | + return p3a.description |
1850 | + |
1851 | + def test_no_subscription(self): |
1852 | + login_person(self.fred) |
1853 | + p3a = getUtility(IArchiveSet).get(self.private_ppa.id) |
1854 | + self.assertRaises(Unauthorized, self._getDescription, p3a) |
1855 | + |
1856 | + def test_subscription(self): |
1857 | + login_person(self.joe) |
1858 | + p3a = getUtility(IArchiveSet).get(self.private_ppa.id) |
1859 | + self.assertEqual(self._getDescription(p3a), "Foo") |
1860 | + |
1861 | |
1862 | === modified file 'utilities/lp-deps.py' |
1863 | --- utilities/lp-deps.py 2010-06-14 22:18:14 +0000 |
1864 | +++ utilities/lp-deps.py 2010-06-25 13:39:35 +0000 |
1865 | @@ -20,6 +20,7 @@ |
1866 | # JS_DIRSET is a tuple of the dir where the code exists, and the name of the |
1867 | # symlink it should be linked as in the icing build directory. |
1868 | JS_DIRSET = [ |
1869 | + (os.path.join('lib', 'lp', 'bugs', 'javascript'), 'bugs'), |
1870 | (os.path.join('lib', 'lp', 'code', 'javascript'), 'code'), |
1871 | (os.path.join('lib', 'lp', 'registry', 'javascript'), 'registry'), |
1872 | (os.path.join('lib', 'lp', 'translations', 'javascript'), 'translations'), |
1873 | |
1874 | === modified file 'utilities/qa-ready' |
1875 | --- utilities/qa-ready 2010-04-27 19:48:39 +0000 |
1876 | +++ utilities/qa-ready 2010-06-25 13:39:35 +0000 |
1877 | @@ -35,7 +35,7 @@ |
1878 | """ |
1879 | t = get_transport('https://edge.launchpad.net/') |
1880 | html = t.get_bytes('index.html') |
1881 | - revision_re = re.compile(r'\(r(\d+)\)') |
1882 | + revision_re = re.compile(r'r(\d+)') |
1883 | for line in html.splitlines(): |
1884 | matches = revision_re.search(line) |
1885 | if matches: |
wrong target branch...