Merge lp:~dooferlad/offspring/ssh_ui_mods into lp:~linaro-automation/offspring/private-builds

Proposed by James Tunnicliffe
Status: Superseded
Proposed branch: lp:~dooferlad/offspring/ssh_ui_mods
Merge into: lp:~linaro-automation/offspring/private-builds
Diff against target: 863 lines (+636/-43)
11 files modified
lib/offspring/web/media/js/jquery.placeholder.js (+106/-0)
lib/offspring/web/media/js/toggle_visible.js (+30/-0)
lib/offspring/web/queuemanager/admin.py (+3/-1)
lib/offspring/web/queuemanager/forms.py (+89/-4)
lib/offspring/web/queuemanager/models.py (+1/-1)
lib/offspring/web/queuemanager/tests/test_views.py (+176/-1)
lib/offspring/web/queuemanager/views.py (+43/-0)
lib/offspring/web/templates/queuemanager/project_create.html (+69/-18)
lib/offspring/web/templates/queuemanager/project_edit.html (+38/-18)
lib/offspring/web/templates/queuemanager/project_edit_credentials.html (+80/-0)
lib/offspring/web/urls.py (+1/-0)
To merge this branch: bzr merge lp:~dooferlad/offspring/ssh_ui_mods
Reviewer Review Type Date Requested Status
James Tunnicliffe (community) Needs Resubmitting
Review via email: mp+80376@code.launchpad.net

This proposal has been superseded by a proposal from 2011-10-28.

Description of the change

Adds to the web UI so Launchpad SSH credentials can be easily added, modified and removed without sharing the value of the SSH private key.

To post a comment you must log in.
Revision history for this message
Guilherme Salgado (salgado) wrote :

Hi James, this branch has a conflict; care to resolve it before I review?

Revision history for this message
James Tunnicliffe (dooferlad) wrote :

Have rebased. Should be fine now.

Revision history for this message
Guilherme Salgado (salgado) wrote :

Hi James,

In my previous review I suggested creating a new page to edit the SSH keys and lp username because it would:

 1. be simpler to implement
 2. as a consequence, less fragile and easier to test
 3. allow us to provide a simple UI: we could use one form button to save an SSHkey/lp_username change and another to remove the SSHkey/username. Remember that these two things need to be removed together as having just one of them will cause the slave to crash

I see you've added a way to remove an ssh key, but using a checkbox for that is poor UI at best, and it doesn't seem to remove the LP user, as we should do, at the same time (if it did, it'd be even more confusing).

I also suggested placing the "An SSH key is stored for this project...." helper text outside of the textarea instead of using JS to place it inside because the latter doesn't add (IMO) much value and can't be unit tested (unless we spend a significant amount of time writing/configuring some way of testing the JS in our views). This also depends on jquery and an extra jquery plugin, which should not be included without some discussion because Offspring already includes a javascript library (http://www.smartclient.com/product/smartclient.jsp).

I'm not trying to make things perfect here, but I think we should try hard to avoid unnecessary complexity, and I do believe my suggestions would make things just as nice (nicer, in some cases) to the user and much simpler for us.

lp:~dooferlad/offspring/ssh_ui_mods updated
80. By James Tunnicliffe

Part way through splitting out SSH Credentials editing into a separate page. It saves and erases correctly, but isn't being fed the data it needs.

81. By James Tunnicliffe

For some reason current values aren't making it to projects/cn/+editcredentials. Other stuff seems to be working...

82. By James Tunnicliffe

Plumbing fixed. Now can edit SSH credentials on a separate page.

83. By James Tunnicliffe

Updated tests for new SSH edit page.
Added some tests that do some value checking.
Removed some obsolete code.

84. By James Tunnicliffe

Minor web interface tidy up.
Deleted unused code.

Revision history for this message
James Tunnicliffe (dooferlad) wrote :

Right, have moved the SSH credentials editing to a new page, removed the extra jquery, added some more unit tests and made sure that help text is always complete outside the placeholder text inside the SSH key entry box.

review: Needs Resubmitting
lp:~dooferlad/offspring/ssh_ui_mods updated
85. By James Tunnicliffe

Removed toggle visible from project_create.html.
Removed unsused showAddAnotherPopup from project_edit_credentials.html.
Removed toggle_visible.js (no longer used).

86. By James Tunnicliffe

* Require both or neither ssh user and key.
* Removed bad tests
* Added test to ensure we can't edit a projects SSH credentials if it is private and we don't have permissions
* Code clean up and tidy.

87. By James Tunnicliffe

Updated and fixed logic for requiring SSH user and key to be both set, or neither set.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lib/offspring/web/media/js/jquery.placeholder.js'
2--- lib/offspring/web/media/js/jquery.placeholder.js 1970-01-01 00:00:00 +0000
3+++ lib/offspring/web/media/js/jquery.placeholder.js 2011-10-28 17:07:26 +0000
4@@ -0,0 +1,106 @@
5+/*
6+* Placeholder plugin for jQuery
7+* ---
8+* Copyright 2010, Daniel Stocks (http://webcloud.se)
9+* Released under the MIT, BSD, and GPL Licenses.
10+*/
11+(function($) {
12+ function Placeholder(input) {
13+ this.input = input;
14+ if (input.attr('type') == 'password') {
15+ this.handlePassword();
16+ }
17+ // Prevent placeholder values from submitting
18+ $(input[0].form).submit(function() {
19+ if (input.hasClass('placeholder') && input[0].value == input.attr('placeholder')) {
20+ input[0].value = '';
21+ }
22+ });
23+ }
24+ Placeholder.prototype = {
25+ show : function(loading) {
26+ // FF and IE saves values when you refresh the page. If the user refreshes the page with
27+ // the placeholders showing they will be the default values and the input fields won't be empty.
28+ if (this.input[0].value === '' || (loading && this.valueIsPlaceholder())) {
29+ if (this.isPassword) {
30+ try {
31+ this.input[0].setAttribute('type', 'text');
32+ } catch (e) {
33+ this.input.before(this.fakePassword.show()).hide();
34+ }
35+ }
36+ this.input.addClass('placeholder');
37+ this.input[0].value = this.input.attr('placeholder');
38+ }
39+ },
40+ hide : function() {
41+ if (this.valueIsPlaceholder() && this.input.hasClass('placeholder')) {
42+ this.input.removeClass('placeholder');
43+ this.input[0].value = '';
44+ if (this.isPassword) {
45+ try {
46+ this.input[0].setAttribute('type', 'password');
47+ } catch (e) { }
48+ // Restore focus for Opera and IE
49+ this.input.show();
50+ this.input[0].focus();
51+ }
52+ }
53+ },
54+ valueIsPlaceholder : function() {
55+ return this.input[0].value == this.input.attr('placeholder');
56+ },
57+ handlePassword: function() {
58+ var input = this.input;
59+ input.attr('realType', 'password');
60+ this.isPassword = true;
61+ // IE < 9 doesn't allow changing the type of password inputs
62+ if ($.browser.msie && input[0].outerHTML) {
63+ var fakeHTML = $(input[0].outerHTML.replace(/type=(['"])?password\1/gi, 'type=$1text$1'));
64+ this.fakePassword = fakeHTML.val(input.attr('placeholder')).addClass('placeholder').focus(function() {
65+ input.trigger('focus');
66+ $(this).hide();
67+ });
68+ $(input[0].form).submit(function() {
69+ fakeHTML.remove();
70+ input.show()
71+ });
72+ }
73+ }
74+ };
75+ var NATIVE_SUPPORT = !!("placeholder" in document.createElement( "input" ));
76+ $.fn.placeholder = function() {
77+ return NATIVE_SUPPORT ? this : this.each(function() {
78+ var input = $(this);
79+ var placeholder = new Placeholder(input);
80+ placeholder.show(true);
81+ input.focus(function() {
82+ placeholder.hide();
83+ });
84+ input.blur(function() {
85+ placeholder.show(false);
86+ });
87+
88+ // On page refresh, IE doesn't re-populate user input
89+ // until the window.onload event is fired.
90+ if ($.browser.msie) {
91+ $(window).load(function() {
92+ if(input.val()) {
93+ input.removeClass("placeholder");
94+ }
95+ placeholder.show(true);
96+ });
97+ // What's even worse, the text cursor disappears
98+ // when tabbing between text inputs, here's a fix
99+ input.focus(function() {
100+ if(this.value == "") {
101+ var range = this.createTextRange();
102+ range.collapse(true);
103+ range.moveStart('character', 0);
104+ range.select();
105+ }
106+ });
107+ }
108+ });
109+ }
110+})(jQuery);
111
112=== added file 'lib/offspring/web/media/js/toggle_visible.js'
113--- lib/offspring/web/media/js/toggle_visible.js 1970-01-01 00:00:00 +0000
114+++ lib/offspring/web/media/js/toggle_visible.js 2011-10-28 17:07:26 +0000
115@@ -0,0 +1,30 @@
116+/* Copyright 2010 Canonical Ltd. This software is licensed under the
117+ * GNU Affero General Public License version 3 (see the file LICENSE).
118+ */
119+
120+/* Simple show/hide element function offspring-web.
121+ */
122+
123+function toggleElementVisible(id)
124+{
125+ if(typeof toggleElementVisible.state == 'undefined')
126+ {
127+ toggleElementVisible.state = {};
128+ }
129+
130+ if(typeof toggleElementVisible.state[id] == 'undefined')
131+ {
132+ toggleElementVisible.state[id] = 'h';
133+ }
134+
135+ if(toggleElementVisible.state[id] == 'h')
136+ {
137+ document.getElementById(id).style.display = 'block';
138+ toggleElementVisible.state[id] = 's'
139+ }
140+ else
141+ {
142+ document.getElementById(id).style.display = 'none';
143+ toggleElementVisible.state[id] = 'h'
144+ }
145+}
146
147=== modified file 'lib/offspring/web/queuemanager/admin.py'
148--- lib/offspring/web/queuemanager/admin.py 2011-02-24 05:08:42 +0000
149+++ lib/offspring/web/queuemanager/admin.py 2011-10-28 17:07:26 +0000
150@@ -43,7 +43,9 @@
151
152
153 class ProjectAdmin(admin.ModelAdmin):
154- fields = ['title', 'name', 'arch', 'project_group', 'launchpad_project', 'suite', 'series', 'priority', 'status', 'is_active', 'config_url', 'notes']
155+ fields = ['title', 'name', 'arch', 'project_group', 'launchpad_project',
156+ 'suite', 'series', 'priority', 'status', 'is_active',
157+ 'config_url', 'lp_user', 'lp_ssh_key_input', 'notes']
158 list_display = ('display_name', 'arch', 'series', 'project_group', 'launchpad_project', 'is_active', 'status', 'priority', 'config_url')
159 list_filter = ['arch', 'series', 'is_active', 'project_group', 'status']
160 search_fields = ['title', 'name', 'arch', 'notes']
161
162=== modified file 'lib/offspring/web/queuemanager/forms.py'
163--- lib/offspring/web/queuemanager/forms.py 2011-10-19 20:23:49 +0000
164+++ lib/offspring/web/queuemanager/forms.py 2011-10-28 17:07:26 +0000
165@@ -1,8 +1,11 @@
166 # Copyright 2010 Canonical Ltd. This software is licensed under the
167 # GNU Affero General Public License version 3 (see the file LICENSE).
168
169+import re
170+
171 from django.contrib.auth.models import User
172 from django.db import models
173+from django import forms
174 from django.forms import (
175 Form, ModelChoiceField, ModelForm, Textarea, TextInput, ValidationError)
176 from django.forms import fields
177@@ -15,9 +18,81 @@
178
179 from offspring.web.queuemanager.widgets import SelectWithAddNew
180
181-class ProjectBaseForm(ModelForm):
182- status = fields.CharField(max_length=200,
183- widget=fields.Select(choices=Project.STATUS_CHOICES), required=True)
184+class SSHPrivateKeyField(forms.Field):
185+
186+ def validate(self, value):
187+ "Check to see if the value looks like an SSH private key"
188+
189+ # Use the parent's handling of required fields, etc.
190+ super(SSHPrivateKeyField, self).validate(value)
191+
192+ key_search_regexp = (r"-----BEGIN \w+ PRIVATE KEY-----"+
193+ r".*"+
194+ r"-----END \w+ PRIVATE KEY-----")
195+ is_key = re.search(key_search_regexp, value, re.DOTALL | re.MULTILINE)
196+
197+ if not is_key and not value == "":
198+ msg = ("The key you entered doesn't appear to be valid. I am "+
199+ "expecting a key in the form:\n "+
200+ "-----BEGIN <type> PRIVATE KEY-----\n"+
201+ "<ASCII key>\n"+
202+ "-----END <type> PRIVATE KEY-----\n")
203+ raise forms.ValidationError(msg)
204+
205+class ProjectSSHCredForm(ModelForm):
206+ ssh_help = ("Enter a private SSH ASCII key block, complete with begin "+
207+ "and end markers.")
208+
209+ lp_ssh_key_input = SSHPrivateKeyField(label="Launchpad User's SSH key",
210+ required=False,
211+ widget=forms.Textarea(
212+ attrs={'cols': 70, 'rows' : 4}),
213+ help_text=ssh_help)
214+
215+ lp_fields_set = False
216+ lp_ssh_set = False
217+ lp_ssh_set_message = ("An SSH key is stored for this project. To replace "+
218+ "it, paste a new SSH private key block here.")
219+ lp_ssh_clear_meessage = ("To enable access to private repositories,"+
220+ "enter an SSH private key here in the form of an "+
221+ "ASCII key block.")
222+
223+ def __init__(self, *args, **kwargs):
224+ super(ProjectSSHCredForm, self).__init__(*args, **kwargs)
225+ # Set the form fields based on the model object
226+ if kwargs.has_key('instance'):
227+ instance = kwargs['instance']
228+ if instance.lp_ssh_key and instance.lp_ssh_key != "":
229+ self.lp_ssh_set = True
230+ else:
231+ self.lp_ssh_set = False
232+
233+ self.lp_fields_set = instance.lp_ssh_key or instance.lp_user
234+
235+ class Meta:
236+ model = Project
237+ widgets = {
238+ 'lp_user' : Textarea(attrs={'cols': 70, 'rows' : 1}),
239+ }
240+
241+class ProjectBaseForm(ProjectSSHCredForm):
242+ def save(self, commit=True):
243+ model = super(ProjectBaseForm, self).save(commit=False)
244+
245+ if 'lp_ssh_key_input' in self.cleaned_data:
246+ # Save the SSH key
247+ new_key = self.cleaned_data['lp_ssh_key_input']
248+
249+ if new_key == "":
250+ pass # No new key entered
251+
252+ else: # Save new key (validated in clean_lp_ssh_key_input)
253+ model.lp_ssh_key = new_key
254+
255+ if commit:
256+ model.save()
257+
258+ return model
259
260 class Meta:
261 model = Project
262@@ -25,7 +100,8 @@
263 'name' : TextInput(attrs={'style': 'text-transform: lowercase;'}),
264 'series' : TextInput(attrs={'style': 'text-transform: lowercase;'}),
265 'config_url': TextInput(attrs={'size': 50}),
266- 'notes' : Textarea(attrs={'cols': 73, 'rows' : 4}),
267+ 'notes' : Textarea(attrs={'cols': 70, 'rows' : 4}),
268+ 'lp_user' : Textarea(attrs={'cols': 70, 'rows' : 1}),
269 }
270
271 def clean_name(self):
272@@ -55,6 +131,15 @@
273 exclude = ('name', 'priority', 'is_active', 'access_groups')
274
275
276+class EditProjectSSHCredentialsForm(ProjectSSHCredForm):
277+ launchpad_project = ModelChoiceField(
278+ LaunchpadProject.objects, widget=SelectWithAddNew, required=False)
279+ class Meta(ProjectSSHCredForm.Meta):
280+ exclude = ('priority', 'is_active', 'suite', 'access_groups', 'arch',
281+ 'config_url', 'name', 'priority', 'series', 'status',
282+ 'title')
283+
284+
285 class AccessGroupMemberForm(Form):
286 new_user = ModelChoiceField(queryset=User.objects, required=False)
287
288
289=== modified file 'lib/offspring/web/queuemanager/models.py'
290--- lib/offspring/web/queuemanager/models.py 2011-10-21 16:44:19 +0000
291+++ lib/offspring/web/queuemanager/models.py 2011-10-28 17:07:26 +0000
292@@ -233,7 +233,7 @@
293 # The Launchpad User and SSH key are stored per project. If we stored them
294 # per LauncpadProject, anyone who could create a Project referencing that
295 # LaunchpadProject could get access to the private data in it.
296- lp_user = models.TextField('Launchpad User', null=True, editable=False,
297+ lp_user = models.TextField('Launchpad User', null=True,
298 blank=True)
299 lp_ssh_key = models.TextField("Launchpad User's SSH Key", blank=True,
300 null=True, editable=False)
301
302=== modified file 'lib/offspring/web/queuemanager/tests/test_views.py'
303--- lib/offspring/web/queuemanager/tests/test_views.py 2011-10-21 14:36:55 +0000
304+++ lib/offspring/web/queuemanager/tests/test_views.py 2011-10-28 17:07:26 +0000
305@@ -8,6 +8,8 @@
306 Template,
307 )
308 from django.test import TestCase
309+from django.test.client import Client
310+from django.contrib.auth.models import User
311
312 from offspring.enums import ProjectBuildStates
313 from offspring.web.queuemanager.models import (
314@@ -71,7 +73,33 @@
315 self.assertEqual(404, response.status_code)
316
317
318-class ProjectDetailsViewTests(TestCase, ProjectViewTestsMixin):
319+class CreateProjectUsingWebMixin(object):
320+ def create_project_using_web(self, lp_ssh_key=""):
321+ user = User.objects.create_user('user', 'email@somewhere.com', 'pass')
322+ user.is_staff = True
323+ user.is_superuser = True
324+ user.save()
325+ c = Client()
326+ c.login(username='user', password='pass')
327+
328+ response = c.post('/projects/+add/',
329+ {'_is_private': 0,
330+ 'owner': "1",
331+ 'name': "testcodename",
332+ 'title': "title",
333+ 'arch': "i386",
334+ 'series': "lucid",
335+ 'status': "experimental",
336+ 'config_url': "bla",
337+ 'lp_user': "lpuser",
338+ 'lp_ssh_key_input': lp_ssh_key,
339+ }, follow=True)
340+
341+ return c, response
342+
343+
344+class ProjectDetailsViewTests(TestCase, ProjectViewTestsMixin,
345+ CreateProjectUsingWebMixin):
346 view_path = 'offspring.web.queuemanager.views.project_details'
347
348 def get_expected_page_heading(self, project):
349@@ -85,6 +113,45 @@
350 response, self.get_expected_page_heading(project),
351 status_code=200, msg_prefix=response.content)
352
353+ def test_public_project_with_ssh_auth(self):
354+ project = factory.makeProject(is_private=False)
355+ project.lp_ssh_key = "1234"
356+ project.lp_user = "user"
357+ project.save()
358+ response = self.client.get(
359+ reverse(self.view_path, args=[project.name]))
360+ self.assertContains(
361+ response, self.get_expected_page_heading(project),
362+ status_code=200, msg_prefix=response.content)
363+
364+ def test_post_to_create_project_with_ssh_auth(self):
365+ key = ("-----BEGIN type PRIVATE KEY-----\n"+
366+ "12qwaszx34erdfcv\n"+
367+ "-----END type PRIVATE KEY-----\n")
368+
369+ c, response = self.create_project_using_web(key)
370+
371+ self.assertEqual(response.redirect_chain[0][0],
372+ "http://testserver/projects/testcodename/")
373+ self.assertNotContains(response, "Select a valid choice")
374+ self.assertNotContains(response, "This field is required.")
375+ self.assertNotContains(response, "is not a valid choice.")
376+ self.assertNotContains(response, "The key you entered doesn")
377+ self.assertNotContains(response, "12qwaszx34erdfcv")
378+
379+ def test_post_to_create_project_with_ssh_auth_bad_key(self):
380+ key = ("-----BEGIN type PRVATE KEY-----\n"+
381+ "12qwaszx34erdfcv\n"+
382+ "-----END type PRIVATE KEY-----\n")
383+
384+ c, response = self.create_project_using_web(key)
385+
386+ self.assertNotContains(response, "Select a valid choice")
387+ self.assertNotContains(response, "This field is required.")
388+ self.assertNotContains(response, "is not a valid choice.")
389+ self.assertContains(response, "The key you entered doesn")
390+ self.assertNotContains(response, "12qwaszx34erdfcv")
391+
392
393 class ProjectEditViewTests(TestCase, ProjectViewTestsMixin):
394 view_path = 'offspring.web.queuemanager.views.project_edit'
395@@ -138,6 +205,114 @@
396 status_code=200, msg_prefix=response.content)
397
398
399+class ProjectEditSSHViewTests(TestCase,
400+ CreateProjectUsingWebMixin):
401+ def test_post_to_edit_test_with_ssh_auth(self):
402+ c, response = self.create_project_using_web()
403+
404+ self.assertNotContains(response, "Select a valid choice")
405+ self.assertNotContains(response, "This field is required.")
406+ self.assertNotContains(response, "is not a valid choice.")
407+ self.assertNotContains(response, "The key you entered doesn")
408+
409+ key = ("-----BEGIN type PRIVATE KEY-----\n"+
410+ "12qwaszx34erdfcv\n"+
411+ "-----END type PRIVATE KEY-----\n")
412+
413+ # Now add a valid SSH key
414+ response = c.post('/projects/testcodename/+editcredentials',
415+ {'lp_user': "lpuser",
416+ 'lp_ssh_key_input': key,
417+ }, follow=True)
418+
419+ self.assertNotContains(response, "Select a valid choice")
420+ self.assertNotContains(response, "This field is required.")
421+ self.assertNotContains(response, "is not a valid choice.")
422+ self.assertNotContains(response, "The key you entered doesn")
423+ self.assertNotContains(response, "12qwaszx34erdfcv")
424+
425+ def test_post_to_edit_test_with_ssh_auth_bad_key(self):
426+ c, response = self.create_project_using_web()
427+
428+ self.assertNotContains(response, "Select a valid choice")
429+ self.assertNotContains(response, "This field is required.")
430+ self.assertNotContains(response, "is not a valid choice.")
431+ self.assertNotContains(response, "The key you entered doesn")
432+
433+ key = ("-----BEGIN type PRIVAE KEY-----\n"+
434+ "12qwaszx34erdfcv\n"+
435+ "-----END type PRIVATE KEY-----\n")
436+
437+ # Now add a valid SSH key
438+ response = c.post('/projects/testcodename/+editcredentials',
439+ {'lp_user': "lpuser",
440+ 'lp_ssh_key_input': key,
441+ }, follow=True)
442+
443+ self.assertNotContains(response, "Select a valid choice")
444+ self.assertNotContains(response, "This field is required.")
445+ self.assertNotContains(response, "is not a valid choice.")
446+ self.assertContains(response, "The key you entered doesn")
447+ self.assertNotContains(response, "12qwaszx34erdfcv")
448+
449+ def test_edit_ssh_auth(self):
450+ project = factory.makeProject(is_private=True)
451+ user = project.owner
452+ grant_permission_to_user(user, 'change_project')
453+ self.assertTrue(
454+ self.client.login(username=user.username, password=user.username))
455+
456+ key = ("-----BEGIN type PRIVATE KEY-----\n"+
457+ "12qwaszx34erdfcv\n"+
458+ "-----END type PRIVATE KEY-----\n")
459+
460+ data = {'lp_user': "lpuser",
461+ 'lp_ssh_key_input': key,
462+ '_save': 1}
463+ response = self.client.post(
464+ reverse('offspring.web.queuemanager.views.project_editsshcredentials',
465+ args=[project.name]),
466+ data, follow=True)
467+
468+ self.assertEqual(200, response.status_code)
469+
470+ # re-load the project from the database to pick up changes
471+ user_visible_objects = Project.all_objects.accessible_by_user(user)
472+ project = get_possibly_private_object(
473+ user, user_visible_objects, pk=project.name)
474+
475+ self.assertEqual('lpuser', project.lp_user)
476+ self.assertEqual(key, project.lp_ssh_key)
477+
478+ def test_edit_ssh_auth_bad_key(self):
479+ project = factory.makeProject(is_private=True)
480+ user = project.owner
481+ grant_permission_to_user(user, 'change_project')
482+ self.assertTrue(
483+ self.client.login(username=user.username, password=user.username))
484+
485+ key = ("-----BEGIN type PR---VATE KEY-----\n"+
486+ "12qwaszx34erdfcv\n"+
487+ "-----END type PRIVATE KEY-----\n")
488+
489+ data = {'lp_user': "lpuser",
490+ 'lp_ssh_key_input': key,
491+ '_save': 1}
492+ response = self.client.post(
493+ reverse('offspring.web.queuemanager.views.project_editsshcredentials',
494+ args=[project.name]),
495+ data, follow=True)
496+
497+ self.assertEqual(200, response.status_code)
498+
499+ # re-load the project from the database to pick up changes
500+ user_visible_objects = Project.all_objects.accessible_by_user(user)
501+ project = get_possibly_private_object(
502+ user, user_visible_objects, pk=project.name)
503+
504+ self.assertNotEqual('lpuser', project.lp_user)
505+ self.assertNotEqual(key, project.lp_ssh_key)
506+
507 class ProjectCreateViewTests(TestCase):
508 view_path = 'offspring.web.queuemanager.views.project_create'
509
510
511=== modified file 'lib/offspring/web/queuemanager/views.py'
512--- lib/offspring/web/queuemanager/views.py 2011-10-20 16:08:24 +0000
513+++ lib/offspring/web/queuemanager/views.py 2011-10-28 17:07:26 +0000
514@@ -45,6 +45,7 @@
515 AccessGroupMemberForm,
516 CreateProjectForm,
517 EditProjectForm,
518+ EditProjectSSHCredentialsForm,
519 LaunchpadProjectForm,
520 ReleaseForm,
521 )
522@@ -356,6 +357,48 @@
523 'queuemanager/project_acl.html', pageData,
524 context_instance=RequestContext(request))
525
526+@permission_required('queuemanager.change_project')
527+def project_editsshcredentials(request, projectName):
528+ user_visible_objects = Project.all_objects.accessible_by_user(
529+ request.user)
530+ project = get_possibly_private_object(
531+ request.user, user_visible_objects, pk=projectName)
532+ access_group = None
533+ allowed_users = []
534+ if len(project.access_groups.all()) > 0:
535+ # We always use the first access group because there's no way for
536+ # users to register more than one access group for any given project.
537+ access_group = project.access_groups.all()[0]
538+ allowed_users = access_group.members.all()
539+
540+ if request.method == 'POST':
541+
542+ form = EditProjectSSHCredentialsForm(request.POST, instance=project)
543+ if form.is_valid():
544+ if "delete" in request.POST:
545+ project.lp_user = ""
546+ project.lp_ssh_key = ""
547+ project.save()
548+
549+ elif "_save" in request.POST:
550+ project.lp_user = form.cleaned_data['lp_user']
551+ if form.cleaned_data['lp_ssh_key_input']:
552+ project.lp_ssh_key = form.cleaned_data['lp_ssh_key_input']
553+ project.save()
554+
555+ return HttpResponseRedirect(".")
556+
557+ else:
558+ form = EditProjectSSHCredentialsForm(instance=project)
559+ pageData = {
560+ 'csrf_token' : csrf.get_token(request),
561+ 'project' : project,
562+ 'form': form,
563+ 'allowed_users': allowed_users,
564+ }
565+ return render_to_response(
566+ 'queuemanager/project_edit_credentials.html', pageData,
567+ context_instance=RequestContext(request))
568
569 def projectgroup_details(request, projectGroupName):
570 pg = get_object_or_404(
571
572=== modified file 'lib/offspring/web/templates/queuemanager/project_create.html'
573--- lib/offspring/web/templates/queuemanager/project_create.html 2011-02-28 22:48:46 +0000
574+++ lib/offspring/web/templates/queuemanager/project_create.html 2011-10-28 17:07:26 +0000
575@@ -5,7 +5,10 @@
576 {% endblock %}
577
578 {% block header_js %}
579-<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
580+<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
581+<script type="text/javascript" src="/media/js/jquery.min.js"></script>
582+<script type="text/javascript" src="/assets/js/jquery.placeholder.js"></script>
583+<script type="text/javascript" src="/assets/js/toggle_visible.js"></script>
584 <script type="text/javascript">
585 /* Override showAddAnotherPopup to use custom height and width. */
586 function showAddAnotherPopup(triggeringLink) {
587@@ -29,29 +32,77 @@
588 {% endblock %}
589
590 {% block content %}
591- <form method="POST" action="">{% csrf_token %}
592+ <form method="POST" action="" name="projsettings">{% csrf_token %}
593 <div class="module aligned ">
594 {% for field in form %}
595- <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}">
596- <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
597- {% if field.is_checkbox %}
598- {{ field }}{{ field.label_tag }}
599- {% else %}
600- {{ field.label_tag }}
601- {% if field.is_readonly %}
602- <p>{{ field.contents }}</p>
603+ {% if field.html_name == "lp_user" or field.html_name == "lp_ssh_key_input" or field.html_name == "erase_ssh_key" %}
604+
605+ {% else %}
606+ <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}">
607+ <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
608+ {% if field.is_checkbox %}
609+ {{ field }}{{ field.label_tag }}
610 {% else %}
611- {{ field }}
612- {% endif %}
613- {% endif %}
614- {% if field.field.field.help_text %}
615- <p class="help">{{ field.field.field.help_text|safe }}</p>
616- {% endif %}
617+ {{ field.label_tag }}
618+ {% if field.is_readonly %}
619+ <p>{{ field.contents }}</p>
620+ {% else %}
621+ {{ field }}
622+ {% endif %}
623+ {% endif %}
624+ {% if field.field.field.help_text %}
625+ <p class="help">{{ field.field.field.help_text|safe }}</p>
626+ {% endif %}
627+ </div>
628+ {{ field.errors }}
629 </div>
630- {{ field.errors }}
631- </div>
632+ {% endif %}
633
634 {% endfor %}
635+
636+ <br>
637+ If the config URL above points to a Bazaar repository that is only
638+ available to authenticated users, you should add a user and private SSH
639+ key to give the builder access. The SSH key will never be revealed. It
640+ can be changed or deleted later. It should not be passphrase protected.
641+ If you would like to add these details, tick here:
642+
643+ <input type="checkbox" name="C1" value="1" onclick="toggleElementVisible('lpssh')" value=1>
644+ <br>
645+ <span id="lpssh" style="display: none;">
646+ <br>
647+
648+ <div class="form-row {{ form.lp_user.name }}">
649+ <div class="field-box">
650+ {{ form.lp_user.label_tag }}
651+ <br>
652+ {{ form.lp_user }}
653+ </div>
654+ {{ form.lp_user.errors }}
655+ </div>
656+
657+ <div class="form-row {{ form.lp_ssh_key_input.name }}">
658+ <div class="field-box">
659+ {{ form.lp_ssh_key_input.label_tag }}
660+ <br>
661+ <textarea id="id_lp_ssh_key_input" rows="4" cols="70" name="lp_ssh_key_input"
662+ placeholder="{% if form.lp_ssh_set %}{{ form.lp_ssh_set_message }}{% else %}{{ form.lp_ssh_clear_meessage }}{% endif %}"></textarea>
663+ {% if form.lp_ssh_key_input.help_text %}
664+ <p class="help">{{ form.lp_ssh_key_input.help_text|safe }}</p>
665+ {% endif %}
666+ </div>
667+ {{ form.lp_ssh_key_input.errors|linebreaksbr }}
668+ </div>
669+
670+ </span>
671+ <br>
672+ {% if form.lp_ssh_key_input.errors or form.lp_user.errors %}
673+ <script>
674+ $(document).ready(function(){
675+ document.projsettings.C1.click();
676+ });
677+ </script>
678+ {% endif %}
679 <div class="submit-row" style="overflow: auto;">
680 <input type="submit" value="Save" class="default" name="_save"/>
681 <input type="button" value="Cancel" class="default" name="_cancel" OnClick="window.location.href = '/';"/>
682
683=== modified file 'lib/offspring/web/templates/queuemanager/project_edit.html'
684--- lib/offspring/web/templates/queuemanager/project_edit.html 2011-03-03 01:50:40 +0000
685+++ lib/offspring/web/templates/queuemanager/project_edit.html 2011-10-28 17:07:26 +0000
686@@ -5,7 +5,7 @@
687 {% endblock %}
688
689 {% block header_js %}
690-<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
691+<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
692 <script type="text/javascript">
693 /* Override showAddAnotherPopup to use custom height and width. */
694 function showAddAnotherPopup(triggeringLink) {
695@@ -29,29 +29,46 @@
696 {% endblock %}
697
698 {% block content %}
699- <form method="POST" action="">{% csrf_token %}
700+ <form method="POST" action="" name="projsettings">{% csrf_token %}
701 <div class="module aligned ">
702 {% for field in form %}
703- <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}">
704- <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
705- {% if field.is_checkbox %}
706- {{ field }}{{ field.label_tag }}
707- {% else %}
708- {{ field.label_tag }}
709- {% if field.is_readonly %}
710- <p>{{ field.contents }}</p>
711+ {% if field.html_name == "lp_user" or field.html_name == "lp_ssh_key_input"%}
712+ {% else %}
713+ <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}">
714+ <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
715+ {% if field.is_checkbox %}
716+ {{ field }}{{ field.label_tag }}
717 {% else %}
718- {{ field }}
719- {% endif %}
720- {% endif %}
721- {% if field.field.field.help_text %}
722- <p class="help">{{ field.field.field.help_text|safe }}</p>
723- {% endif %}
724+ {{ field.label_tag }}
725+ {% if field.is_readonly %}
726+ <p>{{ field.contents }}</p>
727+ {% else %}
728+ {{ field }}
729+ {% endif %}
730+ {% endif %}
731+ {% if field.help_text %}
732+ <p class="help">{{ field.help_text|safe }}</p>
733+ {% endif %}
734+ </div>
735+ {{ field.errors }}
736 </div>
737- {{ field.errors }}
738- </div>
739+ {% endif %}
740
741 {% endfor %}
742+
743+ <br>
744+ If the config URL above points to a Bazaar repository that is only
745+ available to authenticated users, you should add a user and private SSH
746+ key to give the builder access. The SSH key will never be revealed. It
747+ can be changed or deleted later. It should not be passphrase protected.
748+
749+ {% if form.lp_fields_set %}
750+ To update or delete these details, use
751+ {% else %}
752+ If you would like to add these details,
753+ {% endif %}
754+ <a href="+editcredentials">use this form.</a>
755+
756 <div class="submit-row" style="overflow: auto;">
757 <input type="submit" value="Save" class="default" name="_save"/>
758 <input type="button" value="Cancel" class="default" name="_cancel" OnClick="window.location.href = '{% url offspring.web.queuemanager.views.project_details project.name %}';"/>
759@@ -61,4 +78,7 @@
760 {% endblock %}
761
762 {% block two-columns %}
763+<script>
764+ $('input[placeholder], textarea[placeholder]').placeholder();
765+</script>
766 {% endblock %}
767
768=== added file 'lib/offspring/web/templates/queuemanager/project_edit_credentials.html'
769--- lib/offspring/web/templates/queuemanager/project_edit_credentials.html 1970-01-01 00:00:00 +0000
770+++ lib/offspring/web/templates/queuemanager/project_edit_credentials.html 2011-10-28 17:07:26 +0000
771@@ -0,0 +1,80 @@
772+{% extends "base.html" %}
773+
774+{% block header_css %}
775+<link rel="stylesheet" type="text/css" href="/media/css/forms.css" />
776+{% endblock %}
777+
778+{% block header_js %}
779+<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
780+<script type="text/javascript" src="/media/js/jquery.min.js"></script>
781+<script type="text/javascript" src="/assets/js/jquery.placeholder.js"></script>
782+<script type="text/javascript">
783+/* Override showAddAnotherPopup to use custom height and width. */
784+function showAddAnotherPopup(triggeringLink) {
785+ var name = triggeringLink.id.replace(/^add_/, '');
786+ name = id_to_windowname(name);
787+ href = triggeringLink.href
788+ if (href.indexOf('?') == -1) {
789+ href += '?_popup=1';
790+ } else {
791+ href += '&_popup=1';
792+ }
793+ var win = window.open(href, name, 'height=150,width=755');
794+ win.focus();
795+ return false;
796+}
797+</script>
798+{% endblock %}
799+
800+{% block title %}
801+Update details for {{project.title|capfirst}}
802+{% endblock %}
803+
804+{% block content %}
805+ <form method="POST" action="" name="projsettings">{% csrf_token %}
806+ <div class="module aligned ">
807+ <div class="form-row {{ form.lp_user.name }}">
808+ <div class="field-box">
809+ {{ form.lp_user.label_tag }}
810+ <br>
811+ {{ form.lp_user }}
812+ </div>
813+ {{ form.lp_user.errors }}
814+ </div>
815+
816+ <div class="form-row {{ form.lp_ssh_key_input.name }}">
817+ <div class="field-box">
818+ {{ form.lp_ssh_key_input.label_tag }}
819+ <br>
820+ <textarea id="id_lp_ssh_key_input" rows="4" cols="70" name="lp_ssh_key_input"
821+ placeholder="{% if form.lp_ssh_set %}{{ form.lp_ssh_set_message }}{% else %}{{ form.lp_ssh_clear_meessage }}{% endif %}"></textarea>
822+ {% if form.lp_ssh_key_input.help_text %}
823+ <p class="help">
824+ {{ form.lp_ssh_key_input.help_text|safe }}
825+ {% if form.lp_ssh_set %}
826+ Your saved key is not shown. To replace it, paste a new key in the above box.
827+ {% else %}
828+ No key is stored for this project. Saved keys are not shown.
829+ {% endif %}
830+ </p>
831+ {% endif %}
832+ </div>
833+ {{ form.lp_ssh_key_input.errors|linebreaksbr }}
834+ </div>
835+
836+ <br>
837+
838+ <div class="submit-row" style="overflow: auto;">
839+ <input type="submit" value="Save Updated Credentials" class="default" name="_save"/>
840+ <input type="submit" value="Delete Existing Credentials" class="default" name="delete"/>
841+ <input type="button" value="Cancel" class="default" name="_cancel" OnClick="window.location.href = '{% url offspring.web.queuemanager.views.project_details project.name %}';"/>
842+ </div>
843+ </div>
844+ </form>
845+{% endblock %}
846+
847+{% block two-columns %}
848+<script>
849+ $('input[placeholder], textarea[placeholder]').placeholder();
850+</script>
851+{% endblock %}
852
853=== modified file 'lib/offspring/web/urls.py'
854--- lib/offspring/web/urls.py 2011-10-18 15:17:15 +0000
855+++ lib/offspring/web/urls.py 2011-10-28 17:07:26 +0000
856@@ -79,6 +79,7 @@
857 (r'^projects/(?P<projectName>[^/]+)/\+api/buildrequest/(?P<request_id>[^/]+)/$', buildrequest_handler),
858 (r'^projects/(?P<projectName>[^/]+)/\+build$', 'offspring.web.queuemanager.views.queue_build'),
859 (r'^projects/(?P<projectName>[^/]+)/\+builds$', 'offspring.web.queuemanager.views.builds'),
860+ (r'^projects/(?P<projectName>[^/]+)/\+editcredentials$', 'offspring.web.queuemanager.views.project_editsshcredentials'),
861 (r'^projects/(?P<projectName>[^/]+)/$', 'offspring.web.queuemanager.views.project_details'),
862 (r'^projects/$', 'offspring.web.queuemanager.views.projects'),
863 (r'^schedule/\+api/milestones/$', milestone_handler),

Subscribers

People subscribed via source and target branches