Merge lp:~dooferlad/offspring/ssh_ui_mods into lp:~linaro-automation/offspring/private-builds
- ssh_ui_mods
- Merge into private-builds
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 |
Related bugs: |
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.
Commit message
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.
Guilherme Salgado (salgado) wrote : | # |
James Tunnicliffe (dooferlad) wrote : | # |
Have rebased. Should be fine now.
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://
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.
- 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/+editcredent ials. 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.
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.
- 85. By James Tunnicliffe
-
Removed toggle visible from project_
create. html.
Removed unsused showAddAnotherPopup from project_edit_credential s.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
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), |
Hi James, this branch has a conflict; care to resolve it before I review?