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

Proposed by James Tunnicliffe
Status: Merged
Merged at revision: 75
Proposed branch: lp:~dooferlad/offspring/ssh_ui_mods
Merge into: lp:~linaro-automation/offspring/private-builds
Diff against target: 767 lines (+556/-44)
10 files modified
lib/offspring/web/media/js/jquery.placeholder.js (+106/-0)
lib/offspring/web/queuemanager/admin.py (+3/-1)
lib/offspring/web/queuemanager/forms.py (+133/-5)
lib/offspring/web/queuemanager/models.py (+1/-1)
lib/offspring/web/queuemanager/tests/test_views.py (+107/-1)
lib/offspring/web/queuemanager/views.py (+47/-0)
lib/offspring/web/templates/queuemanager/project_create.html (+59/-18)
lib/offspring/web/templates/queuemanager/project_edit.html (+35/-18)
lib/offspring/web/templates/queuemanager/project_edit_credentials.html (+64/-0)
lib/offspring/web/urls.py (+1/-0)
To merge this branch: bzr merge lp:~dooferlad/offspring/ssh_ui_mods
Reviewer Review Type Date Requested Status
Guilherme Salgado Pending
James Tunnicliffe Pending
Review via email: mp+81217@code.launchpad.net

This proposal supersedes a proposal from 2011-10-29.

Description of the change

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

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

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

Revision history for this message
James Tunnicliffe (dooferlad) wrote : Posted in a previous version of this proposal

Have rebased. Should be fine now.

Revision history for this message
Guilherme Salgado (salgado) wrote : Posted in a previous version of this proposal

Hi James,

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

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

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

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

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

Revision history for this message
James Tunnicliffe (dooferlad) wrote : Posted in a previous version of this proposal

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

review: Needs Resubmitting
Revision history for this message
Guilherme Salgado (salgado) wrote : Posted in a previous version of this proposal

Hi James,
This one still has stuff that ought to have been removed, like the two js files and changes to project_create.html. Please go through the diff when you submit a MP so that the person who's reviewing the code can focus on the interesting things rather than trying to guess what's left over from previous commits and what's actually used in the current code.

review: Needs Resubmitting
Revision history for this message
James Tunnicliffe (dooferlad) wrote : Posted in a previous version of this proposal

Sorry, I was under the impression that I was keeping in the updates to
project_create.html, which its why it and the js files are still there. I
will get the changes reverted and remove the js. If we want something more
fancy in the future I can go back and revisit it.

I am out at dinner now so don't expect any updates tonight.

James
On Oct 28, 2011 8:53 PM, "Guilherme Salgado" <email address hidden>
wrote:

> Review: Resubmit
>
> Hi James,
> This one still has stuff that ought to have been removed, like the two js
> files and changes to project_create.html. Please go through the diff when
> you submit a MP so that the person who's reviewing the code can focus on the
> interesting things rather than trying to guess what's left over from
> previous commits and what's actually used in the current code.
> --
> https://code.launchpad.net/~dooferlad/offspring/ssh_ui_mods/+merge/80701
> You are the owner of lp:~dooferlad/offspring/ssh_ui_mods.
>

Revision history for this message
Guilherme Salgado (salgado) wrote : Posted in a previous version of this proposal

Oh, sorry, I mixed up project_create with project_edit. You're right that we want to allow people to enter the ssh key when creating a new project, but I don't see a reason for having the ssh_key/lp_user fields hidden initially, so we could probably get rid of the other js file. There's also the showAddAnotherPopup() function in the new template, which probably comes from the edit template that you used as a base, although I don't think is needed?

I'm not fond of using javascript (and specially a jquery placeholder plugin, as that's not the js library used in Offspring) to have the help text displayed inside the text area, but if you feel strong about it, that's fine.

Revision history for this message
James Tunnicliffe (dooferlad) wrote : Posted in a previous version of this proposal

No worries. I have got rid of the show/hide bit on the project add page and showAddAnotherPopup(). I have left the jquery placeholder plugin in because I think it improves the user experience and provides backwards compatibility for those using lesser browsers. I am not sure if it is a strong opinion though!

Revision history for this message
Guilherme Salgado (salgado) wrote : Posted in a previous version of this proposal
Download full text (28.1 KiB)

Here are the things we discussed in person today...

> === modified file 'lib/offspring/web/queuemanager/forms.py'
> --- lib/offspring/web/queuemanager/forms.py 2011-10-19 20:23:49 +0000
> +++ lib/offspring/web/queuemanager/forms.py 2011-10-31 15:48:00 +0000
> @@ -1,8 +1,11 @@
> # Copyright 2010 Canonical Ltd. This software is licensed under the
> # GNU Affero General Public License version 3 (see the file LICENSE).
>
> +import re
> +
> from django.contrib.auth.models import User
> from django.db import models
> +from django import forms
> from django.forms import (
> Form, ModelChoiceField, ModelForm, Textarea, TextInput, ValidationError)
> from django.forms import fields
> @@ -15,9 +18,81 @@
>
> from offspring.web.queuemanager.widgets import SelectWithAddNew
>
> -class ProjectBaseForm(ModelForm):
> - status = fields.CharField(max_length=200,
> - widget=fields.Select(choices=Project.STATUS_CHOICES), required=True)
> +class SSHPrivateKeyField(forms.Field):
> +
> + def validate(self, value):
> + "Check to see if the value looks like an SSH private key"
> +
> + # Use the parent's handling of required fields, etc.
> + super(SSHPrivateKeyField, self).validate(value)
> +
> + key_search_regexp = (r"-----BEGIN \w+ PRIVATE KEY-----"+
> + r".*"+
> + r"-----END \w+ PRIVATE KEY-----")
> + is_key = re.search(key_search_regexp, value, re.DOTALL | re.MULTILINE)
> +
> + if not is_key and not value == "":
> + msg = ("The key you entered doesn't appear to be valid. I am "+
> + "expecting a key in the form:\n "+
> + "-----BEGIN <type> PRIVATE KEY-----\n"+
> + "<ASCII key>\n"+
> + "-----END <type> PRIVATE KEY-----\n")
> + raise forms.ValidationError(msg)
> +
> +class ProjectSSHCredForm(ModelForm):
> + ssh_help = ("Enter a private SSH ASCII key block, complete with begin "+
> + "and end markers.")
> +
> + lp_ssh_key_input = SSHPrivateKeyField(label="Launchpad User's SSH key",
> + required=False,
> + widget=forms.Textarea(
> + attrs={'cols': 70, 'rows' : 4}),
> + help_text=ssh_help)
> +
> + lp_fields_set = False
> + lp_ssh_set = False
> + lp_ssh_set_message = ("An SSH key is stored for this project. To replace "+
> + "it, paste a new SSH private key block here.")
> + lp_ssh_clear_meessage = ("To enable access to private repositories,"+
> + "enter an SSH private key here in the form of an "+
> + "ASCII key block.")
> +
> + def __init__(self, *args, **kwargs):
> + super(ProjectSSHCredForm, self).__init__(*args, **kwargs)
> + # Set the form fields based on the model object
> + if kwargs.has_key('instance'):
> + instance = kwargs['instance']
> + if instance.lp_ssh_key and instance.lp_ssh_key != "":

The second clause is redundant her...

review: Needs Fixing
Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (9.5 KiB)

On Thu, 2011-11-03 at 22:21 +0000, James Tunnicliffe wrote:
> === modified file 'lib/offspring/web/templates/queuemanager/project_create.html'
> --- lib/offspring/web/templates/queuemanager/project_create.html 2011-02-28 22:48:46 +0000
> +++ lib/offspring/web/templates/queuemanager/project_create.html 2011-11-03 22:20:29 +0000
> @@ -5,7 +5,9 @@
> {% endblock %}
>
> {% block header_js %}
> -<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
> +<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
> +<script type="text/javascript" src="/media/js/jquery.min.js"></script>
> +<script type="text/javascript" src="/assets/js/jquery.placeholder.js"></script>
> <script type="text/javascript">
> /* Override showAddAnotherPopup to use custom height and width. */
> function showAddAnotherPopup(triggeringLink) {
> @@ -29,29 +31,68 @@
> {% endblock %}
>
> {% block content %}
> - <form method="POST" action="">{% csrf_token %}
> + <form method="POST" action="" name="projsettings">{% csrf_token %}
> <div class="module aligned ">
> {% for field in form %}
> - <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}">
> - <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
> - {% if field.is_checkbox %}
> - {{ field }}{{ field.label_tag }}
> - {% else %}
> - {{ field.label_tag }}
> - {% if field.is_readonly %}
> - <p>{{ field.contents }}</p>
> + {% if field.html_name == "lp_user" or field.html_name == "lp_ssh_key_input" %}
> + <!-- This catches lp_user and lp_ssh_key_input so they aren't shown in the top of
> + the page. They are displayed below in their own section after some help text -->
> + {% else %}
> + <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}">
> + <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
> + {% if field.is_checkbox %}
> + {{ field }}{{ field.label_tag }}
> {% else %}
> - {{ field }}
> - {% endif %}
> - {% endif %}
> - {% if field.field.field.help_text %}
> - <p class="help">{{ field.field.field.help_text|safe }}</p>
> - {% endif %}
> + {{ field.label_tag }}
> + {% if field.is_readonly %}
> + <p>{{ field.contents }}</p>
> + {% else %}
> + {{ field }}
> + {% endif %}
> + {% endif %}
> + {% if field.field.field.help_text %}
> + <p class...

Read more...

Revision history for this message
James Tunnicliffe (dooferlad) wrote :
Download full text (5.3 KiB)

On 4 November 2011 20:46, Guilherme Salgado
<email address hidden> wrote:
> On Thu, 2011-11-03 at 22:21 +0000, James Tunnicliffe wrote:
>> === modified file 'lib/offspring/web/templates/queuemanager/project_create.html'
<snip>
>> +            If the config URL above points to a Bazaar repository that is only
>> +            available to authenticated users, you should add a user and private SSH
>> +            key to give the builder access. The SSH key will never be revealed. It
>> +            can be changed or deleted later. It should not be passphrase protected.
>> +            If you would like to add these details, tick here:
>
> Hmm. There's nothing to tick here anymore, right?

Good catch!

>> === modified file 'lib/offspring/web/templates/queuemanager/project_edit.html'
>> --- lib/offspring/web/templates/queuemanager/project_edit.html        2011-03-03 01:50:40 +0000
>> +++ lib/offspring/web/templates/queuemanager/project_edit.html        2011-11-03 22:20:29 +0000
>> @@ -5,7 +5,7 @@
>>  {% endblock %}
>>
>>  {% block header_js %}
>> -<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
>> +<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
>>  <script type="text/javascript">
>>  /* Override showAddAnotherPopup to use custom height and width. */
>>  function showAddAnotherPopup(triggeringLink) {
>> @@ -29,29 +29,46 @@
>>  {% endblock %}
>>
>>  {% block content %}
>> -    <form method="POST" action="">{% csrf_token %}
>> +    <form method="POST" action="" name="projsettings">{% csrf_token %}
>>          <div class="module aligned ">
>>               {% for field in form %}
>> -                <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}">
>> -                    <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
>> -                        {% if field.is_checkbox %}
>> -                            {{ field }}{{ field.label_tag }}
>> -                        {% else %}
>> -                            {{ field.label_tag }}
>> -                            {% if field.is_readonly %}
>> -                                <p>{{ field.contents }}</p>
>> +                {% if field.html_name == "lp_user" or field.html_name == "lp_ssh_key_input"%}
>
> This is no longer needed, right?  We've agreed on that in the previous
> review already, IIRC.

It is still required because the new fields are part of the project
form, so they need to be filtered out where they aren't needed.
Actually I just noticed that since I have switched lp_user to a
TextField to match the internal representation of Launchpad, it can
now contain newlines and the form doesn't stop this. I see my options
as:

01234567890123456789012345678901234567890123456789012345678901234567890123456789
1. Leave it as it is and add a custom validate to exclude newlines
2. Switch it back to a CharField in the DB and set max_length to None, or if
   that isn't allowed, something large, like 256.
3. Do something similar to the ssh input field - leave the internal
   representation as is, and have a CharField input box with a max_length
   set to something massive.
...

Read more...

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (6.6 KiB)

On Mon, 2011-11-07 at 17:10 +0000, James Tunnicliffe wrote:
> >> === modified file 'lib/offspring/web/templates/queuemanager/project_edit.html'
> >> --- lib/offspring/web/templates/queuemanager/project_edit.html 2011-03-03 01:50:40 +0000
> >> +++ lib/offspring/web/templates/queuemanager/project_edit.html 2011-11-03 22:20:29 +0000
> >> @@ -5,7 +5,7 @@
> >> {% endblock %}
> >>
> >> {% block header_js %}
> >> -<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
> >> +<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
> >> <script type="text/javascript">
> >> /* Override showAddAnotherPopup to use custom height and width. */
> >> function showAddAnotherPopup(triggeringLink) {
> >> @@ -29,29 +29,46 @@
> >> {% endblock %}
> >>
> >> {% block content %}
> >> - <form method="POST" action="">{% csrf_token %}
> >> + <form method="POST" action="" name="projsettings">{% csrf_token %}
> >> <div class="module aligned ">
> >> {% for field in form %}
> >> - <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}">
> >> - <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
> >> - {% if field.is_checkbox %}
> >> - {{ field }}{{ field.label_tag }}
> >> - {% else %}
> >> - {{ field.label_tag }}
> >> - {% if field.is_readonly %}
> >> - <p>{{ field.contents }}</p>
> >> + {% if field.html_name == "lp_user" or field.html_name == "lp_ssh_key_input"%}
> >
> > This is no longer needed, right? We've agreed on that in the previous
> > review already, IIRC.
>
> It is still required because the new fields are part of the project
> form, so they need to be filtered out where they aren't needed.

But the template is the worst place to do that, because the templating
language is not as powerfull as python and testing templates is harder
(and usually slower) than testing forms/views. And the above looks like
an ugly hack to me because of the empty if block, without even a
comment.

Having said that, I've just noticed that you already exclude the
unwanted fields in the form used by this template, so unless I'm missing
something this isn't needed.

        class EditProjectForm(ProjectBaseForm):
            launchpad_project = ModelChoiceField(
                LaunchpadProject.objects, widget=SelectWithAddNew, required=False)
            class Meta(ProjectBaseForm.Meta):
                exclude = ('name', 'priority', 'is_active', 'access_groups',
                           'lp_user', 'lp_ssh_key_input')

I thought the above would be enough to get rid of the lp_* fields on the
edit page, but I've just reverted the template changes locally and the
ssh key text area is still present on the form. That's probably why you
ended up doing the template changes, but instead of doing that we need
to find out why the ssh key field is still shown and fix it so that we
don't need the template hack to omit the field. I'll keep di...

Read more...

Revision history for this message
James Tunnicliffe (dooferlad) wrote :
Download full text (8.0 KiB)

On 8 November 2011 15:46, Guilherme Salgado
<email address hidden> wrote:
> On Mon, 2011-11-07 at 17:10 +0000, James Tunnicliffe wrote:
>> >> === modified file 'lib/offspring/web/templates/queuemanager/project_edit.html'
>> >> --- lib/offspring/web/templates/queuemanager/project_edit.html        2011-03-03 01:50:40 +0000
>> >> +++ lib/offspring/web/templates/queuemanager/project_edit.html        2011-11-03 22:20:29 +0000
>> >> @@ -5,7 +5,7 @@
>> >>  {% endblock %}
>> >>
>> >>  {% block header_js %}
>> >> -<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
>> >> +<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
>> >>  <script type="text/javascript">
>> >>  /* Override showAddAnotherPopup to use custom height and width. */
>> >>  function showAddAnotherPopup(triggeringLink) {
>> >> @@ -29,29 +29,46 @@
>> >>  {% endblock %}
>> >>
>> >>  {% block content %}
>> >> -    <form method="POST" action="">{% csrf_token %}
>> >> +    <form method="POST" action="" name="projsettings">{% csrf_token %}
>> >>          <div class="module aligned ">
>> >>               {% for field in form %}
>> >> -                <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}">
>> >> -                    <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
>> >> -                        {% if field.is_checkbox %}
>> >> -                            {{ field }}{{ field.label_tag }}
>> >> -                        {% else %}
>> >> -                            {{ field.label_tag }}
>> >> -                            {% if field.is_readonly %}
>> >> -                                <p>{{ field.contents }}</p>
>> >> +                {% if field.html_name == "lp_user" or field.html_name == "lp_ssh_key_input"%}
>> >
>> > This is no longer needed, right?  We've agreed on that in the previous
>> > review already, IIRC.
>>
>> It is still required because the new fields are part of the project
>> form, so they need to be filtered out where they aren't needed.
>
> But the template is the worst place to do that, because the templating
> language is not as powerfull as python and testing templates is harder
> (and usually slower) than testing forms/views. And the above looks like
> an ugly hack to me because of the empty if block, without even a
> comment.
>
> Having said that, I've just noticed that you already exclude the
> unwanted fields in the form used by this template, so unless I'm missing
> something this isn't needed.
>
>        class EditProjectForm(ProjectBaseForm):
>            launchpad_project = ModelChoiceField(
>                LaunchpadProject.objects, widget=SelectWithAddNew, required=False)
>            class Meta(ProjectBaseForm.Meta):
>                exclude = ('name', 'priority', 'is_active', 'access_groups',
>                           'lp_user', 'lp_ssh_key_input')
>
> I thought the above would be enough to get rid of the lp_* fields on the
> edit page, but I've just reverted the template changes locally and the
> ssh key text area is still present on the form. That's probably why you
> ended up doing the template changes, but instead o...

Read more...

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

On 8 November 2011 16:43, James Tunnicliffe <email address hidden> wrote:

>> Did you change anything to make field.help_text work or is that just an
>> existing shortcut to field.field.field.help_text?
>
> I remember field.field.field.help_text not working, which is why I
> changed it to field.help_text, but I will try reverting it and seeing
> if it is still the case. It could be that I broke something during
> development.

Just confirmed that field.field.field.help_text doesn't work, so this
is a bug fix.

--
James Tunnicliffe

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

Got it (I think). Multiple inheritance saves the day!

class ProjectBaseWithSSHForm(ProjectBaseForm, ProjectSSHCredForm):
    pass

Using this as the form for ProjectAdmin and the base for
CreateProjectForm, leaving EditProjectForm as is, it creates the
correct combined forms when we need them. Will submit a new patch
after I have cleaned things up and verified the DB changes.

Revision history for this message
Guilherme Salgado (salgado) wrote :
Download full text (8.0 KiB)

On Tue, 2011-11-08 at 16:43 +0000, James Tunnicliffe wrote:
> On 8 November 2011 15:46, Guilherme Salgado
> <email address hidden> wrote:
> > On Mon, 2011-11-07 at 17:10 +0000, James Tunnicliffe wrote:
> >> >> === modified file 'lib/offspring/web/templates/queuemanager/project_edit.html'
> >> >> --- lib/offspring/web/templates/queuemanager/project_edit.html 2011-03-03 01:50:40 +0000
> >> >> +++ lib/offspring/web/templates/queuemanager/project_edit.html 2011-11-03 22:20:29 +0000
> >> >> @@ -5,7 +5,7 @@
> >> >> {% endblock %}
> >> >>
> >> >> {% block header_js %}
> >> >> -<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
> >> >> +<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
> >> >> <script type="text/javascript">
> >> >> /* Override showAddAnotherPopup to use custom height and width. */
> >> >> function showAddAnotherPopup(triggeringLink) {
> >> >> @@ -29,29 +29,46 @@
> >> >> {% endblock %}
> >> >>
> >> >> {% block content %}
> >> >> - <form method="POST" action="">{% csrf_token %}
> >> >> + <form method="POST" action="" name="projsettings">{% csrf_token %}
> >> >> <div class="module aligned ">
> >> >> {% for field in form %}
> >> >> - <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}">
> >> >> - <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
> >> >> - {% if field.is_checkbox %}
> >> >> - {{ field }}{{ field.label_tag }}
> >> >> - {% else %}
> >> >> - {{ field.label_tag }}
> >> >> - {% if field.is_readonly %}
> >> >> - <p>{{ field.contents }}</p>
> >> >> + {% if field.html_name == "lp_user" or field.html_name == "lp_ssh_key_input"%}
> >> >
> >> > This is no longer needed, right? We've agreed on that in the previous
> >> > review already, IIRC.
> >>
> >> It is still required because the new fields are part of the project
> >> form, so they need to be filtered out where they aren't needed.
> >
> > But the template is the worst place to do that, because the templating
> > language is not as powerfull as python and testing templates is harder
> > (and usually slower) than testing forms/views. And the above looks like
> > an ugly hack to me because of the empty if block, without even a
> > comment.
> >
> > Having said that, I've just noticed that you already exclude the
> > unwanted fields in the form used by this template, so unless I'm missing
> > something this isn't needed.
> >
> > class EditProjectForm(ProjectBaseForm):
> > launchpad_project = ModelChoiceField(
> > LaunchpadProject.objects, widget=SelectWithAddNew, required=False)
> > class Meta(ProjectBaseForm.Meta):
> > exclude = ('name', 'priority', 'is_active', 'access_groups',
> > 'lp_user', 'lp_ssh_key_input')
> >
> > I thought the above would be enough to get rid of the lp_* fields on the
> > edit page, but I've j...

Read more...

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

Ok, so after admitting this code was too complicated for my small brain I decided to try and simplify it a bit. lp:~salgado/offspring/ssh_ui_mods has a simpler version of this, with a much simpler form class, no need for the custom field and validation on the lowest possible level. As a bonus I've also fixed a bug in which the +editcredentials page was setting some attributes back to their default values because these fields were not in Meta.exclude, but also weren't rendered in the template (the list of fields rendered there are hard-coded to just lp_user and lp_ssh_key).

The key was using separate form classes depending on whether you're creating or editing a project and moving the validation to the model so that it's used in both forms. Please let me know if it looks good, James, or if you see any issues with it: https://code.launchpad.net/~salgado/offspring/ssh_ui_mods/+merge/81641

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'lib/offspring/web/media/js/jquery.placeholder.js'
--- lib/offspring/web/media/js/jquery.placeholder.js 1970-01-01 00:00:00 +0000
+++ lib/offspring/web/media/js/jquery.placeholder.js 2011-11-03 22:20:29 +0000
@@ -0,0 +1,106 @@
1/*
2* Placeholder plugin for jQuery
3* ---
4* Copyright 2010, Daniel Stocks (http://webcloud.se)
5* Released under the MIT, BSD, and GPL Licenses.
6*/
7(function($) {
8 function Placeholder(input) {
9 this.input = input;
10 if (input.attr('type') == 'password') {
11 this.handlePassword();
12 }
13 // Prevent placeholder values from submitting
14 $(input[0].form).submit(function() {
15 if (input.hasClass('placeholder') && input[0].value == input.attr('placeholder')) {
16 input[0].value = '';
17 }
18 });
19 }
20 Placeholder.prototype = {
21 show : function(loading) {
22 // FF and IE saves values when you refresh the page. If the user refreshes the page with
23 // the placeholders showing they will be the default values and the input fields won't be empty.
24 if (this.input[0].value === '' || (loading && this.valueIsPlaceholder())) {
25 if (this.isPassword) {
26 try {
27 this.input[0].setAttribute('type', 'text');
28 } catch (e) {
29 this.input.before(this.fakePassword.show()).hide();
30 }
31 }
32 this.input.addClass('placeholder');
33 this.input[0].value = this.input.attr('placeholder');
34 }
35 },
36 hide : function() {
37 if (this.valueIsPlaceholder() && this.input.hasClass('placeholder')) {
38 this.input.removeClass('placeholder');
39 this.input[0].value = '';
40 if (this.isPassword) {
41 try {
42 this.input[0].setAttribute('type', 'password');
43 } catch (e) { }
44 // Restore focus for Opera and IE
45 this.input.show();
46 this.input[0].focus();
47 }
48 }
49 },
50 valueIsPlaceholder : function() {
51 return this.input[0].value == this.input.attr('placeholder');
52 },
53 handlePassword: function() {
54 var input = this.input;
55 input.attr('realType', 'password');
56 this.isPassword = true;
57 // IE < 9 doesn't allow changing the type of password inputs
58 if ($.browser.msie && input[0].outerHTML) {
59 var fakeHTML = $(input[0].outerHTML.replace(/type=(['"])?password\1/gi, 'type=$1text$1'));
60 this.fakePassword = fakeHTML.val(input.attr('placeholder')).addClass('placeholder').focus(function() {
61 input.trigger('focus');
62 $(this).hide();
63 });
64 $(input[0].form).submit(function() {
65 fakeHTML.remove();
66 input.show()
67 });
68 }
69 }
70 };
71 var NATIVE_SUPPORT = !!("placeholder" in document.createElement( "input" ));
72 $.fn.placeholder = function() {
73 return NATIVE_SUPPORT ? this : this.each(function() {
74 var input = $(this);
75 var placeholder = new Placeholder(input);
76 placeholder.show(true);
77 input.focus(function() {
78 placeholder.hide();
79 });
80 input.blur(function() {
81 placeholder.show(false);
82 });
83
84 // On page refresh, IE doesn't re-populate user input
85 // until the window.onload event is fired.
86 if ($.browser.msie) {
87 $(window).load(function() {
88 if(input.val()) {
89 input.removeClass("placeholder");
90 }
91 placeholder.show(true);
92 });
93 // What's even worse, the text cursor disappears
94 // when tabbing between text inputs, here's a fix
95 input.focus(function() {
96 if(this.value == "") {
97 var range = this.createTextRange();
98 range.collapse(true);
99 range.moveStart('character', 0);
100 range.select();
101 }
102 });
103 }
104 });
105 }
106})(jQuery);
0107
=== modified file 'lib/offspring/web/queuemanager/admin.py'
--- lib/offspring/web/queuemanager/admin.py 2011-02-24 05:08:42 +0000
+++ lib/offspring/web/queuemanager/admin.py 2011-11-03 22:20:29 +0000
@@ -43,7 +43,9 @@
4343
4444
45class ProjectAdmin(admin.ModelAdmin):45class ProjectAdmin(admin.ModelAdmin):
46 fields = ['title', 'name', 'arch', 'project_group', 'launchpad_project', 'suite', 'series', 'priority', 'status', 'is_active', 'config_url', 'notes']46 fields = ['title', 'name', 'arch', 'project_group', 'launchpad_project',
47 'suite', 'series', 'priority', 'status', 'is_active',
48 'config_url', 'lp_user', 'lp_ssh_key_input', 'notes']
47 list_display = ('display_name', 'arch', 'series', 'project_group', 'launchpad_project', 'is_active', 'status', 'priority', 'config_url')49 list_display = ('display_name', 'arch', 'series', 'project_group', 'launchpad_project', 'is_active', 'status', 'priority', 'config_url')
48 list_filter = ['arch', 'series', 'is_active', 'project_group', 'status']50 list_filter = ['arch', 'series', 'is_active', 'project_group', 'status']
49 search_fields = ['title', 'name', 'arch', 'notes']51 search_fields = ['title', 'name', 'arch', 'notes']
5052
=== modified file 'lib/offspring/web/queuemanager/forms.py'
--- lib/offspring/web/queuemanager/forms.py 2011-10-19 20:23:49 +0000
+++ lib/offspring/web/queuemanager/forms.py 2011-11-03 22:20:29 +0000
@@ -1,8 +1,11 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4import re
5
4from django.contrib.auth.models import User6from django.contrib.auth.models import User
5from django.db import models7from django.db import models
8from django import forms
6from django.forms import (9from django.forms import (
7 Form, ModelChoiceField, ModelForm, Textarea, TextInput, ValidationError)10 Form, ModelChoiceField, ModelForm, Textarea, TextInput, ValidationError)
8from django.forms import fields11from django.forms import fields
@@ -15,9 +18,125 @@
1518
16from offspring.web.queuemanager.widgets import SelectWithAddNew19from offspring.web.queuemanager.widgets import SelectWithAddNew
1720
18class ProjectBaseForm(ModelForm):21class SSHPrivateKeyField(forms.Field):
19 status = fields.CharField(max_length=200,22
20 widget=fields.Select(choices=Project.STATUS_CHOICES), required=True)23 def validate(self, value):
24 "Check to see if the value looks like an SSH private key"
25
26 # Use the parent's handling of required fields, etc.
27 super(SSHPrivateKeyField, self).validate(value)
28 if not value: # If nothing is passed, don't try and validate it.
29 return
30
31 key_search_regexp = (r"-----BEGIN \w+ PRIVATE KEY-----"+
32 r".*"+
33 r"-----END \w+ PRIVATE KEY-----")
34 is_key = re.search(key_search_regexp, value, re.DOTALL | re.MULTILINE)
35
36 if not is_key:
37 msg = ("The key you entered doesn't appear to be valid. I am "+
38 "expecting a key in the form:\n "+
39 "-----BEGIN <type> PRIVATE KEY-----\n"+
40 "<ASCII key>\n"+
41 "-----END <type> PRIVATE KEY-----\n")
42 raise forms.ValidationError(msg)
43
44class ProjectSSHCredForm(ModelForm):
45 ssh_help = ("Enter a private SSH ASCII key block, complete with begin "+
46 "and end markers.")
47
48 lp_ssh_key_input = SSHPrivateKeyField(label="Launchpad User's SSH key",
49 required=False,
50 widget=forms.Textarea(
51 attrs={'cols': 70, 'rows' : 4}),
52 help_text=ssh_help)
53
54 lp_fields_set = False
55 lp_ssh_set = False
56 lp_ssh_set_message = ("An SSH key is stored for this project. To replace "+
57 "it, paste a new SSH private key block here.")
58 lp_ssh_clear_meessage = ("To enable access to private repositories,"+
59 "enter an SSH private key here in the form of an "+
60 "ASCII key block.")
61
62 def __init__(self, *args, **kwargs):
63 super(ProjectSSHCredForm, self).__init__(*args, **kwargs)
64 # Set the form fields based on the model object
65 if kwargs.has_key('instance'):
66 self.has_instance = True
67 instance = kwargs['instance']
68 if instance.lp_ssh_key:
69 self.lp_ssh_set = True
70 else:
71 self.lp_ssh_set = False
72
73 if instance.lp_user:
74 self.lp_user_set = True
75 else:
76 self.lp_user_set = False
77
78 self.lp_fields_set = instance.lp_ssh_key or instance.lp_user
79 else:
80 self.has_instance = None
81
82 def get_ssh_key_set(self):
83 if self.has_instance: # self.lp_user_set valid if instance exists
84 return self.lp_ssh_set or self.data['lp_ssh_key_input']
85 return self.data['lp_ssh_key_input'] != ""
86
87 def get_lp_user_set(self):
88 if self.has_instance: # self.lp_user_set valid if instance exists
89 return self.lp_user_set or self.data['lp_user']
90 return self.data['lp_user'] != ""
91
92 def clean_lp_user(self):
93 # We don't want to check that lp_user and SSH key are both set or clear
94 # if we are on a project edit page that doesn't provide these fields.
95 key_set = self.get_ssh_key_set()
96 user_set = self.get_lp_user_set()
97
98 if not user_set and key_set:
99 raise forms.ValidationError("Launchpad User is required.")
100
101 return self.cleaned_data['lp_user']
102
103 def clean_lp_ssh_key_input(self):
104 # We have to manually check the exclude list for lp_ssh_key_input
105 # because it isn't excluded automatically, which seems to be the case
106 # because it was defined in this class.
107 if "lp_ssh_key_input" not in self.Meta.exclude:
108 key_set = self.get_ssh_key_set()
109 user_set = self.get_lp_user_set()
110
111 if not key_set and user_set:
112 raise forms.ValidationError("SSH Key is required.")
113
114 return self.cleaned_data['lp_ssh_key_input']
115
116 class Meta:
117 model = Project
118 widgets = {
119 'lp_user' : Textarea(attrs={'cols': 70, 'rows' : 1}),
120 }
121
122class ProjectBaseForm(ProjectSSHCredForm):
123 def save(self, commit=True):
124 model = super(ProjectBaseForm, self).save(commit=False)
125
126 if 'lp_ssh_key_input' in self.cleaned_data:
127 # Save the SSH key
128 new_key = self.cleaned_data['lp_ssh_key_input']
129
130 if new_key == "":
131 pass # No new key entered
132
133 else: # Save new key (validated in clean_lp_ssh_key_input)
134 model.lp_ssh_key = new_key
135
136 if commit:
137 model.save()
138
139 return model
21140
22 class Meta:141 class Meta:
23 model = Project142 model = Project
@@ -25,7 +144,8 @@
25 'name' : TextInput(attrs={'style': 'text-transform: lowercase;'}),144 'name' : TextInput(attrs={'style': 'text-transform: lowercase;'}),
26 'series' : TextInput(attrs={'style': 'text-transform: lowercase;'}),145 'series' : TextInput(attrs={'style': 'text-transform: lowercase;'}),
27 'config_url': TextInput(attrs={'size': 50}),146 'config_url': TextInput(attrs={'size': 50}),
28 'notes' : Textarea(attrs={'cols': 73, 'rows' : 4}),147 'notes' : Textarea(attrs={'cols': 70, 'rows' : 4}),
148 'lp_user' : Textarea(attrs={'cols': 70, 'rows' : 1}),
29 }149 }
30150
31 def clean_name(self):151 def clean_name(self):
@@ -52,7 +172,15 @@
52 launchpad_project = ModelChoiceField(172 launchpad_project = ModelChoiceField(
53 LaunchpadProject.objects, widget=SelectWithAddNew, required=False)173 LaunchpadProject.objects, widget=SelectWithAddNew, required=False)
54 class Meta(ProjectBaseForm.Meta):174 class Meta(ProjectBaseForm.Meta):
55 exclude = ('name', 'priority', 'is_active', 'access_groups')175 exclude = ('name', 'priority', 'is_active', 'access_groups',
176 'lp_user', 'lp_ssh_key_input')
177
178
179class EditProjectSSHCredentialsForm(ProjectSSHCredForm):
180 class Meta(ProjectSSHCredForm.Meta):
181 exclude = ('priority', 'is_active', 'suite', 'access_groups', 'arch',
182 'config_url', 'name', 'priority', 'series', 'status',
183 'title')
56184
57185
58class AccessGroupMemberForm(Form):186class AccessGroupMemberForm(Form):
59187
=== modified file 'lib/offspring/web/queuemanager/models.py'
--- lib/offspring/web/queuemanager/models.py 2011-10-21 16:44:19 +0000
+++ lib/offspring/web/queuemanager/models.py 2011-11-03 22:20:29 +0000
@@ -233,7 +233,7 @@
233 # The Launchpad User and SSH key are stored per project. If we stored them233 # The Launchpad User and SSH key are stored per project. If we stored them
234 # per LauncpadProject, anyone who could create a Project referencing that234 # per LauncpadProject, anyone who could create a Project referencing that
235 # LaunchpadProject could get access to the private data in it.235 # LaunchpadProject could get access to the private data in it.
236 lp_user = models.TextField('Launchpad User', null=True, editable=False,236 lp_user = models.TextField('Launchpad User', null=True,
237 blank=True)237 blank=True)
238 lp_ssh_key = models.TextField("Launchpad User's SSH Key", blank=True,238 lp_ssh_key = models.TextField("Launchpad User's SSH Key", blank=True,
239 null=True, editable=False)239 null=True, editable=False)
240240
=== modified file 'lib/offspring/web/queuemanager/tests/test_views.py'
--- lib/offspring/web/queuemanager/tests/test_views.py 2011-10-21 14:36:55 +0000
+++ lib/offspring/web/queuemanager/tests/test_views.py 2011-11-03 22:20:29 +0000
@@ -8,6 +8,8 @@
8 Template,8 Template,
9 )9 )
10from django.test import TestCase10from django.test import TestCase
11from django.test.client import Client
12from django.contrib.auth.models import User
1113
12from offspring.enums import ProjectBuildStates14from offspring.enums import ProjectBuildStates
13from offspring.web.queuemanager.models import (15from offspring.web.queuemanager.models import (
@@ -138,6 +140,108 @@
138 status_code=200, msg_prefix=response.content)140 status_code=200, msg_prefix=response.content)
139141
140142
143class ProjectEditSSHViewTests(TestCase):
144 def test_edit_ssh_auth(self):
145 project = factory.makeProject(is_private=True)
146 user = project.owner
147 grant_permission_to_user(user, 'change_project')
148 self.assertTrue(
149 self.client.login(username=user.username, password=user.username))
150
151 key = ("-----BEGIN type PRIVATE KEY-----\n"+
152 "12qwaszx34erdfcv\n"+
153 "-----END type PRIVATE KEY-----\n")
154
155 data = {'lp_user': "lpuser",
156 'lp_ssh_key_input': key,
157 '_save': 1}
158 response = self.client.post(
159 reverse('offspring.web.queuemanager.views.project_editsshcredentials',
160 args=[project.name]),
161 data, follow=True)
162
163 self.assertEqual(200, response.status_code)
164
165 # re-load the project from the database to pick up changes
166 user_visible_objects = Project.all_objects.accessible_by_user(user)
167 project = get_possibly_private_object(
168 user, user_visible_objects, pk=project.name)
169
170 self.assertEqual('lpuser', project.lp_user)
171 self.assertEqual(key, project.lp_ssh_key)
172
173 def test_edit_ssh_auth_bad_key(self):
174 project = factory.makeProject(is_private=True)
175 user = project.owner
176 grant_permission_to_user(user, 'change_project')
177 self.assertTrue(
178 self.client.login(username=user.username, password=user.username))
179
180 key = ("-----BEGIN type PR---VATE KEY-----\n"+
181 "12qwaszx34erdfcv\n"+
182 "-----END type PRIVATE KEY-----\n")
183
184 data = {'lp_user': "lpuser",
185 'lp_ssh_key_input': key,
186 '_save': 1}
187 response = self.client.post(
188 reverse('offspring.web.queuemanager.views.project_editsshcredentials',
189 args=[project.name]),
190 data, follow=True)
191
192 self.assertEqual(200, response.status_code)
193
194 # re-load the project from the database to pick up changes
195 user_visible_objects = Project.all_objects.accessible_by_user(user)
196 project = get_possibly_private_object(
197 user, user_visible_objects, pk=project.name)
198
199 self.assertNotEqual('lpuser', project.lp_user)
200 self.assertNotEqual(key, project.lp_ssh_key)
201
202 def test_edit_ssh_auth_private_protected(self):
203 project = factory.makeProject(is_private=True)
204 user = factory.makeUser()
205 self.assertTrue(
206 self.client.login(username=user.username, password=user.username))
207
208 # Check can't read a private projects ssh cred page
209 response = self.client.get(
210 reverse('offspring.web.queuemanager.views.project_editsshcredentials',
211 args=[project.name]), follow=True)
212 self.assertEqual(200, response.status_code)
213 self.assertContains(response, "Lexbuilder Control Administration")
214 self.assertContains(response, "Username")
215 self.assertContains(response, "Password")
216
217 # Check can't write to a private projects ssh cred page
218 key = ("-----BEGIN type PR---VATE KEY-----\n"+
219 "12qwaszx34erdfcv\n"+
220 "-----END type PRIVATE KEY-----\n")
221
222 data = {'lp_user': "lpuser",
223 'lp_ssh_key_input': key,
224 '_save': 1}
225 response = self.client.post(
226 reverse('offspring.web.queuemanager.views.project_editsshcredentials',
227 args=[project.name]),
228 data, follow=True)
229
230 self.assertEqual(200, response.status_code)
231 self.assertContains(response, "Lexbuilder Control Administration")
232 self.assertContains(response, "Username")
233 self.assertContains(response, "Password")
234
235 # re-load the project from the database to pick up changes
236 user_visible_objects = Project.all_objects.accessible_by_user(
237 project.owner)
238 project = get_possibly_private_object(
239 project.owner, user_visible_objects, pk=project.name)
240
241 self.assertNotEqual('lpuser', project.lp_user)
242 self.assertNotEqual(key, project.lp_ssh_key)
243
244
141class ProjectCreateViewTests(TestCase):245class ProjectCreateViewTests(TestCase):
142 view_path = 'offspring.web.queuemanager.views.project_create'246 view_path = 'offspring.web.queuemanager.views.project_create'
143247
@@ -156,7 +260,9 @@
156 '_is_private': [u'on'],260 '_is_private': [u'on'],
157 '_save': [u'Save'],261 '_save': [u'Save'],
158 'series': [u'natty'],262 'series': [u'natty'],
159 'arch': [u'amd64']}263 'arch': [u'amd64'],
264 'lp_user': '',
265 'lp_ssh_key_input': ''}
160 response = self.client.post(reverse(self.view_path), data)266 response = self.client.post(reverse(self.view_path), data)
161 self.assertContains(267 self.assertContains(
162 response, 'A private project needs an owner',268 response, 'A private project needs an owner',
163269
=== modified file 'lib/offspring/web/queuemanager/views.py'
--- lib/offspring/web/queuemanager/views.py 2011-10-20 16:08:24 +0000
+++ lib/offspring/web/queuemanager/views.py 2011-11-03 22:20:29 +0000
@@ -45,6 +45,7 @@
45 AccessGroupMemberForm,45 AccessGroupMemberForm,
46 CreateProjectForm,46 CreateProjectForm,
47 EditProjectForm,47 EditProjectForm,
48 EditProjectSSHCredentialsForm,
48 LaunchpadProjectForm,49 LaunchpadProjectForm,
49 ReleaseForm,50 ReleaseForm,
50)51)
@@ -356,6 +357,52 @@
356 'queuemanager/project_acl.html', pageData,357 'queuemanager/project_acl.html', pageData,
357 context_instance=RequestContext(request))358 context_instance=RequestContext(request))
358359
360@permission_required('queuemanager.change_project')
361def project_editsshcredentials(request, projectName):
362 user_visible_objects = Project.all_objects.accessible_by_user(
363 request.user)
364 project = get_possibly_private_object(
365 request.user, user_visible_objects, pk=projectName)
366 access_group = None
367 allowed_users = []
368 if len(project.access_groups.all()) > 0:
369 # We always use the first access group because there's no way for
370 # users to register more than one access group for any given project.
371 access_group = project.access_groups.all()[0]
372 allowed_users = access_group.members.all()
373
374 if request.method == 'POST':
375
376 form = EditProjectSSHCredentialsForm(request.POST, instance=project)
377
378 # Don't validate the data (call form.is_valid) before deleting - we
379 # don't care what the form input is since we don't use it.
380 if "delete" in request.POST:
381 project.lp_user = ""
382 project.lp_ssh_key = ""
383 project.save()
384 return HttpResponseRedirect(".")
385
386 if form.is_valid():
387 if "_save" in request.POST:
388 project.lp_user = form.cleaned_data['lp_user']
389 if form.cleaned_data['lp_ssh_key_input']:
390 project.lp_ssh_key = form.cleaned_data['lp_ssh_key_input']
391 project.save()
392
393 return HttpResponseRedirect(".")
394
395 else:
396 form = EditProjectSSHCredentialsForm(instance=project)
397 pageData = {
398 'csrf_token' : csrf.get_token(request),
399 'project' : project,
400 'form': form,
401 'allowed_users': allowed_users,
402 }
403 return render_to_response(
404 'queuemanager/project_edit_credentials.html', pageData,
405 context_instance=RequestContext(request))
359406
360def projectgroup_details(request, projectGroupName):407def projectgroup_details(request, projectGroupName):
361 pg = get_object_or_404(408 pg = get_object_or_404(
362409
=== modified file 'lib/offspring/web/templates/queuemanager/project_create.html'
--- lib/offspring/web/templates/queuemanager/project_create.html 2011-02-28 22:48:46 +0000
+++ lib/offspring/web/templates/queuemanager/project_create.html 2011-11-03 22:20:29 +0000
@@ -5,7 +5,9 @@
5{% endblock %}5{% endblock %}
66
7{% block header_js %}7{% block header_js %}
8<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script> 8<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
9<script type="text/javascript" src="/media/js/jquery.min.js"></script>
10<script type="text/javascript" src="/assets/js/jquery.placeholder.js"></script>
9<script type="text/javascript">11<script type="text/javascript">
10/* Override showAddAnotherPopup to use custom height and width. */12/* Override showAddAnotherPopup to use custom height and width. */
11function showAddAnotherPopup(triggeringLink) {13function showAddAnotherPopup(triggeringLink) {
@@ -29,29 +31,68 @@
29{% endblock %}31{% endblock %}
3032
31{% block content %}33{% block content %}
32 <form method="POST" action="">{% csrf_token %}34 <form method="POST" action="" name="projsettings">{% csrf_token %}
33 <div class="module aligned ">35 <div class="module aligned ">
34 {% for field in form %}36 {% for field in form %}
35 <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}">37 {% if field.html_name == "lp_user" or field.html_name == "lp_ssh_key_input" %}
36 <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>38 <!-- This catches lp_user and lp_ssh_key_input so they aren't shown in the top of
37 {% if field.is_checkbox %}39 the page. They are displayed below in their own section after some help text -->
38 {{ field }}{{ field.label_tag }}40 {% else %}
39 {% else %}41 <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}">
40 {{ field.label_tag }}42 <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
41 {% if field.is_readonly %}43 {% if field.is_checkbox %}
42 <p>{{ field.contents }}</p>44 {{ field }}{{ field.label_tag }}
43 {% else %}45 {% else %}
44 {{ field }}46 {{ field.label_tag }}
45 {% endif %}47 {% if field.is_readonly %}
46 {% endif %}48 <p>{{ field.contents }}</p>
47 {% if field.field.field.help_text %}49 {% else %}
48 <p class="help">{{ field.field.field.help_text|safe }}</p>50 {{ field }}
49 {% endif %}51 {% endif %}
52 {% endif %}
53 {% if field.field.field.help_text %}
54 <p class="help">{{ field.field.field.help_text|safe }}</p>
55 {% endif %}
56 </div>
57 {{ field.errors }}
50 </div>58 </div>
51 {{ field.errors }}59 {% endif %}
52 </div>
53 60
54 {% endfor %}61 {% endfor %}
62
63 <br>
64 If the config URL above points to a Bazaar repository that is only
65 available to authenticated users, you should add a user and private SSH
66 key to give the builder access. The SSH key will never be revealed. It
67 can be changed or deleted later. It should not be passphrase protected.
68 If you would like to add these details, tick here:
69
70 <br>
71
72 <div class="form-row {{ form.lp_user.name }}">
73 <div class="field-box">
74 {{ form.lp_user.label_tag }}
75 <br>
76 {{ form.lp_user }}
77 </div>
78 {{ form.lp_user.errors }}
79 </div>
80
81 <div class="form-row {{ form.lp_ssh_key_input.name }}">
82 <div class="field-box">
83 {{ form.lp_ssh_key_input.label_tag }}
84 <br>
85 <textarea id="id_lp_ssh_key_input" rows="4" cols="70" name="lp_ssh_key_input"
86 placeholder="{% if form.lp_ssh_set %}{{ form.lp_ssh_set_message }}{% else %}{{ form.lp_ssh_clear_meessage }}{% endif %}"></textarea>
87 {% if form.lp_ssh_key_input.help_text %}
88 <p class="help">{{ form.lp_ssh_key_input.help_text|safe }}</p>
89 {% endif %}
90 </div>
91 {{ form.lp_ssh_key_input.errors|linebreaksbr }}
92 </div>
93
94 <br>
95
55 <div class="submit-row" style="overflow: auto;">96 <div class="submit-row" style="overflow: auto;">
56 <input type="submit" value="Save" class="default" name="_save"/>97 <input type="submit" value="Save" class="default" name="_save"/>
57 <input type="button" value="Cancel" class="default" name="_cancel" OnClick="window.location.href = '/';"/>98 <input type="button" value="Cancel" class="default" name="_cancel" OnClick="window.location.href = '/';"/>
5899
=== modified file 'lib/offspring/web/templates/queuemanager/project_edit.html'
--- lib/offspring/web/templates/queuemanager/project_edit.html 2011-03-03 01:50:40 +0000
+++ lib/offspring/web/templates/queuemanager/project_edit.html 2011-11-03 22:20:29 +0000
@@ -5,7 +5,7 @@
5{% endblock %}5{% endblock %}
66
7{% block header_js %}7{% block header_js %}
8<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script> 8<script type="text/javascript" src="/media/js/admin/RelatedObjectLookups.js"></script>
9<script type="text/javascript">9<script type="text/javascript">
10/* Override showAddAnotherPopup to use custom height and width. */10/* Override showAddAnotherPopup to use custom height and width. */
11function showAddAnotherPopup(triggeringLink) {11function showAddAnotherPopup(triggeringLink) {
@@ -29,29 +29,46 @@
29{% endblock %}29{% endblock %}
3030
31{% block content %}31{% block content %}
32 <form method="POST" action="">{% csrf_token %}32 <form method="POST" action="" name="projsettings">{% csrf_token %}
33 <div class="module aligned ">33 <div class="module aligned ">
34 {% for field in form %}34 {% for field in form %}
35 <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}">35 {% if field.html_name == "lp_user" or field.html_name == "lp_ssh_key_input"%}
36 <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>36 {% else %}
37 {% if field.is_checkbox %}37 <div class="form-row {% if line.errors %} errors{% endif %} {{ field.name }}">
38 {{ field }}{{ field.label_tag }}38 <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
39 {% else %}39 {% if field.is_checkbox %}
40 {{ field.label_tag }}40 {{ field }}{{ field.label_tag }}
41 {% if field.is_readonly %}
42 <p>{{ field.contents }}</p>
43 {% else %}41 {% else %}
44 {{ field }}42 {{ field.label_tag }}
45 {% endif %}43 {% if field.is_readonly %}
46 {% endif %}44 <p>{{ field.contents }}</p>
47 {% if field.field.field.help_text %}45 {% else %}
48 <p class="help">{{ field.field.field.help_text|safe }}</p>46 {{ field }}
49 {% endif %}47 {% endif %}
48 {% endif %}
49 {% if field.help_text %}
50 <p class="help">{{ field.help_text|safe }}</p>
51 {% endif %}
52 </div>
53 {{ field.errors }}
50 </div>54 </div>
51 {{ field.errors }}55 {% endif %}
52 </div>
53 56
54 {% endfor %}57 {% endfor %}
58
59 <br>
60 If the config URL above points to a Bazaar repository that is only
61 available to authenticated users, you should add a user and private SSH
62 key to give the builder access. The SSH key will never be revealed. It
63 can be changed or deleted later. It should not be passphrase protected.
64
65 {% if form.lp_fields_set %}
66 To update or delete these details, use
67 {% else %}
68 If you would like to add these details,
69 {% endif %}
70 <a href="+editcredentials">use this form.</a>
71
55 <div class="submit-row" style="overflow: auto;">72 <div class="submit-row" style="overflow: auto;">
56 <input type="submit" value="Save" class="default" name="_save"/>73 <input type="submit" value="Save" class="default" name="_save"/>
57 <input type="button" value="Cancel" class="default" name="_cancel" OnClick="window.location.href = '{% url offspring.web.queuemanager.views.project_details project.name %}';"/>74 <input type="button" value="Cancel" class="default" name="_cancel" OnClick="window.location.href = '{% url offspring.web.queuemanager.views.project_details project.name %}';"/>
5875
=== added file 'lib/offspring/web/templates/queuemanager/project_edit_credentials.html'
--- lib/offspring/web/templates/queuemanager/project_edit_credentials.html 1970-01-01 00:00:00 +0000
+++ lib/offspring/web/templates/queuemanager/project_edit_credentials.html 2011-11-03 22:20:29 +0000
@@ -0,0 +1,64 @@
1{% extends "base.html" %}
2
3{% block header_css %}
4<link rel="stylesheet" type="text/css" href="/media/css/forms.css" />
5{% endblock %}
6
7{% block header_js %}
8<script type="text/javascript" src="/media/js/jquery.min.js"></script>
9<script type="text/javascript" src="/assets/js/jquery.placeholder.js"></script>
10{% endblock %}
11
12{% block title %}
13Update SSH Credentials for {{project.title|capfirst}}
14{% endblock %}
15
16{% block content %}
17 <form method="POST" action="" name="projsettings">{% csrf_token %}
18 {{ formset.non_form_errors }}
19 <div class="module aligned ">
20 <div class="form-row {{ form.lp_user.name }}">
21 <div class="field-box">
22 {{ form.lp_user.label_tag }}
23 <br>
24 {{ form.lp_user }}
25 </div>
26 {{ form.lp_user.errors }}
27 </div>
28
29 <div class="form-row {{ form.lp_ssh_key_input.name }}">
30 <div class="field-box">
31 {{ form.lp_ssh_key_input.label_tag }}
32 <br>
33 <textarea id="id_lp_ssh_key_input" rows="4" cols="70" name="lp_ssh_key_input"
34 placeholder="{% if form.lp_ssh_set %}{{ form.lp_ssh_set_message }}{% else %}{{ form.lp_ssh_clear_meessage }}{% endif %}"></textarea>
35 {% if form.lp_ssh_key_input.help_text %}
36 <p class="help">
37 {{ form.lp_ssh_key_input.help_text|safe }}
38 {% if form.lp_ssh_set %}
39 Your saved key is not shown. To replace it, paste a new key in the above box.
40 {% else %}
41 No key is stored for this project. Saved keys are not shown.
42 {% endif %}
43 </p>
44 {% endif %}
45 </div>
46 {{ form.lp_ssh_key_input.errors|linebreaksbr }}
47 </div>
48
49 <br>
50
51 <div class="submit-row" style="overflow: auto;">
52 <input type="submit" value="Save Updated Credentials" class="default" name="_save"/>
53 <input type="submit" value="Delete Existing Credentials" class="default" name="delete"/>
54 <input type="button" value="Cancel" class="default" name="_cancel" OnClick="window.location.href = '{% url offspring.web.queuemanager.views.project_details project.name %}';"/>
55 </div>
56 </div>
57 </form>
58{% endblock %}
59
60{% block two-columns %}
61<script>
62 $('input[placeholder], textarea[placeholder]').placeholder();
63</script>
64{% endblock %}
065
=== modified file 'lib/offspring/web/urls.py'
--- lib/offspring/web/urls.py 2011-10-18 15:17:15 +0000
+++ lib/offspring/web/urls.py 2011-11-03 22:20:29 +0000
@@ -79,6 +79,7 @@
79 (r'^projects/(?P<projectName>[^/]+)/\+api/buildrequest/(?P<request_id>[^/]+)/$', buildrequest_handler),79 (r'^projects/(?P<projectName>[^/]+)/\+api/buildrequest/(?P<request_id>[^/]+)/$', buildrequest_handler),
80 (r'^projects/(?P<projectName>[^/]+)/\+build$', 'offspring.web.queuemanager.views.queue_build'),80 (r'^projects/(?P<projectName>[^/]+)/\+build$', 'offspring.web.queuemanager.views.queue_build'),
81 (r'^projects/(?P<projectName>[^/]+)/\+builds$', 'offspring.web.queuemanager.views.builds'),81 (r'^projects/(?P<projectName>[^/]+)/\+builds$', 'offspring.web.queuemanager.views.builds'),
82 (r'^projects/(?P<projectName>[^/]+)/\+editcredentials$', 'offspring.web.queuemanager.views.project_editsshcredentials'),
82 (r'^projects/(?P<projectName>[^/]+)/$', 'offspring.web.queuemanager.views.project_details'),83 (r'^projects/(?P<projectName>[^/]+)/$', 'offspring.web.queuemanager.views.project_details'),
83 (r'^projects/$', 'offspring.web.queuemanager.views.projects'),84 (r'^projects/$', 'offspring.web.queuemanager.views.projects'),
84 (r'^schedule/\+api/milestones/$', milestone_handler),85 (r'^schedule/\+api/milestones/$', milestone_handler),

Subscribers

People subscribed via source and target branches