← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/com-script-ui into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/com-script-ui into lp:maas.

Commit message:
UI to manage (add & delete) commissioning scripts.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/com-script-ui/+merge/137532

This branch adds the UI to manage (add & delete) commissioning scripts.

It does not yet feature the JS code to fold/unfold the content of the scripts.  This will be done in a follow-up branch.
-- 
https://code.launchpad.net/~rvb/maas/com-script-ui/+merge/137532
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/com-script-ui into lp:maas.
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py	2012-11-14 10:32:25 +0000
+++ src/maasserver/forms.py	2012-12-03 11:17:23 +0000
@@ -13,6 +13,7 @@
 __all__ = [
     "AdminNodeWithMACAddressesForm",
     "CommissioningForm",
+    "CommissioningScriptForm",
     "get_action_form",
     "get_node_edit_form",
     "get_node_create_form",
@@ -81,6 +82,8 @@
 from maasserver.node_action import compile_node_actions
 from maasserver.power_parameters import POWER_TYPE_PARAMETERS
 from maasserver.utils import strip_domain
+from metadataserver.fields import Bin
+from metadataserver.models import CommissioningScript
 from provisioningserver.enum import (
     POWER_TYPE,
     POWER_TYPE_CHOICES,
@@ -614,7 +617,7 @@
 
 
 class CommissioningForm(ConfigForm):
-    """Settings page, CommissioningF section."""
+    """Settings page, Commissioning section."""
     check_compatibility = forms.BooleanField(
         label="Check component compatibility and certification",
         required=False)
@@ -863,3 +866,26 @@
             msg = 'Invalid xpath expression: %s' % (e,)
             raise ValidationError({'definition': [msg]})
         return definition
+
+
+class CommissioningScriptForm(forms.Form):
+
+    content = forms.FileField(
+        label="Commissioning script", allow_empty_file=False)
+
+    def __init__(self, instance=None, *args, **kwargs):
+        super(CommissioningScriptForm, self).__init__(*args, **kwargs)
+
+    def clean_content(self):
+        content = self.cleaned_data['content']
+        name = content.name
+        if CommissioningScript.objects.filter(name=name).exists():
+            raise forms.ValidationError(
+                "A script with that name already exists.")
+        return content
+
+    def save(self, *args, **kwargs):
+        content = self.cleaned_data['content']
+        CommissioningScript.objects.create(
+            name=content.name,
+            content=Bin(content.read()))

=== modified file 'src/maasserver/templates/maasserver/settings.html'
--- src/maasserver/templates/maasserver/settings.html	2012-11-13 10:44:22 +0000
+++ src/maasserver/templates/maasserver/settings.html	2012-12-03 11:17:23 +0000
@@ -75,6 +75,11 @@
       <div class="clear"></div>
     </div>
     <div class="divider"></div>
+    <div id="commissioning_scripts" class="block size11 first">
+      {% include "maasserver/settings_commissioning_scripts.html" %}
+      <div class="clear"></div>
+    </div>
+    <div class="divider"></div>
     <div id="commissioning" class="block size7 first">
       <h2>Commissioning</h2>
       <form action="{% url "settings" %}" method="post">

=== added file 'src/maasserver/templates/maasserver/settings_add_commissioning_script.html'
--- src/maasserver/templates/maasserver/settings_add_commissioning_script.html	1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/settings_add_commissioning_script.html	2012-12-03 11:17:23 +0000
@@ -0,0 +1,20 @@
+{% extends "maasserver/base.html" %}
+
+{% block nav-active-settings %}active{% endblock %}
+{% block title %}Add commissioning script{% endblock %}
+{% block page-title %}Add commissioning script{% endblock %}
+
+{% block content %}
+  <form enctype="multipart/form-data" method="post" action="."
+        class="block auto-width">
+    {% csrf_token %}
+    <ul>
+    {% for field in form %}
+      {% include "maasserver/form_field.html" %}
+    {% endfor %}
+    </ul>
+    <input type="submit" class="right" value="Upload" />
+    <a class="link-button"
+       href="{% url 'settings' %}#commissioning_scripts">Cancel</a>
+  </form>
+{% endblock %}

=== added file 'src/maasserver/templates/maasserver/settings_commissioning_scripts.html'
--- src/maasserver/templates/maasserver/settings_commissioning_scripts.html	1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/settings_commissioning_scripts.html	2012-12-03 11:17:23 +0000
@@ -0,0 +1,36 @@
+<h2>Commissioning scripts</h2>
+  <table class="list">
+    <tbody>
+    {% for script in commissioning_scripts %}
+      <tr class="script" id="{{ script.name }}">
+        <td>
+          <a>{{ script.name}}</a></br>
+        </td>
+        <td width="30px">
+        <a title="Delete commissioning script {{ script.name }}"
+           href="{% url 'commissioning-script-delete' script.id %}">
+           <img src="{{ STATIC_URL }}img/delete.png" alt="delete" />
+        </a>
+        </td>
+      </tr>
+      <tr>
+        <td colspan="2" border-width="0"
+            style="padding: 0 0;">
+          <div class="size11">
+          <pre align="left">{{ script.content }}</pre>
+          </div>
+        </td>
+      </tr>
+    </tbody>
+    {% empty %}
+    No commissioning scripts.
+    {% endfor %}
+  </table>
+<div>
+  <a class="button right space-top-small"
+     href="{% url 'commissioning-script-add' %}">
+    Upload script
+  </a>
+</div>
+<div class="clear"></div>
+

=== added file 'src/maasserver/templates/maasserver/settings_confirm_delete_commissioning_script.html'
--- src/maasserver/templates/maasserver/settings_confirm_delete_commissioning_script.html	1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/settings_confirm_delete_commissioning_script.html	2012-12-03 11:17:23 +0000
@@ -0,0 +1,23 @@
+{% extends "maasserver/base.html" %}
+
+{% block title %}Delete commissioning script{% endblock %}
+{% block page-title %}Delete commissioning script{% endblock %}
+
+{% block content %}
+    <div class="block auto-width">
+        <h2>
+          Are you sure you want to delete the commissioning script
+          '{{ script_to_delete.name }}'?
+          <br />
+        </h2>
+        <p>This action is permanent and can not be undone.</p>
+        <p>
+          <form action="." method="post">{% csrf_token %}
+            <input type="hidden" name="post" value="yes" />
+            <input type="submit" value="Delete commissioning script"
+                   class="right" />
+            <a href="{% url 'settings' %}#commissioning_scripts">Cancel</a>
+          </form>
+        </p>
+    </div>
+{% endblock %}

=== modified file 'src/maasserver/tests/test_forms.py'
--- src/maasserver/tests/test_forms.py	2012-11-23 10:51:43 +0000
+++ src/maasserver/tests/test_forms.py	2012-12-03 11:17:23 +0000
@@ -17,6 +17,7 @@
 from django import forms
 from django.contrib.auth.models import User
 from django.core.exceptions import PermissionDenied
+from django.core.files.uploadedfile import SimpleUploadedFile
 from django.http import QueryDict
 from maasserver.enum import (
     ARCHITECTURE,
@@ -29,6 +30,7 @@
 from maasserver.forms import (
     AdminNodeForm,
     AdminNodeWithMACAddressesForm,
+    CommissioningScriptForm,
     ConfigForm,
     EditUserForm,
     get_action_form,
@@ -62,6 +64,7 @@
 from maasserver.testing import reload_object
 from maasserver.testing.factory import factory
 from maasserver.testing.testcase import TestCase
+from metadataserver.models import CommissioningScript
 from netaddr import IPNetwork
 from provisioningserver.enum import POWER_TYPE_CHOICES
 from testtools.matchers import (
@@ -944,3 +947,29 @@
         self.assertTrue(form.is_valid())
         form.save()
         self.assertEqual(data['name'], reload_object(nodegroup).name)
+
+
+class TestCommissioningScriptForm(TestCase):
+
+    def test_creates_commissioning_script(self):
+        content = factory.getRandomString()
+        name = factory.make_name('filename')
+        uploaded_file = SimpleUploadedFile(content=content, name=name)
+        form = CommissioningScriptForm(files={'content': uploaded_file})
+        self.assertTrue(form.is_valid(), form._errors)
+        form.save()
+        new_script = CommissioningScript.objects.get(name=name)
+        self.assertThat(
+            new_script,
+            MatchesStructure.byEquality(name=name, content=content))
+
+    def test_raises_if_duplicated_name(self):
+        content = factory.getRandomString()
+        name = factory.make_name('filename')
+        factory.make_commissioning_script(name=name)
+        uploaded_file = SimpleUploadedFile(content=content, name=name)
+        form = CommissioningScriptForm(files={'content': uploaded_file})
+        self.assertEqual(
+            (False, {'content':
+                [u'A script with that name already exists.']}),
+            (form.is_valid(), form._errors))

=== added file 'src/maasserver/tests/test_views_settings_commissioning_scripts.py'
--- src/maasserver/tests/test_views_settings_commissioning_scripts.py	1970-01-01 00:00:00 +0000
+++ src/maasserver/tests/test_views_settings_commissioning_scripts.py	2012-12-03 11:17:23 +0000
@@ -0,0 +1,100 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test maasserver clusters views."""
+
+from __future__ import (
+    absolute_import,
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = []
+
+import httplib
+
+from django.core.urlresolvers import reverse
+from lxml.html import fromstring
+from maasserver.testing import (
+    extract_redirect,
+    get_content_links,
+    )
+from maasserver.testing.factory import factory
+from maasserver.testing.testcase import AdminLoggedInTestCase
+from maasserver.views.settings_commissioning_scripts import (
+    COMMISSIONING_SCRIPTS_ANCHOR,
+    )
+from maastesting.matchers import ContainsAll
+from metadataserver.models import CommissioningScript
+from testtools.matchers import MatchesStructure
+
+
+class CommissioningScriptListingTest(AdminLoggedInTestCase):
+
+    def test_settings_contains_names_and_content_of_scripts(self):
+        scripts = {
+            factory.make_commissioning_script(),
+            factory.make_commissioning_script(),
+            }
+        response = self.client.get(reverse('settings'))
+        names = [script.name for script in scripts]
+        contents = [script.content for script in scripts]
+        self.assertThat(response.content, ContainsAll(names + contents))
+
+    def test_settings_link_to_upload_script(self):
+        links = get_content_links(self.client.get(reverse('settings')))
+        script_add_link = reverse('commissioning-script-add')
+        self.assertIn(script_add_link, links)
+
+    def test_settings_contains_links_to_delete_scripts(self):
+        scripts = {
+            factory.make_commissioning_script(),
+            factory.make_commissioning_script(),
+            }
+        links = get_content_links(self.client.get(reverse('settings')))
+        script_delete_links = [
+            reverse('commissioning-script-delete', args=[script.id])
+            for script in scripts]
+        self.assertThat(links, ContainsAll(script_delete_links))
+
+    def test_settings_contains_commissioning_scripts_slot_anchor(self):
+        response = self.client.get(reverse('settings'))
+        document = fromstring(response.content)
+        slots = document.xpath(
+            "//div[@id='%s']" % COMMISSIONING_SCRIPTS_ANCHOR)
+        self.assertEqual(
+            1, len(slots),
+            "Missing anchor '%s'" % COMMISSIONING_SCRIPTS_ANCHOR)
+
+
+class CommissioningScriptDeleteTest(AdminLoggedInTestCase):
+
+    def test_can_delete_commissioning_script(self):
+        script = factory.make_commissioning_script()
+        delete_link = reverse('commissioning-script-delete', args=[script.id])
+        response = self.client.post(delete_link, {'post': 'yes'})
+        self.assertEqual(
+            (httplib.FOUND, reverse('settings')),
+            (response.status_code, extract_redirect(response)))
+        self.assertFalse(
+            CommissioningScript.objects.filter(id=script.id).exists())
+
+
+class CommissioningScriptUploadTest(AdminLoggedInTestCase):
+
+    def test_can_create_commissioning_script(self):
+        content = factory.getRandomString()
+        name = factory.make_name('filename')
+        create_link = reverse('commissioning-script-add')
+        filepath = self.make_file(name=name, contents=content)
+        with open(filepath) as fp:
+            response = self.client.post(
+                create_link, {'name': name, 'content': fp})
+        self.assertEqual(
+            (httplib.FOUND, reverse('settings')),
+            (response.status_code, extract_redirect(response)))
+        new_script = CommissioningScript.objects.get(name=name)
+        self.assertThat(
+            new_script,
+            MatchesStructure.byEquality(name=name, content=content))

=== modified file 'src/maasserver/urls.py'
--- src/maasserver/urls.py	2012-11-13 10:44:22 +0000
+++ src/maasserver/urls.py	2012-12-03 11:17:23 +0000
@@ -54,6 +54,10 @@
     ClusterInterfaceDelete,
     ClusterInterfaceEdit,
     )
+from maasserver.views.settings_commissioning_scripts import (
+    CommissioningScriptCreate,
+    CommissioningScriptDelete,
+    )
 from maasserver.views.tags import TagView
 
 
@@ -166,6 +170,14 @@
     adminurl(
         r'^accounts/(?P<username>\w+)/del/$', AccountsDelete.as_view(),
         name='accounts-del'),
+    adminurl(
+        r'^commissioning-scripts/(?P<id>[\w\-]+)/delete/$',
+        CommissioningScriptDelete.as_view(),
+        name='commissioning-script-delete'),
+    adminurl(
+        r'^commissioning-scripts/add/$',
+        CommissioningScriptCreate.as_view(),
+        name='commissioning-script-add'),
 )
 
 # Tag views.

=== modified file 'src/maasserver/views/settings.py'
--- src/maasserver/views/settings.py	2012-11-13 10:44:22 +0000
+++ src/maasserver/views/settings.py	2012-12-03 11:17:23 +0000
@@ -51,6 +51,7 @@
     UserProfile,
     )
 from maasserver.views import process_form
+from metadataserver.models import CommissioningScript
 
 
 class AccountsView(DetailView):
@@ -205,7 +206,7 @@
         messages.info(request, message)
         return HttpResponseRedirect(reverse('settings'))
 
-    # Cluster listings:
+    # Cluster listings.
     accepted_clusters = NodeGroup.objects.filter(
         status=NODEGROUP_STATUS.ACCEPTED).order_by('cluster_name')
     pending_clusters = NodeGroup.objects.filter(
@@ -213,10 +214,14 @@
     rejected_clusters = NodeGroup.objects.filter(
         status=NODEGROUP_STATUS.REJECTED).order_by('cluster_name')
 
+    # Commissioning scripts.
+    commissioning_scripts = CommissioningScript.objects.all()
+
     return render_to_response(
         'maasserver/settings.html',
         {
             'user_list': user_list,
+            'commissioning_scripts': commissioning_scripts,
             'accepted_clusters': accepted_clusters,
             'pending_clusters': pending_clusters,
             'rejected_clusters': rejected_clusters,

=== added file 'src/maasserver/views/settings_commissioning_scripts.py'
--- src/maasserver/views/settings_commissioning_scripts.py	1970-01-01 00:00:00 +0000
+++ src/maasserver/views/settings_commissioning_scripts.py	2012-12-03 11:17:23 +0000
@@ -0,0 +1,64 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Commissioning Scripts Settings views."""
+
+from __future__ import (
+    absolute_import,
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = [
+    "CommissioningScriptCreate",
+    "CommissioningScriptDelete",
+    ]
+
+from django.contrib import messages
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
+from django.shortcuts import get_object_or_404
+from django.views.generic import (
+    CreateView,
+    DeleteView,
+    )
+from maasserver.forms import CommissioningScriptForm
+from metadataserver.models import CommissioningScript
+
+# The anchor of the commissioning scripts slot on the settings page.
+COMMISSIONING_SCRIPTS_ANCHOR = 'commissioning_scripts'
+
+
+class CommissioningScriptDelete(DeleteView):
+
+    template_name = (
+        'maasserver/settings_confirm_delete_commissioning_script.html')
+    context_object_name = 'script_to_delete'
+
+    def get_object(self):
+        id = self.kwargs.get('id', None)
+        return get_object_or_404(CommissioningScript, id=id)
+
+    def get_next_url(self):
+        return reverse('settings') + '#%s' % COMMISSIONING_SCRIPTS_ANCHOR
+
+    def delete(self, request, *args, **kwargs):
+        script = self.get_object()
+        script.delete()
+        messages.info(
+            request, "Commissioning script %s deleted." % script.name)
+        return HttpResponseRedirect(self.get_next_url())
+
+
+class CommissioningScriptCreate(CreateView):
+    template_name = 'maasserver/settings_add_commissioning_script.html'
+    form_class = CommissioningScriptForm
+    context_object_name = 'commissioningscript'
+
+    def get_success_url(self):
+        return reverse('settings') + '#%s' % COMMISSIONING_SCRIPTS_ANCHOR
+
+    def form_valid(self, form):
+        messages.info(self.request, "Commissioning script created.")
+        return super(CommissioningScriptCreate, self).form_valid(form)

=== modified file 'src/metadataserver/models/commissioningscript.py'
--- src/metadataserver/models/commissioningscript.py	2012-11-29 20:35:29 +0000
+++ src/metadataserver/models/commissioningscript.py	2012-12-03 11:17:23 +0000
@@ -64,5 +64,5 @@
 
     objects = CommissioningScriptManager()
 
-    name = CharField(max_length=255, null=False, editable=False, unique=True)
+    name = CharField(max_length=255, null=False, editable=True, unique=True)
     content = BinaryField(null=False)