Merge lp:~salgado/offspring/project-ssh-key-ui into lp:offspring

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
Reviewer Review Type Date Requested Status
CE Infrastructure Pending
Offspring Committers Pending
Review via email: mp+88259@code.launchpad.net

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'),

Subscribers

People subscribed via source and target branches