Merge lp:~adeuring/launchpad/bug-598484 into lp:launchpad/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
Reviewer Review Type Date Requested Status
Abel Deuring (community) Disapprove
Review via email: mp+28504@code.launchpad.net

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-text-pages.txt

no lint

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) wrote :

wrong target branch...

review: Disapprove

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&hellip;</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:

Subscribers

People subscribed via source and target branches

to status/vote changes: