launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #14686
[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)