launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06941
[Merge] lp:~rvb/maas/maas-admin-approve-nodes-ui into lp:maas
Raphaël Badin has proposed merging lp:~rvb/maas/maas-admin-approve-nodes-ui into lp:maas with lp:~rvb/maas/maas-admin-approve-nodes as a prerequisite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~rvb/maas/maas-admin-approve-nodes-ui/+merge/100176
This branch adds the UI to enlist a node. This only adds the button that is on a "node view" page but it is done in a very generic fashion that will allow us to add other actions easily.
The necessity of the method get_transition_form that simply creates a subclass of NodeTransitionForm with a proper user set might not be apparent if you don't know about Django generic classes (https://docs.djangoproject.com/en/dev/topics/class-based-views/). This is all to be able to provide a form class in NodeView.get_form_class that has the right shape be exploited by Django's machinery.
--
https://code.launchpad.net/~rvb/maas/maas-admin-approve-nodes-ui/+merge/100176
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-admin-approve-nodes-ui into lp:maas.
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py 2012-03-29 11:40:39 +0000
+++ src/maasserver/forms.py 2012-03-30 16:19:33 +0000
@@ -11,10 +11,12 @@
__metaclass__ = type
__all__ = [
"CommissioningForm",
+ "get_transition_form",
"HostnameFormField",
"NodeForm",
"MACAddressForm",
"MAASAndNetworkForm",
+ "NodeTransitionForm",
"SSHKeyForm",
"UbuntuForm",
"UIAdminNodeEditForm",
@@ -26,8 +28,14 @@
UserChangeForm,
UserCreationForm,
)
-from django.contrib.auth.models import User
-from django.core.exceptions import ValidationError
+from django.contrib.auth.models import (
+ AnonymousUser,
+ User,
+ )
+from django.core.exceptions import (
+ PermissionDenied,
+ ValidationError,
+ )
from django.core.validators import URLValidator
from django.forms import (
CharField,
@@ -184,6 +192,55 @@
return node
+class NodeTransitionForm(forms.Form):
+ """A form used to perform a status change on a Node.
+
+ That form class should not be used directly but through subclasses
+ created using `get_transition_form`.
+ """
+
+ user = AnonymousUser()
+
+ # The name of the input button used with this form.
+ input_name = 'node_transition'
+
+ def __init__(self, instance, *args, **kwargs):
+ super(NodeTransitionForm, self).__init__(*args, **kwargs)
+ self.node = instance
+ self.transitions = self.node.available_transition_methods(
+ self.user)
+ self.transition_dict = dict(
+ [(transition['name'],
+ (transition['method'], transition['permission']))
+ for transition in self.transitions])
+
+ def save(self):
+ transition_name = self.data.get(self.input_name)
+ method_name, permission = self.transition_dict.get(
+ transition_name, (None, None))
+ if method_name is not None:
+ if not self.user.has_perm(permission, self.node):
+ raise PermissionDenied()
+ method = getattr(self.node, method_name)
+ method()
+ else:
+ raise PermissionDenied()
+
+
+def get_transition_form(user):
+ """Return a class derived from NodeTransitionForm for a specific user.
+
+ :param user: The user for which to build a form derived from
+ NodeTransitionForm.
+ :type user: :class:`django.contrib.auth.models.User`
+ :return: A form class derived from NodeTransitionForm.
+ :rtype: class:`django.forms.Form`
+ """
+ return type(
+ str("SpecificNodeTransitionForm"), (NodeTransitionForm,),
+ {'user': user})
+
+
class ProfileForm(ModelForm):
# We use the field 'last_name' to store the user's full name (and
# don't display Django's 'first_name' field).
=== modified file 'src/maasserver/templates/maasserver/node_view.html'
--- src/maasserver/templates/maasserver/node_view.html 2012-03-21 18:53:44 +0000
+++ src/maasserver/templates/maasserver/node_view.html 2012-03-30 16:19:33 +0000
@@ -12,7 +12,19 @@
<a href="{% url 'node-edit' node.id %}" class="button secondary">
Edit node
</a>
- {% endif%}
+ {% endif %}
+ {% if form.transitions %}
+ <form id="node_actions" method="post" action=".">
+ {% for transition in form.transitions %}
+ {% if forloop.first %}<br /><br /><h4>Actions</h4>{% endif %}
+ <input class="secondary"
+ type="submit"
+ name={{ form.input_name }}
+ value="{{ transition.name }}" />
+ {% if not forloop.last %}<br /><br />{% endif %}
+ {% endfor %}
+ </form>
+ {% endif %}
</div>
{% endblock %}
=== modified file 'src/maasserver/tests/test_forms.py'
--- src/maasserver/tests/test_forms.py 2012-03-22 17:34:36 +0000
+++ src/maasserver/tests/test_forms.py 2012-03-30 16:19:33 +0000
@@ -14,13 +14,18 @@
import random
from django import forms
-from django.core.exceptions import ValidationError
+from django.core.exceptions import (
+ PermissionDenied,
+ ValidationError,
+ )
from django.http import QueryDict
from maasserver.forms import (
ConfigForm,
EditUserForm,
+ get_transition_form,
HostnameFormField,
NewUserCreationForm,
+ NodeTransitionForm,
NodeWithMACAddressesForm,
ProfileForm,
UIAdminNodeEditForm,
@@ -32,6 +37,7 @@
Config,
DEFAULT_CONFIG,
NODE_AFTER_COMMISSIONING_ACTION_CHOICES,
+ NODE_STATUS,
POWER_TYPE_CHOICES,
)
from maasserver.testing.factory import factory
@@ -251,6 +257,66 @@
self.assertItemsEqual(['', user.id], user_ids)
+class TestNodeTransitionForm(TestCase):
+
+ def test_get_transition_form_creates_form_class_with_attributes(self):
+ user = factory.make_admin()
+ form_class = get_transition_form(user)
+
+ self.assertEqual(user, form_class.user)
+
+ def test_get_transition_form_creates_form_instance(self):
+ user = factory.make_admin()
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ form = get_transition_form(user)(node)
+
+ self.assertIsInstance(form, NodeTransitionForm)
+ self.assertEqual(node, form.node)
+
+ def test_get_transition_form_for_admin(self):
+ admin = factory.make_admin()
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ form = get_transition_form(admin)(node)
+
+ self.assertItemsEqual(
+ {"Enlist node": ('accept_enlistment', 'admin')},
+ form.transition_dict)
+
+ def test_get_transition_form_for_user(self):
+ user = factory.make_user()
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ form = get_transition_form(user)(node)
+
+ self.assertIsInstance(form, NodeTransitionForm)
+ self.assertEqual(node, form.node)
+ self.assertItemsEqual({}, form.transition_dict)
+
+ def test_get_transition_form_node_for_admin_save(self):
+ admin = factory.make_admin()
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ form = get_transition_form(admin)(
+ node, {NodeTransitionForm.input_name: "Enlist node"})
+ form.save()
+
+ self.assertEqual(NODE_STATUS.READY, node.status)
+
+ def test_get_transition_form_for_user_save(self):
+ user = factory.make_user()
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ form = get_transition_form(user)(
+ node, {NodeTransitionForm.input_name: "Enlist node"})
+
+ self.assertRaises(PermissionDenied, form.save)
+
+ def test_get_transition_form_for_user_save_unknown_trans(self):
+ user = factory.make_user()
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ form = get_transition_form(user)(
+ node, {NodeTransitionForm.input_name: factory.getRandomString()})
+
+ self.assertRaises(PermissionDenied, form.save)
+
+
class TestHostnameFormField(TestCase):
def test_validate_hostname_validates_valid_hostnames(self):
=== modified file 'src/maasserver/tests/test_views.py'
--- src/maasserver/tests/test_views.py 2012-03-29 11:40:39 +0000
+++ src/maasserver/tests/test_views.py 2012-03-30 16:19:33 +0000
@@ -27,9 +27,11 @@
views,
)
from maasserver.exceptions import NoRabbit
+from maasserver.forms import NodeTransitionForm
from maasserver.models import (
Config,
NODE_AFTER_COMMISSIONING_ACTION,
+ NODE_STATUS,
POWER_TYPE_CHOICES,
SSHKey,
UserProfile,
@@ -528,6 +530,44 @@
self.assertEqual(
after_commissioning_action, node.after_commissioning_action)
+ def test_view_node_admin_has_button_to_accept_enlistement(self):
+ self.logged_in_user.is_superuser = True
+ self.logged_in_user.save()
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ node_link = reverse('node-view', args=[node.id])
+ response = self.client.get(node_link)
+ doc = fromstring(response.content)
+ inputs = [
+ input for input in doc.cssselect('form#node_actions input')
+ if input.name == NodeTransitionForm.input_name]
+
+ self.assertSequenceEqual(
+ ['Enlist node'], [input.value for input in inputs])
+
+ def test_view_node_POST_admin_can_enlist_node(self):
+ self.logged_in_user.is_superuser = True
+ self.logged_in_user.save()
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ node_link = reverse('node-view', args=[node.id])
+ response = self.client.post(
+ node_link,
+ data={
+ NodeTransitionForm.input_name: 'Enlist node',
+ })
+
+ self.assertEqual(httplib.FOUND, response.status_code)
+ self.assertEqual(
+ NODE_STATUS.READY, reload_object(node).status)
+
+ def test_view_node_has_button_to_accept_enlistement_for_user(self):
+ # A simple user can't see the button to enlist a declared node.
+ node = factory.make_node(status=NODE_STATUS.DECLARED)
+ node_link = reverse('node-view', args=[node.id])
+ response = self.client.get(node_link)
+ doc = fromstring(response.content)
+
+ self.assertEqual(0, len(doc.cssselect('form#node_actions input')))
+
class AdminNodeViewsTest(AdminLoggedInTestCase):
=== modified file 'src/maasserver/views.py'
--- src/maasserver/views.py 2012-03-28 16:13:04 +0000
+++ src/maasserver/views.py 2012-03-30 16:19:33 +0000
@@ -72,6 +72,7 @@
AddArchiveForm,
CommissioningForm,
EditUserForm,
+ get_transition_form,
MAASAndNetworkForm,
NewUserCreationForm,
ProfileForm,
@@ -101,7 +102,7 @@
return dj_logout(request, next_page=reverse('login'))
-class NodeView(DetailView):
+class NodeView(UpdateView):
template_name = 'maasserver/node_view.html'
@@ -111,12 +112,18 @@
id = self.kwargs.get('id', None)
return get_object_or_404(Node, id=id)
+ def get_form_class(self):
+ return get_transition_form(self.request.user)
+
def get_context_data(self, **kwargs):
context = super(NodeView, self).get_context_data(**kwargs)
node = self.get_object()
context['can_edit'] = self.request.user.has_perm('edit', node)
return context
+ def get_success_url(self):
+ return reverse('node-view', args=[self.get_object().id])
+
class NodeEdit(UpdateView):
Follow ups