Merge lp:~bloodearnest/django-preflight/gargoyle2 into lp:django-preflight
- gargoyle2
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Michael Foord |
Approved revision: | 26 |
Merged at revision: | 25 |
Proposed branch: | lp:~bloodearnest/django-preflight/gargoyle2 |
Merge into: | lp:django-preflight |
Diff against target: |
490 lines (+248/-22) 10 files modified
.bzrignore (+1/-0) doc/conf.py (+1/-1) doc/install.rst (+3/-3) preflight/models.py (+36/-0) preflight/templates/preflight/overview.html (+68/-7) preflight/tests.py (+111/-1) preflight/views.py (+5/-0) preflight_example_project/run.py (+2/-2) preflight_example_project/settings.py (+7/-2) setup.py (+14/-6) |
To merge this branch: | bzr merge lp:~bloodearnest/django-preflight/gargoyle2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Foord (community) | Approve | ||
Review via email: mp+166845@code.launchpad.net |
Commit message
Add support for gargoyle flags in preflight
Description of the change
Add support for gargoyle flags in preflight
Simon Davy (bloodearnest) wrote : | # |
Michael Foord (mfoord) wrote : | # |
The code should not depend on settings.
Other than that all looks good.
Ricardo Kirkner (ricardokirkner) wrote : | # |
- swap l. 72 and 73 around
- l 109, 120, 143, 155: fix whitespace before :
- l.280: why not add in l.270?
- l.398: what's the need of renaming the example_project folder?
- l.428: since you renamed the db, why add the old name to bzrignore? (l. 9)
Ricardo Kirkner (ricardokirkner) wrote : | # |
Can you also include a screenshot of how the new template looks like with the flags?
Simon Davy (bloodearnest) wrote : | # |
> The code should not depend on settings.
>
> Other than that all looks good.
Done
Simon Davy (bloodearnest) wrote : | # |
> - swap l. 72 and 73 around
> - l 109, 120, 143, 155: fix whitespace before :
> - l.280: why not add in l.270?
Done the above
> - l.398: what's the need of renaming the example_project folder?
This is unfortunately necessary because the nexus package (a dep of gargoyle) is higher in sys.pth when run with "python setup.py test", and it erroneously includes its example_project directory, meaning that the test_suite in setup.py ("example_
Pull request to exclude example_project upstream: https:/
> - l.428: since you renamed the db, why add the old name to bzrignore? (l. 9)
Done - a left over from earlier
- 26. By Simon Davy
-
addressing review comments
Simon Davy (bloodearnest) wrote : | # |
Screen shot
Michael Foord (mfoord) wrote : | # |
Good work. Thanks.
Preview Diff
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2012-05-02 21:37:14 +0000 |
3 | +++ .bzrignore 2013-06-03 10:45:34 +0000 |
4 | @@ -4,3 +4,4 @@ |
5 | .tox |
6 | *.egg-info |
7 | doc/_build |
8 | +preflight.wp* |
9 | |
10 | === modified file 'doc/conf.py' |
11 | --- doc/conf.py 2011-02-09 16:26:04 +0000 |
12 | +++ doc/conf.py 2013-06-03 10:45:34 +0000 |
13 | @@ -18,7 +18,7 @@ |
14 | # documentation root, use os.path.abspath to make it absolute, like shown here. |
15 | sys.path.append(os.path.abspath('..')) |
16 | |
17 | -os.environ['DJANGO_SETTINGS_MODULE'] = 'example_project.settings' |
18 | +os.environ['DJANGO_SETTINGS_MODULE'] = 'preflight_example_project.settings' |
19 | |
20 | # -- General configuration ----------------------------------------------------- |
21 | |
22 | |
23 | === modified file 'doc/install.rst' |
24 | --- doc/install.rst 2013-05-17 14:52:42 +0000 |
25 | +++ doc/install.rst 2013-06-03 10:45:34 +0000 |
26 | @@ -56,10 +56,10 @@ |
27 | |
28 | import preflight |
29 | import preflight.urls |
30 | - |
31 | - |
32 | + |
33 | + |
34 | preflight.autodiscover() |
35 | - |
36 | + |
37 | urlpatterns = patterns('', |
38 | (r'^preflight/', include(preflight.urls)), |
39 | ) |
40 | |
41 | === modified file 'preflight/models.py' |
42 | --- preflight/models.py 2013-02-20 21:15:50 +0000 |
43 | +++ preflight/models.py 2013-06-03 10:45:34 +0000 |
44 | @@ -142,3 +142,39 @@ |
45 | To be able to access you need to have permission from every preflight class. |
46 | """ |
47 | return all(class_().authenticate(request) for class_ in REGISTRY) |
48 | + |
49 | + |
50 | +def gather_switches(): |
51 | + """Gather all switches into one list.""" |
52 | + switches = {} |
53 | + gargoyle_switches = gather_gargoyle() |
54 | + if gargoyle_switches: |
55 | + switches['gargoyle'] = gargoyle_switches |
56 | + return switches |
57 | + |
58 | + |
59 | +def gather_gargoyle(): |
60 | + try: |
61 | + from gargoyle.models import Switch |
62 | + except ImportError: |
63 | + return None |
64 | + return [_format_gargoyle(switch) for switch in Switch.objects.all()] |
65 | + |
66 | + |
67 | +def _format_gargoyle(switch): |
68 | + """Formats a gargoyle switch into std format for display""" |
69 | + from gargoyle import gargoyle |
70 | + d = switch.to_dict(gargoyle) |
71 | + conditions = [] |
72 | + for condition in d['conditions']: |
73 | + params = ','.join('%s=%s' % (c[0], c[2]) |
74 | + for c in condition['conditions']) |
75 | + conditions.append("%s(%s)" % (condition['label'], params)) |
76 | + |
77 | + return dict( |
78 | + name=d['key'], |
79 | + description=d['description'], |
80 | + status=d['status'], |
81 | + status_text=d['statusLabel'], |
82 | + conditions=conditions, |
83 | + ) |
84 | |
85 | === modified file 'preflight/templates/preflight/overview.html' |
86 | --- preflight/templates/preflight/overview.html 2012-05-02 21:37:14 +0000 |
87 | +++ preflight/templates/preflight/overview.html 2013-06-03 10:45:34 +0000 |
88 | @@ -11,8 +11,21 @@ |
89 | <div id="preflight"> |
90 | <h1>{% trans "Pre-flight Applications Checks" %}</h1> |
91 | |
92 | + <ul id="links"> |
93 | + {% for application in applications %} |
94 | + <li><a href="#{{ application.name|slugify }}">{{ application.name }}</a></li> |
95 | + {% endfor %} |
96 | + <li><a href="#versions">Versions</a></li> |
97 | + <li><a href="#settings">Settings</a></li> |
98 | + {% if switches %} |
99 | + <li><a href="#switches">Switches</a></li> |
100 | + {% endif %} |
101 | + </ul> |
102 | + |
103 | {% for application in applications %} |
104 | - <h2>{{ application.name }}</h2> |
105 | + <a href="#links" style="text-decoration: none;"> |
106 | + <h2 id="{{ application.name|slugify }}">{{ application.name }}</h2> |
107 | + </a> |
108 | <table class="{{ preflight_table_class }}"> |
109 | <thead> |
110 | <tr> |
111 | @@ -39,7 +52,9 @@ |
112 | </table> |
113 | {% endfor %} |
114 | |
115 | - <h1>{% trans "Versions" %}</h1> |
116 | + <a href="#links" style="text-decoration: none;"> |
117 | + <h1 id="versions">{% trans "Versions" %}</h1> |
118 | + </a> |
119 | <table class="{{ preflight_table_class }}"> |
120 | <thead> |
121 | <tr> |
122 | @@ -49,15 +64,17 @@ |
123 | </thead> |
124 | <tbody> |
125 | {% for item in versions %} |
126 | - <tr> |
127 | - <td>{{ item.name }}</td> |
128 | - <td>{{ item.version }}</td> |
129 | - </tr> |
130 | + <tr> |
131 | + <td>{{ item.name }}</td> |
132 | + <td>{{ item.version }}</td> |
133 | + </tr> |
134 | {% endfor %} |
135 | </tbody> |
136 | </table> |
137 | |
138 | - <h1>{% trans "Settings" %}</h1> |
139 | + <a href="#links" style="text-decoration: none;"> |
140 | + <h1 id="settings">{% trans "Settings" %}</h1> |
141 | + </a> |
142 | <dl class="{{ preflight_table_class }}"> |
143 | {% for setting in settings %} |
144 | <dt><b>{{ setting.name }}</b></dt> |
145 | @@ -65,6 +82,50 @@ |
146 | {% if setting.location %}<dd>Location: {{ setting.location}}</dd>{% endif %} |
147 | {% endfor %} |
148 | </dl> |
149 | + |
150 | + {% if switches %} |
151 | + <a href="#links" style="text-decoration: none;"> |
152 | + <h1 id="switches">{% trans "Switches" %}</h1> |
153 | + </a> |
154 | + <table id="switches-table" class="{{ preflight_table_class }}"> |
155 | + <thead> |
156 | + <tr> |
157 | + <th>{% trans "Name" %}</th> |
158 | + <th>{% trans "Description" %}</th> |
159 | + <th>{% trans "Conditions" %}</th> |
160 | + <th>{% trans "Status" %}</th> |
161 | + </tr> |
162 | + </thead> |
163 | + <tbody> |
164 | + {% for type, type_switches in switches.items %} |
165 | + <tr><th colspan="4">{{ type }}</th></tr> |
166 | + {% for switch in type_switches %} |
167 | + <tr class="switch"> |
168 | + <td>{{ switch.name }}</td> |
169 | + <td>{{ switch.description }}</td> |
170 | + <td> |
171 | + {% if switch.conditions %} |
172 | + <ul> |
173 | + {% for condition in switch.conditions %} |
174 | + <li>{{ condition|first }}: {{ condition|last }}</li> |
175 | + {% endfor %} |
176 | + </ul> |
177 | + {% endif %} |
178 | + </td> |
179 | + <td>{{ switch.status_text }}</td> |
180 | + {% endfor %} |
181 | + </tr> |
182 | + {% endfor %} |
183 | + </tbody> |
184 | + </table> |
185 | + <p>Switch data is included in JSON format in a script tag with id |
186 | + "switches-json" to aid automated scripting</p> |
187 | + <script id="switches-json"> |
188 | + {{ switches_json|safe }} |
189 | + </script> |
190 | + {% else %} |
191 | + <p>No switches.</p> |
192 | + {% endif %} |
193 | </div> |
194 | |
195 | <p>View generated at: {{ now }}</p> |
196 | |
197 | === modified file 'preflight/tests.py' |
198 | --- preflight/tests.py 2013-05-17 14:51:54 +0000 |
199 | +++ preflight/tests.py 2013-06-03 10:45:34 +0000 |
200 | @@ -1,18 +1,26 @@ |
201 | # Copyright 2010 Canonical Ltd. This software is licensed under the |
202 | # GNU Affero General Public License version 3 (see the file LICENSE). |
203 | import sys |
204 | +import json |
205 | +try: |
206 | + from unittest import skipIf |
207 | +except ImportError: |
208 | + from unittest2 import skipIf # NOQA |
209 | |
210 | +from django.conf import settings |
211 | from django.core.management import call_command |
212 | from django.core.urlresolvers import reverse |
213 | from django.http import HttpResponse |
214 | from django.test import TestCase |
215 | -from django.template import RequestContext, TemplateDoesNotExist |
216 | +from django.template import RequestContext |
217 | +from django.template.loader import render_to_string |
218 | from mock import ( |
219 | Mock, |
220 | patch, |
221 | ) |
222 | |
223 | from cStringIO import StringIO |
224 | +from pyquery import PyQuery |
225 | |
226 | from . import Preflight |
227 | from .models import ( |
228 | @@ -21,9 +29,12 @@ |
229 | REGISTRY, |
230 | authenticate, |
231 | gather_checks, |
232 | + gather_switches, |
233 | + gather_gargoyle, |
234 | gather_settings, |
235 | gather_versions, |
236 | ) |
237 | +from preflight.conf import BASE_TEMPLATE, TABLE_CLASS |
238 | |
239 | |
240 | class CheckTestCase(TestCase): |
241 | @@ -142,6 +153,7 @@ |
242 | def authenticate(self, request): |
243 | return False |
244 | |
245 | + |
246 | def clear_registry(): |
247 | while REGISTRY: |
248 | REGISTRY.pop() |
249 | @@ -182,6 +194,7 @@ |
250 | def versions(self): |
251 | return [{'name': 'spam', 'version': 'ni'}] |
252 | |
253 | + |
254 | class ExtraVersionAsList(Preflight): |
255 | |
256 | versions = [{'name': 'eggs', 'version': 'peng'}] |
257 | @@ -294,3 +307,100 @@ |
258 | mock_settings.PREFLIGHT_HIDDEN_SETTINGS = '' |
259 | settings = gather_settings() |
260 | self.assertEqual([], settings) |
261 | + |
262 | + |
263 | +@skipIf(not settings.USE_GARGOYLE, 'skipping for Django 1.1') |
264 | +class GargoyleTestCase(TestCase): |
265 | + from gargoyle import gargoyle # NOQA |
266 | + from gargoyle.builtins import IPAddressConditionSet # NOQA |
267 | + from gargoyle.models import ( # NOQA |
268 | + Switch, |
269 | + DISABLED, |
270 | + SELECTIVE, |
271 | + GLOBAL, |
272 | + INHERIT |
273 | + ) |
274 | + |
275 | + def setUp(self): |
276 | + super(GargoyleTestCase, self).setUp() |
277 | + self.gargoyle.register(self.IPAddressConditionSet()) |
278 | + |
279 | + def get_switches(self): |
280 | + switches = [ |
281 | + self.Switch(key='DISABLED', status=self.DISABLED, |
282 | + description='switch 1'), |
283 | + self.Switch(key='SELECTIVE_1', status=self.SELECTIVE), |
284 | + self.Switch(key='SELECTIVE_2', status=self.SELECTIVE), |
285 | + self.Switch(key='GLOBAL', status=self.GLOBAL), |
286 | + self.Switch(key='INHERIT', status=self.INHERIT), |
287 | + ] |
288 | + selective = switches[2] |
289 | + selective.add_condition( |
290 | + self.gargoyle, |
291 | + condition_set='gargoyle.builtins.IPAddressConditionSet', |
292 | + field_name='ip_address', |
293 | + condition='127.0.0.1', |
294 | + ) |
295 | + return switches |
296 | + |
297 | + @patch.dict(sys.modules, **{'gargoyle.models': None}) |
298 | + def test_gather_switches_no_gargoyle(self): |
299 | + self.assertEqual(gather_gargoyle(), None) |
300 | + |
301 | + def assert_switches_dict(self, actual): |
302 | + expected = [ |
303 | + dict(name='DISABLED', |
304 | + status=self.DISABLED, |
305 | + description='switch 1', |
306 | + status_text=self.Switch.STATUS_LABELS[self.DISABLED], |
307 | + conditions=[]), |
308 | + dict(name='SELECTIVE_1', |
309 | + status=self.SELECTIVE, |
310 | + description=None, |
311 | + status_text=self.Switch.STATUS_LABELS[self.GLOBAL], |
312 | + conditions=[]), |
313 | + dict(name='SELECTIVE_2', status=self.SELECTIVE, |
314 | + description=None, |
315 | + status_text=self.Switch.STATUS_LABELS[self.SELECTIVE], |
316 | + conditions=['IP Address(ip_address=127.0.0.1)']), |
317 | + dict(name='GLOBAL', status=self.GLOBAL, |
318 | + description=None, |
319 | + status_text=self.Switch.STATUS_LABELS[self.GLOBAL], |
320 | + conditions=[]), |
321 | + dict(name='INHERIT', status=self.INHERIT, |
322 | + description=None, |
323 | + status_text=self.Switch.STATUS_LABELS[self.INHERIT], |
324 | + conditions=[]), |
325 | + ] |
326 | + self.assertEqual(actual, expected) |
327 | + |
328 | + @patch('gargoyle.models.Switch.objects.all') |
329 | + def test_gather_switches(self, mock_all): |
330 | + mock_all.return_value = self.get_switches() |
331 | + self.assert_switches_dict(gather_gargoyle()) |
332 | + |
333 | + @patch('gargoyle.models.Switch.objects.all') |
334 | + def test_gargoyle_template(self, mock_all): |
335 | + switches = self.get_switches() |
336 | + mock_all.return_value = switches |
337 | + the_switches = gather_switches() |
338 | + context = { |
339 | + "switches": the_switches, |
340 | + "switches_json": json.dumps(the_switches), |
341 | + "preflight_base_template": BASE_TEMPLATE, |
342 | + "preflight_table_class": TABLE_CLASS, |
343 | + } |
344 | + response = render_to_string('preflight/overview.html', context) |
345 | + dom = PyQuery(response) |
346 | + table = dom.find('#switches-table tbody') |
347 | + self.assertEqual(table.find('tr')[0][0].text, 'gargoyle') |
348 | + |
349 | + for row, switch in zip(table.find('tr.switch'), switches): |
350 | + self.assertEqual(row[0].text, switch.key) |
351 | + self.assertEqual(row[1].text, str(switch.description)) |
352 | + self.assertEqual(row[3].text, switch.get_status_label()) |
353 | + |
354 | + data = json.loads(dom.find('#switches-json').text()) |
355 | + self.assertTrue('gargoyle' in data) |
356 | + json_switches = data['gargoyle'] |
357 | + self.assert_switches_dict(json_switches) |
358 | |
359 | === modified file 'preflight/views.py' |
360 | --- preflight/views.py 2013-05-17 14:51:29 +0000 |
361 | +++ preflight/views.py 2013-06-03 10:45:34 +0000 |
362 | @@ -2,6 +2,7 @@ |
363 | # GNU Affero General Public License version 3 (see the file LICENSE). |
364 | |
365 | from datetime import datetime |
366 | +import json |
367 | |
368 | from django.views.decorators.cache import never_cache |
369 | from django.http import Http404 |
370 | @@ -13,6 +14,7 @@ |
371 | gather_checks, |
372 | gather_settings, |
373 | gather_versions, |
374 | + gather_switches, |
375 | ) |
376 | from .conf import BASE_TEMPLATE, TABLE_CLASS |
377 | |
378 | @@ -22,10 +24,13 @@ |
379 | if not authenticate(request): |
380 | raise Http404 |
381 | |
382 | + switches = gather_switches() |
383 | context = RequestContext(request, { |
384 | "applications": gather_checks(), |
385 | "versions": gather_versions(), |
386 | "settings": gather_settings(), |
387 | + "switches": switches, |
388 | + "switches_json": json.dumps(switches), |
389 | "now": datetime.now(), |
390 | "preflight_base_template": BASE_TEMPLATE, |
391 | "preflight_table_class": TABLE_CLASS, |
392 | |
393 | === renamed directory 'example_project' => 'preflight_example_project' |
394 | === modified file 'preflight_example_project/run.py' |
395 | --- example_project/run.py 2011-02-07 16:06:51 +0000 |
396 | +++ preflight_example_project/run.py 2013-06-03 10:45:34 +0000 |
397 | @@ -1,8 +1,8 @@ |
398 | import os |
399 | import sys |
400 | |
401 | -os.environ['DJANGO_SETTINGS_MODULE'] = 'example_project.settings' |
402 | -sys.path.insert(0, 'example_project') |
403 | +os.environ['DJANGO_SETTINGS_MODULE'] = 'preflight_example_project.settings' |
404 | +sys.path.insert(0, 'preflight_example_project') |
405 | |
406 | try: |
407 | from django.test.utils import get_runner |
408 | |
409 | === modified file 'preflight_example_project/settings.py' |
410 | --- example_project/settings.py 2013-05-17 14:51:54 +0000 |
411 | +++ preflight_example_project/settings.py 2013-06-03 10:45:34 +0000 |
412 | @@ -1,4 +1,5 @@ |
413 | import os |
414 | +import django |
415 | |
416 | DEBUG = True |
417 | TEMPLATE_DEBUG = DEBUG |
418 | @@ -9,7 +10,7 @@ |
419 | DATABASES = { |
420 | 'default': { |
421 | 'ENGINE': 'django.db.backends.sqlite3', |
422 | - 'NAME': 'database.sqlit3', |
423 | + 'NAME': 'database.sqlite3', |
424 | } |
425 | } |
426 | |
427 | @@ -25,7 +26,7 @@ |
428 | ADMIN_MEDIA_PREFIX = '/media/' |
429 | SECRET_KEY = 'j0-wo!y4zwi)tejv1u(0skc41_379ls^@qbh91%#5o=greje6(' |
430 | |
431 | -ROOT_URLCONF = 'example_project.urls' |
432 | +ROOT_URLCONF = 'preflight_example_project.urls' |
433 | |
434 | TEMPLATE_DIRS = ( |
435 | os.path.join(os.path.realpath(os.path.dirname(__file__)), 'templates'), |
436 | @@ -52,4 +53,8 @@ |
437 | 'app', |
438 | ) |
439 | |
440 | +USE_GARGOYLE = django.VERSION[:2] > (1, 1) |
441 | +if USE_GARGOYLE: |
442 | + INSTALLED_APPS = INSTALLED_APPS + ('gargoyle',) |
443 | + |
444 | PREFLIGHT_BASE_TEMPLATE = 'base.html' |
445 | |
446 | === modified file 'setup.py' |
447 | --- setup.py 2013-05-17 14:55:49 +0000 |
448 | +++ setup.py 2013-06-03 10:45:34 +0000 |
449 | @@ -1,6 +1,8 @@ |
450 | # -*- encoding: utf-8 -*- |
451 | # Copyright 2010 Canonical Ltd. This software is licensed under the |
452 | # GNU Affero General Public License version 3 (see the file LICENSE). |
453 | +import sys |
454 | + |
455 | try: |
456 | from ast import PyCF_ONLY_AST |
457 | except ImportError: |
458 | @@ -14,6 +16,14 @@ |
459 | for line in open('preflight/__init__.py') |
460 | if line.startswith('__version__')][0] |
461 | |
462 | +tests_require = [ |
463 | + 'mock > 0.6', |
464 | + 'gargoyle >= 0.6.0', |
465 | + 'pyquery', |
466 | +] |
467 | + |
468 | +if sys.version_info[:2] < (2, 7): |
469 | + tests_require.append('unittest2') |
470 | |
471 | setup( |
472 | name='django-preflight', |
473 | @@ -46,13 +56,11 @@ |
474 | 'preflight': ['templates/preflight/*.html'], |
475 | }, |
476 | install_requires=[ |
477 | - 'django >= 1.1' |
478 | - ], |
479 | - tests_require=[ |
480 | - 'mock > 0.6' |
481 | - ], |
482 | + 'django >= 1.1', |
483 | + ], |
484 | + tests_require=tests_require, |
485 | extras_require={ |
486 | 'docs': ['Sphinx'], |
487 | }, |
488 | - test_suite='example_project.run.tests', |
489 | + test_suite='preflight_example_project.run.tests', |
490 | ) |
Continuation of mfoords MP: https:/ /code.launchpad .net/~canonical -isd-hackers/ django- preflight/ gargoyle/ +merge/ 106374