Merge lp:~edwin-grubbs/launchpad/bug-446074-blacklist-form into lp:launchpad
- bug-446074-blacklist-form
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Curtis Hovey | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 11921 | ||||
Proposed branch: | lp:~edwin-grubbs/launchpad/bug-446074-blacklist-form | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
952 lines (+679/-64) 13 files modified
lib/canonical/launchpad/browser/launchpad.py (+10/-8) lib/canonical/launchpad/security.py (+25/-1) lib/lp/registry/browser/configure.zcml (+51/-0) lib/lp/registry/browser/nameblacklist.py (+175/-0) lib/lp/registry/browser/tests/nameblacklist-views.txt (+96/-0) lib/lp/registry/browser/tests/test_peoplemerge.py (+2/-0) lib/lp/registry/configure.zcml (+23/-0) lib/lp/registry/interfaces/nameblacklist.py (+44/-0) lib/lp/registry/model/nameblacklist.py (+70/-0) lib/lp/registry/templates/nameblacklists-index.pt (+39/-0) lib/lp/registry/tests/test_nameblacklist.py (+132/-40) lib/lp/soyuz/tests/test_archive_subscriptions.py (+5/-0) lib/lp/testing/_login.py (+7/-15) |
||||
To merge this branch: | bzr merge lp:~edwin-grubbs/launchpad/bug-446074-blacklist-form | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Curtis Hovey (community) | code | Approve | |
Review via email: mp+40405@code.launchpad.net |
Commit message
Description of the change
Summary
-------
Added ability for the Registry Experts team to view, add, and edit the
NameBlacklist table.
Tests
-----
./bin/test -vv -t nameblacklist
Demo and Q/A
------------
* Open http://
* Click on "Add blacklist expression"
* It should not be possible to add "(" as an expression.
* Open http://
* Click on a yellow edit icon.
* It should not be possible to add "(" as an expression.
Edwin Grubbs (edwin-grubbs) wrote : | # |
On Wed, Nov 10, 2010 at 9:42 AM, Curtis Hovey
<email address hidden> wrote:
> Review: Needs Fixing code
> Hi Edwin.
>
> Thank you very much for providing this fine implementation. There are many
> places where I discovered you had implemented the feature as I would hope
> (using RegistryEditFor
> is a chance for an oops in the code, and there are some historic code that
> I hope you can clean up.
>
> The page titles in the browser are broken, breadcrumbs are missing. This is
> because the there is no breadcrumb adapter This might help:
>
> {{{
>
> @adapter(
> class NameBlacklistSe
> """Return a breadcrumb for an `INameBlackList
> text = "Name Blacklist"
>
> @adapter(
> class NameBlacklistBr
> """Return a breadcrumb for an `INameBlackList
>
> @property
> def text(self):
> return self.context.regexp
>
> }}}
Fixed.
>> === added file 'lib/lp/
>> --- lib/lp/
>> +++ lib/lp/
> ...
>> +class NameBlacklistAd
>> + """View for adding a blacklist expression."""
>> +
>> + schema = INameBlacklist
>> + field_names = ['regexp', 'comment']
>> + label = "Add a new blacklist expression"
>> +
>> + custom_
>> +
>> + @property
>> + def page_title(self):
>> + """The page title."""
>> + return self.label
>
> This could be
> page_title = label
Fixed.
> ...
>
>> +class NameBlacklistSe
>> + """View for /+nameblacklists top level collection."""
>> +
>> + page_title = (
>> + 'Blacklist for names of Launchpad pillars, persons, and teams')
>> + label = page_title
>
> I think
> Blacklist for names of Launchpad pillars and persons
> is good enough. I think user and team are really subclasses of IPerson.
> One day we will also have agent for the bots we have impersonating users.
Fixed.
>> +class NameBlacklistSe
>> +
>> + usedfor = INameBlacklistSet
>> +
>> + def traverse(self, name):
>> + return self.context.
>
> Do I get a 404 or a 500 for
> http://
>
> A 404 is correct. The interface says None will be returned (404), but int()
> will raise an Exception. Maybe int() should be pushed into get() or this
> method chooses to raise the 404 itself when the name cannot be cast.
Fixed.
> ...
>
>> === added file 'lib/lp/
>> --- lib/lp/
>> +++ lib/lp/
> ...
>> + def get(self, id):
>> + """See `INameBlacklist
>> + store = IStore(
>> + return store.find(
>
> I think this method could cast the id to an int and return None when an
> exception is raised to av...
Curtis Hovey (sinzui) wrote : | # |
Thanks for addressing my concerns.
Robert Collins (lifeless) wrote : | # |
>> + self.failUnless
>> + self.name_
>> + self.foo_exp.id)
>
> from https:/
> When asserting for equality use the form assertEqual
> Can you update all these old tests to assertEqual?
This might be nicer still as
self.assertThat
(with a trivial IsBlacklisted matcher).
-Rob
Preview Diff
1 | === modified file 'lib/canonical/launchpad/browser/launchpad.py' |
2 | --- lib/canonical/launchpad/browser/launchpad.py 2010-11-02 20:10:56 +0000 |
3 | +++ lib/canonical/launchpad/browser/launchpad.py 2010-11-12 06:04:05 +0000 |
4 | @@ -89,6 +89,14 @@ |
5 | INavigationMenu, |
6 | ) |
7 | from canonical.launchpad.webapp.publisher import RedirectionView |
8 | +from canonical.launchpad.webapp.url import urlappend |
9 | +from canonical.launchpad.webapp.vhosts import allvhosts |
10 | +from canonical.lazr import ( |
11 | + ExportedFolder, |
12 | + ExportedImageFolder, |
13 | + ) |
14 | +from canonical.widgets.project import ProjectScopeWidget |
15 | +from lp.answers.interfaces.questioncollection import IQuestionSet |
16 | # XXX SteveAlexander 2005-09-22: this is imported here because there is no |
17 | # general timedelta to duration format adapter available. This should |
18 | # be factored out into a generally available adapter for both this |
19 | @@ -99,14 +107,6 @@ |
20 | MenuAPI, |
21 | PageTemplateContextsAPI, |
22 | ) |
23 | -from canonical.launchpad.webapp.url import urlappend |
24 | -from canonical.launchpad.webapp.vhosts import allvhosts |
25 | -from canonical.lazr import ( |
26 | - ExportedFolder, |
27 | - ExportedImageFolder, |
28 | - ) |
29 | -from canonical.widgets.project import ProjectScopeWidget |
30 | -from lp.answers.interfaces.questioncollection import IQuestionSet |
31 | from lp.app.errors import ( |
32 | GoneError, |
33 | NotFoundError, |
34 | @@ -131,6 +131,7 @@ |
35 | from lp.registry.interfaces.codeofconduct import ICodeOfConductSet |
36 | from lp.registry.interfaces.distribution import IDistributionSet |
37 | from lp.registry.interfaces.karma import IKarmaActionSet |
38 | +from lp.registry.interfaces.nameblacklist import INameBlacklistSet |
39 | from lp.registry.interfaces.person import IPersonSet |
40 | from lp.registry.interfaces.pillar import IPillarNameSet |
41 | from lp.registry.interfaces.product import ( |
42 | @@ -583,6 +584,7 @@ |
43 | 'karmaaction': IKarmaActionSet, |
44 | '+imports': ITranslationImportQueue, |
45 | '+languages': ILanguageSet, |
46 | + '+nameblacklist': INameBlacklistSet, |
47 | 'package-sets': IPackagesetSet, |
48 | 'people': IPersonSet, |
49 | 'pillars': IPillarNameSet, |
50 | |
51 | === modified file 'lib/canonical/launchpad/security.py' |
52 | --- lib/canonical/launchpad/security.py 2010-10-21 01:42:14 +0000 |
53 | +++ lib/canonical/launchpad/security.py 2010-11-12 06:04:05 +0000 |
54 | @@ -51,6 +51,7 @@ |
55 | ) |
56 | from lp.blueprints.interfaces.sprint import ISprint |
57 | from lp.blueprints.interfaces.sprintspecification import ISprintSpecification |
58 | +from lp.bugs.interfaces.bugtarget import IOfficialBugTagTargetRestricted |
59 | from lp.buildmaster.interfaces.builder import ( |
60 | IBuilder, |
61 | IBuilderSet, |
62 | @@ -61,7 +62,6 @@ |
63 | IBuildFarmJobOld, |
64 | ) |
65 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild |
66 | -from lp.bugs.interfaces.bugtarget import IOfficialBugTagTargetRestricted |
67 | from lp.code.interfaces.branch import ( |
68 | IBranch, |
69 | user_has_special_branch_access, |
70 | @@ -114,6 +114,10 @@ |
71 | IMilestone, |
72 | IProjectGroupMilestone, |
73 | ) |
74 | +from lp.registry.interfaces.nameblacklist import ( |
75 | + INameBlacklist, |
76 | + INameBlacklistSet, |
77 | + ) |
78 | from lp.registry.interfaces.packaging import IPackaging |
79 | from lp.registry.interfaces.person import ( |
80 | IPerson, |
81 | @@ -1664,6 +1668,26 @@ |
82 | return team in user.person.getAdministratedTeams() |
83 | |
84 | |
85 | +class ViewNameBlacklist(EditByRegistryExpertsOrAdmins): |
86 | + permission = 'launchpad.View' |
87 | + usedfor = INameBlacklist |
88 | + |
89 | + |
90 | +class EditNameBlacklist(EditByRegistryExpertsOrAdmins): |
91 | + permission = 'launchpad.Edit' |
92 | + usedfor = INameBlacklist |
93 | + |
94 | + |
95 | +class ViewNameBlacklistSet(EditByRegistryExpertsOrAdmins): |
96 | + permission = 'launchpad.View' |
97 | + usedfor = INameBlacklistSet |
98 | + |
99 | + |
100 | +class EditNameBlacklistSet(EditByRegistryExpertsOrAdmins): |
101 | + permission = 'launchpad.Edit' |
102 | + usedfor = INameBlacklistSet |
103 | + |
104 | + |
105 | class ViewLanguageSet(AnonymousAuthorization): |
106 | """Anyone can view an ILangaugeSet.""" |
107 | usedfor = ILanguageSet |
108 | |
109 | === modified file 'lib/lp/registry/browser/configure.zcml' |
110 | --- lib/lp/registry/browser/configure.zcml 2010-10-11 09:35:29 +0000 |
111 | +++ lib/lp/registry/browser/configure.zcml 2010-11-12 06:04:05 +0000 |
112 | @@ -1560,6 +1560,57 @@ |
113 | class="lp.registry.browser.product.ProductReviewLicenseView" |
114 | permission="launchpad.Moderate" |
115 | template="../templates/product-review-license.pt"/> |
116 | + |
117 | + <browser:page |
118 | + for="lp.registry.interfaces.nameblacklist.INameBlacklist" |
119 | + permission="launchpad.Edit" |
120 | + facet="overview" |
121 | + class="lp.registry.browser.nameblacklist.NameBlacklistEditView" |
122 | + name="+edit" |
123 | + template="../../app/templates/generic-edit.pt"/> |
124 | + <browser:url |
125 | + for="lp.registry.interfaces.nameblacklist.INameBlacklistSet" |
126 | + path_expression="string:+nameblacklist" |
127 | + parent_utility="canonical.launchpad.webapp.interfaces.ILaunchpadRoot" |
128 | + /> |
129 | + <browser:url |
130 | + for="lp.registry.interfaces.nameblacklist.INameBlacklist" |
131 | + path_expression="string:${id}" |
132 | + parent_utility="lp.registry.interfaces.nameblacklist.INameBlacklistSet" |
133 | + /> |
134 | + <browser:defaultView |
135 | + for="lp.registry.interfaces.nameblacklist.INameBlacklistSet" |
136 | + name="+index"/> |
137 | + <browser:page |
138 | + for="lp.registry.interfaces.nameblacklist.INameBlacklistSet" |
139 | + permission="launchpad.View" |
140 | + facet="overview" |
141 | + class="lp.registry.browser.nameblacklist.NameBlacklistSetView" |
142 | + name="+index" |
143 | + template="../templates/nameblacklists-index.pt"/> |
144 | + <browser:page |
145 | + for="lp.registry.interfaces.nameblacklist.INameBlacklistSet" |
146 | + permission="launchpad.Edit" |
147 | + facet="overview" |
148 | + class="lp.registry.browser.nameblacklist.NameBlacklistAddView" |
149 | + name="+add" |
150 | + template="../../app/templates/generic-edit.pt"/> |
151 | + <browser:navigation |
152 | + module="lp.registry.browser.nameblacklist" |
153 | + classes="NameBlacklistSetNavigation"/> |
154 | + <browser:menus |
155 | + classes=" |
156 | + NameBlacklistNavigationMenu |
157 | + NameBlacklistSetNavigationMenu |
158 | + " |
159 | + module="lp.registry.browser.nameblacklist"/> |
160 | + <adapter |
161 | + factory="lp.registry.browser.nameblacklist.NameBlacklistSetBreadcrumb" |
162 | + /> |
163 | + <adapter |
164 | + factory="lp.registry.browser.nameblacklist.NameBlacklistBreadcrumb" |
165 | + /> |
166 | + |
167 | <browser:page |
168 | name="+addseries" |
169 | for="lp.registry.interfaces.product.IProduct" |
170 | |
171 | === added file 'lib/lp/registry/browser/nameblacklist.py' |
172 | --- lib/lp/registry/browser/nameblacklist.py 1970-01-01 00:00:00 +0000 |
173 | +++ lib/lp/registry/browser/nameblacklist.py 2010-11-12 06:04:05 +0000 |
174 | @@ -0,0 +1,175 @@ |
175 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
176 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
177 | + |
178 | +__metaclass__ = type |
179 | +__all__ = [ |
180 | + 'NameBlacklistAddView', |
181 | + 'NameBlacklistEditView', |
182 | + 'NameBlacklistNavigationMenu', |
183 | + 'NameBlacklistSetNavigationMenu', |
184 | + 'NameBlacklistSetView', |
185 | + ] |
186 | + |
187 | +import re |
188 | + |
189 | +from zope.app.form.browser import TextWidget |
190 | +from zope.component import ( |
191 | + adapter, |
192 | + getUtility, |
193 | + ) |
194 | +from zope.interface import implements |
195 | + |
196 | +from canonical.launchpad.webapp import action |
197 | +from canonical.launchpad.webapp.breadcrumb import Breadcrumb |
198 | +from canonical.launchpad.webapp.interfaces import IBreadcrumb |
199 | +from canonical.launchpad.webapp.launchpadform import ( |
200 | + custom_widget, |
201 | + LaunchpadFormView, |
202 | + ) |
203 | +from canonical.launchpad.webapp.menu import ( |
204 | + ApplicationMenu, |
205 | + enabled_with_permission, |
206 | + Link, |
207 | + NavigationMenu, |
208 | + ) |
209 | +from canonical.launchpad.webapp.publisher import ( |
210 | + canonical_url, |
211 | + LaunchpadView, |
212 | + Navigation, |
213 | + ) |
214 | +from lp.registry.browser import RegistryEditFormView |
215 | +from lp.registry.interfaces.nameblacklist import ( |
216 | + INameBlacklist, |
217 | + INameBlacklistSet, |
218 | + ) |
219 | + |
220 | + |
221 | +class NameBlacklistValidationMixin: |
222 | + """Validate regular expression when adding or editing.""" |
223 | + |
224 | + def validate(self, data): |
225 | + """Validate regular expression.""" |
226 | + regexp = data['regexp'] |
227 | + try: |
228 | + re.compile(regexp) |
229 | + name_blacklist_set = getUtility(INameBlacklistSet) |
230 | + if (INameBlacklistSet.providedBy(self.context) |
231 | + or self.context.regexp != regexp): |
232 | + # Check if the regular expression already exists if a |
233 | + # new expression is being created or if an existing |
234 | + # regular expression has been modified. |
235 | + if name_blacklist_set.getByRegExp(regexp) is not None: |
236 | + self.setFieldError( |
237 | + 'regexp', |
238 | + 'This regular expression already exists.') |
239 | + except re.error, e: |
240 | + self.setFieldError( |
241 | + 'regexp', |
242 | + 'Invalid regular expression: %s' % e) |
243 | + |
244 | + |
245 | +class NameBlacklistEditView(NameBlacklistValidationMixin, |
246 | + RegistryEditFormView): |
247 | + """View for editing a blacklist expression.""" |
248 | + |
249 | + schema = INameBlacklist |
250 | + field_names = ['regexp', 'comment'] |
251 | + label = "Edit a blacklist expression" |
252 | + page_title = label |
253 | + |
254 | + @property |
255 | + def cancel_url(self): |
256 | + return canonical_url(getUtility(INameBlacklistSet)) |
257 | + |
258 | + next_url = cancel_url |
259 | + |
260 | + |
261 | +class NameBlacklistAddView(NameBlacklistValidationMixin, LaunchpadFormView): |
262 | + """View for adding a blacklist expression.""" |
263 | + |
264 | + schema = INameBlacklist |
265 | + field_names = ['regexp', 'comment'] |
266 | + label = "Add a new blacklist expression" |
267 | + page_title = label |
268 | + |
269 | + custom_widget('regexp', TextWidget, displayWidth=60) |
270 | + |
271 | + @property |
272 | + def cancel_url(self): |
273 | + """See `LaunchpadFormView`.""" |
274 | + return canonical_url(self.context) |
275 | + |
276 | + next_url = cancel_url |
277 | + |
278 | + @action("Add to blacklist", name='add') |
279 | + def add_action(self, action, data): |
280 | + name_blacklist_set = getUtility(INameBlacklistSet) |
281 | + name_blacklist_set.create( |
282 | + regexp=data['regexp'], |
283 | + comment=data['comment'], |
284 | + ) |
285 | + self.request.response.addInfoNotification( |
286 | + 'Regular expression "%s" has been added to the name blacklist.' |
287 | + % data['regexp']) |
288 | + |
289 | + |
290 | +class NameBlacklistSetView(LaunchpadView): |
291 | + """View for /+nameblacklists top level collection.""" |
292 | + |
293 | + label = ( |
294 | + 'Blacklist for names of Launchpad pillars and persons') |
295 | + page_title = label |
296 | + |
297 | + |
298 | +class NameBlacklistSetNavigation(Navigation): |
299 | + |
300 | + usedfor = INameBlacklistSet |
301 | + |
302 | + def traverse(self, name): |
303 | + return self.context.get(name) |
304 | + |
305 | + |
306 | +class NameBlacklistSetNavigationMenu(NavigationMenu): |
307 | + """Action menu for NameBlacklistSet.""" |
308 | + usedfor = INameBlacklistSet |
309 | + facet = 'overview' |
310 | + links = [ |
311 | + 'add_blacklist_expression', |
312 | + ] |
313 | + |
314 | + @enabled_with_permission('launchpad.Edit') |
315 | + def add_blacklist_expression(self): |
316 | + return Link('+add', 'Add blacklist expression', icon='add') |
317 | + |
318 | + |
319 | +class NameBlacklistNavigationMenu(ApplicationMenu): |
320 | + """Action menu for NameBlacklist.""" |
321 | + usedfor = INameBlacklist |
322 | + facet = 'overview' |
323 | + links = [ |
324 | + 'edit_blacklist_expression', |
325 | + ] |
326 | + |
327 | + @enabled_with_permission('launchpad.Edit') |
328 | + def edit_blacklist_expression(self): |
329 | + return Link('+edit', 'Edit blacklist expression', icon='edit') |
330 | + |
331 | + |
332 | +@adapter(INameBlacklistSet) |
333 | +class NameBlacklistSetBreadcrumb(Breadcrumb): |
334 | + """Return a breadcrumb for an `INameBlackListSet`.""" |
335 | + |
336 | + implements(IBreadcrumb) |
337 | + |
338 | + text = "Name Blacklist" |
339 | + |
340 | + |
341 | +@adapter(INameBlacklist) |
342 | +class NameBlacklistBreadcrumb(Breadcrumb): |
343 | + """Return a breadcrumb for an `INameBlackList`.""" |
344 | + |
345 | + implements(IBreadcrumb) |
346 | + |
347 | + @property |
348 | + def text(self): |
349 | + return self.context.regexp |
350 | |
351 | === added file 'lib/lp/registry/browser/tests/nameblacklist-views.txt' |
352 | --- lib/lp/registry/browser/tests/nameblacklist-views.txt 1970-01-01 00:00:00 +0000 |
353 | +++ lib/lp/registry/browser/tests/nameblacklist-views.txt 2010-11-12 06:04:05 +0000 |
354 | @@ -0,0 +1,96 @@ |
355 | +NameBlacklist pages |
356 | +=================== |
357 | + |
358 | + >>> from zope.component import getUtility |
359 | + >>> from lp.testing.sampledata import ADMIN_EMAIL |
360 | + >>> from canonical.launchpad.interfaces.launchpad import ( |
361 | + ... ILaunchpadCelebrities) |
362 | + >>> from lp.registry.interfaces.nameblacklist import INameBlacklistSet |
363 | + >>> name_blacklist_set = getUtility(INameBlacklistSet) |
364 | + >>> from canonical.launchpad.testing.pages import ( |
365 | + ... extract_text, find_tag_by_id) |
366 | + >>> registry_experts = getUtility(ILaunchpadCelebrities).registry_experts |
367 | + >>> registry_expert = factory.makePerson() |
368 | + >>> login(ADMIN_EMAIL) |
369 | + >>> ignore = registry_experts.addMember(registry_expert, registry_expert) |
370 | + |
371 | + |
372 | +View all |
373 | +-------- |
374 | + |
375 | +All the blacklisted regular expressions that filter pillar names and |
376 | +person names can be seen on the /+nameblacklist page. |
377 | + |
378 | + >>> login_person(registry_expert) |
379 | + >>> view = create_initialized_view(name_blacklist_set, '+index', |
380 | + ... principal=registry_expert) |
381 | + >>> print extract_text(find_tag_by_id(view.render(), 'blacklist')) |
382 | + Regular Expression Comment |
383 | + ^admin Edit blacklist expression |
384 | + blacklist Edit blacklist expression For testing purposes |
385 | + |
386 | + |
387 | +Add expression to blacklist |
388 | +--------------------------- |
389 | + |
390 | +An invalid regular expression cannot be added. |
391 | + |
392 | + >>> form = { |
393 | + ... 'field.regexp': u'(', |
394 | + ... 'field.comment': u'old-comment', |
395 | + ... 'field.actions.add': 'Add to blacklist', |
396 | + ... } |
397 | + >>> view = create_initialized_view(name_blacklist_set, '+add', form=form) |
398 | + >>> for error in view.errors: |
399 | + ... print error |
400 | + Invalid regular expression: unbalanced parenthesis |
401 | + |
402 | +A duplicate regular expression cannot be added. |
403 | + |
404 | + >>> form['field.regexp'] = u'blacklist' |
405 | + >>> view = create_initialized_view(name_blacklist_set, '+add', form=form) |
406 | + >>> for error in view.errors: |
407 | + ... print error |
408 | + This regular expression already exists. |
409 | + |
410 | +After adding a regular expression, a notification will be displayed. |
411 | + |
412 | + >>> form['field.regexp'] = u'foo' |
413 | + >>> view = create_initialized_view(name_blacklist_set, '+add', form=form) |
414 | + >>> for notification in view.request.response.notifications: |
415 | + ... print notification.message |
416 | + Regular expression "foo" has been added to the name blacklist. |
417 | + |
418 | + |
419 | +Edit expression in blacklist |
420 | +---------------------------- |
421 | + |
422 | +When a regular expression is edited, it still must be valid. |
423 | + |
424 | + >>> import transaction |
425 | + >>> transaction.commit() |
426 | + >>> foo_exp = name_blacklist_set.getByRegExp(u'foo') |
427 | + >>> form = { |
428 | + ... 'field.regexp': u'(', |
429 | + ... 'field.comment': u'new-comment', |
430 | + ... 'field.actions.change': 'Change', |
431 | + ... } |
432 | + >>> view = create_initialized_view(foo_exp, '+edit', form=form) |
433 | + >>> for error in view.errors: |
434 | + ... print error |
435 | + Invalid regular expression: unbalanced parenthesis |
436 | + |
437 | +It cannot changed to conflict with another regular expression. |
438 | + |
439 | + >>> form['field.regexp'] = u'blacklist' |
440 | + >>> view = create_initialized_view(foo_exp, '+edit', form=form) |
441 | + >>> for error in view.errors: |
442 | + ... print error |
443 | + This regular expression already exists. |
444 | + |
445 | +Otherwise, the change will be successful. |
446 | + |
447 | + >>> form['field.regexp'] = u'bar' |
448 | + >>> view = create_initialized_view(foo_exp, '+edit', form=form) |
449 | + >>> print foo_exp.regexp, foo_exp.comment |
450 | + bar new-comment |
451 | |
452 | === modified file 'lib/lp/registry/browser/tests/test_peoplemerge.py' |
453 | --- lib/lp/registry/browser/tests/test_peoplemerge.py 2010-11-01 20:39:17 +0000 |
454 | +++ lib/lp/registry/browser/tests/test_peoplemerge.py 2010-11-12 06:04:05 +0000 |
455 | @@ -128,7 +128,9 @@ |
456 | # Super team memberships are removed. |
457 | self.target_team = getUtility(ILaunchpadCelebrities).registry_experts |
458 | super_team = self.factory.makeTeam() |
459 | + login_celebrity('admin') |
460 | self.dupe_team.join(super_team, self.dupe_team.teamowner) |
461 | + login_celebrity('registry_experts') |
462 | view = self.getView() |
463 | self.assertEqual([], view.errors) |
464 | self.assertEqual(self.target_team, self.dupe_team.merged) |
465 | |
466 | === modified file 'lib/lp/registry/configure.zcml' |
467 | --- lib/lp/registry/configure.zcml 2010-11-09 16:26:11 +0000 |
468 | +++ lib/lp/registry/configure.zcml 2010-11-12 06:04:05 +0000 |
469 | @@ -102,6 +102,29 @@ |
470 | interface="lp.registry.interfaces.persontransferjob.IMembershipNotificationJob"/> |
471 | </class> |
472 | |
473 | + <!-- INameBlacklist --> |
474 | + <securedutility |
475 | + class="lp.registry.model.nameblacklist.NameBlacklistSet" |
476 | + provides="lp.registry.interfaces.nameblacklist.INameBlacklistSet"> |
477 | + <allow |
478 | + interface="lp.registry.interfaces.nameblacklist.INameBlacklistSet"/> |
479 | + </securedutility> |
480 | + |
481 | + <class class="lp.registry.model.nameblacklist.NameBlacklistSet"> |
482 | + <require |
483 | + permission="launchpad.Edit" |
484 | + interface="lp.registry.interfaces.nameblacklist.INameBlacklistSet"/> |
485 | + </class> |
486 | + |
487 | + <class class="lp.registry.model.nameblacklist.NameBlacklist"> |
488 | + <require |
489 | + permission="launchpad.View" |
490 | + interface="lp.registry.interfaces.nameblacklist.INameBlacklist"/> |
491 | + <require |
492 | + permission="launchpad.Edit" |
493 | + set_schema="lp.registry.interfaces.nameblacklist.INameBlacklist"/> |
494 | + </class> |
495 | + |
496 | <!-- Location --> |
497 | |
498 | <class |
499 | |
500 | === added file 'lib/lp/registry/interfaces/nameblacklist.py' |
501 | --- lib/lp/registry/interfaces/nameblacklist.py 1970-01-01 00:00:00 +0000 |
502 | +++ lib/lp/registry/interfaces/nameblacklist.py 2010-11-12 06:04:05 +0000 |
503 | @@ -0,0 +1,44 @@ |
504 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
505 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
506 | + |
507 | +"""NameBlacklist interfaces.""" |
508 | + |
509 | +__metaclass__ = type |
510 | + |
511 | +__all__ = [ |
512 | + 'INameBlacklist', |
513 | + 'INameBlacklistSet', |
514 | + ] |
515 | + |
516 | +from zope.interface import Interface |
517 | +from zope.schema import ( |
518 | + Int, |
519 | + Text, |
520 | + TextLine, |
521 | + ) |
522 | + |
523 | +from canonical.launchpad import _ |
524 | + |
525 | + |
526 | +class INameBlacklist(Interface): |
527 | + """The interface for the NameBlacklist table.""" |
528 | + |
529 | + id = Int(title=_('ID'), required=True, readonly=True) |
530 | + regexp = TextLine(title=_('Regular expression'), required=True) |
531 | + comment = Text(title=_('Comment'), required=False) |
532 | + |
533 | + |
534 | +class INameBlacklistSet(Interface): |
535 | + """The set of INameBlacklist objects.""" |
536 | + |
537 | + def getAll(): |
538 | + """Return all the name blacklist expressions.""" |
539 | + |
540 | + def create(regexp, comment=None): |
541 | + """Create and return a new NameBlacklist with given arguments.""" |
542 | + |
543 | + def get(id): |
544 | + """Return the NameBlacklist with the given id or None.""" |
545 | + |
546 | + def getByRegExp(regexp): |
547 | + """Return the NameBlacklist with the given regexp or None.""" |
548 | |
549 | === added file 'lib/lp/registry/model/nameblacklist.py' |
550 | --- lib/lp/registry/model/nameblacklist.py 1970-01-01 00:00:00 +0000 |
551 | +++ lib/lp/registry/model/nameblacklist.py 2010-11-12 06:04:05 +0000 |
552 | @@ -0,0 +1,70 @@ |
553 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
554 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
555 | + |
556 | +"""Classes for managing the NameBlacklist table.""" |
557 | + |
558 | +__metaclass__ = type |
559 | +__all__ = [ |
560 | + 'NameBlacklist', |
561 | + 'NameBlacklistSet', |
562 | + ] |
563 | + |
564 | + |
565 | +from storm.base import Storm |
566 | +from storm.locals import ( |
567 | + Int, |
568 | + Unicode, |
569 | + ) |
570 | +from zope.interface import implements |
571 | + |
572 | +from canonical.launchpad.interfaces.lpstorm import IStore |
573 | +from lp.registry.interfaces.nameblacklist import ( |
574 | + INameBlacklist, |
575 | + INameBlacklistSet, |
576 | + ) |
577 | + |
578 | + |
579 | +class NameBlacklist(Storm): |
580 | + """Class for the NameBlacklist table.""" |
581 | + |
582 | + implements(INameBlacklist) |
583 | + |
584 | + __storm_table__ = 'NameBlacklist' |
585 | + |
586 | + id = Int(primary=True) |
587 | + regexp = Unicode(name='regexp', allow_none=False) |
588 | + comment = Unicode(name='comment', allow_none=True) |
589 | + |
590 | + |
591 | +class NameBlacklistSet: |
592 | + """Class for creating and retrieving NameBlacklist objects.""" |
593 | + |
594 | + implements(INameBlacklistSet) |
595 | + |
596 | + def getAll(self): |
597 | + """See `INameBlacklistSet`.""" |
598 | + store = IStore(NameBlacklist) |
599 | + return store.find(NameBlacklist).order_by(NameBlacklist.regexp) |
600 | + |
601 | + def create(self, regexp, comment=None): |
602 | + """See `INameBlacklistSet`.""" |
603 | + nameblacklist = NameBlacklist() |
604 | + nameblacklist.regexp = regexp |
605 | + nameblacklist.comment = comment |
606 | + store = IStore(NameBlacklist) |
607 | + store.add(nameblacklist) |
608 | + return nameblacklist |
609 | + |
610 | + def get(self, id): |
611 | + """See `INameBlacklistSet`.""" |
612 | + try: |
613 | + id = int(id) |
614 | + except ValueError: |
615 | + return None |
616 | + store = IStore(NameBlacklist) |
617 | + return store.find(NameBlacklist, NameBlacklist.id == id).one() |
618 | + |
619 | + def getByRegExp(self, regexp): |
620 | + """See `INameBlacklistSet`.""" |
621 | + store = IStore(NameBlacklist) |
622 | + return store.find(NameBlacklist, NameBlacklist.regexp == regexp).one() |
623 | |
624 | === renamed file 'lib/canonical/launchpad/pagetests/standalone/xx-nameblacklist.txt' => 'lib/lp/registry/stories/object/xx-nameblacklist.txt' |
625 | === added file 'lib/lp/registry/templates/nameblacklists-index.pt' |
626 | --- lib/lp/registry/templates/nameblacklists-index.pt 1970-01-01 00:00:00 +0000 |
627 | +++ lib/lp/registry/templates/nameblacklists-index.pt 2010-11-12 06:04:05 +0000 |
628 | @@ -0,0 +1,39 @@ |
629 | +<html |
630 | + xmlns="http://www.w3.org/1999/xhtml" |
631 | + xmlns:tal="http://xml.zope.org/namespaces/tal" |
632 | + xmlns:metal="http://xml.zope.org/namespaces/metal" |
633 | + xmlns:i18n="http://xml.zope.org/namespaces/i18n" |
634 | + xml:lang="en" |
635 | + lang="en" |
636 | + dir="ltr" |
637 | + metal:use-macro="view/macro:page/main_side" |
638 | + i18n:domain="launchpad"> |
639 | + |
640 | + <body> |
641 | + |
642 | + <tal:side metal:fill-slot="side"> |
643 | + <tal:menu replace="structure context/@@+global-actions" /> |
644 | + </tal:side> |
645 | + |
646 | + <div metal:fill-slot="main" class="main-portlet"> |
647 | + <table id="blacklist" class="listing sortable"> |
648 | + <thead> |
649 | + <th>Regular Expression</th> |
650 | + <th>Comment</th> |
651 | + </thead> |
652 | + <tbody> |
653 | + <tr tal:repeat="item context/getAll"> |
654 | + <td> |
655 | + <tt tal:content="item/regexp"/> |
656 | + <tal:link replace=" |
657 | + structure |
658 | + item/menu:overview/edit_blacklist_expression/fmt:icon"/> |
659 | + </td> |
660 | + <td tal:content="item/comment"/> |
661 | + </tr> |
662 | + </tbody> |
663 | + </table> |
664 | + </div> |
665 | + |
666 | + </body> |
667 | +</html> |
668 | |
669 | === modified file 'lib/lp/registry/tests/test_nameblacklist.py' |
670 | --- lib/lp/registry/tests/test_nameblacklist.py 2010-10-04 19:50:45 +0000 |
671 | +++ lib/lp/registry/tests/test_nameblacklist.py 2010-11-12 06:04:05 +0000 |
672 | @@ -5,42 +5,52 @@ |
673 | |
674 | __metaclass__ = type |
675 | |
676 | -import unittest |
677 | - |
678 | -from canonical.testing.layers import LaunchpadLayer |
679 | - |
680 | - |
681 | -class TestNameBlacklist(unittest.TestCase): |
682 | - layer = LaunchpadLayer |
683 | + |
684 | +from zope.component import getUtility |
685 | +from zope.interface.verify import verifyObject |
686 | + |
687 | +from canonical.launchpad.interfaces.lpstorm import IStore |
688 | +from canonical.launchpad.webapp.authorization import check_permission |
689 | +from canonical.testing.layers import ( |
690 | + DatabaseFunctionalLayer, |
691 | + ZopelessDatabaseLayer, |
692 | + ) |
693 | +from lp.registry.interfaces.nameblacklist import ( |
694 | + INameBlacklist, |
695 | + INameBlacklistSet, |
696 | + ) |
697 | +from lp.testing import ( |
698 | + ANONYMOUS, |
699 | + login, |
700 | + login_celebrity, |
701 | + TestCaseWithFactory, |
702 | + ) |
703 | + |
704 | + |
705 | +class TestNameBlacklist(TestCaseWithFactory): |
706 | + layer = ZopelessDatabaseLayer |
707 | |
708 | def setUp(self): |
709 | - self.con = self.layer.connect() |
710 | - self.cur = self.con.cursor() |
711 | - |
712 | - # Create a couple of blacklist entries |
713 | - self.cur.execute(""" |
714 | - INSERT INTO NameBlacklist(id, regexp) VALUES (-200, '^foo') |
715 | - """) |
716 | - self.cur.execute(""" |
717 | - INSERT INTO NameBlacklist(id, regexp) VALUES (-100, 'foo') |
718 | - """) |
719 | - self.cur.execute(""" |
720 | - INSERT INTO NameBlacklist(id, regexp) VALUES (-50, 'v e r b o s e') |
721 | - """) |
722 | - |
723 | - def tearDown(self): |
724 | - self.con.close() |
725 | + super(TestNameBlacklist, self).setUp() |
726 | + self.name_blacklist_set = getUtility(INameBlacklistSet) |
727 | + self.caret_foo_exp = self.name_blacklist_set.create(u'^foo') |
728 | + self.foo_exp = self.name_blacklist_set.create(u'foo') |
729 | + self.verbose_exp = self.name_blacklist_set.create(u'v e r b o s e') |
730 | + self.store = IStore(self.foo_exp) |
731 | + self.store.flush() |
732 | |
733 | def name_blacklist_match(self, name): |
734 | '''Return the result of the name_blacklist_match stored procedure.''' |
735 | - self.cur.execute("SELECT name_blacklist_match(%(name)s)", vars()) |
736 | - return self.cur.fetchone()[0] |
737 | + result = self.store.execute( |
738 | + "SELECT name_blacklist_match(%s)", (name,)) |
739 | + return result.get_one()[0] |
740 | |
741 | def is_blacklisted_name(self, name): |
742 | '''Call the is_blacklisted_name stored procedure and return the result |
743 | ''' |
744 | - self.cur.execute("SELECT is_blacklisted_name(%(name)s)", vars()) |
745 | - blacklisted = self.cur.fetchone()[0] |
746 | + result = self.store.execute( |
747 | + "SELECT is_blacklisted_name(%s)", (name,)) |
748 | + blacklisted = result.get_one()[0] |
749 | self.failIf(blacklisted is None, 'is_blacklisted_name returned NULL') |
750 | return bool(blacklisted) |
751 | |
752 | @@ -52,21 +62,25 @@ |
753 | # A name that is blacklisted returns the id of the row in the |
754 | # NameBlacklist table that matched. Rows are tried in order, and the |
755 | # first match is returned. |
756 | - self.failUnlessEqual(self.name_blacklist_match("foobar"), -200) |
757 | - self.failUnlessEqual(self.name_blacklist_match("barfoo"), -100) |
758 | + self.assertEqual( |
759 | + self.name_blacklist_match("foobar"), |
760 | + self.caret_foo_exp.id) |
761 | + self.assertEqual( |
762 | + self.name_blacklist_match("barfoo"), |
763 | + self.foo_exp.id) |
764 | |
765 | def test_name_blacklist_match_cache(self): |
766 | # If the blacklist is changed in the DB, these changes are noticed. |
767 | # This test is needed because the stored procedure keeps a cache |
768 | # of the compiled regular expressions. |
769 | - self.failUnlessEqual(self.name_blacklist_match("foobar"), -200) |
770 | - self.cur.execute( |
771 | - "UPDATE NameBlacklist SET regexp='nomatch' where id=-200" |
772 | - ) |
773 | - self.failUnlessEqual(self.name_blacklist_match("foobar"), -100) |
774 | - self.cur.execute( |
775 | - "UPDATE NameBlacklist SET regexp='nomatch2' where id=-100" |
776 | - ) |
777 | + self.assertEqual( |
778 | + self.name_blacklist_match("foobar"), |
779 | + self.caret_foo_exp.id) |
780 | + self.caret_foo_exp.regexp = u'nomatch' |
781 | + self.assertEqual( |
782 | + self.name_blacklist_match("foobar"), |
783 | + self.foo_exp.id) |
784 | + self.foo_exp.regexp = u'nomatch2' |
785 | self.failUnless(self.name_blacklist_match("foobar") is None) |
786 | |
787 | def test_is_blacklisted_name(self): |
788 | @@ -74,7 +88,8 @@ |
789 | # that is friendlier to use in a boolean context. |
790 | self.failUnless(self.is_blacklisted_name("bar") is False) |
791 | self.failUnless(self.is_blacklisted_name("foo") is True) |
792 | - self.cur.execute("UPDATE NameBlacklist SET regexp='bar' || regexp") |
793 | + self.caret_foo_exp.regexp = u'bar' |
794 | + self.foo_exp.regexp = u'bar2' |
795 | self.failUnless(self.is_blacklisted_name("foo") is False) |
796 | |
797 | def test_case_insensitive(self): |
798 | @@ -85,5 +100,82 @@ |
799 | self.failUnless(self.is_blacklisted_name("verbose") is True) |
800 | |
801 | |
802 | -def test_suite(): |
803 | - return unittest.TestLoader().loadTestsFromName(__name__) |
804 | +class TestNameBlacklistSet(TestCaseWithFactory): |
805 | + |
806 | + layer = DatabaseFunctionalLayer |
807 | + |
808 | + def setUp(self): |
809 | + super(TestNameBlacklistSet, self).setUp() |
810 | + login_celebrity('registry_experts') |
811 | + self.name_blacklist_set = getUtility(INameBlacklistSet) |
812 | + |
813 | + def test_create_with_one_arg(self): |
814 | + # Test NameBlacklistSet.create(regexp). |
815 | + name_blacklist = self.name_blacklist_set.create(u'foo') |
816 | + self.assertTrue(verifyObject(INameBlacklist, name_blacklist)) |
817 | + self.assertEqual(u'foo', name_blacklist.regexp) |
818 | + self.assertIs(None, name_blacklist.comment) |
819 | + |
820 | + def test_create_with_two_args(self): |
821 | + # Test NameBlacklistSet.create(regexp, comment). |
822 | + name_blacklist = self.name_blacklist_set.create(u'foo', u'bar') |
823 | + self.assertTrue(verifyObject(INameBlacklist, name_blacklist)) |
824 | + self.assertEqual(u'foo', name_blacklist.regexp) |
825 | + self.assertEqual(u'bar', name_blacklist.comment) |
826 | + |
827 | + def test_get_int(self): |
828 | + # Test NameBlacklistSet.get() with int id. |
829 | + name_blacklist = self.name_blacklist_set.create(u'foo', u'bar') |
830 | + store = IStore(name_blacklist) |
831 | + store.flush() |
832 | + retrieved = self.name_blacklist_set.get(name_blacklist.id) |
833 | + self.assertEqual(name_blacklist, retrieved) |
834 | + |
835 | + def test_get_string(self): |
836 | + # Test NameBlacklistSet.get() with string id. |
837 | + name_blacklist = self.name_blacklist_set.create(u'foo', u'bar') |
838 | + store = IStore(name_blacklist) |
839 | + store.flush() |
840 | + retrieved = self.name_blacklist_set.get(str(name_blacklist.id)) |
841 | + self.assertEqual(name_blacklist, retrieved) |
842 | + |
843 | + def test_get_returns_None_instead_of_ValueError(self): |
844 | + # Test that NameBlacklistSet.get() will return None instead of |
845 | + # raising a ValueError when it tries to cast the id to an int, |
846 | + # so that traversing an invalid url causes a Not Found error |
847 | + # instead of an error that is recorded as an oops. |
848 | + self.assertIs(None, self.name_blacklist_set.get('asdf')) |
849 | + |
850 | + def test_getAll(self): |
851 | + # Test NameBlacklistSet.getAll(). |
852 | + result = [ |
853 | + (item.regexp, item.comment) |
854 | + for item in self.name_blacklist_set.getAll()] |
855 | + expected = [ |
856 | + ('^admin', None), |
857 | + ('blacklist', 'For testing purposes'), |
858 | + ] |
859 | + self.assertEqual(expected, result) |
860 | + |
861 | + def test_NameBlacklistSet_permissions(self): |
862 | + # Verify that non-registry-experts do not have permission to |
863 | + # access the NameBlacklistSet. |
864 | + self.assertTrue( |
865 | + check_permission('launchpad.View', self.name_blacklist_set)) |
866 | + self.assertTrue( |
867 | + check_permission('launchpad.Edit', self.name_blacklist_set)) |
868 | + login(ANONYMOUS) |
869 | + self.assertFalse( |
870 | + check_permission('launchpad.View', self.name_blacklist_set)) |
871 | + self.assertFalse( |
872 | + check_permission('launchpad.Edit', self.name_blacklist_set)) |
873 | + |
874 | + def test_NameBlacklist_permissions(self): |
875 | + # Verify that non-registry-experts do not have permission to |
876 | + # access the NameBlacklist. |
877 | + name_blacklist = self.name_blacklist_set.create(u'foo') |
878 | + self.assertTrue(check_permission('launchpad.View', name_blacklist)) |
879 | + self.assertTrue(check_permission('launchpad.Edit', name_blacklist)) |
880 | + login(ANONYMOUS) |
881 | + self.assertFalse(check_permission('launchpad.View', name_blacklist)) |
882 | + self.assertFalse(check_permission('launchpad.Edit', name_blacklist)) |
883 | |
884 | === modified file 'lib/lp/soyuz/tests/test_archive_subscriptions.py' |
885 | --- lib/lp/soyuz/tests/test_archive_subscriptions.py 2010-10-26 15:47:24 +0000 |
886 | +++ lib/lp/soyuz/tests/test_archive_subscriptions.py 2010-11-12 06:04:05 +0000 |
887 | @@ -72,6 +72,11 @@ |
888 | with celebrity_logged_in('commercial_admin'): |
889 | self.archive.commercial = True |
890 | |
891 | + # Logging in as a celebrity team causes an email to be sent |
892 | + # because a person is added as a member of the team, so this |
893 | + # needs to be cleared out before calling newSubscription(). |
894 | + pop_notifications() |
895 | + |
896 | self.archive.newSubscription( |
897 | self.subscriber, registrant=self.archive.owner) |
898 | |
899 | |
900 | === modified file 'lib/lp/testing/_login.py' |
901 | --- lib/lp/testing/_login.py 2010-10-26 15:47:24 +0000 |
902 | +++ lib/lp/testing/_login.py 2010-11-12 06:04:05 +0000 |
903 | @@ -23,7 +23,6 @@ |
904 | ] |
905 | |
906 | from contextlib import contextmanager |
907 | -import random |
908 | |
909 | from zope.component import getUtility |
910 | from zope.security.management import endInteraction |
911 | @@ -39,10 +38,12 @@ |
912 | from canonical.launchpad.webapp.servers import LaunchpadTestRequest |
913 | from canonical.launchpad.webapp.vhosts import allvhosts |
914 | from lp.services.utils import decorate_with |
915 | +from lp.testing.sampledata import ADMIN_EMAIL |
916 | |
917 | |
918 | _logged_in = False |
919 | |
920 | + |
921 | def is_logged_in(): |
922 | global _logged_in |
923 | return _logged_in |
924 | @@ -93,24 +94,15 @@ |
925 | setupInteractionForPerson(person, participation) |
926 | |
927 | |
928 | -def _get_arbitrary_team_member(team): |
929 | - """Get an arbitrary member of 'team'. |
930 | - |
931 | - :param team: An `ITeam`. |
932 | - """ |
933 | - # Set up the interaction. |
934 | - login(ANONYMOUS) |
935 | - return random.choice(list(team.allmembers)) |
936 | - |
937 | - |
938 | def login_team(team, participation=None): |
939 | """Login as a member of 'team'.""" |
940 | - # This check isn't strictly necessary (it depends on the implementation of |
941 | - # _get_arbitrary_team_member), but this gives us a nice error message, |
942 | - # which can save time when debugging. |
943 | + # Prevent import loop. |
944 | + from lp.testing.factory import LaunchpadObjectFactory |
945 | if not team.is_team: |
946 | raise ValueError("Got person, expected team: %r" % (team,)) |
947 | - person = _get_arbitrary_team_member(team) |
948 | + login(ADMIN_EMAIL) |
949 | + person = LaunchpadObjectFactory().makePerson() |
950 | + team.addMember(person, person) |
951 | login_person(person, participation=participation) |
952 | return person |
953 |
Hi Edwin.
Thank you very much for providing this fine implementation. There are many mView, and making the table sortable). I think there
places where I discovered you had implemented the feature as I would hope
(using RegistryEditFor
is a chance for an oops in the code, and there are some historic code that
I hope you can clean up.
The page titles in the browser are broken, breadcrumbs are missing. This is
because the there is no breadcrumb adapter This might help:
{{{
@adapter( INameBlackListS et) tBreadcrumb( Breadcrumb) : Set`."" "
class NameBlacklistSe
"""Return a breadcrumb for an `INameBlackList
text = "Name Blacklist"
@adapter( INameBlackList) eadcrumb( Breadcrumb) : `."""
class NameBlacklistBr
"""Return a breadcrumb for an `INameBlackList
@property
def text(self):
return self.context.regexp
}}}
> === added file 'lib/lp/ registry/ browser/ nameblacklist. py' registry/ browser/ nameblacklist. py 1970-01-01 00:00:00 +0000 registry/ browser/ nameblacklist. py 2010-11-09 05:26:23 +0000 dView(NameBlack listValidationM ixin, LaunchpadFormView): widget( 'regexp' , TextWidget, displayWidth=60)
> --- lib/lp/
> +++ lib/lp/
...
> +class NameBlacklistAd
> + """View for adding a blacklist expression."""
> +
> + schema = INameBlacklist
> + field_names = ['regexp', 'comment']
> + label = "Add a new blacklist expression"
> +
> + custom_
> +
> + @property
> + def page_title(self):
> + """The page title."""
> + return self.label
This could be
page_title = label
...
> +class NameBlacklistSe tView(Launchpad View):
> + """View for /+nameblacklists top level collection."""
> +
> + page_title = (
> + 'Blacklist for names of Launchpad pillars, persons, and teams')
> + label = page_title
I think
Blacklist for names of Launchpad pillars and persons
is good enough. I think user and team are really subclasses of IPerson.
One day we will also have agent for the bots we have impersonating users.
> +class NameBlacklistSe tNavigation( Navigation) : get(int( name))
> +
> + usedfor = INameBlacklistSet
> +
> + def traverse(self, name):
> + return self.context.
Do I get a 404 or a 500 for launchpad. net/+nameblackl ist/word
http://
A 404 is correct. The interface says None will be returned (404), but int()
will raise an Exception. Maybe int() should be pushed into get() or this
method chooses to raise the 404 itself when the name cannot be cast.
...
> === added file 'lib/lp/ registry/ model/nameblack list.py' registry/ model/nameblack list.py 1970-01-01 00:00:00 +0000 registry/ model/nameblack list.py 2010-11-09 05:26:23 +0000 Set`."" " NameBlacklist) NameBlacklist, NameBlacklist.id == id).one()
> --- lib/lp/
> +++ lib/lp/
...
> + def get(self, id):
> + """See `INameBlacklist
> + store = IStore(
> + return store.find(
I think this method could cast the id to an int and return None when an
exception is raised to avoid a query that will always return None.
...
> === added file 'lib/lp/ registry/ templates/ nameblacklists- index.pt' registry/ templates/ nameblacklists- index.pt 1970-01-01 00:00:00 +0000 registry/ templates/ nameblacklists- index.pt 2010-11-09 05:2...
> --- lib/lp/
> +++ lib/lp/