Merge lp:~salgado/offspring/project-ssh-key-ui into lp:offspring
- project-ssh-key-ui
- Merge into trunk
Proposed by
Guilherme Salgado
Status: | Work in progress |
---|---|
Proposed branch: | lp:~salgado/offspring/project-ssh-key-ui |
Merge into: | lp:offspring |
Prerequisite: | lp:~salgado/offspring/private-project-requires-owner |
Diff against target: |
722 lines (+488/-36) 10 files modified
lib/offspring/web/media/js/jquery.placeholder.js (+106/-0) lib/offspring/web/queuemanager/admin.py (+5/-3) lib/offspring/web/queuemanager/forms.py (+35/-6) lib/offspring/web/queuemanager/models.py (+38/-4) lib/offspring/web/queuemanager/tests/test_views.py (+112/-1) lib/offspring/web/queuemanager/views.py (+46/-2) lib/offspring/web/templates/queuemanager/project_create.html (+58/-18) lib/offspring/web/templates/queuemanager/project_edit.html (+16/-2) lib/offspring/web/templates/queuemanager/project_edit_credentials.html (+70/-0) lib/offspring/web/urls.py (+2/-0) |
To merge this branch: | bzr merge lp:~salgado/offspring/project-ssh-key-ui |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
CE Infrastructure | Pending | ||
Offspring Committers | Pending | ||
Review via email: mp+88259@code.launchpad.net |
Commit message
Description of the change
This branch adds the UI for setting the private SSH key and LP user ID. It uses a jquery plugin to display some help text inside a text area. Because of that it uses also the jquery that is included in Django (for the admin UI).
Also, the view code is not as simple as I'd like because we want to hide the SSH key when a user comes to the +editcredentials page after a key was set.
To post a comment you must log in.
- 118. By Guilherme Salgado
-
Merge trunk
- 119. By Guilherme Salgado
-
merge trunk
Unmerged revisions
- 119. By Guilherme Salgado
-
merge trunk
- 118. By Guilherme Salgado
-
Merge trunk
- 117. By Guilherme Salgado
-
Make it possible to set per-project SSH key and LP user to use when fetching private branches from Launchpad
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 2012-01-16 18:18:24 +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 | === modified file 'lib/offspring/web/queuemanager/admin.py' |
113 | --- lib/offspring/web/queuemanager/admin.py 2011-09-26 20:02:27 +0000 |
114 | +++ lib/offspring/web/queuemanager/admin.py 2012-01-16 18:18:24 +0000 |
115 | @@ -3,7 +3,7 @@ |
116 | |
117 | from django.contrib import admin |
118 | |
119 | -from offspring.web.queuemanager.forms import ProjectBaseForm |
120 | +from offspring.web.queuemanager.forms import ProjectSSHCredForm |
121 | from offspring.web.queuemanager.models import ( |
122 | BuildRequest, |
123 | BuildResult, |
124 | @@ -43,12 +43,14 @@ |
125 | |
126 | |
127 | class ProjectAdmin(admin.ModelAdmin): |
128 | - fields = ['title', 'name', 'arch', 'project_group', 'launchpad_project', 'suite', 'series', 'priority', 'status', 'is_active', 'config_url', 'notes'] |
129 | + fields = ['title', 'name', 'arch', 'project_group', 'launchpad_project', |
130 | + 'suite', 'series', 'priority', 'status', 'is_active', |
131 | + 'config_url', 'lp_user', 'lp_ssh_key', 'notes'] |
132 | list_display = ('display_name', 'arch', 'series', 'project_group', 'launchpad_project', 'is_active', 'status', 'priority', 'config_url') |
133 | list_filter = ['arch', 'series', 'is_active', 'project_group', 'status'] |
134 | search_fields = ['title', 'name', 'arch', 'notes'] |
135 | actions = ['raise_priority', 'drop_priority', 'make_disabled', 'make_enabled'] |
136 | - form = ProjectBaseForm |
137 | + form = ProjectSSHCredForm |
138 | |
139 | def raise_priority(self, request, queryset): |
140 | for project in queryset: |
141 | |
142 | === modified file 'lib/offspring/web/queuemanager/forms.py' |
143 | --- lib/offspring/web/queuemanager/forms.py 2012-01-11 18:26:45 +0000 |
144 | +++ lib/offspring/web/queuemanager/forms.py 2012-01-16 18:18:24 +0000 |
145 | @@ -3,8 +3,8 @@ |
146 | |
147 | from django.contrib.auth.models import User |
148 | from django.forms import ( |
149 | - Form, ModelChoiceField, ModelForm, Textarea, TextInput, ValidationError) |
150 | -from django.forms import fields |
151 | + Form, ModelChoiceField, ModelForm, Select, Textarea, TextInput, |
152 | + ValidationError) |
153 | |
154 | from offspring.web.queuemanager.models import ( |
155 | Project, |
156 | @@ -14,9 +14,8 @@ |
157 | |
158 | from offspring.web.queuemanager.widgets import SelectWithAddNew |
159 | |
160 | + |
161 | class ProjectBaseForm(ModelForm): |
162 | - status = fields.CharField(max_length=200, |
163 | - widget=fields.Select(choices=Project.STATUS_CHOICES), required=True) |
164 | |
165 | class Meta: |
166 | model = Project |
167 | @@ -24,7 +23,10 @@ |
168 | 'name' : TextInput(attrs={'style': 'text-transform: lowercase;'}), |
169 | 'series' : TextInput(attrs={'style': 'text-transform: lowercase;'}), |
170 | 'config_url': TextInput(attrs={'size': 50}), |
171 | - 'notes' : Textarea(attrs={'cols': 73, 'rows' : 4}), |
172 | + 'notes' : Textarea(attrs={'cols': 70, 'rows' : 4}), |
173 | + 'status': Select(choices=Project.STATUS_CHOICES), |
174 | + 'lp_user' : TextInput(attrs={'size': 50}), |
175 | + 'lp_ssh_key' : Textarea(attrs={'cols': 70, 'rows' : 4}), |
176 | } |
177 | |
178 | def clean_name(self): |
179 | @@ -50,11 +52,38 @@ |
180 | exclude = ("priority", "is_active", "suite", "access_groups") |
181 | |
182 | |
183 | +class ProjectSSHCredForm(ProjectBaseForm): |
184 | + lp_ssh_set = False |
185 | + lp_ssh_set_message = ( |
186 | + "An SSH key is stored for this project. To replace it, paste a new " |
187 | + "SSH private key block here. Leave this empty if you do not want to " |
188 | + "change the existing key.") |
189 | + lp_ssh_clear_meessage = ( |
190 | + "To enable access to private repositories, enter an SSH private key " |
191 | + "here in the form of an ASCII key block.") |
192 | + |
193 | + def clean_lp_ssh_key(self): |
194 | + # The text entry for lp_ssh_key will never contain the existing value, |
195 | + # so when it's not present in the request it's because the user didn't |
196 | + # change it, so we just return the existing value for the rest of the |
197 | + # validation to proceed normally. |
198 | + new_key = self.cleaned_data["lp_ssh_key"] |
199 | + if not new_key: |
200 | + return self.instance.lp_ssh_key |
201 | + return self.cleaned_data["lp_ssh_key"] |
202 | + |
203 | + |
204 | class EditProjectForm(ProjectBaseForm): |
205 | launchpad_project = ModelChoiceField( |
206 | LaunchpadProject.objects, widget=SelectWithAddNew, required=False) |
207 | class Meta(ProjectBaseForm.Meta): |
208 | - exclude = ("name", "priority", "is_active", "access_groups") |
209 | + exclude = ("name", "priority", "is_active", "access_groups", |
210 | + "lp_user", "lp_ssh_key") |
211 | + |
212 | + |
213 | +class EditProjectSSHCredentialsForm(ProjectSSHCredForm): |
214 | + class Meta(ProjectSSHCredForm.Meta): |
215 | + fields = ("lp_user", "lp_ssh_key") |
216 | |
217 | |
218 | class AccessGroupMemberForm(Form): |
219 | |
220 | === modified file 'lib/offspring/web/queuemanager/models.py' |
221 | --- lib/offspring/web/queuemanager/models.py 2012-01-16 17:28:38 +0000 |
222 | +++ lib/offspring/web/queuemanager/models.py 2012-01-16 18:18:24 +0000 |
223 | @@ -2,9 +2,11 @@ |
224 | # GNU Affero General Public License version 3 (see the file LICENSE). |
225 | |
226 | import math |
227 | +import re |
228 | from datetime import date, datetime, timedelta |
229 | |
230 | from django.contrib.auth.models import AnonymousUser, User |
231 | +from django.core.exceptions import ValidationError |
232 | from django.db import ( |
233 | connection, |
234 | models |
235 | @@ -198,6 +200,25 @@ |
236 | return is_visible_to(user) |
237 | |
238 | |
239 | +def validate_lp_ssh_key(value): |
240 | + """Check to see if the value looks like an SSH private key""" |
241 | + if not value: |
242 | + return |
243 | + |
244 | + key_search_regexp = (r"-----BEGIN \w+ PRIVATE KEY-----" |
245 | + r".*" |
246 | + r"-----END \w+ PRIVATE KEY-----") |
247 | + is_key = re.search(key_search_regexp, value, re.DOTALL | re.MULTILINE) |
248 | + |
249 | + if not is_key: |
250 | + msg = ("The key you entered doesn't appear to be valid. I am " |
251 | + "expecting a key in the form:\n " |
252 | + "-----BEGIN <type> PRIVATE KEY-----\n" |
253 | + "<ASCII key>\n" |
254 | + "-----END <type> PRIVATE KEY-----\n") |
255 | + raise ValidationError(msg) |
256 | + |
257 | + |
258 | class Project(AccessGroupMixin, MaybePrivateMixin): |
259 | #XXX: This should probably be managed in the database. |
260 | STATUS_CHOICES = ( |
261 | @@ -233,10 +254,12 @@ |
262 | # The Launchpad User and SSH key are stored per project. If we stored them |
263 | # per LaunchpadProject, anyone who could create a Project referencing that |
264 | # LaunchpadProject could get access to the private data in it. |
265 | - lp_user = models.TextField("Launchpad User", null=True, editable=False, |
266 | - blank=True) |
267 | - lp_ssh_key = models.TextField("Launchpad User's SSH Key", blank=True, |
268 | - null=True, editable=False) |
269 | + lp_user = models.TextField("Launchpad User", null=True, blank=True) |
270 | + lp_ssh_key = models.TextField( |
271 | + "Launchpad User's SSH Key", blank=True, null=True, editable=True, |
272 | + validators=[validate_lp_ssh_key], |
273 | + help_text=("Enter a private SSH ASCII key block, complete with begin " |
274 | + "and end markers.")) |
275 | |
276 | notes = models.TextField(blank=True, null=True) |
277 | |
278 | @@ -249,6 +272,17 @@ |
279 | def __unicode__(self): |
280 | return self.display_name() |
281 | |
282 | + def clean(self): |
283 | + if self.lp_user and not self.lp_ssh_key: |
284 | + raise ValidationError( |
285 | + "You must specify the SSH key for the given Launchpad user") |
286 | + elif self.lp_ssh_key and not self.lp_user: |
287 | + raise ValidationError( |
288 | + "You must specify the Launchpad user for the given SSH key") |
289 | + else: |
290 | + # All good, move on. |
291 | + pass |
292 | + |
293 | def _is_visible_to(self, user): |
294 | if not self._is_private or user == self.owner: |
295 | return True |
296 | |
297 | === modified file 'lib/offspring/web/queuemanager/tests/test_views.py' |
298 | --- lib/offspring/web/queuemanager/tests/test_views.py 2012-01-13 15:07:07 +0000 |
299 | +++ lib/offspring/web/queuemanager/tests/test_views.py 2012-01-16 18:18:24 +0000 |
300 | @@ -179,6 +179,115 @@ |
301 | status_code=200, msg_prefix=response.content) |
302 | |
303 | |
304 | +class ProjectEditSSHViewTests(TestCase): |
305 | + def test_edit_ssh_auth(self): |
306 | + project = factory.make_project(is_private=True) |
307 | + user = project.owner |
308 | + grant_permission_to_user(user, "change_project") |
309 | + self.assertTrue( |
310 | + self.client.login(username=user.username, password=user.username)) |
311 | + |
312 | + key = ("-----BEGIN type PRIVATE KEY-----\n" |
313 | + "12qwaszx34erdfcv\n" |
314 | + "-----END type PRIVATE KEY-----\n") |
315 | + |
316 | + data = {"lp_user": "lpuser", |
317 | + "lp_ssh_key": key, |
318 | + "_save": 1} |
319 | + response = self.client.post( |
320 | + reverse( |
321 | + "offspring.web.queuemanager.views.project_editsshcredentials", |
322 | + args=[project.name]), |
323 | + data, follow=True) |
324 | + |
325 | + self.assertNotContains( |
326 | + response, "form-errors", status_code=200, |
327 | + msg_prefix=response.content) |
328 | + |
329 | + # Re-load the project from the database to pick up changes |
330 | + user_visible_objects = Project.all_objects.accessible_by_user(user) |
331 | + project = get_possibly_private_object( |
332 | + user, user_visible_objects, pk=project.name) |
333 | + |
334 | + self.assertEqual("lpuser", project.lp_user) |
335 | + self.assertEqual(key, project.lp_ssh_key) |
336 | + |
337 | + def test_edit_ssh_auth_bad_key(self): |
338 | + project = factory.make_project(is_private=True) |
339 | + user = project.owner |
340 | + grant_permission_to_user(user, "change_project") |
341 | + self.assertTrue( |
342 | + self.client.login(username=user.username, password=user.username)) |
343 | + |
344 | + key = ("-----BEGIN type PR---VATE KEY-----\n" |
345 | + "12qwaszx34erdfcv\n" |
346 | + "-----END type PRIVATE KEY-----\n") |
347 | + |
348 | + data = {"lp_user": "lpuser", |
349 | + "lp_ssh_key": key, |
350 | + '_save': 1} |
351 | + response = self.client.post( |
352 | + reverse( |
353 | + "offspring.web.queuemanager.views.project_editsshcredentials", |
354 | + args=[project.name]), |
355 | + data, follow=True) |
356 | + |
357 | + self.assertEqual(200, response.status_code) |
358 | + |
359 | + # re-load the project from the database to pick up changes |
360 | + user_visible_objects = Project.all_objects.accessible_by_user(user) |
361 | + project = get_possibly_private_object( |
362 | + user, user_visible_objects, pk=project.name) |
363 | + |
364 | + self.assertNotEqual("lpuser", project.lp_user) |
365 | + self.assertNotEqual(key, project.lp_ssh_key) |
366 | + |
367 | + def test_edit_ssh_auth_private_project(self): |
368 | + project = factory.make_project(is_private=True) |
369 | + user = factory.make_user() |
370 | + self.assertTrue( |
371 | + self.client.login(username=user.username, password=user.username)) |
372 | + |
373 | + # Check can't read a private projects ssh cred page |
374 | + response = self.client.get( |
375 | + reverse( |
376 | + "offspring.web.queuemanager.views.project_editsshcredentials", |
377 | + args=[project.name]), |
378 | + follow=True) |
379 | + self.assertEqual(200, response.status_code) |
380 | + self.assertContains(response, "Lexbuilder Control Administration") |
381 | + self.assertContains(response, "Username") |
382 | + self.assertContains(response, "Password") |
383 | + |
384 | + # Check can't write to a private projects ssh cred page |
385 | + key = ("-----BEGIN type PR---VATE KEY-----\n" |
386 | + "12qwaszx34erdfcv\n" |
387 | + "-----END type PRIVATE KEY-----\n") |
388 | + |
389 | + data = {"lp_user": "lpuser", |
390 | + "lp_ssh_key": key, |
391 | + "_save": 1} |
392 | + response = self.client.post( |
393 | + reverse( |
394 | + "offspring.web.queuemanager.views.project_editsshcredentials", |
395 | + args=[project.name]), |
396 | + data, follow=True) |
397 | + |
398 | + self.assertEqual(200, response.status_code) |
399 | + self.assertContains(response, "Lexbuilder Control Administration") |
400 | + self.assertContains(response, "Username") |
401 | + self.assertContains(response, "Password") |
402 | + |
403 | + # re-load the project from the database to pick up changes |
404 | + user_visible_objects = Project.all_objects.accessible_by_user( |
405 | + project.owner) |
406 | + project = get_possibly_private_object( |
407 | + project.owner, user_visible_objects, pk=project.name) |
408 | + |
409 | + self.assertNotEqual("lpuser", project.lp_user) |
410 | + self.assertNotEqual(key, project.lp_ssh_key) |
411 | + |
412 | + |
413 | class ProjectCreateViewTests(TestCase): |
414 | view_path = 'offspring.web.queuemanager.views.project_create' |
415 | |
416 | @@ -199,7 +308,9 @@ |
417 | "_is_private": [u"on"], |
418 | "_save": [u"Save"], |
419 | "series": [u"natty"], |
420 | - "arch": [u"amd64"]} |
421 | + "arch": [u"amd64"], |
422 | + "lp_user": "", |
423 | + "lp_ssh_key": ""} |
424 | response = self.client.post(reverse(self.view_path), data) |
425 | self.assertContains( |
426 | response, "A private project needs an owner", |
427 | |
428 | === modified file 'lib/offspring/web/queuemanager/views.py' |
429 | --- lib/offspring/web/queuemanager/views.py 2012-01-11 16:46:24 +0000 |
430 | +++ lib/offspring/web/queuemanager/views.py 2012-01-16 18:18:24 +0000 |
431 | @@ -42,6 +42,7 @@ |
432 | AccessGroupMemberForm, |
433 | CreateProjectForm, |
434 | EditProjectForm, |
435 | + EditProjectSSHCredentialsForm, |
436 | LaunchpadProjectForm, |
437 | ReleaseForm, |
438 | ) |
439 | @@ -359,8 +360,8 @@ |
440 | else: |
441 | form = AccessGroupMemberForm() |
442 | pageData = { |
443 | - "csrf_token" : csrf.get_token(request), |
444 | - "project" : project, |
445 | + "csrf_token": csrf.get_token(request), |
446 | + "project": project, |
447 | "form": form, |
448 | "allowed_users": allowed_users, |
449 | } |
450 | @@ -369,6 +370,49 @@ |
451 | context_instance=RequestContext(request)) |
452 | |
453 | |
454 | +@permission_required("queuemanager.change_project") |
455 | +def project_editsshcredentials(request, projectName): |
456 | + user_visible_objects = Project.all_objects.accessible_by_user( |
457 | + request.user) |
458 | + project = get_possibly_private_object( |
459 | + request.user, user_visible_objects, pk=projectName) |
460 | + access_group = None |
461 | + allowed_users = [] |
462 | + if len(project.access_groups.all()) > 0: |
463 | + # We always use the first access group because there's no way for |
464 | + # users to register more than one access group for any given project. |
465 | + access_group = project.access_groups.all()[0] |
466 | + allowed_users = access_group.members.all() |
467 | + |
468 | + if request.method == "POST": |
469 | + |
470 | + form = EditProjectSSHCredentialsForm(request.POST, instance=project) |
471 | + |
472 | + # Don't validate the data (call form.is_valid) before deleting - we |
473 | + # don't care what the form input is since we don't use it. |
474 | + if "delete" in request.POST: |
475 | + project.lp_user = "" |
476 | + project.lp_ssh_key = "" |
477 | + project.save() |
478 | + return HttpResponseRedirect(".") |
479 | + |
480 | + if form.is_valid(): |
481 | + if "_save" in request.POST: |
482 | + form.save() |
483 | + return HttpResponseRedirect(".") |
484 | + |
485 | + else: |
486 | + form = EditProjectSSHCredentialsForm(instance=project) |
487 | + pageData = { |
488 | + "csrf_token": csrf.get_token(request), |
489 | + "project": project, |
490 | + "form": form, |
491 | + "allowed_users": allowed_users} |
492 | + return render_to_response( |
493 | + "queuemanager/project_edit_credentials.html", pageData, |
494 | + context_instance=RequestContext(request)) |
495 | + |
496 | + |
497 | def projectgroup_details(request, projectGroupName): |
498 | pg = get_object_or_404( |
499 | ProjectGroup, pk=projectGroupName) |
500 | |
501 | === modified file 'lib/offspring/web/templates/queuemanager/project_create.html' |
502 | --- lib/offspring/web/templates/queuemanager/project_create.html 2011-02-28 22:48:46 +0000 |
503 | +++ lib/offspring/web/templates/queuemanager/project_create.html 2012-01-16 18:18:24 +0000 |
504 | @@ -5,7 +5,9 @@ |
505 | {% endblock %} |
506 | |
507 | {% block header_js %} |
508 | -<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script> |
509 | +<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script> |
510 | +<script type="text/javascript" src="/media/js/jquery.min.js"></script> |
511 | +<script type="text/javascript" src="/assets/js/jquery.placeholder.js"></script> |
512 | <script type="text/javascript"> |
513 | /* Override showAddAnotherPopup to use custom height and width. */ |
514 | function showAddAnotherPopup(triggeringLink) { |
515 | @@ -29,29 +31,67 @@ |
516 | {% endblock %} |
517 | |
518 | {% block content %} |
519 | - <form method="POST" action="">{% csrf_token %} |
520 | + {{ form.non_field_errors }} |
521 | + <form method="POST" action="" name="projsettings">{% csrf_token %} |
522 | <div class="module aligned "> |
523 | {% for field in form %} |
524 | - <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}"> |
525 | - <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}> |
526 | - {% if field.is_checkbox %} |
527 | - {{ field }}{{ field.label_tag }} |
528 | - {% else %} |
529 | - {{ field.label_tag }} |
530 | - {% if field.is_readonly %} |
531 | - <p>{{ field.contents }}</p> |
532 | + {% if field.html_name == "lp_user" or field.html_name == "lp_ssh_key" %} |
533 | + <!-- This catches lp_user and lp_ssh_key so they aren't shown in the top of |
534 | + the page. They are displayed below in their own section after some help text --> |
535 | + {% else %} |
536 | + <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}"> |
537 | + <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}> |
538 | + {% if field.is_checkbox %} |
539 | + {{ field }}{{ field.label_tag }} |
540 | {% else %} |
541 | - {{ field }} |
542 | - {% endif %} |
543 | - {% endif %} |
544 | - {% if field.field.field.help_text %} |
545 | - <p class="help">{{ field.field.field.help_text|safe }}</p> |
546 | - {% endif %} |
547 | + {{ field.label_tag }} |
548 | + {% if field.is_readonly %} |
549 | + <p>{{ field.contents }}</p> |
550 | + {% else %} |
551 | + {{ field }} |
552 | + {% endif %} |
553 | + {% endif %} |
554 | + {% if field.field.field.help_text %} |
555 | + <p class="help">{{ field.field.field.help_text|safe }}</p> |
556 | + {% endif %} |
557 | + </div> |
558 | + {{ field.errors }} |
559 | </div> |
560 | - {{ field.errors }} |
561 | - </div> |
562 | + {% endif %} |
563 | |
564 | {% endfor %} |
565 | + |
566 | + <br> |
567 | + If the config URL above points to a Bazaar repository that is only |
568 | + available to authenticated users, you should add a user and private SSH |
569 | + key to give the builder access. The SSH key will never be revealed. It |
570 | + can be changed or deleted later. It should not be passphrase protected. |
571 | + If you would like to add these details, tick here: |
572 | + |
573 | + <br> |
574 | + |
575 | + <div class="form-row {{ form.lp_user.name }}"> |
576 | + <div class="field-box"> |
577 | + {{ form.lp_user.label_tag }} |
578 | + {{ form.lp_user }} |
579 | + </div> |
580 | + {{ form.lp_user.errors }} |
581 | + </div> |
582 | + |
583 | + <div class="form-row {{ form.lp_ssh_key.name }}"> |
584 | + <div class="field-box"> |
585 | + {{ form.lp_ssh_key.label_tag }} |
586 | + <br> |
587 | + {{ form.lp_ssh_key }} |
588 | + {% if form.lp_ssh_key.help_text %} |
589 | + <p class="help">{{ form.lp_ssh_key.help_text|safe }}</p> |
590 | + {% endif %} |
591 | + </div> |
592 | + {{ form.lp_ssh_key.errors|linebreaksbr }} |
593 | + </div> |
594 | + |
595 | + <br> |
596 | + |
597 | <div class="submit-row" style="overflow: auto;"> |
598 | <input type="submit" value="Save" class="default" name="_save"/> |
599 | <input type="button" value="Cancel" class="default" name="_cancel" OnClick="window.location.href = '/';"/> |
600 | |
601 | === modified file 'lib/offspring/web/templates/queuemanager/project_edit.html' |
602 | --- lib/offspring/web/templates/queuemanager/project_edit.html 2011-03-03 01:50:40 +0000 |
603 | +++ lib/offspring/web/templates/queuemanager/project_edit.html 2012-01-16 18:18:24 +0000 |
604 | @@ -44,14 +44,28 @@ |
605 | {{ field }} |
606 | {% endif %} |
607 | {% endif %} |
608 | - {% if field.field.field.help_text %} |
609 | - <p class="help">{{ field.field.field.help_text|safe }}</p> |
610 | + {% if field.help_text %} |
611 | + <p class="help">{{ field.help_text|safe }}</p> |
612 | {% endif %} |
613 | </div> |
614 | {{ field.errors }} |
615 | </div> |
616 | |
617 | {% endfor %} |
618 | + |
619 | + <br> |
620 | + If the config URL above points to a Bazaar repository that is only |
621 | + available to authenticated users, you should add a user and private SSH |
622 | + key to give the builder access. The SSH key will never be revealed. It |
623 | + can be changed or deleted later. It should not be passphrase protected. |
624 | + |
625 | + {% if form.instance.lp_ssh_key %} |
626 | + To update or delete these details, use |
627 | + {% else %} |
628 | + If you would like to add these details, |
629 | + {% endif %} |
630 | + <a href="+editcredentials">use this form.</a> |
631 | + |
632 | <div class="submit-row" style="overflow: auto;"> |
633 | <input type="submit" value="Save" class="default" name="_save"/> |
634 | <input type="button" value="Cancel" class="default" name="_cancel" OnClick="window.location.href = '{% url offspring.web.queuemanager.views.project_details project.name %}';"/> |
635 | |
636 | === added file 'lib/offspring/web/templates/queuemanager/project_edit_credentials.html' |
637 | --- lib/offspring/web/templates/queuemanager/project_edit_credentials.html 1970-01-01 00:00:00 +0000 |
638 | +++ lib/offspring/web/templates/queuemanager/project_edit_credentials.html 2012-01-16 18:18:24 +0000 |
639 | @@ -0,0 +1,70 @@ |
640 | +{% extends "base.html" %} |
641 | + |
642 | +{% block header_css %} |
643 | +<link rel="stylesheet" type="text/css" href="/media/css/forms.css" /> |
644 | +{% endblock %} |
645 | + |
646 | +{% block header_js %} |
647 | +<script type="text/javascript" src="/media/js/jquery.min.js"></script> |
648 | +<script type="text/javascript" src="/assets/js/jquery.placeholder.js"></script> |
649 | +{% endblock %} |
650 | + |
651 | +{% block title %} |
652 | +Update SSH Credentials for {{project.title|capfirst}} |
653 | +{% endblock %} |
654 | + |
655 | +{% block content %} |
656 | + <div id="form-errors">{{ form.non_field_errors }}</div> |
657 | + <form method="POST" action="" name="projsettings">{% csrf_token %} |
658 | + <div class="module aligned "> |
659 | + <div class="form-row {{ form.lp_user.name }}"> |
660 | + <div class="field-box"> |
661 | + {{ form.lp_user.label_tag }} |
662 | + {{ form.lp_user }} |
663 | + </div> |
664 | + {{ form.lp_user.errors }} |
665 | + </div> |
666 | + |
667 | + <div class="form-row {{ form.lp_ssh_key.name }}"> |
668 | + <div class="field-box"> |
669 | + {{ form.lp_ssh_key.label_tag }} |
670 | + <br> |
671 | + {% if form.errors and form.data.lp_ssh_key %} |
672 | + <!-- The user submitted a non-empty SSH key and there's |
673 | + a form error, so we show the submitted key to help |
674 | + them figure out what is wrong. --> |
675 | + {{ form.lp_ssh_key }} |
676 | + {% else %} |
677 | + <textarea id="id_lp_ssh_key" rows="4" cols="70" name="lp_ssh_key" |
678 | + placeholder="{% if form.instance.lp_ssh_key %}{{ form.lp_ssh_set_message }}{% else %}{{ form.lp_ssh_clear_meessage }}{% endif %}"></textarea> |
679 | + {% endif %} |
680 | + {% if form.lp_ssh_key.help_text %} |
681 | + <p class="help"> |
682 | + {{ form.lp_ssh_key.help_text|safe }} |
683 | + {% if form.instance.lp_ssh_key %} |
684 | + Your saved key is not shown. To replace it, paste a new key in the above box. |
685 | + {% else %} |
686 | + No key is stored for this project. Saved keys are not shown. |
687 | + {% endif %} |
688 | + </p> |
689 | + {% endif %} |
690 | + </div> |
691 | + {{ form.lp_ssh_key.errors|linebreaksbr }} |
692 | + </div> |
693 | + |
694 | + <br> |
695 | + |
696 | + <div class="submit-row" style="overflow: auto;"> |
697 | + <input type="submit" value="Save Updated Credentials" class="default" name="_save"/> |
698 | + <input type="submit" value="Delete Existing Credentials" class="default" name="delete"/> |
699 | + <input type="button" value="Cancel" class="default" name="_cancel" OnClick="window.location.href = '{% url offspring.web.queuemanager.views.project_details project.name %}';"/> |
700 | + </div> |
701 | + </div> |
702 | + </form> |
703 | +{% endblock %} |
704 | + |
705 | +{% block two-columns %} |
706 | +<script> |
707 | + $('input[placeholder], textarea[placeholder]').placeholder(); |
708 | +</script> |
709 | +{% endblock %} |
710 | |
711 | === modified file 'lib/offspring/web/urls.py' |
712 | --- lib/offspring/web/urls.py 2011-12-15 13:58:03 +0000 |
713 | +++ lib/offspring/web/urls.py 2012-01-16 18:18:24 +0000 |
714 | @@ -75,6 +75,8 @@ |
715 | (r'^projects/(?P<projectName>[^/]+)/\+build$', 'queue_build'), |
716 | (r'^projects/(?P<projectName>[^/]+)/\+builds$', 'builds'), |
717 | (r'^projects/(?P<projectName>[^/]+)/$', 'project_details'), |
718 | + (r'^projects/(?P<projectName>[^/]+)/\+editcredentials$', |
719 | + 'project_editsshcredentials'), |
720 | (r'^projects/$', 'projects'), |
721 | (r'^schedule/\+api/milestones/$', milestone_handler), |
722 | (r'^schedule/\+(?P<milestoneType>[^/]+)-milestones$', 'milestones'), |