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