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
=== modified file '.bzrignore'
--- .bzrignore 2010-05-27 07:12:50 +0000
+++ .bzrignore 2010-06-25 13:39:35 +0000
@@ -68,3 +68,4 @@
68lp.sfood68lp.sfood
69apidocs69apidocs
70twistd.pid70twistd.pid
71lib/canonical/launchpad/apidoc
7172
=== modified file 'Makefile'
--- Makefile 2010-06-15 01:52:28 +0000
+++ Makefile 2010-06-25 13:39:35 +0000
@@ -61,8 +61,7 @@
61$(API_INDEX): $(BZR_VERSION_INFO)61$(API_INDEX): $(BZR_VERSION_INFO)
62 mkdir -p $(APIDOC_DIR).tmp62 mkdir -p $(APIDOC_DIR).tmp
63 LPCONFIG=$(LPCONFIG) $(PY) ./utilities/create-lp-wadl-and-apidoc.py "$(WADL_TEMPLATE)"63 LPCONFIG=$(LPCONFIG) $(PY) ./utilities/create-lp-wadl-and-apidoc.py "$(WADL_TEMPLATE)"
64 mv $(APIDOC_DIR).tmp/* $(APIDOC_DIR)64 mv $(APIDOC_DIR).tmp $(APIDOC_DIR)
65 rmdir $(APIDOC_DIR).tmp
6665
67apidoc: compile $(API_INDEX)66apidoc: compile $(API_INDEX)
6867
@@ -340,7 +339,7 @@
340 $(RM) -r lib/mailman339 $(RM) -r lib/mailman
341 $(RM) -rf lib/canonical/launchpad/icing/build/*340 $(RM) -rf lib/canonical/launchpad/icing/build/*
342 $(RM) -r $(CODEHOSTING_ROOT)341 $(RM) -r $(CODEHOSTING_ROOT)
343 $(RM) $(APIDOC_DIR)/wadl*.xml $(APIDOC_DIR)/*.html342 $(RM) -rf $(APIDOC_DIR)
344 $(RM) -rf $(APIDOC_DIR).tmp343 $(RM) -rf $(APIDOC_DIR).tmp
345 $(RM) $(BZR_VERSION_INFO)344 $(RM) $(BZR_VERSION_INFO)
346 $(RM) +config-overrides.zcml345 $(RM) +config-overrides.zcml
347346
=== removed directory 'lib/canonical/launchpad/apidoc'
=== modified file 'lib/canonical/launchpad/icing/style-3-0.css.in'
--- lib/canonical/launchpad/icing/style-3-0.css.in 2010-06-17 00:39:03 +0000
+++ lib/canonical/launchpad/icing/style-3-0.css.in 2010-06-25 13:39:35 +0000
@@ -126,6 +126,20 @@
126 Universal presentation126 Universal presentation
127 Block elements.127 Block elements.
128*/128*/
129/* XXX EdwinGrubbs 2010-06-18 bug=570354
130 * The PrettyOverlay css uses static values for the width, but
131 * the overlay needs to stretch for forms with wide input fields.
132 */
133.yui-pretty-overlay {
134 width: auto !important;
135 min-width: 402px;
136 }
137
138.yui-pretty-overlay #yui-pretty-overlay-modal {
139 width: auto !important;
140 min-width: 340px;
141 }
142
129html, body {143html, body {
130 font-family: "dejavu sans", "bitstream vera sans", verdana, sans-serif;144 font-family: "dejavu sans", "bitstream vera sans", verdana, sans-serif;
131 font-size: 93%;145 font-size: 93%;
132146
=== renamed directory 'lib/lp/bugs/javascript' => 'lib/canonical/launchpad/javascript/bugs'
=== modified file 'lib/canonical/launchpad/mailnotification.py'
--- lib/canonical/launchpad/mailnotification.py 2010-06-23 21:24:13 +0000
+++ lib/canonical/launchpad/mailnotification.py 2010-06-25 13:39:35 +0000
@@ -16,157 +16,50 @@
16from email.MIMEText import MIMEText16from email.MIMEText import MIMEText
17from email.MIMEMultipart import MIMEMultipart17from email.MIMEMultipart import MIMEMultipart
18from email.MIMEMessage import MIMEMessage18from email.MIMEMessage import MIMEMessage
19from email.Utils import formataddr, formatdate, make_msgid19from email.Utils import formataddr, make_msgid
2020
21import re21import re
22import rfc822
2322
24from zope.component import getAdapter, getUtility23from zope.component import getAdapter, getUtility
25from zope.interface import implements
2624
27from canonical.config import config25from canonical.config import config
28from canonical.database.sqlbase import block_implicit_flushes26from canonical.database.sqlbase import block_implicit_flushes
29from lp.bugs.adapters.bugdelta import BugDelta
30from lp.bugs.adapters.bugchange import (
31 BugDuplicateChange, get_bug_changes, BugTaskAssigneeChange)
32from canonical.launchpad.helpers import (27from canonical.launchpad.helpers import (
33 get_contact_email_addresses, get_email_template, shortlist)28 get_contact_email_addresses, get_email_template)
34from canonical.launchpad.interfaces import (29from canonical.launchpad.interfaces import (
35 IEmailAddressSet, IHeldMessageDetails, ILaunchpadCelebrities,30 IHeldMessageDetails, IPerson, IPersonSet, ISpecification,
36 IPerson, IPersonSet, ISpecification, IStructuralSubscriptionTarget,31 IStructuralSubscriptionTarget, ITeamMembershipSet, IUpstreamBugTask,
37 ITeamMembershipSet, IUpstreamBugTask, TeamMembershipStatus)32 TeamMembershipStatus)
38from lp.bugs.interfaces.bugchange import IBugChange
39from canonical.launchpad.interfaces.launchpad import ILaunchpadRoot33from canonical.launchpad.interfaces.launchpad import ILaunchpadRoot
40from canonical.launchpad.interfaces.message import (34from canonical.launchpad.interfaces.message import (
41 IDirectEmailAuthorization, QuotaReachedError)35 IDirectEmailAuthorization, QuotaReachedError)
42from lp.registry.interfaces.structuralsubscription import (
43 BugNotificationLevel)
44from canonical.launchpad.mail import (36from canonical.launchpad.mail import (
45 sendmail, simple_sendmail, simple_sendmail_from_person, format_address)37 sendmail, simple_sendmail, simple_sendmail_from_person, format_address)
46from lp.services.mail.mailwrapper import MailWrapper
47from canonical.launchpad.webapp.publisher import canonical_url38from canonical.launchpad.webapp.publisher import canonical_url
48from canonical.launchpad.webapp.url import urlappend39from canonical.launchpad.webapp.url import urlappend
4940
41from lp.bugs.adapters.bugdelta import BugDelta
42from lp.bugs.adapters.bugchange import (
43 BugDuplicateChange, get_bug_changes, BugTaskAssigneeChange)
44from lp.bugs.interfaces.bugchange import IBugChange
45from lp.bugs.mail.bugnotificationbuilder import get_bugmail_error_address
46from lp.registry.interfaces.structuralsubscription import (
47 BugNotificationLevel)
48from lp.services.mail.mailwrapper import MailWrapper
49
50# XXX 2010-06-16 gmb bug=59498550# XXX 2010-06-16 gmb bug=594985
51# This shouldn't be here, but if we take it out lots of things cry,51# This shouldn't be here, but if we take it out lots of things cry,
52# which is sad.52# which is sad.
53from lp.services.mail.notificationrecipientset import (53from lp.services.mail.notificationrecipientset import (
54 NotificationRecipientSet)54 NotificationRecipientSet)
5555
56from lp.bugs.mail.bugnotificationbuilder import (
57 BugNotificationBuilder)
56from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients58from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
5759
58CC = "CC"60CC = "CC"
5961
6062
61def format_rfc2822_date(date):
62 """Formats a date according to RFC2822's desires."""
63 return formatdate(rfc822.mktime_tz(date.utctimetuple() + (0, )))
64
65
66class BugNotificationBuilder:
67 """Constructs a MIMEText message for a bug notification.
68
69 Takes a bug and a set of headers and returns a new MIMEText
70 object. Common and expensive to calculate headers are cached
71 up-front.
72 """
73
74 def __init__(self, bug):
75 self.bug = bug
76
77 # Pre-calculate common headers.
78 self.common_headers = [
79 ('Reply-To', get_bugmail_replyto_address(bug)),
80 ('Sender', config.canonical.bounce_address),
81 ]
82
83 # X-Launchpad-Bug
84 self.common_headers.extend(
85 ('X-Launchpad-Bug', bugtask.asEmailHeaderValue())
86 for bugtask in bug.bugtasks)
87
88 # X-Launchpad-Bug-Tags
89 if len(bug.tags) > 0:
90 self.common_headers.append(
91 ('X-Launchpad-Bug-Tags', ' '.join(bug.tags)))
92
93 # Add the X-Launchpad-Bug-Private header. This is a simple
94 # yes/no value denoting privacy for the bug.
95 if bug.private:
96 self.common_headers.append(
97 ('X-Launchpad-Bug-Private', 'yes'))
98 else:
99 self.common_headers.append(
100 ('X-Launchpad-Bug-Private', 'no'))
101
102 # Add the X-Launchpad-Bug-Security-Vulnerability header to
103 # denote security for this bug. This follows the same form as
104 # the -Bug-Private header.
105 if bug.security_related:
106 self.common_headers.append(
107 ('X-Launchpad-Bug-Security-Vulnerability', 'yes'))
108 else:
109 self.common_headers.append(
110 ('X-Launchpad-Bug-Security-Vulnerability', 'no'))
111
112 # Add the -Bug-Commenters header, a space-separated list of
113 # distinct IDs of people who have commented on the bug. The
114 # list is sorted to aid testing.
115 commenters = set(message.owner.name for message in bug.messages)
116 self.common_headers.append(
117 ('X-Launchpad-Bug-Commenters', ' '.join(sorted(commenters))))
118
119 # Add the -Bug-Reporter header to identify the owner of the bug
120 # and the original bug task for filtering
121 self.common_headers.append(
122 ('X-Launchpad-Bug-Reporter',
123 '%s (%s)' % ( bug.owner.displayname, bug.owner.name )))
124
125 def build(self, from_address, to_address, body, subject, email_date,
126 rationale=None, references=None, message_id=None):
127 """Construct the notification.
128
129 :param from_address: The From address of the notification.
130 :param to_address: The To address for the notification.
131 :param body: The body text of the notification.
132 :type body: unicode
133 :param subject: The Subject of the notification.
134 :param email_date: The Date for the notification.
135 :param rationale: The rationale for why the recipient is
136 receiving this notification.
137 :param references: A value for the References header.
138 :param message_id: A value for the Message-ID header.
139
140 :return: An `email.MIMEText.MIMEText` object.
141 """
142 message = MIMEText(body.encode('utf8'), 'plain', 'utf8')
143 message['Date'] = format_rfc2822_date(email_date)
144 message['From'] = from_address
145 message['To'] = to_address
146
147 # Add the common headers.
148 for header in self.common_headers:
149 message.add_header(*header)
150
151 if references is not None:
152 message['References'] = ' '.join(references)
153 if message_id is not None:
154 message['Message-Id'] = message_id
155
156 subject_prefix = "[Bug %d]" % self.bug.id
157 if subject is None:
158 message['Subject'] = subject_prefix
159 elif subject_prefix in subject:
160 message['Subject'] = subject
161 else:
162 message['Subject'] = "%s %s" % (subject_prefix, subject)
163
164 if rationale is not None:
165 message.add_header('X-Launchpad-Message-Rationale', rationale)
166
167 return message
168
169
170def _send_bug_details_to_new_bug_subscribers(63def _send_bug_details_to_new_bug_subscribers(
171 bug, previous_subscribers, current_subscribers, subscribed_by=None,64 bug, previous_subscribers, current_subscribers, subscribed_by=None,
172 event_creator=None):65 event_creator=None):
@@ -234,65 +127,12 @@
234 if (bugtask_before_modification.product !=127 if (bugtask_before_modification.product !=
235 bugtask_after_modification.product):128 bugtask_after_modification.product):
236 new_product = bugtask_after_modification.product129 new_product = bugtask_after_modification.product
237 if bugtask_before_modification.bug.security_related and new_product.security_contact:130 if (bugtask_before_modification.bug.security_related and
131 new_product.security_contact):
238 bugtask_after_modification.bug.subscribe(132 bugtask_after_modification.bug.subscribe(
239 new_product.security_contact, IPerson(event.user))133 new_product.security_contact, IPerson(event.user))
240134
241135
242def get_bugmail_from_address(person, bug):
243 """Returns the right From: address to use for a bug notification."""
244 if person == getUtility(ILaunchpadCelebrities).janitor:
245 return format_address(
246 'Launchpad Bug Tracker',
247 "%s@%s" % (bug.id, config.launchpad.bugs_domain))
248
249 if person.hide_email_addresses:
250 return format_address(
251 person.displayname,
252 "%s@%s" % (bug.id, config.launchpad.bugs_domain))
253
254 if person.preferredemail is not None:
255 return format_address(person.displayname, person.preferredemail.email)
256
257 # XXX: Bjorn Tillenius 2006-04-05:
258 # The person doesn't have a preferred email set, but he
259 # added a comment (either via the email UI, or because he was
260 # imported as a deaf reporter). It shouldn't be possible to use the
261 # email UI if you don't have a preferred email set, but work around
262 # it for now by trying hard to find the right email address to use.
263 email_addresses = shortlist(
264 getUtility(IEmailAddressSet).getByPerson(person))
265 if not email_addresses:
266 # XXX: Bjorn Tillenius 2006-05-21 bug=33427:
267 # A user should always have at least one email address,
268 # but due to bug #33427, this isn't always the case.
269 return format_address(person.displayname,
270 "%s@%s" % (bug.id, config.launchpad.bugs_domain))
271
272 # At this point we have no validated emails to use: if any of the
273 # person's emails had been validated the preferredemail would be
274 # set. Since we have no idea of which email address is best to use,
275 # we choose the first one.
276 return format_address(person.displayname, email_addresses[0].email)
277
278
279def get_bugmail_replyto_address(bug):
280 """Return an appropriate bugmail Reply-To address.
281
282 :bug: the IBug.
283
284 :user: an IPerson whose name will appear in the From address, e.g.:
285
286 From: Foo Bar via Malone <123@bugs...>
287 """
288 return u"Bug %d <%s@%s>" % (bug.id, bug.id, config.launchpad.bugs_domain)
289
290
291def get_bugmail_error_address():
292 """Return a suitable From address for a bug transaction error email."""
293 return config.malone.bugmail_error_from_address
294
295
296def send_process_error_notification(to_address, subject, error_msg,136def send_process_error_notification(to_address, subject, error_msg,
297 original_msg, failing_command=None):137 original_msg, failing_command=None):
298 """Send a mail about an error occurring while using the email interface.138 """Send a mail about an error occurring while using the email interface.
299139
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py 2010-06-16 18:49:47 +0000
+++ lib/canonical/launchpad/security.py 2010-06-25 13:39:35 +0000
@@ -19,7 +19,7 @@
19 IArchivePermissionSet)19 IArchivePermissionSet)
20from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken20from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken
21from lp.soyuz.interfaces.archivesubscriber import (21from lp.soyuz.interfaces.archivesubscriber import (
22 IArchiveSubscriber, IPersonalArchiveSubscription)22 IArchiveSubscriber, IArchiveSubscriberSet, IPersonalArchiveSubscription)
23from lp.code.interfaces.branch import (23from lp.code.interfaces.branch import (
24 IBranch, user_has_special_branch_access)24 IBranch, user_has_special_branch_access)
25from lp.code.interfaces.branchmergeproposal import (25from lp.code.interfaces.branchmergeproposal import (
@@ -2040,6 +2040,13 @@
2040 if self.obj.is_ppa and self.obj.checkArchivePermission(user.person):2040 if self.obj.is_ppa and self.obj.checkArchivePermission(user.person):
2041 return True2041 return True
20422042
2043 # Subscribers can view private PPAs.
2044 if self.obj.is_ppa and self.obj.private:
2045 archive_subs = getUtility(IArchiveSubscriberSet).getBySubscriber(
2046 user.person, self.obj).any()
2047 if archive_subs:
2048 return True
2049
2043 return False2050 return False
20442051
2045 def checkUnauthenticated(self):2052 def checkUnauthenticated(self):
20462053
=== modified file 'lib/canonical/launchpad/webapp/launchpadform.py'
--- lib/canonical/launchpad/webapp/launchpadform.py 2010-03-15 16:58:49 +0000
+++ lib/canonical/launchpad/webapp/launchpadform.py 2010-06-25 13:39:35 +0000
@@ -16,6 +16,7 @@
16 ]16 ]
1717
18import transaction18import transaction
19
19from zope.interface import classImplements, providedBy20from zope.interface import classImplements, providedBy
20from zope.interface.advice import addClassAdvisor21from zope.interface.advice import addClassAdvisor
21from zope.event import notify22from zope.event import notify
@@ -243,7 +244,7 @@
243 self.errors.append(cleanmsg)244 self.errors.append(cleanmsg)
244245
245 @staticmethod246 @staticmethod
246 def validate_none(self, action, data):247 def validate_none(form, action, data):
247 """Do not do any validation.248 """Do not do any validation.
248249
249 This is to be used in subclasses that have actions in which no250 This is to be used in subclasses that have actions in which no
@@ -473,6 +474,10 @@
473 if referrer is None:474 if referrer is None:
474 # "referer" is misspelled in the HTTP specification.475 # "referer" is misspelled in the HTTP specification.
475 referrer = self.request.getHeader('referer')476 referrer = self.request.getHeader('referer')
477 # Windmill doesn't pass in a correct referer.
478 if (referrer is not None
479 and '/windmill-serv/remote.html' in referrer):
480 referrer = None
476 else:481 else:
477 attribute_name = self.request.form.get('_return_attribute_name')482 attribute_name = self.request.form.get('_return_attribute_name')
478 attribute_value = self.request.form.get('_return_attribute_value')483 attribute_value = self.request.form.get('_return_attribute_value')
479484
=== modified file 'lib/canonical/widgets/popup.py'
--- lib/canonical/widgets/popup.py 2010-01-29 10:52:58 +0000
+++ lib/canonical/widgets/popup.py 2010-06-25 13:39:35 +0000
@@ -180,6 +180,56 @@
180 return '/people/'180 return '/people/'
181181
182182
183class BugTrackerPickerWidget(VocabularyPickerWidget):
184 link_template = """
185 or (<a id="%(activator_id)s" href="/bugs/bugtrackers/+newbugtracker"
186 >Register an external bug tracker&hellip;</a>)
187 <script>
188 LPS.use('lp.bugs.bugtracker_overlay', function(Y) {
189 if (Y.UA.ie) {
190 return;
191 }
192 Y.on('domready', function () {
193 // After the success handler finishes, it calls the
194 // next_step function.
195 var next_step = function(bug_tracker) {
196 // Fill in the text field with either the name of
197 // the newly created bug tracker or the name of an
198 // existing bug tracker whose base_url matches.
199 var bugtracker_text_box = Y.one(
200 Y.DOM.byId('field.bugtracker.bugtracker'));
201 if (bugtracker_text_box !== null) {
202 bugtracker_text_box.set(
203 'value', bug_tracker.get('name'));
204 // It doesn't appear possible to use onChange
205 // event, so the onKeyPress event is explicitely
206 // fired here.
207 if (bugtracker_text_box.get('onkeypress')) {
208 bugtracker_text_box.get('onkeypress')();
209 }
210 bugtracker_text_box.scrollIntoView();
211 }
212 }
213 Y.lp.bugs.bugtracker_overlay.attach_widget({
214 activate_node: Y.get('#%(activator_id)s'),
215 next_step: next_step
216 });
217 });
218 });
219 </script>
220 """
221
222 def chooseLink(self):
223 link = super(BugTrackerPickerWidget, self).chooseLink()
224 link += self.link_template % dict(
225 activator_id='create-bugtracker-link')
226 return link
227
228 @property
229 def nonajax_uri(self):
230 return '/bugs/bugtrackers/'
231
232
183class SearchForUpstreamPopupWidget(VocabularyPickerWidget):233class SearchForUpstreamPopupWidget(VocabularyPickerWidget):
184 """A SinglePopupWidget with a custom error message.234 """A SinglePopupWidget with a custom error message.
185235
186236
=== modified file 'lib/canonical/widgets/product.py'
--- lib/canonical/widgets/product.py 2010-06-16 16:56:58 +0000
+++ lib/canonical/widgets/product.py 2010-06-25 13:39:35 +0000
@@ -37,7 +37,7 @@
37from canonical.launchpad.webapp import canonical_url37from canonical.launchpad.webapp import canonical_url
38from canonical.widgets.itemswidgets import (38from canonical.widgets.itemswidgets import (
39 CheckBoxMatrixWidget, LaunchpadRadioWidget)39 CheckBoxMatrixWidget, LaunchpadRadioWidget)
40from canonical.widgets.popup import VocabularyPickerWidget40from canonical.widgets.popup import BugTrackerPickerWidget
41from canonical.widgets.textwidgets import (41from canonical.widgets.textwidgets import (
42 LowerCaseTextWidget, StrippedTextWidget)42 LowerCaseTextWidget, StrippedTextWidget)
43from lp.registry.interfaces.product import IProduct43from lp.registry.interfaces.product import IProduct
@@ -57,7 +57,7 @@
57 self.bugtracker = Choice(57 self.bugtracker = Choice(
58 vocabulary="WebBugTracker",58 vocabulary="WebBugTracker",
59 __name__='bugtracker')59 __name__='bugtracker')
60 self.bugtracker_widget = CustomWidgetFactory(VocabularyPickerWidget)60 self.bugtracker_widget = CustomWidgetFactory(BugTrackerPickerWidget)
61 setUpWidget(61 setUpWidget(
62 self, 'bugtracker', self.bugtracker, IInputWidget,62 self, 'bugtracker', self.bugtracker, IInputWidget,
63 prefix=self.name, value=field.context.bugtracker,63 prefix=self.name, value=field.context.bugtracker,
@@ -82,7 +82,7 @@
82 if self.upstream_email_address_widget.extra is None:82 if self.upstream_email_address_widget.extra is None:
83 self.upstream_email_address_widget.extra = ''83 self.upstream_email_address_widget.extra = ''
84 self.upstream_email_address_widget.extra += (84 self.upstream_email_address_widget.extra += (
85 ' onkeypress="selectWidget(\'%s.3\', event);"' % self.name)85 ''' onkeypress="selectWidget('%s.3', event);"\n''' % self.name)
8686
87 def _renderItem(self, index, text, value, name, cssClass, checked=False):87 def _renderItem(self, index, text, value, name, cssClass, checked=False):
88 # This form has a custom need to render their labels separately,88 # This form has a custom need to render their labels separately,
@@ -192,7 +192,7 @@
192 self.upstream_email_address_widget.setRenderedValue(192 self.upstream_email_address_widget.setRenderedValue(
193 value.baseurl.lstrip('mailto:'))193 value.baseurl.lstrip('mailto:'))
194 external_bugtracker_email_text = "%s %s" % (194 external_bugtracker_email_text = "%s %s" % (
195 self._renderLabel("By emailing an upstream bug contact:", 3),195 self._renderLabel("By emailing an upstream bug contact:\n", 3),
196 self.upstream_email_address_widget())196 self.upstream_email_address_widget())
197 external_bugtracker_email_arguments = dict(197 external_bugtracker_email_arguments = dict(
198 index=3, text=external_bugtracker_email_text,198 index=3, text=external_bugtracker_email_text,
199199
=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
--- lib/lp/app/templates/base-layout-macros.pt 2010-06-17 19:25:53 +0000
+++ lib/lp/app/templates/base-layout-macros.pt 2010-06-25 13:39:35 +0000
@@ -181,6 +181,8 @@
181 <script type="text/javascript"181 <script type="text/javascript"
182 tal:attributes="src string:${lp_js}/lp/mapping.js"></script>182 tal:attributes="src string:${lp_js}/lp/mapping.js"></script>
183 <script type="text/javascript"183 <script type="text/javascript"
184 tal:attributes="src string:${lp_js}/bugs/bugtracker_overlay.js"></script>
185 <script type="text/javascript"
184 tal:attributes="src string:${lp_js}/registry/milestoneoverlay.js"></script>186 tal:attributes="src string:${lp_js}/registry/milestoneoverlay.js"></script>
185 <script type="text/javascript"187 <script type="text/javascript"
186 tal:attributes="src string:${lp_js}/registry/milestonetable.js"></script>188 tal:attributes="src string:${lp_js}/registry/milestonetable.js"></script>
187189
=== modified file 'lib/lp/bugs/browser/bug.py'
--- lib/lp/bugs/browser/bug.py 2010-06-07 19:48:29 +0000
+++ lib/lp/bugs/browser/bug.py 2010-06-25 13:39:35 +0000
@@ -58,9 +58,10 @@
58from lp.bugs.interfaces.cve import ICveSet58from lp.bugs.interfaces.cve import ICveSet
59from lp.bugs.interfaces.bugattachment import IBugAttachmentSet59from lp.bugs.interfaces.bugattachment import IBugAttachmentSet
60from lp.bugs.interfaces.bugnomination import IBugNominationSet60from lp.bugs.interfaces.bugnomination import IBugNominationSet
61from lp.bugs.mail.bugnotificationbuilder import format_rfc2822_date
6162
62from canonical.launchpad.mailnotification import (63from canonical.launchpad.mailnotification import (
63 MailWrapper, format_rfc2822_date)64 MailWrapper)
64from canonical.launchpad.searchbuilder import any, greater_than65from canonical.launchpad.searchbuilder import any, greater_than
65from canonical.launchpad.webapp import (66from canonical.launchpad.webapp import (
66 ContextMenu, LaunchpadEditFormView, LaunchpadFormView, LaunchpadView,67 ContextMenu, LaunchpadEditFormView, LaunchpadFormView, LaunchpadView,
6768
=== modified file 'lib/lp/bugs/browser/bugtracker.py'
--- lib/lp/bugs/browser/bugtracker.py 2009-09-04 08:17:15 +0000
+++ lib/lp/bugs/browser/bugtracker.py 2010-06-25 13:39:35 +0000
@@ -82,8 +82,8 @@
82 page_title = u"Register an external bug tracker"82 page_title = u"Register an external bug tracker"
83 schema = IBugTracker83 schema = IBugTracker
84 label = page_title84 label = page_title
85 field_names = ['name', 'bugtrackertype', 'title', 'summary',85 field_names = ['bugtrackertype', 'name', 'title', 'baseurl', 'summary',
86 'baseurl', 'contactdetails']86 'contactdetails']
8787
88 def setUpWidgets(self, context=None):88 def setUpWidgets(self, context=None):
89 # We only show those bug tracker types for which there can be89 # We only show those bug tracker types for which there can be
9090
=== modified file 'lib/lp/bugs/browser/configure.zcml'
--- lib/lp/bugs/browser/configure.zcml 2010-06-18 10:41:48 +0000
+++ lib/lp/bugs/browser/configure.zcml 2010-06-25 13:39:35 +0000
@@ -59,6 +59,12 @@
59 name="+bugs-text"59 name="+bugs-text"
60 attribute="__call__"/>60 attribute="__call__"/>
61 <browser:page61 <browser:page
62 for="lp.registry.interfaces.projectgroup.IProjectGroup"
63 class="lp.bugs.browser.bugtask.TextualBugTaskSearchListingView"
64 permission="zope.Public"
65 name="+bugs-text"
66 attribute="__call__"/>
67 <browser:page
62 for="lp.bugs.interfaces.bugtarget.IHasBugs"68 for="lp.bugs.interfaces.bugtarget.IHasBugs"
63 class="lp.bugs.browser.bugtask.BugTaskSearchListingView"69 class="lp.bugs.browser.bugtask.BugTaskSearchListingView"
64 permission="zope.Public"70 permission="zope.Public"
6571
=== modified file 'lib/lp/bugs/doc/bugnotification-email.txt'
--- lib/lp/bugs/doc/bugnotification-email.txt 2010-06-23 21:24:13 +0000
+++ lib/lp/bugs/doc/bugnotification-email.txt 2010-06-25 13:39:35 +0000
@@ -424,7 +424,7 @@
424The Reply-To: and From: addresses used to send email are generated in a424The Reply-To: and From: addresses used to send email are generated in a
425pair of handy functions defined in mailnotification.py:425pair of handy functions defined in mailnotification.py:
426426
427 >>> from canonical.launchpad.mailnotification import (427 >>> from lp.bugs.mail.bugnotificationbuilder import (
428 ... get_bugmail_from_address, get_bugmail_replyto_address)428 ... get_bugmail_from_address, get_bugmail_replyto_address)
429429
430The Reply-To address generation is straightforward:430The Reply-To address generation is straightforward:
431431
=== modified file 'lib/lp/bugs/interfaces/bugtarget.py'
--- lib/lp/bugs/interfaces/bugtarget.py 2010-06-18 07:54:36 +0000
+++ lib/lp/bugs/interfaces/bugtarget.py 2010-06-25 13:39:35 +0000
@@ -21,7 +21,7 @@
21 ]21 ]
2222
23from zope.interface import Interface, Attribute23from zope.interface import Interface, Attribute
24from zope.schema import Bool, Choice, List, Object, Text, TextLine24from zope.schema import Bool, Choice, Datetime, List, Object, Text, TextLine
2525
26from canonical.launchpad import _26from canonical.launchpad import _
27from canonical.launchpad.fields import Tag27from canonical.launchpad.fields import Tag
@@ -164,7 +164,13 @@
164 title=(164 title=(
165 u"Search for bugs that are linked to branches or for bugs "165 u"Search for bugs that are linked to branches or for bugs "
166 "that are not linked to branches."),166 "that are not linked to branches."),
167 vocabulary=BugBranchSearch, required=False))167 vocabulary=BugBranchSearch, required=False),
168 modified_since=Datetime(
169 title=(
170 u"Search for bugs that have been modified since the given "
171 "date."),
172 required=False),
173 )
168 @operation_returns_collection_of(IBugTask)174 @operation_returns_collection_of(IBugTask)
169 @export_read_operation()175 @export_read_operation()
170 def searchTasks(search_params, user=None,176 def searchTasks(search_params, user=None,
@@ -186,7 +192,7 @@
186 hardware_owner_is_affected_by_bug=False,192 hardware_owner_is_affected_by_bug=False,
187 hardware_owner_is_subscribed_to_bug=False,193 hardware_owner_is_subscribed_to_bug=False,
188 hardware_is_linked_to_bug=False, linked_branches=None,194 hardware_is_linked_to_bug=False, linked_branches=None,
189 structural_subscriber=None):195 structural_subscriber=None, modified_since=None):
190 """Search the IBugTasks reported on this entity.196 """Search the IBugTasks reported on this entity.
191197
192 :search_params: a BugTaskSearchParams object198 :search_params: a BugTaskSearchParams object
193199
=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py 2010-06-08 14:54:22 +0000
+++ lib/lp/bugs/interfaces/bugtask.py 2010-06-25 13:39:35 +0000
@@ -478,6 +478,12 @@
478 "Confirmed."),478 "Confirmed."),
479 readonly=True,479 readonly=True,
480 required=False))480 required=False))
481 date_incomplete = exported(
482 Datetime(title=_("Date Incomplete"),
483 description=_("The date on which this task was marked "
484 "Incomplete."),
485 readonly=True,
486 required=False))
481 date_inprogress = exported(487 date_inprogress = exported(
482 Datetime(title=_("Date In Progress"),488 Datetime(title=_("Date In Progress"),
483 description=_("The date on which this task was marked "489 description=_("The date on which this task was marked "
@@ -1084,8 +1090,8 @@
1084 hardware_owner_is_affected_by_bug=False,1090 hardware_owner_is_affected_by_bug=False,
1085 hardware_owner_is_subscribed_to_bug=False,1091 hardware_owner_is_subscribed_to_bug=False,
1086 hardware_is_linked_to_bug=False,1092 hardware_is_linked_to_bug=False,
1087 linked_branches=None, structural_subscriber=None1093 linked_branches=None, structural_subscriber=None,
1088 ):1094 modified_since=None):
10891095
1090 self.bug = bug1096 self.bug = bug
1091 self.searchtext = searchtext1097 self.searchtext = searchtext
@@ -1130,6 +1136,7 @@
1130 self.hardware_is_linked_to_bug = hardware_is_linked_to_bug1136 self.hardware_is_linked_to_bug = hardware_is_linked_to_bug
1131 self.linked_branches = linked_branches1137 self.linked_branches = linked_branches
1132 self.structural_subscriber = structural_subscriber1138 self.structural_subscriber = structural_subscriber
1139 self.modified_since = None
11331140
1134 def setProduct(self, product):1141 def setProduct(self, product):
1135 """Set the upstream context on which to filter the search."""1142 """Set the upstream context on which to filter the search."""
@@ -1203,7 +1210,7 @@
1203 hardware_owner_is_affected_by_bug=False,1210 hardware_owner_is_affected_by_bug=False,
1204 hardware_owner_is_subscribed_to_bug=False,1211 hardware_owner_is_subscribed_to_bug=False,
1205 hardware_is_linked_to_bug=False, linked_branches=None,1212 hardware_is_linked_to_bug=False, linked_branches=None,
1206 structural_subscriber=None):1213 structural_subscriber=None, modified_since=None):
1207 """Create and return a new instance using the parameter list."""1214 """Create and return a new instance using the parameter list."""
1208 search_params = cls(user=user, orderby=order_by)1215 search_params = cls(user=user, orderby=order_by)
12091216
@@ -1272,6 +1279,7 @@
1272 hardware_is_linked_to_bug)1279 hardware_is_linked_to_bug)
1273 search_params.linked_branches=linked_branches1280 search_params.linked_branches=linked_branches
1274 search_params.structural_subscriber = structural_subscriber1281 search_params.structural_subscriber = structural_subscriber
1282 search_params.modified_since = modified_since
12751283
1276 return search_params1284 return search_params
12771285
12781286
=== modified file 'lib/lp/bugs/interfaces/bugtracker.py'
--- lib/lp/bugs/interfaces/bugtracker.py 2010-04-15 08:45:31 +0000
+++ lib/lp/bugs/interfaces/bugtracker.py 2010-06-25 13:39:35 +0000
@@ -68,7 +68,8 @@
68 bugtracker = getUtility(IBugTrackerSet).queryByBaseURL(input)68 bugtracker = getUtility(IBugTrackerSet).queryByBaseURL(input)
69 if bugtracker is not None and bugtracker != self.context:69 if bugtracker is not None and bugtracker != self.context:
70 raise LaunchpadValidationError(70 raise LaunchpadValidationError(
71 "%s is already registered in Launchpad." % input)71 '%s is already registered in Launchpad as "%s" (%s).'
72 % (input, bugtracker.title, bugtracker.name))
7273
7374
74class BugTrackerType(DBEnumeratedType):75class BugTrackerType(DBEnumeratedType):
@@ -183,7 +184,7 @@
183 BugTrackerNameField(184 BugTrackerNameField(
184 title=_('Name'),185 title=_('Name'),
185 constraint=name_validator,186 constraint=name_validator,
186 description=_('An URL-friendly name for the bug tracker, '187 description=_('A URL-friendly name for the bug tracker, '
187 'such as "mozilla-bugs".')))188 'such as "mozilla-bugs".')))
188 title = exported(189 title = exported(
189 TextLine(190 TextLine(
190191
=== added directory 'lib/lp/bugs/javascript'
=== added file 'lib/lp/bugs/javascript/bugtracker_overlay.js'
--- lib/lp/bugs/javascript/bugtracker_overlay.js 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/javascript/bugtracker_overlay.js 2010-06-25 13:39:35 +0000
@@ -0,0 +1,131 @@
1/* Copyright 2010 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * A bugtracker form overlay that can create a bugtracker within any page.
5 *
6 * @namespace Y.lp.bugs.bugtracker_overlay
7 * @requires dom, node, io-base, lazr.anim, lazr.formoverlay
8 */
9YUI.add('lp.bugs.bugtracker_overlay', function(Y) {
10 Y.log('loading lp.bugs.bugtracker_overlay');
11 var namespace = Y.namespace('lp.bugs.bugtracker_overlay');
12
13 var bugtracker_form;
14 var next_step;
15
16 var save_new_bugtracker = function(data) {
17
18 var parameters = {
19 bug_tracker_type: data['field.bugtrackertype'][0],
20 name: data['field.name'][0].toLowerCase(),
21 title: data['field.title'][0],
22 base_url: data['field.baseurl'][0],
23 summary: data['field.summary'][0],
24 contact_details: data['field.contactdetails'][0]
25 };
26
27 var finish_new_bugtracker = function(entry) {
28 bugtracker_form.clearError();
29 bugtracker_form.hide();
30 // Reset the HTML form inside the widget.
31 bugtracker_form.get('contentBox').one('form').reset();
32 next_step(entry);
33 };
34
35 var client = new LP.client.Launchpad();
36 client.named_post('/bugs/bugtrackers', 'ensureBugTracker', {
37 parameters: parameters,
38 on: {
39 success: finish_new_bugtracker,
40 failure: function (ignore, response, args) {
41 var error_box = Y.one('#bugtracker-error');
42 var error_message = response.statusText + '\n\n' +
43 response.responseText;
44 bugtracker_form.showError(error_message);
45 // XXX EdwinGrubbs 2007-06-18 bug=596025
46 // This should be done by FormOverlay.showError().
47 bugtracker_form.error_node.scrollIntoView();
48 }
49 }
50 });
51 };
52
53
54 var setup_bugtracker_form = function () {
55 var form_submit_button = Y.Node.create(
56 '<input type="submit" name="field.actions.register" ' +
57 'id="formoverlay-add-bugtracker" value="Create bug tracker"/>');
58 bugtracker_form = new Y.lazr.FormOverlay({
59 headerContent: '<h2>Create Bug Tracker</h2>',
60 form_submit_button: form_submit_button,
61 centered: true,
62 form_submit_callback: save_new_bugtracker,
63 visible: false
64 });
65 bugtracker_form.loadFormContentAndRender(
66 '/bugs/bugtrackers/+newbugtracker/++form++');
67 // XXX EdwinGrubbs 2010-06-18 bug=596130
68 // render() and show() will actually be called before the
69 // asynchronous io call finishes, so the widget appears first
70 // without any content. However, this is better than loading the
71 // form every time the page loads despite the form overlay being
72 // used rarely.
73 bugtracker_form.render();
74 bugtracker_form.show();
75 };
76
77 var show_bugtracker_form = function(e) {
78 e.preventDefault();
79 if (bugtracker_form) {
80 bugtracker_form.show();
81 } else {
82 // This function call is asynchronous, so we can move
83 // bugtracker_form.show() below it.
84 setup_bugtracker_form();
85 }
86
87 // XXX EdwinGrubbs 2010-06-18 bug=596113
88 // FormOverlay calls centered(), which can cause this tall form
89 // to be position where the top of the form is no longer
90 // accessible.
91 var bounding_box = bugtracker_form.get('boundingBox');
92 var min_top = 10;
93 if (bounding_box.get('offsetTop') < min_top) {
94 bounding_box.setStyle('top', min_top + 'px');
95 }
96 };
97
98 /**
99 * Attaches a bugtracker form overlay widget to an element.
100 *
101 * @method attach_widget
102 * @param {Object} config Object literal of config name/value pairs.
103 * activate_node is the node that shows the form
104 * when it is clicked.
105 * next_step is the function to be called after
106 * the bugtracker is created.
107 */
108 namespace.attach_widget = function(config) {
109 Y.log('lp.bugs.bugtracker_overlay.attach_widget()');
110 if (Y.UA.ie) {
111 return;
112 }
113 if (config === undefined) {
114 throw new Error(
115 "Missing attach_widget config for bugtracker_overlay.");
116 }
117 if (config.activate_node === undefined ||
118 config.next_step === undefined) {
119 throw new Error(
120 "attach_widget config for bugtracker_overlay has " +
121 "undefined properties.");
122 }
123 next_step = config.next_step;
124 Y.log('lp.bugs.bugtracker_overlay.attach_widget() setup onclick');
125 config.activate_node.addClass('js-action');
126 config.activate_node.on('click', show_bugtracker_form);
127 };
128
129}, "0.1", {"requires": [
130 "dom", "node", "io-base", "lazr.anim", "lazr.formoverlay", "lp.calendar"
131 ]});
0132
=== added directory 'lib/lp/bugs/javascript/tests'
=== added file 'lib/lp/bugs/mail/bugnotificationbuilder.py'
--- lib/lp/bugs/mail/bugnotificationbuilder.py 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/mail/bugnotificationbuilder.py 2010-06-25 13:39:35 +0000
@@ -0,0 +1,187 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Bug notification building code."""
5
6__metaclass__ = type
7__all__ = [
8 'BugNotificationBuilder',
9 'format_rfc2822_date',
10 'get_bugmail_error_address',
11 'get_bugmail_from_address',
12 ]
13
14import rfc822
15from email.MIMEText import MIMEText
16from email.Utils import formatdate
17
18from zope.component import getUtility
19
20from canonical.config import config
21from canonical.launchpad.helpers import shortlist
22from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
23from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
24from canonical.launchpad.mail import format_address
25
26
27def format_rfc2822_date(date):
28 """Formats a date according to RFC2822's desires."""
29 return formatdate(rfc822.mktime_tz(date.utctimetuple() + (0, )))
30
31
32def get_bugmail_from_address(person, bug):
33 """Returns the right From: address to use for a bug notification."""
34 if person == getUtility(ILaunchpadCelebrities).janitor:
35 return format_address(
36 'Launchpad Bug Tracker',
37 "%s@%s" % (bug.id, config.launchpad.bugs_domain))
38
39 if person.hide_email_addresses:
40 return format_address(
41 person.displayname,
42 "%s@%s" % (bug.id, config.launchpad.bugs_domain))
43
44 if person.preferredemail is not None:
45 return format_address(person.displayname, person.preferredemail.email)
46
47 # XXX: Bjorn Tillenius 2006-04-05:
48 # The person doesn't have a preferred email set, but he
49 # added a comment (either via the email UI, or because he was
50 # imported as a deaf reporter). It shouldn't be possible to use the
51 # email UI if you don't have a preferred email set, but work around
52 # it for now by trying hard to find the right email address to use.
53 email_addresses = shortlist(
54 getUtility(IEmailAddressSet).getByPerson(person))
55 if not email_addresses:
56 # XXX: Bjorn Tillenius 2006-05-21 bug=33427:
57 # A user should always have at least one email address,
58 # but due to bug #33427, this isn't always the case.
59 return format_address(person.displayname,
60 "%s@%s" % (bug.id, config.launchpad.bugs_domain))
61
62 # At this point we have no validated emails to use: if any of the
63 # person's emails had been validated the preferredemail would be
64 # set. Since we have no idea of which email address is best to use,
65 # we choose the first one.
66 return format_address(person.displayname, email_addresses[0].email)
67
68
69def get_bugmail_replyto_address(bug):
70 """Return an appropriate bugmail Reply-To address.
71
72 :bug: the IBug.
73
74 :user: an IPerson whose name will appear in the From address, e.g.:
75
76 From: Foo Bar via Malone <123@bugs...>
77 """
78 return u"Bug %d <%s@%s>" % (bug.id, bug.id, config.launchpad.bugs_domain)
79
80
81def get_bugmail_error_address():
82 """Return a suitable From address for a bug transaction error email."""
83 return config.malone.bugmail_error_from_address
84
85
86class BugNotificationBuilder:
87 """Constructs a MIMEText message for a bug notification.
88
89 Takes a bug and a set of headers and returns a new MIMEText
90 object. Common and expensive to calculate headers are cached
91 up-front.
92 """
93
94 def __init__(self, bug):
95 self.bug = bug
96
97 # Pre-calculate common headers.
98 self.common_headers = [
99 ('Reply-To', get_bugmail_replyto_address(bug)),
100 ('Sender', config.canonical.bounce_address),
101 ]
102
103 # X-Launchpad-Bug
104 self.common_headers.extend(
105 ('X-Launchpad-Bug', bugtask.asEmailHeaderValue())
106 for bugtask in bug.bugtasks)
107
108 # X-Launchpad-Bug-Tags
109 if len(bug.tags) > 0:
110 self.common_headers.append(
111 ('X-Launchpad-Bug-Tags', ' '.join(bug.tags)))
112
113 # Add the X-Launchpad-Bug-Private header. This is a simple
114 # yes/no value denoting privacy for the bug.
115 if bug.private:
116 self.common_headers.append(
117 ('X-Launchpad-Bug-Private', 'yes'))
118 else:
119 self.common_headers.append(
120 ('X-Launchpad-Bug-Private', 'no'))
121
122 # Add the X-Launchpad-Bug-Security-Vulnerability header to
123 # denote security for this bug. This follows the same form as
124 # the -Bug-Private header.
125 if bug.security_related:
126 self.common_headers.append(
127 ('X-Launchpad-Bug-Security-Vulnerability', 'yes'))
128 else:
129 self.common_headers.append(
130 ('X-Launchpad-Bug-Security-Vulnerability', 'no'))
131
132 # Add the -Bug-Commenters header, a space-separated list of
133 # distinct IDs of people who have commented on the bug. The
134 # list is sorted to aid testing.
135 commenters = set(message.owner.name for message in bug.messages)
136 self.common_headers.append(
137 ('X-Launchpad-Bug-Commenters', ' '.join(sorted(commenters))))
138
139 # Add the -Bug-Reporter header to identify the owner of the bug
140 # and the original bug task for filtering
141 self.common_headers.append(
142 ('X-Launchpad-Bug-Reporter',
143 '%s (%s)' % ( bug.owner.displayname, bug.owner.name )))
144
145 def build(self, from_address, to_address, body, subject, email_date,
146 rationale=None, references=None, message_id=None):
147 """Construct the notification.
148
149 :param from_address: The From address of the notification.
150 :param to_address: The To address for the notification.
151 :param body: The body text of the notification.
152 :type body: unicode
153 :param subject: The Subject of the notification.
154 :param email_date: The Date for the notification.
155 :param rationale: The rationale for why the recipient is
156 receiving this notification.
157 :param references: A value for the References header.
158 :param message_id: A value for the Message-ID header.
159
160 :return: An `email.MIMEText.MIMEText` object.
161 """
162 message = MIMEText(body.encode('utf8'), 'plain', 'utf8')
163 message['Date'] = format_rfc2822_date(email_date)
164 message['From'] = from_address
165 message['To'] = to_address
166
167 # Add the common headers.
168 for header in self.common_headers:
169 message.add_header(*header)
170
171 if references is not None:
172 message['References'] = ' '.join(references)
173 if message_id is not None:
174 message['Message-Id'] = message_id
175
176 subject_prefix = "[Bug %d]" % self.bug.id
177 if subject is None:
178 message['Subject'] = subject_prefix
179 elif subject_prefix in subject:
180 message['Subject'] = subject
181 else:
182 message['Subject'] = "%s %s" % (subject_prefix, subject)
183
184 if rationale is not None:
185 message.add_header('X-Launchpad-Message-Rationale', rationale)
186
187 return message
0188
=== modified file 'lib/lp/bugs/model/bugtarget.py'
--- lib/lp/bugs/model/bugtarget.py 2010-06-11 09:41:07 +0000
+++ lib/lp/bugs/model/bugtarget.py 2010-06-25 13:39:35 +0000
@@ -65,7 +65,8 @@
65 hardware_owner_is_bug_reporter=None,65 hardware_owner_is_bug_reporter=None,
66 hardware_owner_is_affected_by_bug=False,66 hardware_owner_is_affected_by_bug=False,
67 hardware_owner_is_subscribed_to_bug=False,67 hardware_owner_is_subscribed_to_bug=False,
68 hardware_is_linked_to_bug=False, linked_branches=None):68 hardware_is_linked_to_bug=False, linked_branches=None,
69 modified_since=None):
69 """See `IHasBugs`."""70 """See `IHasBugs`."""
70 if status is None:71 if status is None:
71 # If no statuses are supplied, default to the72 # If no statuses are supplied, default to the
7273
=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py 2010-06-23 22:39:15 +0000
+++ lib/lp/bugs/model/bugtask.py 2010-06-25 13:39:35 +0000
@@ -1833,6 +1833,11 @@
1833 # we don't need to add any clause.1833 # we don't need to add any clause.
1834 pass1834 pass
18351835
1836 if params.modified_since:
1837 extra_clauses.append(
1838 "Bug.date_last_updated > %s" % (
1839 sqlvalues(params.modified_since,)))
1840
1836 orderby_arg = self._processOrderBy(params)1841 orderby_arg = self._processOrderBy(params)
18371842
1838 query = " AND ".join(extra_clauses)1843 query = " AND ".join(extra_clauses)
18391844
=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
--- lib/lp/bugs/scripts/bugnotification.py 2009-11-17 17:33:28 +0000
+++ lib/lp/bugs/scripts/bugnotification.py 2010-06-25 13:39:35 +0000
@@ -16,8 +16,9 @@
16from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities16from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
17from lp.registry.interfaces.person import IPersonSet17from lp.registry.interfaces.person import IPersonSet
18from canonical.launchpad.mailnotification import (18from canonical.launchpad.mailnotification import (
19 generate_bug_add_email, MailWrapper, BugNotificationBuilder,19 generate_bug_add_email, MailWrapper)
20 get_bugmail_from_address)20from lp.bugs.mail.bugnotificationbuilder import (
21 BugNotificationBuilder, get_bugmail_from_address)
21from canonical.launchpad.scripts.logger import log22from canonical.launchpad.scripts.logger import log
22from canonical.launchpad.webapp import canonical_url23from canonical.launchpad.webapp import canonical_url
2324
2425
=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt 2010-06-03 21:49:47 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt 2010-06-25 13:39:35 +0000
@@ -281,6 +281,14 @@
281 >>> print anon_browser.contents281 >>> print anon_browser.contents
282 10282 10
283283
284This page is also available for project groups.
285
286 >>> anon_browser.open('http://launchpad.dev/mozilla/+bugs-text')
287 >>> print anon_browser.contents
288 15
289 5
290 4
291
284292
285== Private bugs ==293== Private bugs ==
286294
287295
=== modified file 'lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt'
--- lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt 2010-06-16 15:56:08 +0000
+++ lib/lp/bugs/stories/bugtracker/xx-bugtracker.txt 2010-06-25 13:39:35 +0000
@@ -72,7 +72,8 @@
72 >>> for message in find_tags_by_class(user_browser.contents, 'message'):72 >>> for message in find_tags_by_class(user_browser.contents, 'message'):
73 ... print extract_text(message)73 ... print extract_text(message)
74 There is 1 error.74 There is 1 error.
75 http://bugzilla.mozilla.org/ is already registered in Launchpad.75 http://bugzilla.mozilla.org/ is already registered in Launchpad
76 as "The Mozilla.org Bug Tracker" (mozilla.org).
7677
77The same happens if the requested URL is aliased to another bug78The same happens if the requested URL is aliased to another bug
78tracker. Aliases can be edited once a bug tracker has been added, but79tracker. Aliases can be edited once a bug tracker has been added, but
@@ -94,7 +95,8 @@
94 >>> for message in find_tags_by_class(user_browser.contents, 'message'):95 >>> for message in find_tags_by_class(user_browser.contents, 'message'):
95 ... print extract_text(message)96 ... print extract_text(message)
96 There is 1 error.97 There is 1 error.
97 http://alias.example.com/ is already registered in Launchpad.98 http://alias.example.com/ is already registered in Launchpad
99 as "GnomeGBug GTracker" (gnome-bugzilla).
98100
99After successfully registering the bug tracker, the user is redirected101After successfully registering the bug tracker, the user is redirected
100to the bug tracker page.102to the bug tracker page.
@@ -201,7 +203,8 @@
201 >>> for message in get_feedback_messages(user_browser.contents):203 >>> for message in get_feedback_messages(user_browser.contents):
202 ... print message204 ... print message
203 There is 1 error.205 There is 1 error.
204 http://bugzilla.mozilla.org/ is already registered in Launchpad.206 http://bugzilla.mozilla.org/ is already registered in Launchpad
207 as "The Mozilla.org Bug Tracker" (mozilla.org).
205208
206If the user inadvertently enters an invalid URL, they are shown an209If the user inadvertently enters an invalid URL, they are shown an
207informative error message explaining why it is invalid.210informative error message explaining why it is invalid.
@@ -304,7 +307,8 @@
304 >>> for message in get_feedback_messages(user_browser.contents):307 >>> for message in get_feedback_messages(user_browser.contents):
305 ... print message308 ... print message
306 There is 1 error.309 There is 1 error.
307 http://bugzilla.mozilla.org/ is already registered in Launchpad.310 http://bugzilla.mozilla.org/ is already registered in Launchpad
311 as "The Mozilla.org Bug Tracker" (mozilla.org).
308312
309Multiple aliases can be entered by separating URLs with whitespace.313Multiple aliases can be entered by separating URLs with whitespace.
310314
311315
=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
--- lib/lp/bugs/stories/webservice/xx-bug.txt 2010-06-10 18:55:22 +0000
+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2010-06-25 13:39:35 +0000
@@ -327,6 +327,7 @@
327 date_fix_committed: None327 date_fix_committed: None
328 date_fix_released: None328 date_fix_released: None
329 date_in_progress: None329 date_in_progress: None
330 date_incomplete: None
330 date_left_closed: None331 date_left_closed: None
331 date_left_new: None332 date_left_new: None
332 date_triaged: None333 date_triaged: None
@@ -1436,6 +1437,15 @@
1436 total_size: 01437 total_size: 0
1437 ---1438 ---
14381439
1440It can also be used to find bugs modified since a certain date.
1441
1442 >>> pprint_collection(webservice.named_get(
1443 ... '/ubuntu', 'searchTasks',
1444 ... modified_since=u'2011-01-01T00:00:00+00:00').jsonBody())
1445 start: None
1446 total_size: 0
1447 ---
1448
1439It is possible to search for bugs targeted to a milestone within a1449It is possible to search for bugs targeted to a milestone within a
1440project group.1450project group.
14411451
14421452
=== modified file 'lib/lp/bugs/tests/test_bugtask.py'
--- lib/lp/bugs/tests/test_bugtask.py 2010-06-21 18:09:47 +0000
+++ lib/lp/bugs/tests/test_bugtask.py 2010-06-25 13:39:35 +0000
@@ -3,6 +3,7 @@
33
4__metaclass__ = type4__metaclass__ = type
55
6from datetime import timedelta
6import unittest7import unittest
78
8from zope.component import getUtility9from zope.component import getUtility
@@ -14,8 +15,10 @@
14from lp.hardwaredb.interfaces.hwdb import HWBus, IHWDeviceSet15from lp.hardwaredb.interfaces.hwdb import HWBus, IHWDeviceSet
15from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities16from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
16from canonical.launchpad.searchbuilder import all, any17from canonical.launchpad.searchbuilder import all, any
17from canonical.testing import LaunchpadFunctionalLayer, LaunchpadZopelessLayer18from canonical.testing import (
19 DatabaseFunctionalLayer, LaunchpadFunctionalLayer, LaunchpadZopelessLayer)
1820
21from lp.bugs.interfaces.bugtarget import IBugTarget
19from lp.bugs.interfaces.bugtask import (22from lp.bugs.interfaces.bugtask import (
20 BugTaskImportance, BugTaskSearchParams, BugTaskStatus)23 BugTaskImportance, BugTaskSearchParams, BugTaskStatus)
21from lp.bugs.model.bugtask import build_tag_search_clause24from lp.bugs.model.bugtask import build_tag_search_clause
@@ -28,7 +31,7 @@
2831
29class TestBugTaskDelta(TestCaseWithFactory):32class TestBugTaskDelta(TestCaseWithFactory):
3033
31 layer = LaunchpadFunctionalLayer34 layer = DatabaseFunctionalLayer
3235
33 def setUp(self):36 def setUp(self):
34 super(TestBugTaskDelta, self).setUp()37 super(TestBugTaskDelta, self).setUp()
@@ -66,7 +69,6 @@
6669
67 def test_get_bugwatch_delta(self):70 def test_get_bugwatch_delta(self):
68 # Exercise getDelta() with a change to bugwatch.71 # Exercise getDelta() with a change to bugwatch.
69 user = self.factory.makePerson()
70 bug_task = self.factory.makeBugTask()72 bug_task = self.factory.makeBugTask()
71 bug_task_before_modification = Snapshot(73 bug_task_before_modification = Snapshot(
72 bug_task, providing=providedBy(bug_task))74 bug_task, providing=providedBy(bug_task))
@@ -501,9 +503,9 @@
501 [bugtask.bug.id for bugtask in bugtasks])503 [bugtask.bug.id for bugtask in bugtasks])
502504
503505
504class TestBugTaskPermissionsToSetAssigneeBase(TestCaseWithFactory):506class TestBugTaskPermissionsToSetAssigneeMixin:
505507
506 layer = LaunchpadFunctionalLayer508 layer = DatabaseFunctionalLayer
507509
508 def setUp(self):510 def setUp(self):
509 """Create the test setup.511 """Create the test setup.
@@ -516,7 +518,7 @@
516 owners, bug supervisors, drivers518 owners, bug supervisors, drivers
517 - bug tasks for the targets519 - bug tasks for the targets
518 """520 """
519 super(TestBugTaskPermissionsToSetAssigneeBase, self).setUp()521 super(TestBugTaskPermissionsToSetAssigneeMixin, self).setUp()
520 self.target_owner_member = self.factory.makePerson()522 self.target_owner_member = self.factory.makePerson()
521 self.target_owner_team = self.factory.makeTeam(523 self.target_owner_team = self.factory.makeTeam(
522 owner=self.target_owner_member)524 owner=self.target_owner_member)
@@ -556,6 +558,14 @@
556 self.target_bugtask.transitionToAssignee(self.regular_user)558 self.target_bugtask.transitionToAssignee(self.regular_user)
557 logout()559 logout()
558560
561 def makeTarget(self):
562 """Create a target and a series.
563
564 The target and series must be assigned as attributes of self:
565 'self.target' and 'self.series'.
566 """
567 raise NotImplementedError(self.makeTarget)
568
559 def test_userCanSetAnyAssignee_anonymous_user(self):569 def test_userCanSetAnyAssignee_anonymous_user(self):
560 # Anonymous users cannot set anybody as an assignee.570 # Anonymous users cannot set anybody as an assignee.
561 login(ANONYMOUS)571 login(ANONYMOUS)
@@ -700,7 +710,7 @@
700710
701711
702class TestProductBugTaskPermissionsToSetAssignee(712class TestProductBugTaskPermissionsToSetAssignee(
703 TestBugTaskPermissionsToSetAssigneeBase):713 TestBugTaskPermissionsToSetAssigneeMixin, TestCaseWithFactory):
704714
705 def makeTarget(self):715 def makeTarget(self):
706 """Create a product and a product series."""716 """Create a product and a product series."""
@@ -709,7 +719,7 @@
709719
710720
711class TestDistributionBugTaskPermissionsToSetAssignee(721class TestDistributionBugTaskPermissionsToSetAssignee(
712 TestBugTaskPermissionsToSetAssigneeBase):722 TestBugTaskPermissionsToSetAssigneeMixin, TestCaseWithFactory):
713723
714 def makeTarget(self):724 def makeTarget(self):
715 """Create a distribution and a distroseries."""725 """Create a distribution and a distroseries."""
@@ -718,14 +728,73 @@
718 self.series = self.factory.makeDistroSeries(self.target)728 self.series = self.factory.makeDistroSeries(self.target)
719729
720730
731class TestBugTaskSearch(TestCaseWithFactory):
732
733 layer = DatabaseFunctionalLayer
734
735 def login(self):
736 # Log in as an arbitrary person.
737 person = self.factory.makePerson()
738 login_person(person)
739 self.addCleanup(logout)
740 return person
741
742 def makeBugTarget(self):
743 """Make an arbitrary bug target with no tasks on it."""
744 return IBugTarget(self.factory.makeProduct())
745
746 def test_no_tasks(self):
747 # A brand new bug target has no tasks.
748 target = self.makeBugTarget()
749 self.assertEqual([], list(target.searchTasks(None)))
750
751 def test_new_task_shows_up(self):
752 # When we create a new bugtask on the target, it shows up in
753 # searchTasks.
754 target = self.makeBugTarget()
755 self.login()
756 task = self.factory.makeBugTask(target=target)
757 self.assertEqual([task], list(target.searchTasks(None)))
758
759 def test_modified_since_excludes_earlier_bugtasks(self):
760 # When we search for bug tasks that have been modified since a certain
761 # time, tasks for bugs that have not been modified since then are
762 # excluded.
763 target = self.makeBugTarget()
764 self.login()
765 task = self.factory.makeBugTask(target=target)
766 date = task.bug.date_last_updated + timedelta(days=1)
767 result = target.searchTasks(None, modified_since=date)
768 self.assertEqual([], list(result))
769
770 def test_modified_since_includes_later_bugtasks(self):
771 # When we search for bug tasks that have been modified since a certain
772 # time, tasks for bugs that have been modified since then are
773 # included.
774 target = self.makeBugTarget()
775 self.login()
776 task = self.factory.makeBugTask(target=target)
777 date = task.bug.date_last_updated - timedelta(days=1)
778 result = target.searchTasks(None, modified_since=date)
779 self.assertEqual([task], list(result))
780
781 def test_modified_since_includes_later_bugtasks_excludes_earlier(self):
782 # When we search for bugs that have been modified since a certain
783 # time, tasks for bugs that have been modified since then are
784 # included, tasks that have not are excluded.
785 target = self.makeBugTarget()
786 self.login()
787 task1 = self.factory.makeBugTask(target=target)
788 date = task1.bug.date_last_updated
789 task1.bug.date_last_updated -= timedelta(days=1)
790 task2 = self.factory.makeBugTask(target=target)
791 task2.bug.date_last_updated += timedelta(days=1)
792 result = target.searchTasks(None, modified_since=date)
793 self.assertEqual([task2], list(result))
794
795
721def test_suite():796def test_suite():
722 suite = unittest.TestSuite()797 suite = unittest.TestSuite()
723 suite.addTest(unittest.makeSuite(TestBugTaskDelta))798 suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
724 suite.addTest(unittest.makeSuite(TestBugTaskTagSearchClauses))
725 suite.addTest(unittest.makeSuite(TestBugTaskHardwareSearch))
726 suite.addTest(unittest.makeSuite(
727 TestProductBugTaskPermissionsToSetAssignee))
728 suite.addTest(unittest.makeSuite(
729 TestDistributionBugTaskPermissionsToSetAssignee))
730 suite.addTest(DocTestSuite('lp.bugs.model.bugtask'))799 suite.addTest(DocTestSuite('lp.bugs.model.bugtask'))
731 return suite800 return suite
732801
=== modified file 'lib/lp/registry/javascript/milestoneoverlay.js'
--- lib/lp/registry/javascript/milestoneoverlay.js 2010-04-29 15:21:05 +0000
+++ lib/lp/registry/javascript/milestoneoverlay.js 2010-06-25 13:39:35 +0000
@@ -71,7 +71,8 @@
71 milestone_form.show();71 milestone_form.show();
72 };72 };
7373
74 show_milestone_form = function(e) {74 var show_milestone_form = function(e) {
75 e.preventDefault();
75 if (milestone_form) {76 if (milestone_form) {
76 milestone_form.show();77 milestone_form.show();
77 } else {78 } else {
@@ -79,7 +80,6 @@
79 // milestone_form.show() below it.80 // milestone_form.show() below it.
80 setup_milestone_form();81 setup_milestone_form();
81 }82 }
82 e.preventDefault();
83 };83 };
8484
85 /**85 /**
8686
=== added file 'lib/lp/registry/windmill/tests/test_add_bugtracker.py'
--- lib/lp/registry/windmill/tests/test_add_bugtracker.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/windmill/tests/test_add_bugtracker.py 2010-06-25 13:39:35 +0000
@@ -0,0 +1,100 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test adding bug tracker in formoverlay."""
5
6__metaclass__ = type
7__all__ = []
8
9import unittest
10
11from canonical.launchpad.windmill.testing import lpuser
12
13from lp.registry.windmill.testing import RegistryWindmillLayer
14from lp.testing import WindmillTestCase
15
16
17def test_inline_add_bugtracker(client, url, name=None, suite='bugtracker',
18 user=lpuser.FOO_BAR):
19 """Test the form overlay for adding a bugtracker.
20
21 :param name: Name of the test.
22 :param url: Starting url.
23 :param suite: The suite in which this test is part of.
24 :param user: The user who should be logged in.
25 """
26 bugtracker_name = u'FOObar'
27 title = u'\xdf-title-%s' % bugtracker_name
28 location = u'http://example.com/%s' % bugtracker_name
29
30 user.ensure_login(client)
31 client.open(url=url)
32 client.waits.forPageLoad(timeout=u'20000')
33
34 client.waits.forElement(id=u'create-bugtracker-link')
35
36 # Click the "Create external bug tracker" link.
37 client.click(id=u'create-bugtracker-link')
38
39 # Submit bugtracker form.
40 client.waits.forElement(id=u'field.name')
41 client.type(id='field.name', text=bugtracker_name)
42 client.type(id='field.title', text=title)
43 client.type(id='field.baseurl', text=location)
44 client.click(id=u'formoverlay-add-bugtracker')
45
46 # Verify that the bugtracker name was entered in the text box.
47 client.waits.sleep(milliseconds='1000')
48 client.asserts.assertProperty(
49 id="field.bugtracker.bugtracker",
50 validator='value|%s' % bugtracker_name.lower())
51 client.asserts.assertChecked(id="field.bugtracker.2")
52
53 # Verify error message when trying to create a bugtracker with a
54 # conflicting name.
55 client.click(id=u'create-bugtracker-link')
56 client.waits.forElement(id=u'field.name')
57 client.type(id='field.name', text=bugtracker_name)
58 client.click(id=u'formoverlay-add-bugtracker')
59 client.waits.forElement(
60 xpath="//div[contains(@class, 'yui-lazr-formoverlay-errors')]/ul/li")
61 client.asserts.assertTextIn(
62 classname='yui-lazr-formoverlay-errors',
63 validator='name: %s is already in use' % bugtracker_name.lower())
64 client.click(classname='close-button')
65
66 # Configure bug tracker for the project.
67 client.click(id=u'field.actions.change')
68
69 # You should now be on the project index page.
70 client.waits.forElement(
71 xpath="//a[contains(@class, 'menu-link-configure_bugtracker')]")
72 client.click(
73 xpath="//a[contains(@class, 'menu-link-configure_bugtracker')]")
74
75 # Verify that the new bug tracker was configured for this project.
76 client.waits.forElement(id="field.bugtracker.bugtracker")
77 client.asserts.assertProperty(
78 id="field.bugtracker.bugtracker",
79 validator='value|%s' % bugtracker_name.lower())
80 client.asserts.assertChecked(id="field.bugtracker.2")
81
82
83class TestAddBugTracker(WindmillTestCase):
84 """Test form overlay widget for adding a bug tracker."""
85
86 # This test doesn't run well in the BugsWindmillLayer, since
87 # submitting the +configure-bugtracker form takes you back to
88 # the project index page, which is not on the bugs.launchpad.dev.
89 layer = RegistryWindmillLayer
90 suite_name = 'AddBugTracker'
91
92 def test_adding_bugtracker_for_project(self):
93 test_inline_add_bugtracker(
94 self.client,
95 url='http://launchpad.dev:8085/bzr/+configure-bugtracker',
96 name='test_inline_add_bugtracker_for_project')
97
98
99def test_suite():
100 return unittest.TestLoader().loadTestsFromName(__name__)
0101
=== modified file 'lib/lp/registry/windmill/tests/test_add_milestone.py'
--- lib/lp/registry/windmill/tests/test_add_milestone.py 2010-02-01 18:37:00 +0000
+++ lib/lp/registry/windmill/tests/test_add_milestone.py 2010-06-25 13:39:35 +0000
@@ -1,7 +1,7 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test for translation import queue behaviour."""4"""Test adding milestone in formoverlay."""
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = []7__all__ = []
@@ -24,9 +24,7 @@
24 :param suite: The suite in which this test is part of.24 :param suite: The suite in which this test is part of.
25 :param user: The user who should be logged in.25 :param user: The user who should be logged in.
26 """26 """
27 # Ensure that the milestone name doesn't conflict with previous27 milestone_name = u'FOObar'
28 # test runs, and test that it correctly lowercases the name.
29 milestone_name = u'FOObar%x' % int(time.time())
30 code_name = u'code-%s' % milestone_name28 code_name = u'code-%s' % milestone_name
3129
32 user.ensure_login(client)30 user.ensure_login(client)
3331
=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py 2010-06-21 19:29:34 +0000
+++ lib/lp/soyuz/browser/archive.py 2010-06-25 13:39:35 +0000
@@ -35,6 +35,7 @@
35from zope.component import getUtility35from zope.component import getUtility
36from zope.formlib import form36from zope.formlib import form
37from zope.interface import implements, Interface37from zope.interface import implements, Interface
38from zope.security.interfaces import Unauthorized
38from zope.security.proxy import removeSecurityProxy39from zope.security.proxy import removeSecurityProxy
39from zope.schema import Choice, List, TextLine40from zope.schema import Choice, List, TextLine
40from zope.schema.interfaces import IContextSourceBinder41from zope.schema.interfaces import IContextSourceBinder
@@ -426,7 +427,12 @@
426427
427 def packages(self):428 def packages(self):
428 text = 'View package details'429 text = 'View package details'
429 return Link('+packages', text, icon='info')430 link = Link('+packages', text, icon='info')
431 # Disable the link for P3As if they don't have upload rights.
432 if self.context.private:
433 if not check_permission('launchpad.Append', self.context):
434 link.enabled = False
435 return link
430436
431 @enabled_with_permission('launchpad.Edit')437 @enabled_with_permission('launchpad.Edit')
432 def delete(self):438 def delete(self):
@@ -500,6 +506,10 @@
500 """Common features for Archive view classes."""506 """Common features for Archive view classes."""
501507
502 @cachedproperty508 @cachedproperty
509 def private(self):
510 return self.context.private
511
512 @cachedproperty
503 def has_sources(self):513 def has_sources(self):
504 """Whether or not this PPA has any sources for the view.514 """Whether or not this PPA has any sources for the view.
505515
@@ -960,6 +970,12 @@
960 """Detailed packages view for an archive."""970 """Detailed packages view for an archive."""
961 implements(IArchivePackagesActionMenu)971 implements(IArchivePackagesActionMenu)
962972
973 def initialize(self):
974 super(ArchivePackagesView, self).initialize()
975 if self.context.private:
976 if not check_permission('launchpad.Append', self.context):
977 raise Unauthorized
978
963 @property979 @property
964 def page_title(self):980 def page_title(self):
965 return smartquote('Packages in "%s"' % self.context.displayname)981 return smartquote('Packages in "%s"' % self.context.displayname)
966982
=== added file 'lib/lp/soyuz/browser/tests/test_archive_packages.py'
--- lib/lp/soyuz/browser/tests/test_archive_packages.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/browser/tests/test_archive_packages.py 2010-06-25 13:39:35 +0000
@@ -0,0 +1,101 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4# pylint: disable-msg=F0401
5
6"""Unit tests for TestP3APackages."""
7
8__metaclass__ = type
9__all__ = [
10 'TestP3APackages',
11 'TestPPAPackages',
12 'test_suite',
13 ]
14
15import unittest
16
17from zope.security.interfaces import Unauthorized
18
19from canonical.testing import LaunchpadFunctionalLayer
20from lp.soyuz.browser.archive import ArchiveNavigationMenu
21from lp.testing import login, login_person, TestCaseWithFactory
22from lp.testing.views import create_initialized_view
23
24
25class TestP3APackages(TestCaseWithFactory):
26 """P3A archive pages are rendered correctly."""
27
28 layer = LaunchpadFunctionalLayer
29
30 def setUp(self):
31 super(TestP3APackages, self).setUp()
32 self.private_ppa = self.factory.makeArchive(description='Foo')
33 login('admin@canonical.com')
34 self.private_ppa.buildd_secret = 'blah'
35 self.private_ppa.private = True
36 self.joe = self.factory.makePerson(name='joe')
37 self.fred = self.factory.makePerson(name='fred')
38 self.mary = self.factory.makePerson(name='mary')
39 login_person(self.private_ppa.owner)
40 self.private_ppa.newSubscription(self.joe, self.private_ppa.owner)
41 self.private_ppa.newComponentUploader(self.mary, 'main')
42
43 def test_packages_unauthorized(self):
44 """A person with no subscription will not be able to view +packages
45 """
46 login_person(self.fred)
47 self.assertRaises(
48 Unauthorized, create_initialized_view, self.private_ppa,
49 "+packages")
50
51 def test_packages_unauthorized_subscriber(self):
52 """A person with a subscription will not be able to view +packages
53 """
54 login_person(self.joe)
55 self.assertRaises(
56 Unauthorized, create_initialized_view, self.private_ppa,
57 "+packages")
58
59 def test_packages_authorized(self):
60 """A person with launchpad.{Append,Edit} will be able to do so"""
61 login_person(self.private_ppa.owner)
62 view = create_initialized_view(self.private_ppa, "+packages")
63 menu = ArchiveNavigationMenu(view)
64 self.assertTrue(menu.packages().enabled)
65
66 def test_packages_uploader(self):
67 """A person with launchpad.Append will also be able to do so"""
68 login_person(self.mary)
69 view = create_initialized_view(self.private_ppa, "+packages")
70 menu = ArchiveNavigationMenu(view)
71 self.assertTrue(menu.packages().enabled)
72
73 def test_packages_link_unauthorized(self):
74 login_person(self.fred)
75 view = create_initialized_view(self.private_ppa, "+index")
76 menu = ArchiveNavigationMenu(view)
77 self.assertFalse(menu.packages().enabled)
78
79 def test_packages_link_subscriber(self):
80 login_person(self.joe)
81 view = create_initialized_view(self.private_ppa, "+index")
82 menu = ArchiveNavigationMenu(view)
83 self.assertFalse(menu.packages().enabled)
84
85
86class TestPPAPackages(TestCaseWithFactory):
87 layer = LaunchpadFunctionalLayer
88
89 def setUp(self):
90 super(TestPPAPackages, self).setUp()
91 self.joe = self.factory.makePerson(name='joe')
92 self.ppa = self.factory.makeArchive()
93
94 def test_ppa_packages(self):
95 login_person(self.joe)
96 view = create_initialized_view(self.ppa, "+index")
97 menu = ArchiveNavigationMenu(view)
98 self.assertTrue(menu.packages().enabled)
99
100def test_suite():
101 return unittest.TestLoader().loadTestsFromName(__name__)
0102
=== modified file 'lib/lp/soyuz/doc/archiveauthtoken.txt'
--- lib/lp/soyuz/doc/archiveauthtoken.txt 2010-04-28 16:28:02 +0000
+++ lib/lp/soyuz/doc/archiveauthtoken.txt 2010-06-25 13:39:35 +0000
@@ -21,8 +21,7 @@
21possible if there is already a valid subscription for the user for21possible if there is already a valid subscription for the user for
22that archive.22that archive.
2323
24First, login as joe and try to create a token for ourselves, even24Create Brad, and his team:
25though we do not yet have a subscription:
2625
27 >>> login("admin@canonical.com")26 >>> login("admin@canonical.com")
28 >>> bradsmith = factory.makePerson(27 >>> bradsmith = factory.makePerson(
@@ -30,12 +29,6 @@
30 ... email="brad@example.com")29 ... email="brad@example.com")
31 >>> teambrad = factory.makeTeam(30 >>> teambrad = factory.makeTeam(
32 ... owner=bradsmith, displayname="Team Brad", name='teambrad')31 ... owner=bradsmith, displayname="Team Brad", name='teambrad')
33 >>> login("brad@example.com")
34 >>> new_token = joe_private_ppa.newAuthToken(bradsmith)
35 Traceback (most recent call last):
36 ...
37 Unauthorized: You do not have a subscription for
38 PPA for Joe Smith.
3932
40Create a subscription for Team Brad to joe's archive:33Create a subscription for Team Brad to joe's archive:
4134
4235
=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py 2010-06-21 19:29:34 +0000
+++ lib/lp/soyuz/interfaces/archive.py 2010-06-25 13:39:35 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4# pylint: disable-msg=E0211,E02134# pylint: disable-msg=E0211,E0213
@@ -629,24 +629,6 @@
629 :return The new `IPackageCopyRequest`629 :return The new `IPackageCopyRequest`
630 """630 """
631631
632 # XXX: noodles 2009-03-02 bug=336779: This should be moved into
633 # IArchiveView once the archive permissions are updated to grant
634 # IArchiveView to archive subscribers.
635 def newAuthToken(person, token=None, date_created=None):
636 """Create a new authorisation token.
637
638 XXX: noodles 2009-03-12 bug=341600 This method should not be exposed
639 through the API as we do not yet check that the callsite has
640 launchpad.Edit on the person.
641
642 :param person: An IPerson whom this token is for
643 :param token: Optional unicode text to use as the token. One will be
644 generated if not given
645 :param date_created: Optional, defaults to now
646
647 :return: A new IArchiveAuthToken
648 """
649
650 @operation_parameters(632 @operation_parameters(
651 person=Reference(schema=IPerson),633 person=Reference(schema=IPerson),
652 # Really IPackageset, corrected in _schema_circular_imports to avoid634 # Really IPackageset, corrected in _schema_circular_imports to avoid
@@ -1112,6 +1094,33 @@
1112 :return: A dictionary of filenames and SHA1s.1094 :return: A dictionary of filenames and SHA1s.
1113 """1095 """
11141096
1097 def getAuthToken(person):
1098 """Returns an IArchiveAuthToken for the archive in question for
1099 IPerson provided.
1100
1101 :return: A IArchiveAuthToken, or None if the user has none.
1102 """
1103
1104 def newAuthToken(person, token=None, date_created=None):
1105 """Create a new authorisation token.
1106
1107 :param person: An IPerson whom this token is for
1108 :param token: Optional unicode text to use as the token. One will be
1109 generated if not given
1110 :param date_created: Optional, defaults to now
1111
1112 :return: A new IArchiveAuthToken
1113 """
1114
1115 @call_with(person=REQUEST_USER)
1116 @export_write_operation()
1117 def getPrivateSourcesList(person):
1118 """Get a text line that is suitable to be used for a sources.list
1119 entry.
1120
1121 It will create a new IArchiveAuthToken if one doesn't already exist.
1122 """
1123
1115class IArchiveAppend(Interface):1124class IArchiveAppend(Interface):
1116 """Archive interface for operations restricted by append privilege."""1125 """Archive interface for operations restricted by append privilege."""
11171126
11181127
=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py 2010-06-21 19:29:34 +0000
+++ lib/lp/soyuz/model/archive.py 2010-06-25 13:39:35 +0000
@@ -1392,30 +1392,26 @@
1392 # Perform the copy, may raise CannotCopy.1392 # Perform the copy, may raise CannotCopy.
1393 do_copy(sources, self, series, pocket, include_binaries)1393 do_copy(sources, self, series, pocket, include_binaries)
13941394
1395 def getAuthToken(self, person):
1396 """See `IArchive`."""
1397
1398 token_set = getUtility(IArchiveAuthTokenSet)
1399 return token_set.getActiveTokenForArchiveAndPerson(self, person)
1400
1395 def newAuthToken(self, person, token=None, date_created=None):1401 def newAuthToken(self, person, token=None, date_created=None):
1396 """See `IArchive`."""1402 """See `IArchive`."""
13971403
1404 # Bail if the archive isn't private
1405 if not self.private:
1406 raise ArchiveNotPrivate("Archive must be private.")
1407
1398 # Tokens can only be created for individuals.1408 # Tokens can only be created for individuals.
1399 if person.is_team:1409 if person.is_team:
1400 raise NoTokensForTeams(1410 raise NoTokensForTeams(
1401 "Subscription tokens can be created for individuals only.")1411 "Subscription tokens can be created for individuals only.")
14021412
1403 # First, ensure that a current subscription exists for the1413 # Ensure that the current subscription does not already have a token
1404 # person and archive:1414 if self.getAuthToken(person) is not None:
1405 # XXX: noodles 2009-03-02 bug=336779: This can be removed once
1406 # newAuthToken() is moved into IArchiveView.
1407 subscription_set = getUtility(IArchiveSubscriberSet)
1408 subscriptions = subscription_set.getBySubscriber(person, archive=self)
1409 if subscriptions.count() == 0:
1410 raise Unauthorized(
1411 "You do not have a subscription for %s." % self.displayname)
1412
1413 # Second, ensure that the current subscription does not already
1414 # have a token:
1415 token_set = getUtility(IArchiveAuthTokenSet)
1416 previous_token = token_set.getActiveTokenForArchiveAndPerson(
1417 self, person)
1418 if previous_token:
1419 raise ArchiveSubscriptionError(1415 raise ArchiveSubscriptionError(
1420 "%s already has a token for %s." % (1416 "%s already has a token for %s." % (
1421 person.displayname, self.displayname))1417 person.displayname, self.displayname))
@@ -1433,6 +1429,14 @@
1433 store.add(archive_auth_token)1429 store.add(archive_auth_token)
1434 return archive_auth_token1430 return archive_auth_token
14351431
1432 def getPrivateSourcesList(self, person):
1433 """See `IArchive`."""
1434
1435 token = self.getAuthToken(person)
1436 if token is None:
1437 token = self.newAuthToken(person)
1438 return token.archive_url
1439
1436 def newSubscription(self, subscriber, registrant, date_expires=None,1440 def newSubscription(self, subscriber, registrant, date_expires=None,
1437 description=None):1441 description=None):
1438 """See `IArchive`."""1442 """See `IArchive`."""
14391443
=== modified file 'lib/lp/soyuz/stories/webservice/xx-archive.txt'
--- lib/lp/soyuz/stories/webservice/xx-archive.txt 2010-06-14 14:16:13 +0000
+++ lib/lp/soyuz/stories/webservice/xx-archive.txt 2010-06-25 13:39:35 +0000
@@ -890,6 +890,16 @@
890 >>> print response.getHeader('Location')890 >>> print response.getHeader('Location')
891 http://.../~cprov/+archive/p3a/+subscriptions/mark891 http://.../~cprov/+archive/p3a/+subscriptions/mark
892892
893We can print the sources.list entry for the archive, which will include an
894AuthToken:
895
896 >>> sources_response = webservice.named_post(
897 ... cprov_private_ppa['self_link'], 'getPrivateSourcesList')
898 >>> print sources_response
899 HTTP/1.1 200 Ok
900 ...
901 "http://salgado:...@private-ppa.launchpad.dev/cprov/p3a/ubuntu"
902
893We publish a subset of the IArchiveSubscriber attributes.903We publish a subset of the IArchiveSubscriber attributes.
894904
895 >>> new_subscription = cprov_webservice.get(905 >>> new_subscription = cprov_webservice.get(
896906
=== modified file 'lib/lp/soyuz/tests/test_archive.py'
--- lib/lp/soyuz/tests/test_archive.py 2010-06-16 18:47:46 +0000
+++ lib/lp/soyuz/tests/test_archive.py 2010-06-25 13:39:35 +0000
@@ -790,6 +790,30 @@
790 self.archive, self.arm).count())790 self.archive, self.arm).count())
791 self.assertFalse(self.archive.arm_builds_allowed)791 self.assertFalse(self.archive.arm_builds_allowed)
792792
793class TestArchiveTokens(TestCaseWithFactory):
794 layer = LaunchpadZopelessLayer
795
796 def setUp(self):
797 super(TestArchiveTokens, self).setUp()
798 owner = self.factory.makePerson()
799 self.private_ppa = self.factory.makeArchive(owner=owner)
800 self.private_ppa.buildd_secret = 'blah'
801 self.private_ppa.private = True
802 self.joe = self.factory.makePerson(name='joe')
803 self.private_ppa.newSubscription(self.joe, owner)
804
805 def test_getAuthToken_with_no_token(self):
806 token = self.private_ppa.getAuthToken(self.joe)
807 self.assertEqual(token, None)
808
809 def test_getAuthToken_with_token(self):
810 token = self.private_ppa.newAuthToken(self.joe)
811 self.assertEqual(self.private_ppa.getAuthToken(self.joe), token)
812
813 def test_getPrivateSourcesList(self):
814 url = self.private_ppa.getPrivateSourcesList(self.joe)
815 token = self.private_ppa.getAuthToken(self.joe)
816 self.assertEqual(token.archive_url, url)
793817
794class TestArchivePrivacySwitching(TestCaseWithFactory):818class TestArchivePrivacySwitching(TestCaseWithFactory):
795819
796820
=== added file 'lib/lp/soyuz/tests/test_archive_privacy.py'
--- lib/lp/soyuz/tests/test_archive_privacy.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/tests/test_archive_privacy.py 2010-06-25 13:39:35 +0000
@@ -0,0 +1,40 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test Archive privacy features."""
5
6from zope.component import getUtility
7from zope.security.interfaces import Unauthorized
8from lp.soyuz.interfaces.archive import IArchiveSet
9
10from canonical.testing import LaunchpadFunctionalLayer
11from lp.testing import login, login_person, TestCaseWithFactory
12
13
14class TestArchivePrivacy(TestCaseWithFactory):
15 layer = LaunchpadFunctionalLayer
16
17 def setUp(self):
18 super(TestArchivePrivacy, self).setUp()
19 self.private_ppa = self.factory.makeArchive(description='Foo')
20 login('admin@canonical.com')
21 self.private_ppa.buildd_secret = 'blah'
22 self.private_ppa.private = True
23 self.joe = self.factory.makePerson(name='joe')
24 self.fred = self.factory.makePerson(name='fred')
25 login_person(self.private_ppa.owner)
26 self.private_ppa.newSubscription(self.joe, self.private_ppa.owner)
27
28 def _getDescription(self, p3a):
29 return p3a.description
30
31 def test_no_subscription(self):
32 login_person(self.fred)
33 p3a = getUtility(IArchiveSet).get(self.private_ppa.id)
34 self.assertRaises(Unauthorized, self._getDescription, p3a)
35
36 def test_subscription(self):
37 login_person(self.joe)
38 p3a = getUtility(IArchiveSet).get(self.private_ppa.id)
39 self.assertEqual(self._getDescription(p3a), "Foo")
40
041
=== modified file 'utilities/lp-deps.py'
--- utilities/lp-deps.py 2010-06-14 22:18:14 +0000
+++ utilities/lp-deps.py 2010-06-25 13:39:35 +0000
@@ -20,6 +20,7 @@
20# JS_DIRSET is a tuple of the dir where the code exists, and the name of the20# JS_DIRSET is a tuple of the dir where the code exists, and the name of the
21# symlink it should be linked as in the icing build directory.21# symlink it should be linked as in the icing build directory.
22JS_DIRSET = [22JS_DIRSET = [
23 (os.path.join('lib', 'lp', 'bugs', 'javascript'), 'bugs'),
23 (os.path.join('lib', 'lp', 'code', 'javascript'), 'code'),24 (os.path.join('lib', 'lp', 'code', 'javascript'), 'code'),
24 (os.path.join('lib', 'lp', 'registry', 'javascript'), 'registry'),25 (os.path.join('lib', 'lp', 'registry', 'javascript'), 'registry'),
25 (os.path.join('lib', 'lp', 'translations', 'javascript'), 'translations'),26 (os.path.join('lib', 'lp', 'translations', 'javascript'), 'translations'),
2627
=== modified file 'utilities/qa-ready'
--- utilities/qa-ready 2010-04-27 19:48:39 +0000
+++ utilities/qa-ready 2010-06-25 13:39:35 +0000
@@ -35,7 +35,7 @@
35 """35 """
36 t = get_transport('https://edge.launchpad.net/')36 t = get_transport('https://edge.launchpad.net/')
37 html = t.get_bytes('index.html')37 html = t.get_bytes('index.html')
38 revision_re = re.compile(r'\(r(\d+)\)')38 revision_re = re.compile(r'r(\d+)')
39 for line in html.splitlines():39 for line in html.splitlines():
40 matches = revision_re.search(line)40 matches = revision_re.search(line)
41 if matches:41 if matches:

Subscribers

People subscribed via source and target branches

to status/vote changes: