Merge lp:~roadmr/isitdeployable/existingdeployments into lp:isitdeployable

Proposed by Daniel Manrique
Status: Merged
Approved by: Daniel Manrique
Approved revision: 314
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: lp:~roadmr/isitdeployable/existingdeployments
Merge into: lp:isitdeployable
Diff against target: 696 lines (+404/-26)
16 files modified
dev_config/supervisor.conf (+2/-2)
requirements.txt (+2/-2)
revtracker/admin.py (+8/-0)
revtracker/data.py (+1/-0)
revtracker/migrations/0002_existingdeployment.py (+28/-0)
revtracker/models.py (+37/-0)
revtracker/static/scss/deploy_tracker.scss (+4/-0)
revtracker/tasks.py (+46/-0)
revtracker/templates/revtracker/index.html (+32/-17)
revtracker/templates/revtracker/project_info.html (+16/-0)
revtracker/tests/factory.py (+17/-0)
revtracker/tests/test_models.py (+30/-0)
revtracker/tests/test_tasks.py (+115/-0)
revtracker/tests/test_views.py (+4/-4)
revtracker/tests/test_views_logged_in.py (+52/-0)
revtracker/views.py (+10/-1)
To merge this branch: bzr merge lp:~roadmr/isitdeployable/existingdeployments
Reviewer Review Type Date Requested Status
Facundo Batista (community) Approve
Maximiliano Bertacchini Approve
Review via email: mp+356594@code.launchpad.net

Commit message

Add querying and displaying of which versions of code are deployed in each service/environment.

Each project can have zero or more "Existing Deployments". Periodically, the service will poke a configured URL for each of these deployments, extract the value from a specific header, and store that for display in both the index page and each project's page.

Since most of our projects expose their current version in a X-VCS-Version or similar, this information can be collected for them, and the index page can then provide at a glance an overview of which versions each project is at.

Description of the change

Add querying and displaying of which versions of code are deployed in each service/environment.

Each project can have zero or more "Existing Deployments". Periodically, the service will poke a configured URL for each of these deployments, extract the value from a specific header, and store that for display in both the index page and each project's page.

Since most of our projects expose their current version in a X-VCS-Version or similar, this information can be collected for them, and the index page can then provide at a glance an overview of which versions each project is at.

To post a comment you must log in.
Revision history for this message
Maximiliano Bertacchini (maxiberta) wrote :

Looks good to me, but have a few comments/questions inline. Thanks!

Revision history for this message
Daniel Manrique (roadmr) wrote :

Thanks! Replied below and fixes coming up.

313. By Daniel Manrique

Space... the final frontier

314. By Daniel Manrique

PEP8 fixes

Revision history for this message
Maximiliano Bertacchini (maxiberta) :
review: Approve
Revision history for this message
Facundo Batista (facundo) wrote :

Looks fine! Added a couple of small questions, but approving it.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'dev_config/supervisor.conf'
2--- dev_config/supervisor.conf 2016-07-25 20:55:05 +0000
3+++ dev_config/supervisor.conf 2018-10-11 19:24:34 +0000
4@@ -12,7 +12,7 @@
5 serverurl=unix:///tmp/supervisord.sock
6
7 [program:celeryd]
8-command=%(here)s/../virtualenv/bin/python %(here)s/../django_project/celeryapp.py worker --pidfile %(here)s/../tmp/celeryd.pid
9+command=%(here)s/../env/bin/python %(here)s/../django_project/celeryapp.py worker --pidfile %(here)s/../tmp/celeryd.pid
10 stdout_logfile=%(here)s/../tmp/celeryd.log
11 stderr_logfile=%(here)s/../tmp/celeryd.log
12 autostart=true
13@@ -20,7 +20,7 @@
14 startsecs=10
15
16 [program:celerybeat]
17-command=%(here)s/../virtualenv/bin/python %(here)s/../django_project/celeryapp.py beat --pidfile %(here)s/../tmp/celerybeat.pid
18+command=%(here)s/../env/bin/python %(here)s/../django_project/celeryapp.py beat --pidfile %(here)s/../tmp/celerybeat.pid
19 stdout_logfile=%(here)s/../tmp/celerybeat.log
20 stderr_logfile=%(here)s/../tmp/celerybeat.log
21 autostart=true
22
23=== modified file 'requirements.txt'
24--- requirements.txt 2018-04-10 19:57:04 +0000
25+++ requirements.txt 2018-10-11 19:24:34 +0000
26@@ -43,9 +43,9 @@
27 oops-wsgi==0.0.11
28 Paste==1.7.5.1
29 psycopg2==2.7.3.2
30-pybars==0.0.4
31+pybars3==0.9.5
32 pygit2==0.24.2
33-PyMeta==0.5.0
34+PyMeta3==0.5.1
35 pyinotify==0.9.3
36 soupmatchers==0.2
37 testresources==0.2.5
38
39=== modified file 'revtracker/admin.py'
40--- revtracker/admin.py 2016-11-11 20:16:01 +0000
41+++ revtracker/admin.py 2018-10-11 19:24:34 +0000
42@@ -1,12 +1,20 @@
43 from django.contrib import admin
44 from revtracker.models import (
45 DeploymentRequest,
46+ ExistingDeployment,
47 Project,
48 )
49
50
51+class ExistingDeploymentInline(admin.TabularInline):
52+ model = ExistingDeployment
53+ fields = ("name", "info_url", "version_header")
54+ extra = 2 # Usually only prod and staging
55+
56+
57 class ProjectAdmin(admin.ModelAdmin):
58 exclude = ('deployed_revision',)
59+ inlines = [ExistingDeploymentInline]
60
61
62 admin.site.register(Project, ProjectAdmin)
63
64=== modified file 'revtracker/data.py'
65--- revtracker/data.py 2018-01-31 00:19:16 +0000
66+++ revtracker/data.py 2018-10-11 19:24:34 +0000
67@@ -16,6 +16,7 @@
68
69 from .models import (
70 DeploymentRequest,
71+ ExistingDeployment,
72 Revision,
73 )
74
75
76=== added file 'revtracker/migrations/0002_existingdeployment.py'
77--- revtracker/migrations/0002_existingdeployment.py 1970-01-01 00:00:00 +0000
78+++ revtracker/migrations/0002_existingdeployment.py 2018-10-11 19:24:34 +0000
79@@ -0,0 +1,28 @@
80+# -*- coding: utf-8 -*-
81+# Generated by Django 1.11.9 on 2018-10-11 15:55
82+from __future__ import unicode_literals
83+
84+from django.db import migrations, models
85+import django.db.models.deletion
86+
87+
88+class Migration(migrations.Migration):
89+
90+ dependencies = [
91+ ('revtracker', '0001_initial'),
92+ ]
93+
94+ operations = [
95+ migrations.CreateModel(
96+ name='ExistingDeployment',
97+ fields=[
98+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
99+ ('name', models.CharField(help_text=b'Descriptive name for the deployment. Examples: Staging, Production, Sandbox.', max_length=512)),
100+ ('date_updated', models.DateTimeField(blank=True, null=True)),
101+ ('info_url', models.CharField(help_text=b'URL from which to get information about this deployment. Must contain the revno/commit id/somethingto identify the deployed version in an HTTP header.', max_length=512)),
102+ ('version_header', models.CharField(help_text=b"Header from the info_url's HTTP response containing thedeployed version's identifier.", max_length=512)),
103+ ('current_version', models.CharField(blank=True, help_text=b"Version identifier as of the date this deployment's data was last updated", max_length=512, null=True)),
104+ ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='revtracker.Project')),
105+ ],
106+ ),
107+ ]
108
109=== modified file 'revtracker/models.py'
110--- revtracker/models.py 2018-01-29 14:57:56 +0000
111+++ revtracker/models.py 2018-10-11 19:24:34 +0000
112@@ -141,3 +141,40 @@
113
114 def __unicode__(self):
115 return u"DeploymentRequest - %s" % self.deploy_revision
116+
117+
118+class ExistingDeployment(models.Model):
119+ project = models.ForeignKey(Project)
120+ name = models.CharField(
121+ max_length=512,
122+ help_text=("Descriptive name for the deployment. Examples: "
123+ "Staging, Production, Sandbox.")
124+ )
125+ date_updated = models.DateTimeField(blank=True, null=True)
126+ info_url = models.CharField(
127+ max_length=512,
128+ help_text=("URL from which to get information about this "
129+ "deployment. Must contain the revno/commit id/something "
130+ "to identify the deployed version in an HTTP header.")
131+ )
132+ version_header = models.CharField(
133+ max_length=512,
134+ help_text=("Header from the info_url's HTTP response containing the "
135+ "deployed version's identifier.")
136+ )
137+ current_version = models.CharField(
138+ max_length=512,
139+ blank=True, null=True,
140+ help_text=("Version identifier as of the date this deployment's data "
141+ "was last updated"))
142+
143+ def __unicode__(self):
144+ project = self.project
145+ verstring = "No current version"
146+ if self.current_version:
147+ verstring = "Version %s" % self.current_version
148+ datestring = "(Not updated yet)"
149+ if self.date_updated:
150+ datestring = "(Updated %s)" % self.date_updated
151+ return u"%s %s - %s %s" % (
152+ project.name, self.name, verstring, datestring)
153
154=== modified file 'revtracker/static/scss/deploy_tracker.scss'
155--- revtracker/static/scss/deploy_tracker.scss 2016-09-28 11:25:47 +0000
156+++ revtracker/static/scss/deploy_tracker.scss 2018-10-11 19:24:34 +0000
157@@ -16,6 +16,10 @@
158 @include box-shadow($shadow-color 0 2px 2px 0);
159 }
160
161+.noshadowplease {
162+ @include box-shadow($shadow-color 0 0 0 0);
163+}
164+
165 .section_content {
166 margin-top: 20px;
167 margin-bottom: 20px;
168
169=== modified file 'revtracker/tasks.py'
170--- revtracker/tasks.py 2018-03-01 17:29:26 +0000
171+++ revtracker/tasks.py 2018-10-11 19:24:34 +0000
172@@ -3,6 +3,7 @@
173 from email.utils import parseaddr
174 import os
175 import re
176+import requests
177 import shutil
178 import tempfile
179
180@@ -19,6 +20,15 @@
181 from django.conf import settings
182 from django.contrib.auth.models import User
183 from django.db.models import Max
184+from django.utils.timezone import now
185+from requests.exceptions import (
186+ ConnectionError,
187+ HTTPError,
188+ InvalidSchema,
189+ InvalidURL,
190+ MissingSchema,
191+ Timeout,
192+ )
193 import pygit2
194 from six.moves.urllib.parse import (
195 unquote,
196@@ -654,6 +664,26 @@
197 return revisions
198
199
200+def get_currently_deployed_version(info_url, version_header):
201+ try:
202+ headers = requests.utils.default_headers()
203+ headers['user-agent'] += " (isitdeployable)"
204+ response = requests.head(info_url, headers=headers, timeout=2)
205+ response.raise_for_status()
206+ except (MissingSchema,
207+ InvalidSchema,
208+ InvalidURL,
209+ ConnectionError,
210+ Timeout,
211+ HTTPError,) as exc:
212+ print("Couldn't get %s: %r" % (info_url, exc))
213+ return (None, None)
214+ if version_header not in response.headers:
215+ print("%s did not have %s header" % (info_url, version_header))
216+ return (None, None)
217+ return (response.headers[version_header], now())
218+
219+
220 def update_revisions(project_id):
221 load_plugins()
222 project = Project.objects.get(pk=project_id)
223@@ -690,15 +720,31 @@
224 branch.unlock()
225
226
227+def update_existing_deployments(project_id):
228+ project = Project.objects.get(pk=project_id)
229+ for ed in project.existingdeployment_set.all():
230+ (version, when) = get_currently_deployed_version(ed.info_url, ed.version_header)
231+ if version and when:
232+ ed.current_version = version
233+ ed.date_updated = when
234+ ed.save()
235+
236+
237 @shared_task
238 def update_revisions_task(project_id):
239 update_revisions(project_id)
240
241
242 @shared_task
243+def update_existing_deployments_task(project_id):
244+ update_existing_deployments(project_id)
245+
246+
247+@shared_task
248 def update_all_projects():
249 for project in Project.objects.all():
250 # Have the subtask expire before the next one will be started to
251 # prevent them piling up if the workers pause.
252 expires = settings.CELERYBEAT_SCHEDULE['update_all_projects']['schedule'].seconds
253 update_revisions_task.subtask((project.pk,), expires=expires).apply_async()
254+ update_existing_deployments_task.subtask((project.pk,), expires=expires).apply_async()
255
256=== modified file 'revtracker/templates/revtracker/index.html'
257--- revtracker/templates/revtracker/index.html 2014-03-28 16:30:03 +0000
258+++ revtracker/templates/revtracker/index.html 2018-10-11 19:24:34 +0000
259@@ -1,24 +1,39 @@
260 <div id="projects">
261 {{#if projects}}
262- {{#each projects}}
263+ {{#each projects}}
264+ <div class="project section sixteen columns alpha">
265 <a href="{{url}}">
266- <div class="project section sixteen columns alpha">
267- <div class="section_content clearfix">
268- <div class="project_name six columns alpha main">{{name}}</div>
269- <div class="undeployed three columns">
270- {{undeployed_count}} undeployed revisions
271- </div>
272- <div class="deployable three columns">
273- {{deployable_count}} deployable revisions
274- </div>
275- <div class="qa_needed three columns omega">
276- {{needing_qa_count}} revisions need QA
277- </div>
278+ <div class="section_content clearfix">
279+ <div class="project_name six columns alpha main">{{name}}</div>
280+ <div class="undeployed three columns">
281+ {{undeployed_count}} undeployed revisions
282+ </div>
283+ <div class="deployable three columns">
284+ {{deployable_count}} deployable revisions
285+ </div>
286+ <div class="qa_needed three columns omega">
287+ {{needing_qa_count}} revisions need QA
288 </div>
289 </div>
290 </a>
291- {{/each}}
292- {{else}}
293- <div>No projects registered.</div>
294- {{/if}}
295+ {{#if existing_deployments}}
296+ <div class="section_content clearfix">
297+ <div class="alpha six columns main">Existing Deployments</div>
298+ <div class="existing_deployment three columns">Name</div>
299+ <div class="existing_deployment three columns">Current version</div>
300+ <div class="existing_deployment three columns omega">Updated on</div>
301+ {{#each existing_deployments}}
302+ <div class="alpha six columns main">&nbsp;</div>
303+ <div class="existing_deployment three columns">{{this.name}}</div>
304+ <div class="existing_deployment three columns">{{this.current_version}}</div>
305+ <div class="existing_deployment three columns omega">{{this.date_updated}}</div>
306+ {{/each}}
307+ </div>
308+ {{/if}}
309+ </div>
310+ {{/each}}
311+</div>
312+{{else}}
313+<div>No projects registered.</div>
314+{{/if}}
315 </div>
316
317=== modified file 'revtracker/templates/revtracker/project_info.html'
318--- revtracker/templates/revtracker/project_info.html 2016-09-22 12:39:14 +0000
319+++ revtracker/templates/revtracker/project_info.html 2018-10-11 19:24:34 +0000
320@@ -15,6 +15,22 @@
321 {{#if revision_blocking_deployment}}
322 <div id="blocking_revision" class="global_info immediate_attention">{{revision_blocking_deployment.title}} is blocking deployment</div>
323 {{/if}}
324+ {{#if existing_deployments}}
325+ <div class=" section fifteen columns alpha noshadowplease">
326+ <div class="section_content clearfix">
327+ <div class="alpha four columns main">Existing Deployments</div>
328+ <div class="existing_deployment three columns">Name</div>
329+ <div class="existing_deployment three columns">Current version</div>
330+ <div class="existing_deployment three columns omega">Updated on</div>
331+ {{#each existing_deployments}}
332+ <div class="alpha four columns main">&nbsp;</div>
333+ <div class="existing_deployment three columns">{{this.name}}</div>
334+ <div class="existing_deployment three columns">{{this.current_version}}</div>
335+ <div class="existing_deployment three columns omega">{{this.date_updated}}</div>
336+ {{/each}}
337+ </div>
338+ </div>
339+ {{/if}}
340 </div>
341
342 {{#if deployment_request}}
343
344=== modified file 'revtracker/tests/factory.py'
345--- revtracker/tests/factory.py 2018-01-30 01:38:57 +0000
346+++ revtracker/tests/factory.py 2018-10-11 19:24:34 +0000
347@@ -12,6 +12,7 @@
348
349 from revtracker.models import (
350 DeploymentRequest,
351+ ExistingDeployment,
352 Project,
353 Revision,
354 )
355@@ -90,3 +91,19 @@
356 user.is_active = is_active
357 user.save()
358 return user
359+
360+ def make_existing_deployment(self, project=None, name=None,
361+ date_updated=None, info_url=None,
362+ version_header=None, current_version=None):
363+ if project is None:
364+ project = self.make_project()
365+ if name is None:
366+ name = "Existing deployment for %s" % project.name
367+ if info_url is None:
368+ info_url = "http://example.com/_status/check"
369+ if version_header is None:
370+ version_header = "X-Vcs-Revision"
371+ return ExistingDeployment.objects.create(
372+ project=project, name=name, date_updated=date_updated,
373+ info_url=info_url, version_header=version_header,
374+ current_version=current_version)
375
376=== added file 'revtracker/tests/test_models.py'
377--- revtracker/tests/test_models.py 1970-01-01 00:00:00 +0000
378+++ revtracker/tests/test_models.py 2018-10-11 19:24:34 +0000
379@@ -0,0 +1,30 @@
380+from django.utils.timezone import now
381+
382+from revtracker.tests import TestCase
383+
384+
385+class ExistingDeploymentTests(TestCase):
386+
387+ def test_unicode_never_updated(self):
388+ depl = self.factory.make_existing_deployment()
389+ expected_str = (
390+ "%s %s - No current version (Not updated "
391+ "yet)" % (depl.project.name, depl.name))
392+ self.assertEqual(str(depl), expected_str)
393+
394+ def test_unicode_updated_empty_version(self):
395+ when = now()
396+ depl = self.factory.make_existing_deployment(date_updated=when)
397+ expected_str = (
398+ "%s %s - No current version (Updated %s)"
399+ % (depl.project.name, depl.name, when))
400+ self.assertEqual(str(depl), expected_str)
401+
402+ def test_unicode_updated_with_version(self):
403+ when = now()
404+ depl = self.factory.make_existing_deployment(
405+ date_updated=when, current_version="deadbeef")
406+ expected_str = (
407+ "%s %s - Version deadbeef (Updated %s)"
408+ % (depl.project.name, depl.name, when))
409+ self.assertEqual(str(depl), expected_str)
410
411=== modified file 'revtracker/tests/test_tasks.py'
412--- revtracker/tests/test_tasks.py 2018-03-01 17:29:26 +0000
413+++ revtracker/tests/test_tasks.py 2018-10-11 19:24:34 +0000
414@@ -1,5 +1,7 @@
415 from email.utils import parseaddr
416+import mock
417 import os.path
418+import requests
419 import shutil
420 import tempfile
421 from test.test_support import captured_stdout
422@@ -7,6 +9,7 @@
423 from breezy.controldir import ControlDir
424 from breezy.tests import TestCaseWithTransport
425 from django.test.utils import override_settings
426+from django.utils.timezone import now
427 import pygit2
428 from six.moves.urllib.request import pathname2url
429 from testscenarios import WithScenarios
430@@ -670,3 +673,115 @@
431 finally:
432 if self.vcs == VCS.VCS_BAZAAR:
433 branch_with_lock.unlock()
434+
435+
436+class GetCurrentlyDeployedVersionTests(TestCase):
437+
438+ def test_response_contains_requested_header(self):
439+ expected_version = "deadbeef"
440+ version_header = "x-foo-version"
441+ the_moment = now()
442+ mock_response = mock.Mock()
443+ mock_response.headers = {version_header: expected_version}
444+ with mock.patch('revtracker.tasks.requests') as mock_requests, mock.patch('revtracker.tasks.now') as mock_now:
445+ mock_now.return_value = the_moment
446+ mock_requests.head.return_value = mock_response
447+ result = tasks.get_currently_deployed_version("http://foo.example", version_header)
448+ self.assertEqual(result, (expected_version, the_moment))
449+
450+ def test_response_doesnt_contain_requested_header(self):
451+ expected_version = "deadbeef"
452+ version_header = "x-foo-version"
453+ mock_response = mock.Mock()
454+ mock_response.headers = {version_header + "lalala": expected_version}
455+ with captured_stdout() as cstdout, mock.patch('revtracker.tasks.requests') as mock_requests:
456+ mock_requests.head.return_value = mock_response
457+ result = tasks.get_currently_deployed_version("http://foo.example", version_header)
458+ self.assertEqual(result, (None, None))
459+ self.assertIn("http://foo.example did not have %s header" % version_header, cstdout.getvalue())
460+
461+ def test_http_request_exception_missing_schema(self):
462+ with captured_stdout() as cstdout, mock.patch('revtracker.tasks.requests') as mock_requests:
463+ mock_requests.head.side_effect = requests.exceptions.MissingSchema()
464+ result = tasks.get_currently_deployed_version("doesnt-matter", "doesnt-matter-either")
465+ self.assertEqual(result, (None, None))
466+ self.assertIn("Couldn't get doesnt-matter: MissingSchema()", cstdout.getvalue())
467+
468+ def test_http_request_exception_invalid_schema(self):
469+ with captured_stdout() as cstdout, mock.patch('revtracker.tasks.requests') as mock_requests:
470+ mock_requests.head.side_effect = requests.exceptions.InvalidSchema()
471+ result = tasks.get_currently_deployed_version("doesnt-matter", "doesnt-matter-either")
472+ self.assertEqual(result, (None, None))
473+ self.assertIn("Couldn't get doesnt-matter: InvalidSchema()", cstdout.getvalue())
474+
475+ def test_http_request_exception_invalid_url(self):
476+ with captured_stdout() as cstdout, mock.patch('revtracker.tasks.requests') as mock_requests:
477+ mock_requests.head.side_effect = requests.exceptions.InvalidURL()
478+ result = tasks.get_currently_deployed_version("doesnt-matter", "doesnt-matter-either")
479+ self.assertEqual(result, (None, None))
480+ self.assertIn("Couldn't get doesnt-matter: InvalidURL()", cstdout.getvalue())
481+
482+ def test_http_request_exception_connection_error(self):
483+ with captured_stdout() as cstdout, mock.patch('revtracker.tasks.requests') as mock_requests:
484+ mock_requests.head.side_effect = requests.exceptions.ConnectionError()
485+ result = tasks.get_currently_deployed_version("doesnt-matter", "doesnt-matter-either")
486+ self.assertEqual(result, (None, None))
487+ self.assertIn("Couldn't get doesnt-matter: ConnectionError()", cstdout.getvalue())
488+
489+ def test_http_request_exception_timeout(self):
490+ with captured_stdout() as cstdout, mock.patch('revtracker.tasks.requests') as mock_requests:
491+ mock_requests.head.side_effect = requests.exceptions.Timeout()
492+ result = tasks.get_currently_deployed_version("doesnt-matter", "doesnt-matter-either")
493+ self.assertEqual(result, (None, None))
494+ self.assertIn("Couldn't get doesnt-matter: Timeout()", cstdout.getvalue())
495+
496+ def test_http_request_exception_http_error(self):
497+ mock_response = mock.Mock()
498+ with captured_stdout() as cstdout, mock.patch('revtracker.tasks.requests') as mock_requests:
499+ mock_requests.head.return_value = mock_response
500+ mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError()
501+ result = tasks.get_currently_deployed_version("doesnt-matter", "doesnt-matter-either")
502+ self.assertEqual(result, (None, None))
503+ mock_response.raise_for_status.assert_called_once()
504+ self.assertIn("Couldn't get doesnt-matter: HTTPError()", cstdout.getvalue())
505+
506+
507+class UpdateExistingDeploymentsTests(TestCase):
508+
509+ def test_happy_path(self):
510+ expected_version = "deadbeef"
511+ version_header = "x-foo-version"
512+ info_url = "http://foo.example"
513+ project = self.factory.make_project()
514+ existing_deployment = self.factory.make_existing_deployment(
515+ project=project, info_url=info_url, version_header=version_header)
516+
517+ the_moment = now()
518+ mock_response = mock.Mock()
519+ mock_response.headers = {version_header: expected_version}
520+
521+ with mock.patch('revtracker.tasks.requests') as mock_requests, mock.patch('revtracker.tasks.now') as mock_now:
522+ mock_now.return_value = the_moment
523+ mock_requests.head.return_value = mock_response
524+ tasks.update_existing_deployments(project.pk)
525+ existing_deployment.refresh_from_db()
526+ self.assertEqual(existing_deployment.current_version, expected_version)
527+ self.assertEqual(existing_deployment.date_updated, the_moment)
528+
529+ def test_no_revision_info(self):
530+ expected_version = "deadbeef"
531+ version_header = "x-foo-version"
532+ info_url = "http://foo.example"
533+ project = self.factory.make_project()
534+ existing_deployment = self.factory.make_existing_deployment(
535+ project=project, info_url=info_url, version_header=version_header)
536+
537+ mock_response = mock.Mock()
538+ mock_response.headers = {version_header + "lalala": expected_version}
539+
540+ with mock.patch('revtracker.tasks.requests') as mock_requests:
541+ mock_requests.head.return_value = mock_response
542+ tasks.update_existing_deployments(project.pk)
543+ existing_deployment.refresh_from_db()
544+ self.assertEqual(existing_deployment.current_version, None)
545+ self.assertEqual(existing_deployment.date_updated, None)
546
547=== modified file 'revtracker/tests/test_views.py'
548--- revtracker/tests/test_views.py 2018-01-30 01:38:57 +0000
549+++ revtracker/tests/test_views.py 2018-10-11 19:24:34 +0000
550@@ -48,7 +48,7 @@
551 def test_query_count(self):
552 project = self.factory.make_project()
553 self.factory.make_revision(project=project, status=Revision.STATUS_OK)
554- self.assertNumQueries(5, views.index_data)
555+ self.assertNumQueries(6, views.index_data)
556
557 def test_query_count_multiple(self):
558 project = self.factory.make_project()
559@@ -56,7 +56,7 @@
560 project = self.factory.make_project()
561 self.factory.make_revision(project=project, status=Revision.STATUS_OK)
562 # FIXME: O(projects)
563- self.assertNumQueries(8, views.index_data)
564+ self.assertNumQueries(9, views.index_data)
565
566
567 class GetProjectInfoTests(TestCase):
568@@ -140,13 +140,13 @@
569 def test_query_count(self):
570 project = self.factory.make_project()
571 self.factory.make_revision(project=project, status=Revision.STATUS_OK)
572- self.assertNumQueries(4, views.get_project_info, project)
573+ self.assertNumQueries(5, views.get_project_info, project)
574
575 def test_query_count_two_revisions(self):
576 project = self.factory.make_project()
577 self.factory.make_revision(project=project, status=Revision.STATUS_OK)
578 self.factory.make_revision(project=project, status=Revision.STATUS_OK)
579- self.assertNumQueries(4, views.get_project_info, project)
580+ self.assertNumQueries(5, views.get_project_info, project)
581
582 def assertNumQueries(self, *args, **kwargs):
583 from django.db import connection
584
585=== modified file 'revtracker/tests/test_views_logged_in.py'
586--- revtracker/tests/test_views_logged_in.py 2018-01-30 23:36:44 +0000
587+++ revtracker/tests/test_views_logged_in.py 2018-10-11 19:24:34 +0000
588@@ -19,6 +19,8 @@
589 TestCase,
590 )
591
592+from testtools import matchers
593+
594
595 @override_settings(ROOT_URLCONF='revtracker.urls')
596 class IndexViewTests(TestCase):
597@@ -109,6 +111,25 @@
598 soupmatchers.Tag('revisions needing QA', 'div', attrs={'class':'qa_needed three columns omega'},
599 text=re.compile("0 revisions need QA")))))
600
601+ def test_index_shows_no_existing_deployments(self):
602+ self.factory.make_project()
603+ response = self.client.get(reverse('index'))
604+ self.assertThat(response, matchers.Not(soupmatchers.HTMLResponseHas(
605+ html_matches=soupmatchers.Tag("existing deployments", "div", text="Existing Deployments"))))
606+
607+ def test_index_shows_existing_deployments(self):
608+ project = self.factory.make_project()
609+ for ed in range(2):
610+ self.factory.make_existing_deployment(project=project, name=str(ed))
611+ response = self.client.get(reverse('index'))
612+ self.assertThat(response, soupmatchers.HTMLResponseHas(
613+ html_matches=soupmatchers.Tag("existing deployments", "div", text="Existing Deployments")))
614+ for ed in project.existingdeployment_set.all():
615+ self.assertThat(response, soupmatchers.HTMLResponseHas(
616+ html_matches=soupmatchers.Tag("existing deployment", "div", text=ed.project.name)))
617+ self.assertThat(response, soupmatchers.HTMLResponseHas(
618+ html_matches=soupmatchers.Tag("existing deployment", "div", text=ed.current_version)))
619+
620
621 @override_settings(ROOT_URLCONF='revtracker.urls')
622 class ProjectViewTests(TestCase):
623@@ -144,6 +165,37 @@
624 self.assertThat(response, soupmatchers.HTMLResponseHas(
625 html_matches=ProjectDeployedRevision(text="Currently deployed: %s" % revision.description)))
626
627+ def test_project_view_shows_no_existing_deployment_if_none(self):
628+ project = self.factory.make_project()
629+ response = self.client.get(reverse('project_view', args=(project.name,)))
630+ self.assertThat(response, matchers.Not(soupmatchers.HTMLResponseHas(
631+ html_matches=soupmatchers.Tag("existing deployments", "div", text="Existing deployments:"))))
632+
633+ def test_project_view_shows_one_existing_deployment(self):
634+ project = self.factory.make_project()
635+ existing_deployment = self.factory.make_existing_deployment(project=project)
636+ response = self.client.get(reverse('project_view', args=(project.name,)))
637+ self.assertThat(response, soupmatchers.HTMLResponseHas(
638+ html_matches=soupmatchers.Tag("existing deployments", "div", text="Existing Deployments")))
639+ for ed in project.existingdeployment_set.all():
640+ self.assertThat(response, soupmatchers.HTMLResponseHas(
641+ html_matches=soupmatchers.Tag("existing deployment", "div", text=ed.project.name)))
642+ self.assertThat(response, soupmatchers.HTMLResponseHas(
643+ html_matches=soupmatchers.Tag("existing deployment", "div", text=ed.current_version)))
644+
645+ def test_project_view_shows_two_existing_deployments(self):
646+ project = self.factory.make_project()
647+ for ed in range(2):
648+ self.factory.make_existing_deployment(project=project, name=str(ed))
649+ response = self.client.get(reverse('project_view', args=(project.name,)))
650+ self.assertThat(response, soupmatchers.HTMLResponseHas(
651+ html_matches=soupmatchers.Tag("existing deployments", "div", text="Existing Deployments")))
652+ for ed in project.existingdeployment_set.all():
653+ self.assertThat(response, soupmatchers.HTMLResponseHas(
654+ html_matches=soupmatchers.Tag("existing deployment", "div", text=ed.project.name)))
655+ self.assertThat(response, soupmatchers.HTMLResponseHas(
656+ html_matches=soupmatchers.Tag("existing deployment", "div", text=ed.current_version)))
657+
658 def test_project_view_all_revisions_deployed(self):
659 project = self.factory.make_project()
660 response = self.client.get(reverse('project_view', args=(project.name,)))
661
662=== modified file 'revtracker/views.py'
663--- revtracker/views.py 2018-01-29 15:34:06 +0000
664+++ revtracker/views.py 2018-10-11 19:24:34 +0000
665@@ -68,6 +68,11 @@
666 }
667 except DeploymentRequest.DoesNotExist:
668 pass
669+ info['existing_deployments'] = [
670+ {'name': ed.name, 'current_version': ed.current_version or "(Not updated yet)",
671+ 'date_updated': str(ed.date_updated or "(Not updated yet)")}
672+ for ed in project.existingdeployment_set.all()]
673+
674 undeployed_revisions = data.project_undeployed_revisions(project,
675 data.project_deployed_revno(project))
676 return info, undeployed_revisions
677@@ -102,6 +107,10 @@
678 info['undeployed_count'] = len(undeployed_revisions)
679 info['deployable_count'] = len(deployable_revisions)
680 info['needing_qa_count'] = len(data.project_revisions_needing_qa(undeployed_revisions))
681+ info['existing_deployments'] = [
682+ {'name': ed.name, 'current_version': ed.current_version or "(Not updated yet)",
683+ 'date_updated': str(ed.date_updated or "(Not updated yet)")}
684+ for ed in project.existingdeployment_set.all()]
685 return info
686
687
688@@ -185,7 +194,7 @@
689
690
691 def index_data():
692- projects = Project.objects.all().prefetch_related('revision_set')
693+ projects = Project.objects.all().prefetch_related('existingdeployment_set', 'revision_set')
694 info = dict()
695 info['projects'] = [index_project_to_info(project) for project in projects]
696 return info

Subscribers

People subscribed via source and target branches