Merge lp:~mwhudson/launchpad/feature-flag-xmlrpc into lp:launchpad
- feature-flag-xmlrpc
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Michael Hudson-Doyle | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 13986 | ||||
Proposed branch: | lp:~mwhudson/launchpad/feature-flag-xmlrpc | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
523 lines (+250/-37) 13 files modified
configs/development/launchpad-lazr.conf (+1/-0) lib/canonical/config/schema-lazr.conf (+6/-0) lib/canonical/launchpad/interfaces/launchpad.py (+2/-0) lib/canonical/launchpad/xmlrpc/application.py (+6/-1) lib/lp/services/features/__init__.py (+12/-0) lib/lp/services/features/configure.zcml (+7/-0) lib/lp/services/features/scopes.py (+45/-26) lib/lp/services/features/tests/test_scopes.py (+5/-3) lib/lp/services/features/tests/test_webapp.py (+3/-3) lib/lp/services/features/tests/test_xmlrpc.py (+99/-0) lib/lp/services/features/xmlrpc.py (+59/-0) lib/lp/testing/factory.py (+2/-2) utilities/page-performance-report.ini (+3/-2) |
||||
To merge this branch: | bzr merge lp:~mwhudson/launchpad/feature-flag-xmlrpc | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robert Collins (community) | Approve | ||
Review via email: mp+75673@code.launchpad.net |
Commit message
[r=lifeless][bug=853635] Add a private XML-RPC interface to query feature flags
Description of the change
This branch adds a private XML-RPC interface to query feature flags. It supports the team: scope (if you pass a username) and arbitrary other scopes.
It's been a while, so I've probably forgotten some coding standards things. I did remember to run "make lint" though :)
Martin Pool (mbp) wrote : | # |
Michael Hudson-Doyle (mwhudson) wrote : | # |
1. I tried a bit, but I just can't make a dict of values for the scopes work in my head. I've rejigged the scope code for more reusability though (and removed a use of ILaunchBag \o/).
2. Sure, I've moved FixedScope into scopes.py now.
3. Where are the developer docs? In my tree lib/lp/
Cheers,
mwh
Robert Collins (lifeless) wrote : | # |
I kindof had it in my head that we could inject more info about the
context in the method - e.g. rather than saying
scopes=
and just inject the current user, current server etc into that.
This is particularly relevant if you consider the scope 'random:0.5'
(true 50% of the time).
I think this needs a (small) bit of rework to be able to delegate to
the stock scope facilities TBH.
Even if the first cut doesn't *need* that, I think the contract should
be one that is amenable to that. IOW, rather than passing in the live
scopes, pass in the context used to derive live scopes, and get rid of
your custom scope lookup code. (It duplicates the team scope handling
anyway, and that also is a must-fix).
-Rob
Robert Collins (lifeless) wrote : | # |
Ah, this is much better.
+ scopes = [
177 DefaultScope(),
181 + PageScope(
182 + ServerScope(),
183 + ]
I would like there to be one-place to add scopes like RandomScope etc - so perhaps:
default_scopes = [DefaultScope()]
and then in the relevant places
scopes = list(default_
scopes.
etc.
What do you think?
Michael Hudson-Doyle (mwhudson) wrote : | # |
Funny you should suggest that, as I'd already written basically exactly that in my working copy...
I've also updated the developer documentation a bit.
Robert Collins (lifeless) wrote : | # |
You are missing a : in your dev docs, other than that looks wicked to me.
Preview Diff
1 | === modified file 'configs/development/launchpad-lazr.conf' |
2 | --- configs/development/launchpad-lazr.conf 2011-09-14 14:37:43 +0000 |
3 | +++ configs/development/launchpad-lazr.conf 2011-09-19 04:10:29 +0000 |
4 | @@ -158,6 +158,7 @@ |
5 | geonames_identity: lpdev |
6 | storm_cache: generational |
7 | storm_cache_size: 100 |
8 | +feature_flags_endpoint: http://xmlrpc-private.launchpad.dev:8087/featureflags/ |
9 | |
10 | [launchpad_session] |
11 | cookie: launchpad_dev |
12 | |
13 | === modified file 'lib/canonical/config/schema-lazr.conf' |
14 | --- lib/canonical/config/schema-lazr.conf 2011-09-16 14:55:43 +0000 |
15 | +++ lib/canonical/config/schema-lazr.conf 2011-09-19 04:10:29 +0000 |
16 | @@ -1211,6 +1211,12 @@ |
17 | # The number of items to display: |
18 | homepage_recent_posts_count: 6 |
19 | |
20 | +# The URL of the XML-RPC endpoint that allows querying of feature |
21 | +# flags. This should implement IFeatureFlagApplication. |
22 | +# |
23 | +# datatype: string |
24 | +feature_flags_endpoint: |
25 | + |
26 | [launchpad_session] |
27 | # The database connection string. |
28 | # datatype: pgconnection |
29 | |
30 | === modified file 'lib/canonical/launchpad/interfaces/launchpad.py' |
31 | --- lib/canonical/launchpad/interfaces/launchpad.py 2011-05-27 19:53:20 +0000 |
32 | +++ lib/canonical/launchpad/interfaces/launchpad.py 2011-09-19 04:10:29 +0000 |
33 | @@ -152,6 +152,8 @@ |
34 | softwarecenteragent = Attribute( |
35 | """Software center agent XML-RPC end point.""") |
36 | |
37 | + featureflags = Attribute("""Feature flag information endpoint""") |
38 | + |
39 | |
40 | class IAuthServerApplication(ILaunchpadApplication): |
41 | """Launchpad legacy AuthServer application root.""" |
42 | |
43 | === modified file 'lib/canonical/launchpad/xmlrpc/application.py' |
44 | --- lib/canonical/launchpad/xmlrpc/application.py 2011-04-28 21:36:47 +0000 |
45 | +++ lib/canonical/launchpad/xmlrpc/application.py 2011-09-19 04:10:29 +0000 |
46 | @@ -36,6 +36,7 @@ |
47 | ) |
48 | from lp.registry.interfaces.mailinglist import IMailingListApplication |
49 | from lp.registry.interfaces.person import ISoftwareCenterAgentApplication |
50 | +from lp.services.features.xmlrpc import IFeatureFlagApplication |
51 | |
52 | |
53 | # NOTE: If you add a traversal here, you should update |
54 | @@ -73,6 +74,11 @@ |
55 | """See `IPrivateApplication`.""" |
56 | return getUtility(ISoftwareCenterAgentApplication) |
57 | |
58 | + @property |
59 | + def featureflags(self): |
60 | + """See `IPrivateApplication`.""" |
61 | + return getUtility(IFeatureFlagApplication) |
62 | + |
63 | |
64 | class ISelfTest(Interface): |
65 | """XMLRPC external interface for testing the XMLRPC external interface.""" |
66 | @@ -127,4 +133,3 @@ |
67 | |
68 | def run_test(self): |
69 | return "OK" |
70 | - |
71 | |
72 | === modified file 'lib/lp/services/features/__init__.py' |
73 | --- lib/lp/services/features/__init__.py 2011-08-02 23:42:08 +0000 |
74 | +++ lib/lp/services/features/__init__.py 2011-09-19 04:10:29 +0000 |
75 | @@ -125,6 +125,18 @@ |
76 | if value: |
77 | print value |
78 | |
79 | +Checking flags without access to the database |
80 | +============================================= |
81 | + |
82 | +Feature flags can also be checked without access to the database by making use |
83 | +of the 'getFeatureFlag' XML-RPC method. |
84 | + |
85 | + server_proxy = xmlrpclib.ServerProxy( |
86 | + config.launchpad.feature_flags_endpoint, allow_none=True) |
87 | + if server_proxy.getFeatureFlag( |
88 | + 'codehosting.use_forking_server', ['user:' + user_name]): |
89 | + pass |
90 | + |
91 | Debugging feature usage |
92 | ======================= |
93 | |
94 | |
95 | === modified file 'lib/lp/services/features/configure.zcml' |
96 | --- lib/lp/services/features/configure.zcml 2010-08-12 06:55:37 +0000 |
97 | +++ lib/lp/services/features/configure.zcml 2011-09-19 04:10:29 +0000 |
98 | @@ -19,4 +19,11 @@ |
99 | handler="lp.services.features.webapp.end_request" |
100 | /> |
101 | |
102 | + <securedutility |
103 | + class="lp.services.features.xmlrpc.FeatureFlagApplication" |
104 | + provides="lp.services.features.xmlrpc.IFeatureFlagApplication"> |
105 | + <allow |
106 | + interface="lp.services.features.xmlrpc.IFeatureFlagApplication"/> |
107 | + </securedutility> |
108 | + |
109 | </configure> |
110 | |
111 | === modified file 'lib/lp/services/features/scopes.py' |
112 | --- lib/lp/services/features/scopes.py 2011-08-02 23:42:08 +0000 |
113 | +++ lib/lp/services/features/scopes.py 2011-09-19 04:10:29 +0000 |
114 | @@ -9,9 +9,14 @@ |
115 | """ |
116 | |
117 | __all__ = [ |
118 | + 'DefaultScope', |
119 | + 'default_scopes', |
120 | + 'FixedScope', |
121 | 'HANDLERS', |
122 | + 'MultiScopeHandler', |
123 | 'ScopesForScript', |
124 | 'ScopesFromRequest', |
125 | + 'TeamScope', |
126 | 'undocumented_scopes', |
127 | ] |
128 | |
129 | @@ -19,9 +24,7 @@ |
130 | |
131 | import re |
132 | |
133 | -from zope.component import getUtility |
134 | - |
135 | -from canonical.launchpad.webapp.interfaces import ILaunchBag |
136 | +from lp.registry.interfaces.person import IPerson |
137 | from lp.services.propertycache import cachedproperty |
138 | import canonical.config |
139 | |
140 | @@ -60,14 +63,7 @@ |
141 | return True |
142 | |
143 | |
144 | -class BaseWebRequestScope(BaseScope): |
145 | - """Base class for scopes that key off web request attributes.""" |
146 | - |
147 | - def __init__(self, request): |
148 | - self.request = request |
149 | - |
150 | - |
151 | -class PageScope(BaseWebRequestScope): |
152 | +class PageScope(BaseScope): |
153 | """The current page ID. |
154 | |
155 | Pageid scopes are written as 'pageid:' + the pageid to match. Pageids |
156 | @@ -81,6 +77,9 @@ |
157 | |
158 | pattern = r'pageid:' |
159 | |
160 | + def __init__(self, pageid): |
161 | + self._pageid = pageid |
162 | + |
163 | def lookup(self, scope_name): |
164 | """Is the given scope match the current pageid?""" |
165 | pageid_scope = scope_name[len('pageid:'):] |
166 | @@ -104,12 +103,11 @@ |
167 | |
168 | @cachedproperty |
169 | def _request_pageid_namespace(self): |
170 | - return tuple(self._pageid_to_namespace( |
171 | - self.request._orig_env.get('launchpad.pageid', ''))) |
172 | + return tuple(self._pageid_to_namespace(self._pageid)) |
173 | |
174 | |
175 | class TeamScope(BaseScope): |
176 | - """The current user's team memberships. |
177 | + """A user's team memberships. |
178 | |
179 | Team ID scopes are written as 'team:' + the team name to match. |
180 | |
181 | @@ -119,6 +117,9 @@ |
182 | |
183 | pattern = r'team:' |
184 | |
185 | + def __init__(self, person): |
186 | + self.person = person |
187 | + |
188 | def lookup(self, scope_name): |
189 | """Is the given scope a team membership? |
190 | |
191 | @@ -127,10 +128,7 @@ |
192 | fixed to reduce this to one query). |
193 | """ |
194 | team_name = scope_name[len('team:'):] |
195 | - person = getUtility(ILaunchBag).user |
196 | - if person is None: |
197 | - return False |
198 | - return person.inTeam(team_name) |
199 | + return self.person.inTeam(team_name) |
200 | |
201 | |
202 | class ServerScope(BaseScope): |
203 | @@ -169,6 +167,20 @@ |
204 | return scope_name == self.script_scope |
205 | |
206 | |
207 | +class FixedScope(BaseScope): |
208 | + """A scope that matches an exact value. |
209 | + |
210 | + Functionally `ScriptScope` and `DefaultScope` are equivalent to instances |
211 | + of this class, but their docstings are used on the +feature-info page. |
212 | + """ |
213 | + |
214 | + def __init__(self, scope): |
215 | + self.pattern = re.escape(scope) + '$' |
216 | + |
217 | + def lookup(self, scope_name): |
218 | + return True |
219 | + |
220 | + |
221 | # These are the handlers for all of the allowable scopes, listed here so that |
222 | # we can for example show all of them in an admin page. Any new scope will |
223 | # need a scope handler and that scope handler has to be added to this list. |
224 | @@ -213,21 +225,28 @@ |
225 | undocumented_scopes.add(scope_name) |
226 | |
227 | |
228 | +default_scopes = (DefaultScope(),) |
229 | + |
230 | + |
231 | class ScopesFromRequest(MultiScopeHandler): |
232 | """Identify feature scopes based on request state.""" |
233 | |
234 | def __init__(self, request): |
235 | - super(ScopesFromRequest, self).__init__([ |
236 | - DefaultScope(), |
237 | - PageScope(request), |
238 | - TeamScope(), |
239 | - ServerScope()]) |
240 | + scopes = list(default_scopes) |
241 | + scopes.extend([ |
242 | + PageScope(request._orig_env.get('launchpad.pageid', '')), |
243 | + ServerScope(), |
244 | + ]) |
245 | + person = IPerson(request.principal, None) |
246 | + if person is not None: |
247 | + scopes.append(TeamScope(person)) |
248 | + super(ScopesFromRequest, self).__init__(scopes) |
249 | |
250 | |
251 | class ScopesForScript(MultiScopeHandler): |
252 | """Identify feature scopes for a given script.""" |
253 | |
254 | def __init__(self, script_name): |
255 | - super(ScopesForScript, self).__init__([ |
256 | - DefaultScope(), |
257 | - ScriptScope(script_name)]) |
258 | + scopes = list(default_scopes) |
259 | + scopes.append(ScriptScope(script_name)) |
260 | + super(ScopesForScript, self).__init__(scopes) |
261 | |
262 | === modified file 'lib/lp/services/features/tests/test_scopes.py' |
263 | --- lib/lp/services/features/tests/test_scopes.py 2011-08-02 23:42:08 +0000 |
264 | +++ lib/lp/services/features/tests/test_scopes.py 2011-09-19 04:10:29 +0000 |
265 | @@ -9,18 +9,20 @@ |
266 | from lp.testing import TestCaseWithFactory |
267 | from lp.services.features.scopes import ( |
268 | BaseScope, |
269 | - BaseWebRequestScope, |
270 | MultiScopeHandler, |
271 | ScopesForScript, |
272 | ScriptScope, |
273 | ) |
274 | |
275 | |
276 | -class FakeScope(BaseWebRequestScope): |
277 | +class FakeScope(BaseScope): |
278 | pattern = r'fake:' |
279 | |
280 | + def __init__(self, name): |
281 | + self.name = name |
282 | + |
283 | def lookup(self, scope_name): |
284 | - return scope_name == (self.pattern + self.request) |
285 | + return scope_name == (self.pattern + self.name) |
286 | |
287 | |
288 | class TestScopes(TestCaseWithFactory): |
289 | |
290 | === modified file 'lib/lp/services/features/tests/test_webapp.py' |
291 | --- lib/lp/services/features/tests/test_webapp.py 2011-01-20 18:33:25 +0000 |
292 | +++ lib/lp/services/features/tests/test_webapp.py 2011-09-19 04:10:29 +0000 |
293 | @@ -86,14 +86,14 @@ |
294 | |
295 | def test_team_scope_outside_team(self): |
296 | request = LaunchpadTestRequest() |
297 | + self.factory.loginAsAnyone(request) |
298 | scopes = webapp.ScopesFromRequest(request) |
299 | - self.factory.loginAsAnyone() |
300 | self.assertFalse(scopes.lookup('team:nonexistent')) |
301 | |
302 | def test_team_scope_in_team(self): |
303 | request = LaunchpadTestRequest() |
304 | - scopes = webapp.ScopesFromRequest(request) |
305 | member = self.factory.makePerson() |
306 | team = self.factory.makeTeam(members=[member]) |
307 | - login_as(member) |
308 | + login_as(member, request) |
309 | + scopes = webapp.ScopesFromRequest(request) |
310 | self.assertTrue(scopes.lookup('team:%s' % team.name)) |
311 | |
312 | === added file 'lib/lp/services/features/tests/test_xmlrpc.py' |
313 | --- lib/lp/services/features/tests/test_xmlrpc.py 1970-01-01 00:00:00 +0000 |
314 | +++ lib/lp/services/features/tests/test_xmlrpc.py 2011-09-19 04:10:29 +0000 |
315 | @@ -0,0 +1,99 @@ |
316 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
317 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
318 | + |
319 | +"""Tests for FeatureFlagApplication.""" |
320 | + |
321 | +__metaclass__ = type |
322 | + |
323 | +import xmlrpclib |
324 | + |
325 | +from canonical.testing.layers import DatabaseFunctionalLayer |
326 | +from canonical.config import config |
327 | +from lp.services import features |
328 | +from lp.services.features.flags import FeatureController |
329 | +from lp.services.features.rulesource import StormFeatureRuleSource |
330 | +from lp.services.features.scopes import ( |
331 | + DefaultScope, |
332 | + FixedScope, |
333 | + MultiScopeHandler, |
334 | + ) |
335 | +from lp.services.features.xmlrpc import FeatureFlagApplication |
336 | +from lp.testing import ( |
337 | + feature_flags, |
338 | + set_feature_flag, |
339 | + TestCaseWithFactory, |
340 | + ) |
341 | +from lp.testing.xmlrpc import XMLRPCTestTransport |
342 | + |
343 | + |
344 | +class TestGetFeatureFlag(TestCaseWithFactory): |
345 | + |
346 | + layer = DatabaseFunctionalLayer |
347 | + |
348 | + def setUp(self): |
349 | + TestCaseWithFactory.setUp(self) |
350 | + self.endpoint = FeatureFlagApplication() |
351 | + |
352 | + def installFeatureController(self, feature_controller): |
353 | + old_features = features.get_relevant_feature_controller() |
354 | + features.install_feature_controller(feature_controller) |
355 | + self.addCleanup( |
356 | + features.install_feature_controller, old_features) |
357 | + |
358 | + def test_getFeatureFlag_returns_None_by_default(self): |
359 | + self.assertIs(None, self.endpoint.getFeatureFlag(u'unknown')) |
360 | + |
361 | + def test_getFeatureFlag_returns_true_for_set_flag(self): |
362 | + flag_name = u'flag' |
363 | + with feature_flags(): |
364 | + set_feature_flag(flag_name, u'1') |
365 | + self.assertEqual(u'1', self.endpoint.getFeatureFlag(flag_name)) |
366 | + |
367 | + def test_getFeatureFlag_ignores_relevant_feature_controller(self): |
368 | + # getFeatureFlag should only consider the scopes it is asked to |
369 | + # consider, not any that happen to be active due to the XML-RPC |
370 | + # request itself. |
371 | + flag_name = u'flag' |
372 | + scope_name = u'scope' |
373 | + self.installFeatureController( |
374 | + FeatureController( |
375 | + MultiScopeHandler( |
376 | + [DefaultScope(), FixedScope(scope_name)]).lookup, |
377 | + StormFeatureRuleSource())) |
378 | + set_feature_flag(flag_name, u'1', scope_name) |
379 | + self.assertEqual(None, self.endpoint.getFeatureFlag(flag_name)) |
380 | + |
381 | + def test_getFeatureFlag_considers_supplied_scope(self): |
382 | + flag_name = u'flag' |
383 | + scope_name = u'scope' |
384 | + with feature_flags(): |
385 | + set_feature_flag(flag_name, u'value', scope_name) |
386 | + self.assertEqual( |
387 | + u'value', |
388 | + self.endpoint.getFeatureFlag(flag_name, [scope_name])) |
389 | + |
390 | + def test_getFeatureFlag_turns_user_into_team_scope(self): |
391 | + flag_name = u'flag' |
392 | + person = self.factory.makePerson() |
393 | + team = self.factory.makeTeam(members=[person]) |
394 | + with feature_flags(): |
395 | + set_feature_flag(flag_name, u'value', u'team:' + team.name) |
396 | + self.assertEqual( |
397 | + u'value', |
398 | + self.endpoint.getFeatureFlag( |
399 | + flag_name, ['user:' + person.name])) |
400 | + |
401 | + def test_xmlrpc_interface_unset(self): |
402 | + sp = xmlrpclib.ServerProxy( |
403 | + config.launchpad.feature_flags_endpoint, |
404 | + transport=XMLRPCTestTransport(), allow_none=True) |
405 | + self.assertEqual(None, sp.getFeatureFlag(u'flag')) |
406 | + |
407 | + def test_xmlrpc_interface_set(self): |
408 | + sp = xmlrpclib.ServerProxy( |
409 | + config.launchpad.feature_flags_endpoint, |
410 | + transport=XMLRPCTestTransport(), allow_none=True) |
411 | + flag_name = u'flag' |
412 | + with feature_flags(): |
413 | + set_feature_flag(flag_name, u'1') |
414 | + self.assertEqual(u'1', sp.getFeatureFlag(flag_name)) |
415 | |
416 | === added file 'lib/lp/services/features/xmlrpc.py' |
417 | --- lib/lp/services/features/xmlrpc.py 1970-01-01 00:00:00 +0000 |
418 | +++ lib/lp/services/features/xmlrpc.py 2011-09-19 04:10:29 +0000 |
419 | @@ -0,0 +1,59 @@ |
420 | +# Copyright 2011 Canonical Ltd. This software is licensed under the |
421 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
422 | + |
423 | +"""FeatureFlagApplication allows access to information about feature flags.""" |
424 | + |
425 | +__metaclass__ = type |
426 | +__all__ = [ |
427 | + 'IFeatureFlagApplication', |
428 | + 'FeatureFlagApplication', |
429 | + ] |
430 | + |
431 | +from zope.component import getUtility |
432 | +from zope.interface import implements |
433 | + |
434 | +from canonical.launchpad.webapp.interfaces import ILaunchpadApplication |
435 | +from lp.registry.interfaces.person import IPersonSet |
436 | +from lp.services.features.flags import FeatureController |
437 | +from lp.services.features.rulesource import StormFeatureRuleSource |
438 | +from lp.services.features.scopes import ( |
439 | + default_scopes, |
440 | + FixedScope, |
441 | + MultiScopeHandler, |
442 | + TeamScope, |
443 | + ) |
444 | + |
445 | + |
446 | +class IFeatureFlagApplication(ILaunchpadApplication): |
447 | + """Mailing lists application root.""" |
448 | + |
449 | + def getFeatureFlag(flag_name, username=None, scopes=()): |
450 | + """Return the value of the given feature flag. |
451 | + |
452 | + :param flag_name: The name of the flag to query. |
453 | + :param username: If supplied, the name of a Person to use in |
454 | + evaluating the 'team:' scope. |
455 | + :param scopes: A list of scopes to consider active. The 'default' |
456 | + scope is always considered to be active, and does not need to be |
457 | + included here. |
458 | + """ |
459 | + |
460 | + |
461 | +class FeatureFlagApplication: |
462 | + |
463 | + implements(IFeatureFlagApplication) |
464 | + |
465 | + def getFeatureFlag(self, flag_name, active_scopes=()): |
466 | + scopes = list(default_scopes) |
467 | + for scope_name in active_scopes: |
468 | + if scope_name.startswith('user:'): |
469 | + person = getUtility(IPersonSet).getByName( |
470 | + scope_name[len('user:'):]) |
471 | + if person is not None: |
472 | + scopes.append(TeamScope(person)) |
473 | + else: |
474 | + scopes.append(FixedScope(scope_name)) |
475 | + flag_name = unicode(flag_name) |
476 | + controller = FeatureController( |
477 | + MultiScopeHandler(scopes).lookup, StormFeatureRuleSource()) |
478 | + return controller.getFlag(flag_name) |
479 | |
480 | === modified file 'lib/lp/testing/factory.py' |
481 | --- lib/lp/testing/factory.py 2011-09-16 18:33:57 +0000 |
482 | +++ lib/lp/testing/factory.py 2011-09-19 04:10:29 +0000 |
483 | @@ -505,7 +505,7 @@ |
484 | for any other required objects. |
485 | """ |
486 | |
487 | - def loginAsAnyone(self): |
488 | + def loginAsAnyone(self, participation=None): |
489 | """Log in as an arbitrary person. |
490 | |
491 | If you want to log in as a celebrity, including admins, see |
492 | @@ -513,7 +513,7 @@ |
493 | """ |
494 | login(ANONYMOUS) |
495 | person = self.makePerson() |
496 | - login_as(person) |
497 | + login_as(person, participation) |
498 | return person |
499 | |
500 | @with_celebrity_logged_in('admin') |
501 | |
502 | === modified file 'utilities/page-performance-report.ini' |
503 | --- utilities/page-performance-report.ini 2011-04-28 21:52:17 +0000 |
504 | +++ utilities/page-performance-report.ini 2011-09-19 04:10:29 +0000 |
505 | @@ -11,7 +11,8 @@ |
506 | [^/]+($|/ |
507 | (?!\+haproxy|\+opstats|\+access-token |
508 | |((authserver|bugs|bazaar|codehosting| |
509 | - codeimportscheduler|mailinglists|softwarecenteragent)/\w+$))) |
510 | + codeimportscheduler|mailinglists|softwarecenteragent| |
511 | + featureflags)/\w+$))) |
512 | Other=^/ |
513 | |
514 | Launchpad Frontpage=^https?://launchpad\.[^/]+(/index\.html)?$ |
515 | @@ -54,7 +55,7 @@ |
516 | Private XML-RPC=^https://(launchpad|xmlrpc)[^/]+/ |
517 | (authserver|bugs|codehosting| |
518 | codeimportscheduler|mailinglists| |
519 | - softwarecenteragent)/\w+$ |
520 | + softwarecenteragent|featureflags)/\w+$ |
521 | |
522 | [metrics] |
523 | ppr_all=All Launchpad except operational pages |
* if we passed a dict of values from the client, would that make it
easier to consider other factors later (like the client ip, bzr
version, front end server, etc)? bearing in mind that different
machines that talk to the xmlrpc server may have different subsets of
these, hardcoding them as parameters may be inflexible. (maybe xmlrpc
can't do a dict but you can do a list of pairs of strings.)
* maybe FixedScope could be somewhere more general (why not have it
available in production, for that matter.)
* there are developer docs in the tree that ought to be updated.
this looks really useful, thanks.