← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/maas-user-mgt into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/maas-user-mgt into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/maas-user-mgt/+merge/93446

This branch adds the user management pages: user list, user add, user edit, user delete and user view.

The user list is the first tab on "Settings" page.  The other pages are accessible from this pages and it's not yet clear where they will fit in in terms of navigation.  I've raised the issue with Huw and he is working on this.

Note that the other tabs in the Settings will be done in a follow-up branch.

The pages display is not yet finished (to say the least) but I'm counting on Huw to add the required elements and necessary polish on these pages.

I've decided to stick to using Django's generic views to minimize the amount of boiler-plate/javascript code for now (have a look at https://docs.djangoproject.com/en/dev/topics/class-based-views/).

http://people.canonical.com/~rvb/user_list.png
http://people.canonical.com/~rvb/user_list_deleted.png
http://people.canonical.com/~rvb/user_edit.png
http://people.canonical.com/~rvb/user_del.png
http://people.canonical.com/~rvb/user_view.png

The only real trick here is:
123	+        self.user.consumers.all().delete()
I should not have had to do that.  The consumers objects should be deleted by the DB (delete cascade).  Tokens are deleted this way.  I was forced to do that because of a bug in piston (I'll report the bug). It's not very Djangoic to delete users (Django recommends disabling them instead) and Piston reacts badly to a user being deleted (look in piston/utils.py:send_consumer_mail if you really want to know).
-- 
https://code.launchpad.net/~rvb/maas/maas-user-mgt/+merge/93446
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-user-mgt into lp:maas.
=== modified file 'src/maasserver/exceptions.py'
--- src/maasserver/exceptions.py	2012-02-14 17:47:00 +0000
+++ src/maasserver/exceptions.py	2012-02-16 16:43:18 +0000
@@ -25,6 +25,10 @@
     """Base class for Maas' exceptions."""
 
 
+class CannotDeleteUserException(Exception):
+    """User can't be deleted."""
+
+
 class MaasAPIException(Exception):
     """Base class for Maas' API exceptions.
 

=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py	2012-02-09 14:29:09 +0000
+++ src/maasserver/forms.py	2012-02-16 16:43:18 +0000
@@ -15,6 +15,10 @@
     ]
 
 from django import forms
+from django.contrib.auth.forms import (
+    UserChangeForm,
+    UserCreationForm,
+    )
 from django.contrib.auth.models import User
 from django.forms import ModelForm
 from maasserver.macaddress import MACAddressFormField
@@ -92,3 +96,27 @@
     class Meta:
         model = User
         fields = ('first_name', 'last_name', 'email')
+
+
+class NewUserCreationForm(UserCreationForm):
+    # Add is_superuser field.
+    is_superuser = forms.BooleanField(
+        label="Administrator status", required=False)
+
+    def save(self, commit=True):
+        user = super(NewUserCreationForm, self).save(commit=False)
+        if self.cleaned_data.get('is_superuser', False):
+            user.is_superuser = True
+        user.save()
+        return user
+
+
+class EditUserForm(UserChangeForm):
+    # Override the default label.
+    is_superuser = forms.BooleanField(
+        label="Administrator status", required=False)
+
+    class Meta:
+        model = User
+        fields = (
+            'username', 'first_name', 'last_name', 'email', 'is_superuser')

=== modified file 'src/maasserver/models.py'
--- src/maasserver/models.py	2012-02-16 14:40:10 +0000
+++ src/maasserver/models.py	2012-02-16 16:43:18 +0000
@@ -15,6 +15,7 @@
     "NODE_STATUS",
     "Node",
     "MACAddress",
+    "UserProfile",
     ]
 
 import datetime
@@ -28,7 +29,10 @@
 from django.db import models
 from django.db.models.signals import post_save
 from django.shortcuts import get_object_or_404
-from maasserver.exceptions import PermissionDenied
+from maasserver.exceptions import (
+    CannotDeleteUserException,
+    PermissionDenied,
+    )
 from maasserver.macaddress import MACAddressField
 from piston.models import (
     Consumer,
@@ -384,6 +388,26 @@
 GENERIC_CONSUMER = 'Maas consumer'
 
 
+class UserProfileManager(models.Manager):
+    """A utility to manage the collection of UserProfile (or User).
+
+    This should be used when dealing with UserProfiles or Users because it
+    deals gracefully with system users.
+    """
+
+    def all_users(self):
+        """Returns all the "real" users (the users which are not system users
+        and thus have a UserProfile object attached to them).
+
+        :return: A QuerySet of the users.
+        :rtype: django.db.models.query.QuerySet_
+
+        """
+        userprofile_ids = UserProfile.objects.filter(
+            ).values_list('id', flat=True)
+        return User.objects.filter(id__in=userprofile_ids)
+
+
 class UserProfile(models.Model):
     """A User profile to store Maas specific methods and fields.
 
@@ -395,8 +419,20 @@
 
     """
 
+    objects = UserProfileManager()
     user = models.OneToOneField(User)
 
+    def delete(self):
+        if self.user.node_set.exists():
+            nb_nodes = self.user.node_set.count()
+            msg = (
+                "User %s cannot be deleted: it still has %d node(s) "
+                "deployed." % (self.user.username, nb_nodes))
+            raise CannotDeleteUserException(msg)
+        self.user.consumers.all().delete()
+        self.user.delete()
+        super(UserProfile, self).delete()
+
     def get_authorisation_tokens(self):
         """Fetches all the user's OAuth tokens.
 

=== added file 'src/maasserver/static/img/edit.png'
Binary files src/maasserver/static/img/edit.png	1970-01-01 00:00:00 +0000 and src/maasserver/static/img/edit.png	2012-02-16 16:43:18 +0000 differ
=== added file 'src/maasserver/static/img/yes.png'
Binary files src/maasserver/static/img/yes.png	1970-01-01 00:00:00 +0000 and src/maasserver/static/img/yes.png	2012-02-16 16:43:18 +0000 differ
=== modified file 'src/maasserver/templates/maasserver/base.html'
--- src/maasserver/templates/maasserver/base.html	2012-02-10 09:11:46 +0000
+++ src/maasserver/templates/maasserver/base.html	2012-02-16 16:43:18 +0000
@@ -16,7 +16,8 @@
       <div id="header" class="center-page-content">
         {% block header %}
           <h1>
-            <a href="{% url 'index' %}" title="Return to dashboard"><img src="{{ STATIC_URL }}img/ubuntu_logo_header.png" alt="Ubuntu logo">
+            <a href="{% url 'index' %}" title="Return to dashboard">
+              <img src="{{ STATIC_URL }}img/ubuntu_logo_header.png" alt="Ubuntu logo" />
               MaaS Cluster</a>
           </h1>
           {% if user.is_authenticated %}
@@ -32,6 +33,11 @@
             <li class="{% block nav-active-node-list %}{% endblock %}">
               <a href="{% url 'node-list' %}">Nodes</a>
             </li>
+            {% if user.is_superuser %}
+            <li class="{% block nav-active-settings %}{% endblock %}">
+              <a href="{% url 'settings' %}">Settings</a>
+            </li>
+            {% endif %}
           </ul>
           {% endif %}
         {% endblock %}

=== added file 'src/maasserver/templates/maasserver/settings.html'
--- src/maasserver/templates/maasserver/settings.html	1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/settings.html	2012-02-16 16:43:18 +0000
@@ -0,0 +1,95 @@
+{% extends "maasserver/base.html" %}
+
+{% block nav-active-settings %}active{% endblock %}
+
+{% block title %}Settings{% endblock %}
+{% block page-title %}Settings{% endblock %}
+
+{% block head %}
+  <script type="text/javascript">
+  <!--
+  YUI().use('tabview', 'io-form', function (Y) {
+    var tabview = new Y.TabView({
+      srcNode: '#settings'
+    });
+    tabview.render();
+    // Select the tab with index {{ tab }}.
+    var tab_index = parseInt('{{ tab }}');
+    var valid_tab_index = (
+        Y.Lang.isValue(tab_index) &&
+        tab_index >= 0 &&
+        tab_index < tabview.size());
+    if (valid_tab_index) {
+        tabview.selectChild(tab_index);
+    }
+
+  });
+  // -->
+  </script>
+{% endblock %}
+
+{% block content %}
+  <div id="settings">
+    <ul>
+      <li><a href="#users">Users</a></li>
+      <li><a href="#maas">MaaS</a></li>
+    </ul>
+    <div>
+      <div id="users">
+        <table>
+          <thead>
+            <tr>
+              <th>User</th>
+              <th>Is superuser?</th>
+              <th>Number of nodes in use</th>
+              <th>Last login</th>
+              <th></th>
+            </tr>
+          </thead>
+          <tbody>
+          {% for user_item in user_list %}
+	    <tr class="user" id="{{ user_item.username }}">
+              <td>
+                <a class="user"
+                   href="{% url 'accounts-view' user_item.username %}">
+                  {{ user_item.username }}
+                </a>
+              </td>
+              <td>
+                {% if user_item.is_superuser %}
+                  <img src="{{ STATIC_URL }}img/yes.png" />
+                {% endif %}
+              </td>
+              <td>{{ user_item.node_set.count }}</td>
+              <td>{{ user_item.last_login }}</td>
+              <td>
+                <a href="{% url 'accounts-edit' user_item.username %}"
+        	   title="Edit user {{ user_item.username }}">
+                  <img src="{{ STATIC_URL}}img/edit.png" />
+                </a>
+                {% if user != user_item %}
+          	  <a title="Delete user {{ user_item.username }}"
+                     class="delete-user"
+                     href="{% url 'accounts-del' user_item.username %}">
+                    <img src="{{ STATIC_URL}}img/delete.png" />
+                  </a>
+                  <form method="POST"
+                        action="{% url 'accounts-del' user_item.username %}">
+                    <input type="hidden" name="username"
+                           value="{{ user_item.username }}" />
+                  </form>
+                {% endif %}
+              </td>
+            </tr>
+          {% endfor %}
+          </tbody>
+        </table>
+        <a href="{% url 'accounts-add' %}">Add user</a>
+      </div>
+      <div id="maas">
+        #TODO Maas prefs
+      </div>
+    </div>
+  </div>
+
+{% endblock %}

=== added file 'src/maasserver/templates/maasserver/user_add.html'
--- src/maasserver/templates/maasserver/user_add.html	1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/user_add.html	2012-02-16 16:43:18 +0000
@@ -0,0 +1,14 @@
+{% extends "maasserver/base.html" %}
+
+{% block nav-active-settings %}active{% endblock %}
+{% block title %}Add user{% endblock %}
+{% block page-title %}Add user{% endblock %}
+
+{% block content %}
+    <form action="." method="post">
+      {{ form.as_p }}
+      <input type="submit" value="Add user" />
+      &nbsp;&nbsp;<a class="link-button" href="{% url 'settings' %}">Cancel</a>
+    </form>
+{% endblock %}
+

=== added file 'src/maasserver/templates/maasserver/user_confirm_delete.html'
--- src/maasserver/templates/maasserver/user_confirm_delete.html	1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/user_confirm_delete.html	2012-02-16 16:43:18 +0000
@@ -0,0 +1,21 @@
+{% extends "maasserver/base.html" %}
+
+{% block nav-active-settings %}active{% endblock %}
+{% block title %}Delete user confirmation{% endblock %}
+{% block page-title %}Delete user confirmation{% endblock %}
+
+{% block content %}
+    <h2>
+      Are you sure you want to delete user {{ user_to_delete.user.username }}?
+    </h2>
+    <p>
+      <form action="." method="post">
+        <input type="hidden" name="post" value="yes" />
+        <input type="submit"
+               value="Yes, delete user {{ user_to_delete.user.username }}" />
+        &nbsp;&nbsp;
+        <a class="link-button" href="{% url 'settings' %}">Cancel</a>
+      </form>
+    </p>
+{% endblock %}
+

=== added file 'src/maasserver/templates/maasserver/user_edit.html'
--- src/maasserver/templates/maasserver/user_edit.html	1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/user_edit.html	2012-02-16 16:43:18 +0000
@@ -0,0 +1,14 @@
+{% extends "maasserver/base.html" %}
+
+{% block nav-active-settings %}active{% endblock %}
+{% block title %}Edit User{% endblock %}
+{% block page-title %}Edit User{% endblock %}
+
+{% block content %}
+    <form action="." method="post">
+      {{ form.as_p }}
+      <input type="submit" value="Save user" />
+      &nbsp;&nbsp;<a class="link-button" href="{% url 'settings' %}">Cancel</a>
+    </form>
+{% endblock %}
+

=== added file 'src/maasserver/templates/maasserver/user_view.html'
--- src/maasserver/templates/maasserver/user_view.html	1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/user_view.html	2012-02-16 16:43:18 +0000
@@ -0,0 +1,18 @@
+{% extends "maasserver/base.html" %}
+
+{% block nav-active-settings %}active{% endblock %}
+{% block title %}View user{% endblock %}
+{% block page-title %}View user{% endblock %}
+
+{% block content %}
+    <p>
+     Username: {{ view_user.username }}
+    </p>
+    <p>
+      Email address: {{ view_user.email }}
+    </p>
+    <p>
+      MaaS Administrator: {{ view_user.is_superuser }}
+    </p>
+{% endblock %}
+

=== modified file 'src/maasserver/tests/test_models.py'
--- src/maasserver/tests/test_models.py	2012-02-16 11:51:13 +0000
+++ src/maasserver/tests/test_models.py	2012-02-16 16:43:18 +0000
@@ -12,12 +12,17 @@
 __all__ = []
 
 import codecs
+from operator import attrgetter
 import os
 import shutil
 
 from django.conf import settings
+from django.contrib.auth.models import User
 from django.core.exceptions import ValidationError
-from maasserver.exceptions import PermissionDenied
+from maasserver.exceptions import (
+    CannotDeleteUserException,
+    PermissionDenied,
+    )
 from maasserver.models import (
     GENERIC_CONSUMER,
     MACAddress,
@@ -320,6 +325,35 @@
         self.assertEqual(2, tokens.count())
         self.assertEqual(token, list(tokens.order_by('id'))[1])
 
+    def test_delete(self):
+        # Deleting a profile also deletes the related user.
+        profile = factory.make_user().get_profile()
+        profile_id = profile.id
+        profile.delete()
+        self.assertFalse(User.objects.filter(id=profile_id).exists())
+        self.assertFalse(
+            UserProfile.objects.filter(id=profile_id).exists())
+
+    def test_delete_consumers_tokens(self):
+        # Deleting a profile deletes the related tokens and consumers.
+        profile = factory.make_user().get_profile()
+        token_ids, consumer_ids = zip(*[
+            map(attrgetter('id'), profile.create_authorisation_token())
+            for i in range(3)])
+        profile.delete()
+        self.assertFalse(Consumer.objects.filter(id__in=consumer_ids).exists())
+        self.assertFalse(Token.objects.filter(id__in=token_ids).exists())
+
+    def test_delete_attached_nodes(self):
+        # Cannot delete a user with nodes attached to it.
+        profile = factory.make_user().get_profile()
+        factory.make_node(owner=profile.user)
+        message = (
+            "User %s cannot be deleted: it still has 1 node\(s\) deployed\." %
+            profile.user.username)
+        self.assertRaisesRegexp(
+            CannotDeleteUserException, message, profile.delete)
+
 
 class FileStorageTest(TestCase):
     """Testing of the :class:`FileStorage` model."""

=== modified file 'src/maasserver/tests/test_views.py'
--- src/maasserver/tests/test_views.py	2012-02-14 10:14:09 +0000
+++ src/maasserver/tests/test_views.py	2012-02-16 16:43:18 +0000
@@ -14,8 +14,13 @@
 import httplib
 
 from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
 from lxml.html import fromstring
-from maasserver.testing import LoggedInTestCase
+from maasserver.models import UserProfile
+from maasserver.testing import (
+    factory,
+    LoggedInTestCase,
+    )
 
 
 class UserPrefsViewTest(LoggedInTestCase):
@@ -88,3 +93,111 @@
         user = User.objects.get(id=self.logged_in_user.id)
         # The password is SHA1ized, we just make sure that it has changed.
         self.assertNotEqual(old_pw, user.password)
+
+
+class AdminLoggedInTestCase(LoggedInTestCase):
+
+    def setUp(self):
+        super(AdminLoggedInTestCase, self).setUp()
+        # Promote the logged-in user to admin.
+        self.logged_in_user.is_superuser = True
+        self.logged_in_user.save()
+
+
+class SettingsTest(AdminLoggedInTestCase):
+
+    def test_settings_list_users(self):
+        # The settings page (users tab) displays a list of the users with
+        # links to view, delete or edit each user.
+        # Note that the link to delete the the logged-in user is not display.
+        [factory.make_user() for i in range(3)]
+        users = UserProfile.objects.all_users()
+        response = self.client.get('/settings/')
+        doc = fromstring(response.content)
+        tab = doc.cssselect('#users')[0]
+        all_links = [elem.get('href') for elem in tab.cssselect('a')]
+        # "Add a user" link.
+        self.assertIn(reverse('accounts-add'), all_links)
+        for user in users:
+            rows = tab.cssselect('tr#%s' % user.username)
+            # Only one row for the user.
+            self.assertEqual(1, len(rows))
+            row = rows[0]
+            links = [elem.get('href') for elem in row.cssselect('a')]
+            # The username is shown...
+            self.assertSequenceEqual(
+                [user.username],
+                [link.text.strip() for link in row.cssselect('a.user')])
+            # ...with a link to view the user's profile.
+            self.assertSequenceEqual(
+                [reverse('accounts-view', args=[user.username])],
+                [link.get('href') for link in row.cssselect('a.user')])
+            # A link to edit the user is shown.
+            self.assertIn(
+                reverse('accounts-edit', args=[user.username]), links)
+            if user != self.logged_in_user:
+                # A link to delete the user is shown.
+                self.assertIn(
+                    reverse('accounts-del', args=[user.username]), links)
+            else:
+                # No link to delete the user is shown if the user is the
+                # logged-in user.
+                self.assertNotIn(
+                    reverse('accounts-del', args=[user.username]), links)
+
+
+class UserManagementTest(AdminLoggedInTestCase):
+
+    def test_add_user_POST(self):
+        response = self.client.post(
+            reverse('accounts-add'),
+            {
+                'username': 'my_user',
+                'password1': 'pw',
+                'password2': 'pw',
+            })
+        self.assertEqual(httplib.FOUND, response.status_code)
+        users = list(User.objects.filter(username='my_user'))
+        self.assertEqual(1, len(users))
+        self.assertTrue(users[0].check_password('pw'))
+
+    def test_delete_user_GET(self):
+        # The user delete page displays a confirmation page with a form.
+        user = factory.make_user()
+        del_link = reverse('accounts-del', args=[user.username])
+        response = self.client.get(del_link)
+        doc = fromstring(response.content)
+        confirmation_message = (
+            'Are you sure you want to delete user %s?' %
+            user.username)
+        self.assertSequenceEqual(
+            [confirmation_message],
+            [elem.text.strip() for elem in doc.cssselect('h2')])
+        # The page features a form that submits to itself.
+        self.assertSequenceEqual(
+            ['.'],
+            [elem.get('action').strip() for elem in doc.cssselect(
+                '#content form')])
+
+    def test_delete_user_POST(self):
+        # A POST request to the user delete finally deletes the user.
+        user = factory.make_user()
+        user_id = user.id
+        del_link = reverse('accounts-del', args=[user.username])
+        response = self.client.post(
+            del_link,
+            {
+                'post': 'yes',
+            })
+        self.assertEqual(httplib.FOUND, response.status_code)
+        self.assertFalse(User.objects.filter(id=user_id).exists())
+
+    def test_view_user(self):
+        # The user page feature the basic information about the user.
+        user = factory.make_user()
+        del_link = reverse('accounts-view', args=[user.username])
+        response = self.client.get(del_link)
+        doc = fromstring(response.content)
+        content_text = doc.cssselect('#content')[0].text_content()
+        self.assertIn(user.username, content_text)
+        self.assertIn(user.email, content_text)

=== modified file 'src/maasserver/urls.py'
--- src/maasserver/urls.py	2012-02-13 10:57:37 +0000
+++ src/maasserver/urls.py	2012-02-16 16:43:18 +0000
@@ -15,6 +15,7 @@
     patterns,
     url,
     )
+from django.contrib.auth.decorators import user_passes_test
 from django.contrib.auth.views import login
 from django.views.generic.simple import (
     direct_to_template,
@@ -32,13 +33,24 @@
     )
 from maasserver.models import Node
 from maasserver.views import (
+    AccountsAdd,
+    AccountsDelete,
+    AccountsEdit,
+    AccountsView,
     logout,
     NodeListView,
     NodesCreateView,
+    settings,
     userprefsview,
     )
 from piston.resource import Resource
 
+
+def adminurl(regexp, view, *args, **kwargs):
+    view = user_passes_test(lambda u: u.is_superuser)(view)
+    return url(regexp, view, *args, **kwargs)
+
+
 # URLs accessible to anonymous users.
 urlpatterns = patterns('maasserver.views',
     url(r'^account/prefs/$', userprefsview, name='prefs'),
@@ -64,6 +76,22 @@
         r'^nodes/create/$', NodesCreateView.as_view(), name='node-create'),
 )
 
+# URLs for admin users.
+urlpatterns += patterns('maasserver.views',
+    adminurl(r'^settings/$', settings, name='settings'),
+    adminurl(r'^accounts/add/$', AccountsAdd.as_view(), name='accounts-add'),
+    adminurl(
+        r'^accounts/(?P<username>\w+)/edit/$', AccountsEdit.as_view(),
+        name='accounts-edit'),
+    adminurl(
+        r'^accounts/(?P<username>\w+)/view/$', AccountsView.as_view(),
+        name='accounts-view'),
+    adminurl(
+        r'^accounts/(?P<username>\w+)/del/$', AccountsDelete.as_view(),
+        name='accounts-del'),
+)
+
+
 # API.
 auth = MaasAPIAuthentication(realm="MaaS API")
 

=== modified file 'src/maasserver/views.py'
--- src/maasserver/views.py	2012-02-09 14:42:54 +0000
+++ src/maasserver/views.py	2012-02-16 16:43:18 +0000
@@ -17,17 +17,32 @@
 
 from django.contrib import messages
 from django.contrib.auth.forms import PasswordChangeForm as PasswordForm
+from django.contrib.auth.models import User
 from django.contrib.auth.views import logout as dj_logout
 from django.core.urlresolvers import reverse
 from django.http import HttpResponseRedirect
-from django.shortcuts import render_to_response
+from django.shortcuts import (
+    get_object_or_404,
+    render_to_response,
+    )
 from django.template import RequestContext
 from django.views.generic import (
     CreateView,
+    DeleteView,
+    DetailView,
     ListView,
-    )
-from maasserver.forms import ProfileForm
-from maasserver.models import Node
+    UpdateView,
+    )
+from maasserver.exceptions import CannotDeleteUserException
+from maasserver.forms import (
+    EditUserForm,
+    NewUserCreationForm,
+    ProfileForm,
+    )
+from maasserver.models import (
+    Node,
+    UserProfile,
+    )
 
 
 def logout(request):
@@ -86,3 +101,79 @@
             'tab': tab  # Tab index to display.
         },
         context_instance=RequestContext(request))
+
+
+class AccountsView(DetailView):
+
+    template_name = 'maasserver/user_view.html'
+
+    context_object_name = 'view_user'
+
+    def get_object(self):
+        username = self.kwargs.get('username', None)
+        user = get_object_or_404(User, username=username)
+        return user
+
+
+class AccountsAdd(CreateView):
+
+    form_class = NewUserCreationForm
+
+    template_name = 'maasserver/user_add.html'
+
+    context_object_name = 'new_user'
+
+    def get_success_url(self):
+        return reverse('settings')
+
+
+class AccountsDelete(DeleteView):
+
+    template_name = 'maasserver/user_confirm_delete.html'
+
+    context_object_name = 'user_to_delete'
+
+    def get_object(self):
+        username = self.kwargs.get('username', None)
+        user = get_object_or_404(User, username=username)
+        return user.get_profile()
+
+    def get_next_url(self):
+        return reverse('settings')
+
+    def delete(self, request, *args, **kwargs):
+        profile = self.get_object()
+        username = profile.user.username
+        try:
+            profile.delete()
+            messages.info(request, "User %s deleted." % username)
+        except CannotDeleteUserException, e:
+            messages.info(request, unicode(e))
+        return HttpResponseRedirect(self.get_next_url())
+
+
+class AccountsEdit(UpdateView):
+
+    form_class = EditUserForm
+
+    template_name = 'maasserver/user_edit.html'
+
+    def get_object(self):
+        username = self.kwargs.get('username', None)
+        user = get_object_or_404(User, username=username)
+        return user
+
+    def get_success_url(self):
+        return reverse('settings')
+
+
+def settings(request):
+    tab = request.GET.get('tab', 0)
+    user_list = UserProfile.objects.all_users().order_by('username')
+    return render_to_response(
+        'maasserver/settings.html',
+        {
+            'user_list': user_list,
+            'tab': tab  # Tab index to display.
+        },
+        context_instance=RequestContext(request))