launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #07200
[Merge] lp:~rvb/maas/views-bug-973215 into lp:maas
Raphaël Badin has proposed merging lp:~rvb/maas/views-bug-973215 into lp:maas.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~rvb/maas/views-bug-973215/+merge/103061
This branch is the first in a series of branches that will refactor src/maasserver/views.py into modules in src/maasserver/views/.
This branch:
- moves src/maasserver/views.py into src/maasserver/views/__init__.py.
- move the accounts views (login/logout) from src/maasserver/views/__init__.py to src/maasserver/views/account.
- move the accounts test from src/maasserver/tests/test_views.py to src/maasserver/tests/test_views_account.py.
--
https://code.launchpad.net/~rvb/maas/views-bug-973215/+merge/103061
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/views-bug-973215 into lp:maas.
=== modified file 'src/maasserver/tests/test_views.py'
--- src/maasserver/tests/test_views.py 2012-04-20 16:00:50 +0000
+++ src/maasserver/tests/test_views.py 2012-04-23 09:15:24 +0000
@@ -122,24 +122,6 @@
doc.cssselect('h2')])
-class TestLogin(TestCase):
-
- def test_login_contains_input_tags_if_user(self):
- factory.make_user()
- response = self.client.get('/accounts/login/')
- doc = fromstring(response.content)
- self.assertFalse(response.context['no_users'])
- self.assertEqual(1, len(doc.cssselect('input#id_username')))
- self.assertEqual(1, len(doc.cssselect('input#id_password')))
-
- def test_login_displays_createsuperuser_message_if_no_user(self):
- path = factory.getRandomString()
- self.patch(settings, 'MAAS_CLI', path)
- response = self.client.get('/accounts/login/')
- self.assertTrue(response.context['no_users'])
- self.assertEqual(path, response.context['create_command'])
-
-
class TestSnippets(LoggedInTestCase):
def assertTemplateExistsAndContains(self, content, template_selector,
=== added file 'src/maasserver/tests/test_views_account.py'
--- src/maasserver/tests/test_views_account.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/tests/test_views_account.py 2012-04-23 09:15:24 +0000
@@ -0,0 +1,36 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test maasserver account views."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = []
+
+from maasserver.testing.testcase import TestCase
+from maasserver.testing.factory import factory
+from lxml.html import fromstring
+from django.conf import settings
+
+
+class TestLogin(TestCase):
+
+ def test_login_contains_input_tags_if_user(self):
+ factory.make_user()
+ response = self.client.get('/accounts/login/')
+ doc = fromstring(response.content)
+ self.assertFalse(response.context['no_users'])
+ self.assertEqual(1, len(doc.cssselect('input#id_username')))
+ self.assertEqual(1, len(doc.cssselect('input#id_password')))
+
+ def test_login_displays_createsuperuser_message_if_no_user(self):
+ path = factory.getRandomString()
+ self.patch(settings, 'MAAS_CLI', path)
+ response = self.client.get('/accounts/login/')
+ self.assertTrue(response.context['no_users'])
+ self.assertEqual(path, response.context['create_command'])
=== modified file 'src/maasserver/urls.py'
--- src/maasserver/urls.py 2012-04-18 10:51:03 +0000
+++ src/maasserver/urls.py 2012-04-23 09:15:24 +0000
@@ -33,8 +33,6 @@
AccountsEdit,
AccountsView,
combo_view,
- login,
- logout,
NodeDelete,
NodeEdit,
NodeListView,
@@ -46,6 +44,10 @@
SSHKeyDeleteView,
userprefsview,
)
+from maasserver.views.account import (
+ login,
+ logout,
+ )
def adminurl(regexp, view, *args, **kwargs):
=== added directory 'src/maasserver/views'
=== removed file 'src/maasserver/views.py'
--- src/maasserver/views.py 2012-04-20 16:00:50 +0000
+++ src/maasserver/views.py 1970-01-01 00:00:00 +0000
@@ -1,605 +0,0 @@
-# Copyright 2012 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Views."""
-
-from __future__ import (
- absolute_import,
- print_function,
- unicode_literals,
- )
-
-__metaclass__ = type
-__all__ = [
- "AccountsAdd",
- "AccountsDelete",
- "AccountsEdit",
- "AccountsView",
- "combo_view",
- "login",
- "logout",
- "NodeListView",
- "NodesCreateView",
- "NodeView",
- "NodeEdit",
- "settings",
- "settings_add_archive",
- "SSHKeyCreateView",
- "SSHKeyDeleteView",
- ]
-
-from abc import (
- ABCMeta,
- abstractmethod,
- )
-from logging import getLogger
-import os
-
-from convoy.combo import (
- combine_files,
- parse_qs,
- )
-from django.conf import settings as django_settings
-from django.contrib import messages
-from django.contrib.auth.forms import (
- AdminPasswordChangeForm,
- PasswordChangeForm,
- )
-from django.contrib.auth.models import User
-from django.contrib.auth.views import (
- login as dj_login,
- logout as dj_logout,
- )
-from django.core.exceptions import PermissionDenied
-from django.core.urlresolvers import reverse
-from django.http import (
- Http404,
- HttpResponse,
- HttpResponseBadRequest,
- HttpResponseNotFound,
- HttpResponseRedirect,
- )
-from django.shortcuts import (
- get_object_or_404,
- render_to_response,
- )
-from django.template import RequestContext
-from django.utils.safestring import mark_safe
-from django.views.generic import (
- CreateView,
- DeleteView,
- DetailView,
- ListView,
- UpdateView,
- )
-from django.views.generic.base import TemplateView
-from django.views.generic.detail import SingleObjectTemplateResponseMixin
-from django.views.generic.edit import ModelFormMixin
-from maasserver.enum import (
- NODE_PERMISSION,
- NODE_STATUS,
- )
-from maasserver.exceptions import (
- CannotDeleteUserException,
- NoRabbit,
- )
-from maasserver.forms import (
- AddArchiveForm,
- CommissioningForm,
- EditUserForm,
- get_action_form,
- MAASAndNetworkForm,
- NewUserCreationForm,
- ProfileForm,
- SSHKeyForm,
- UbuntuForm,
- UIAdminNodeEditForm,
- UINodeEditForm,
- )
-from maasserver.messages import messaging
-from maasserver.models import (
- Node,
- SSHKey,
- UserProfile,
- )
-
-
-def login(request):
- extra_context = {
- 'no_users': UserProfile.objects.all_users().count() == 0,
- 'create_command': django_settings.MAAS_CLI,
- }
- return dj_login(request, extra_context=extra_context)
-
-
-def logout(request):
- messages.info(request, "You have been logged out.")
- return dj_logout(request, next_page=reverse('login'))
-
-
-class HelpfulDeleteView(DeleteView):
- """Extension to Django's :class:`django.views.generic.DeleteView`.
-
- This modifies `DeleteView` in a few ways:
- - Deleting a nonexistent object is considered successful.
- - There's a callback that lets you describe the object to the user.
- - User feedback is built in.
- - get_success_url defaults to returning the "next" URL.
- - Confirmation screen also deals nicely with already-deleted object.
-
- :ivar model: The model class this view is meant to delete.
- """
-
- __metaclass__ = ABCMeta
-
- @abstractmethod
- def get_object(self):
- """Retrieve the object to be deleted."""
-
- @abstractmethod
- def get_next_url(self):
- """URL of page to proceed to after deleting."""
-
- def delete(self, *args, **kwargs):
- """Delete result of self.get_object(), if any."""
- try:
- self.object = self.get_object()
- except Http404:
- feedback = self.compose_feedback_nonexistent()
- else:
- self.object.delete()
- feedback = self.compose_feedback_deleted(self.object)
- return self.move_on(feedback)
-
- def get(self, *args, **kwargs):
- """Prompt for confirmation of deletion request in the UI.
-
- This is where the view acts as a regular template view.
-
- If the object has been deleted in the meantime though, don't bother:
- we'll just redirect to the next URL and show a notice that the object
- is no longer there.
- """
- try:
- return super(HelpfulDeleteView, self).get(*args, **kwargs)
- except Http404:
- return self.move_on(self.compose_feedback_nonexistent())
-
- def compose_feedback_nonexistent(self):
- """Compose feedback message: "obj was already deleted"."""
- return "Not deleting: %s not found." % self.model._meta.verbose_name
-
- def compose_feedback_deleted(self, obj):
- """Compose feedback message: "obj has been deleted"."""
- return ("%s deleted." % self.name_object(obj)).capitalize()
-
- def name_object(self, obj):
- """Overridable: describe object being deleted to the user.
-
- The result text will be included in a user notice along the lines of
- "<Object> deleted."
-
- :param obj: Object that's been deleted from the database.
- :return: Description of the object, along the lines of
- "User <obj.username>".
- """
- return obj._meta.verbose_name
-
- def show_notice(self, notice):
- """Wrapper for messages.info."""
- messages.info(self.request, notice)
-
- def move_on(self, feedback_message):
- """Redirect to the post-deletion page, showing the given message."""
- self.show_notice(feedback_message)
- return HttpResponseRedirect(self.get_next_url())
-
-
-# Info message displayed on the node page for COMMISSIONING
-# or READY nodes.
-NODE_BOOT_INFO = mark_safe("""
-You can boot this node using Avahi enabled boot media or an
-adequately configured dhcp server, see
-<a href="https://wiki.ubuntu.com/ServerTeam/MAAS/AvahiBoot">
-https://wiki.ubuntu.com/ServerTeam/MAAS/AvahiBoot</a> for
-details.
-""")
-
-
-class NodeView(UpdateView):
-
- template_name = 'maasserver/node_view.html'
-
- context_object_name = 'node'
-
- def get_object(self):
- system_id = self.kwargs.get('system_id', None)
- node = Node.objects.get_node_or_404(
- system_id=system_id, user=self.request.user,
- perm=NODE_PERMISSION.VIEW)
- return node
-
- def get_form_class(self):
- return get_action_form(self.request.user, self.request)
-
- 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(
- NODE_PERMISSION.EDIT, node)
- context['can_delete'] = self.request.user.has_perm(
- NODE_PERMISSION.ADMIN, node)
- if node.status in (NODE_STATUS.COMMISSIONING, NODE_STATUS.READY):
- messages.info(self.request, NODE_BOOT_INFO)
- context['error_text'] = (
- node.error if node.status == NODE_STATUS.FAILED_TESTS else None)
- context['status_text'] = (
- node.error if node.status != NODE_STATUS.FAILED_TESTS else None)
- return context
-
- def get_success_url(self):
- return reverse('node-view', args=[self.get_object().system_id])
-
-
-class NodeEdit(UpdateView):
-
- template_name = 'maasserver/node_edit.html'
-
- def get_object(self):
- system_id = self.kwargs.get('system_id', None)
- node = Node.objects.get_node_or_404(
- system_id=system_id, user=self.request.user,
- perm=NODE_PERMISSION.EDIT)
- return node
-
- def get_form_class(self):
- if self.request.user.is_superuser:
- return UIAdminNodeEditForm
- else:
- return UINodeEditForm
-
- def get_success_url(self):
- return reverse('node-view', args=[self.get_object().system_id])
-
-
-class NodeDelete(HelpfulDeleteView):
-
- template_name = 'maasserver/node_confirm_delete.html'
- context_object_name = 'node_to_delete'
- model = Node
-
- def get_object(self):
- system_id = self.kwargs.get('system_id', None)
- node = Node.objects.get_node_or_404(
- system_id=system_id, user=self.request.user,
- perm=NODE_PERMISSION.ADMIN)
- if node.status == NODE_STATUS.ALLOCATED:
- raise PermissionDenied()
- return node
-
- def get_next_url(self):
- return reverse('node-list')
-
- def name_object(self, obj):
- """See `HelpfulDeleteView`."""
- return "Node %s" % obj.system_id
-
-
-def get_longpoll_context():
- if messaging is not None and django_settings.LONGPOLL_PATH is not None:
- try:
- return {
- 'longpoll_queue': messaging.getQueue().name,
- 'LONGPOLL_PATH': django_settings.LONGPOLL_PATH,
- }
- except NoRabbit as e:
- getLogger('maasserver').warn(
- "Could not connect to RabbitMQ: %s", e)
- return {}
- else:
- return {}
-
-
-class NodeListView(ListView):
-
- context_object_name = "node_list"
-
- def get_queryset(self):
- # Return node list sorted, newest first.
- return Node.objects.get_nodes(
- user=self.request.user,
- perm=NODE_PERMISSION.VIEW).order_by('-id')
-
- def get_context_data(self, **kwargs):
- context = super(NodeListView, self).get_context_data(**kwargs)
- context.update(get_longpoll_context())
- return context
-
-
-class NodesCreateView(CreateView):
-
- model = Node
-
- def get_success_url(self):
- return reverse('index')
-
-
-class SSHKeyCreateView(CreateView):
-
- form_class = SSHKeyForm
- template_name = 'maasserver/prefs_add_sshkey.html'
-
- def get_form_kwargs(self):
- kwargs = super(SSHKeyCreateView, self).get_form_kwargs()
- kwargs['user'] = self.request.user
- return kwargs
-
- def form_valid(self, form):
- messages.info(self.request, "SSH key added.")
- return super(SSHKeyCreateView, self).form_valid(form)
-
- def get_success_url(self):
- return reverse('prefs')
-
-
-class SSHKeyDeleteView(HelpfulDeleteView):
-
- template_name = 'maasserver/prefs_confirm_delete_sshkey.html'
- context_object_name = 'key'
- model = SSHKey
-
- def get_object(self):
- keyid = self.kwargs.get('keyid', None)
- key = get_object_or_404(SSHKey, id=keyid)
- if key.user != self.request.user:
- raise PermissionDenied("Can't delete this key. It's not yours.")
- return key
-
- def get_next_url(self):
- return reverse('prefs')
-
-
-def process_form(request, form_class, redirect_url, prefix,
- success_message=None, form_kwargs=None):
- """Utility method to process subforms (i.e. forms with a prefix).
-
- :param request: The request which contains the data to be validated.
- :type request: django.http.HttpRequest
- :param form_class: The form class used to perform the validation.
- :type form_class: django.forms.Form
- :param redirect_url: The url where the user should be redirected if the
- form validates successfully.
- :type redirect_url: basestring
- :param prefix: The prefix of the form.
- :type prefix: basestring
- :param success_message: An optional message that will be displayed if the
- form validates successfully.
- :type success_message: basestring
- :param form_kwargs: An optional dict that will passed to the form creation
- method.
- :type form_kwargs: dict or None
- :return: A tuple of the validated form and a response (the response will
- not be None only if the form has been validated correctly).
- :rtype: tuple
-
- """
- if form_kwargs is None:
- form_kwargs = {}
- if '%s_submit' % prefix in request.POST:
- form = form_class(
- data=request.POST, prefix=prefix, **form_kwargs)
- if form.is_valid():
- if success_message is not None:
- messages.info(request, success_message)
- form.save()
- return form, HttpResponseRedirect(redirect_url)
- else:
- form = form_class(prefix=prefix, **form_kwargs)
- return form, None
-
-
-def userprefsview(request):
- user = request.user
- # Process the profile update form.
- profile_form, response = process_form(
- request, ProfileForm, reverse('prefs'), 'profile', "Profile updated.",
- {'instance': user})
- if response is not None:
- return response
-
- # Process the password change form.
- password_form, response = process_form(
- request, PasswordChangeForm, reverse('prefs'), 'password',
- "Password updated.", {'user': user})
- if response is not None:
- return response
-
- return render_to_response(
- 'maasserver/prefs.html',
- {
- 'profile_form': profile_form,
- 'password_form': password_form,
- },
- context_instance=RequestContext(request))
-
-
-class AccountsView(DetailView):
- """Read-only view of user's account information."""
-
- 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):
- """Add-user view."""
-
- form_class = NewUserCreationForm
-
- template_name = 'maasserver/user_add.html'
-
- context_object_name = 'new_user'
-
- def get_success_url(self):
- return reverse('settings')
-
- def form_valid(self, form):
- messages.info(self.request, "User added.")
- return super(AccountsAdd, self).form_valid(form)
-
-
-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 as e:
- messages.info(request, unicode(e))
- return HttpResponseRedirect(self.get_next_url())
-
-
-class AccountsEdit(TemplateView, ModelFormMixin,
- SingleObjectTemplateResponseMixin):
-
- model = User
- template_name = 'maasserver/user_edit.html'
-
- def get_object(self):
- username = self.kwargs.get('username', None)
- return get_object_or_404(User, username=username)
-
- def respond(self, request, profile_form, password_form):
- """Generate a response."""
- return self.render_to_response({
- 'profile_form': profile_form,
- 'password_form': password_form,
- })
-
- def get(self, request, *args, **kwargs):
- """Called by `TemplateView`: handle a GET request."""
- self.object = user = self.get_object()
- profile_form = EditUserForm(instance=user, prefix='profile')
- password_form = AdminPasswordChangeForm(user=user, prefix='password')
- return self.respond(request, profile_form, password_form)
-
- def post(self, request, *args, **kwargs):
- """Called by `TemplateView`: handle a POST request."""
- self.object = user = self.get_object()
- next_page = reverse('settings')
-
- # Process the profile-editing form, if that's what was submitted.
- profile_form, response = process_form(
- request, EditUserForm, next_page, 'profile', "Profile updated.",
- {'instance': user})
- if response is not None:
- return response
-
- # Process the password change form, if that's what was submitted.
- password_form, response = process_form(
- request, AdminPasswordChangeForm, next_page, 'password',
- "Password updated.", {'user': user})
- if response is not None:
- return response
-
- return self.respond(request, profile_form, password_form)
-
-
-def settings(request):
- user_list = UserProfile.objects.all_users().order_by('username')
- # Process the MAAS & network form.
- maas_and_network_form, response = process_form(
- request, MAASAndNetworkForm, reverse('settings'), 'maas_and_network',
- "Configuration updated.")
- if response is not None:
- return response
-
- # Process the Commissioning form.
- commissioning_form, response = process_form(
- request, CommissioningForm, reverse('settings'), 'commissioning',
- "Configuration updated.")
- if response is not None:
- return response
-
- # Process the Ubuntu form.
- ubuntu_form, response = process_form(
- request, UbuntuForm, reverse('settings'), 'ubuntu',
- "Configuration updated.")
- if response is not None:
- return response
-
- return render_to_response(
- 'maasserver/settings.html',
- {
- 'user_list': user_list,
- 'maas_and_network_form': maas_and_network_form,
- 'commissioning_form': commissioning_form,
- 'ubuntu_form': ubuntu_form,
- },
- context_instance=RequestContext(request))
-
-
-def settings_add_archive(request):
- if request.method == 'POST':
- form = AddArchiveForm(request.POST)
- if form.is_valid():
- form.save()
- messages.info(request, "Archive added.")
- return HttpResponseRedirect(reverse('settings'))
- else:
- form = AddArchiveForm()
-
- return render_to_response(
- 'maasserver/settings_add_archive.html',
- {'form': form},
- context_instance=RequestContext(request))
-
-
-def get_yui_location():
- if django_settings.STATIC_ROOT:
- return os.path.join(
- django_settings.STATIC_ROOT, 'jslibs', 'yui')
- else:
- return os.path.join(
- os.path.dirname(__file__), 'static', 'jslibs', 'yui')
-
-
-def combo_view(request):
- """Handle a request for combining a set of files."""
- fnames = parse_qs(request.META.get("QUERY_STRING", ""))
- YUI_LOCATION = get_yui_location()
-
- if fnames:
- if fnames[0].endswith('.js'):
- content_type = 'text/javascript; charset=UTF-8'
- elif fnames[0].endswith('.css'):
- content_type = 'text/css'
- else:
- return HttpResponseBadRequest("Invalid file type requested.")
- content = b"".join(
- combine_files(
- fnames, YUI_LOCATION, resource_prefix='/',
- rewrite_urls=True))
-
- return HttpResponse(
- content_type=content_type, status=200, content=content)
-
- return HttpResponseNotFound()
=== added file 'src/maasserver/views/__init__.py'
--- src/maasserver/views/__init__.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/views/__init__.py 2012-04-23 09:15:24 +0000
@@ -0,0 +1,587 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Views."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = [
+ "AccountsAdd",
+ "AccountsDelete",
+ "AccountsEdit",
+ "AccountsView",
+ "combo_view",
+ "NodeListView",
+ "NodesCreateView",
+ "NodeView",
+ "NodeEdit",
+ "settings",
+ "settings_add_archive",
+ "SSHKeyCreateView",
+ "SSHKeyDeleteView",
+ ]
+
+from abc import (
+ ABCMeta,
+ abstractmethod,
+ )
+from logging import getLogger
+import os
+
+from convoy.combo import (
+ combine_files,
+ parse_qs,
+ )
+from django.conf import settings as django_settings
+from django.contrib import messages
+from django.contrib.auth.forms import (
+ AdminPasswordChangeForm,
+ PasswordChangeForm,
+ )
+from django.contrib.auth.models import User
+from django.core.exceptions import PermissionDenied
+from django.core.urlresolvers import reverse
+from django.http import (
+ Http404,
+ HttpResponse,
+ HttpResponseBadRequest,
+ HttpResponseNotFound,
+ HttpResponseRedirect,
+ )
+from django.shortcuts import (
+ get_object_or_404,
+ render_to_response,
+ )
+from django.template import RequestContext
+from django.utils.safestring import mark_safe
+from django.views.generic import (
+ CreateView,
+ DeleteView,
+ DetailView,
+ ListView,
+ UpdateView,
+ )
+from django.views.generic.base import TemplateView
+from django.views.generic.detail import SingleObjectTemplateResponseMixin
+from django.views.generic.edit import ModelFormMixin
+from maasserver.enum import (
+ NODE_PERMISSION,
+ NODE_STATUS,
+ )
+from maasserver.exceptions import (
+ CannotDeleteUserException,
+ NoRabbit,
+ )
+from maasserver.forms import (
+ AddArchiveForm,
+ CommissioningForm,
+ EditUserForm,
+ get_action_form,
+ MAASAndNetworkForm,
+ NewUserCreationForm,
+ ProfileForm,
+ SSHKeyForm,
+ UbuntuForm,
+ UIAdminNodeEditForm,
+ UINodeEditForm,
+ )
+from maasserver.messages import messaging
+from maasserver.models import (
+ Node,
+ SSHKey,
+ UserProfile,
+ )
+
+
+class HelpfulDeleteView(DeleteView):
+ """Extension to Django's :class:`django.views.generic.DeleteView`.
+
+ This modifies `DeleteView` in a few ways:
+ - Deleting a nonexistent object is considered successful.
+ - There's a callback that lets you describe the object to the user.
+ - User feedback is built in.
+ - get_success_url defaults to returning the "next" URL.
+ - Confirmation screen also deals nicely with already-deleted object.
+
+ :ivar model: The model class this view is meant to delete.
+ """
+
+ __metaclass__ = ABCMeta
+
+ @abstractmethod
+ def get_object(self):
+ """Retrieve the object to be deleted."""
+
+ @abstractmethod
+ def get_next_url(self):
+ """URL of page to proceed to after deleting."""
+
+ def delete(self, *args, **kwargs):
+ """Delete result of self.get_object(), if any."""
+ try:
+ self.object = self.get_object()
+ except Http404:
+ feedback = self.compose_feedback_nonexistent()
+ else:
+ self.object.delete()
+ feedback = self.compose_feedback_deleted(self.object)
+ return self.move_on(feedback)
+
+ def get(self, *args, **kwargs):
+ """Prompt for confirmation of deletion request in the UI.
+
+ This is where the view acts as a regular template view.
+
+ If the object has been deleted in the meantime though, don't bother:
+ we'll just redirect to the next URL and show a notice that the object
+ is no longer there.
+ """
+ try:
+ return super(HelpfulDeleteView, self).get(*args, **kwargs)
+ except Http404:
+ return self.move_on(self.compose_feedback_nonexistent())
+
+ def compose_feedback_nonexistent(self):
+ """Compose feedback message: "obj was already deleted"."""
+ return "Not deleting: %s not found." % self.model._meta.verbose_name
+
+ def compose_feedback_deleted(self, obj):
+ """Compose feedback message: "obj has been deleted"."""
+ return ("%s deleted." % self.name_object(obj)).capitalize()
+
+ def name_object(self, obj):
+ """Overridable: describe object being deleted to the user.
+
+ The result text will be included in a user notice along the lines of
+ "<Object> deleted."
+
+ :param obj: Object that's been deleted from the database.
+ :return: Description of the object, along the lines of
+ "User <obj.username>".
+ """
+ return obj._meta.verbose_name
+
+ def show_notice(self, notice):
+ """Wrapper for messages.info."""
+ messages.info(self.request, notice)
+
+ def move_on(self, feedback_message):
+ """Redirect to the post-deletion page, showing the given message."""
+ self.show_notice(feedback_message)
+ return HttpResponseRedirect(self.get_next_url())
+
+
+# Info message displayed on the node page for COMMISSIONING
+# or READY nodes.
+NODE_BOOT_INFO = mark_safe("""
+You can boot this node using Avahi enabled boot media or an
+adequately configured dhcp server, see
+<a href="https://wiki.ubuntu.com/ServerTeam/MAAS/AvahiBoot">
+https://wiki.ubuntu.com/ServerTeam/MAAS/AvahiBoot</a> for
+details.
+""")
+
+
+class NodeView(UpdateView):
+
+ template_name = 'maasserver/node_view.html'
+
+ context_object_name = 'node'
+
+ def get_object(self):
+ system_id = self.kwargs.get('system_id', None)
+ node = Node.objects.get_node_or_404(
+ system_id=system_id, user=self.request.user,
+ perm=NODE_PERMISSION.VIEW)
+ return node
+
+ def get_form_class(self):
+ return get_action_form(self.request.user, self.request)
+
+ 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(
+ NODE_PERMISSION.EDIT, node)
+ context['can_delete'] = self.request.user.has_perm(
+ NODE_PERMISSION.ADMIN, node)
+ if node.status in (NODE_STATUS.COMMISSIONING, NODE_STATUS.READY):
+ messages.info(self.request, NODE_BOOT_INFO)
+ context['error_text'] = (
+ node.error if node.status == NODE_STATUS.FAILED_TESTS else None)
+ context['status_text'] = (
+ node.error if node.status != NODE_STATUS.FAILED_TESTS else None)
+ return context
+
+ def get_success_url(self):
+ return reverse('node-view', args=[self.get_object().system_id])
+
+
+class NodeEdit(UpdateView):
+
+ template_name = 'maasserver/node_edit.html'
+
+ def get_object(self):
+ system_id = self.kwargs.get('system_id', None)
+ node = Node.objects.get_node_or_404(
+ system_id=system_id, user=self.request.user,
+ perm=NODE_PERMISSION.EDIT)
+ return node
+
+ def get_form_class(self):
+ if self.request.user.is_superuser:
+ return UIAdminNodeEditForm
+ else:
+ return UINodeEditForm
+
+ def get_success_url(self):
+ return reverse('node-view', args=[self.get_object().system_id])
+
+
+class NodeDelete(HelpfulDeleteView):
+
+ template_name = 'maasserver/node_confirm_delete.html'
+ context_object_name = 'node_to_delete'
+ model = Node
+
+ def get_object(self):
+ system_id = self.kwargs.get('system_id', None)
+ node = Node.objects.get_node_or_404(
+ system_id=system_id, user=self.request.user,
+ perm=NODE_PERMISSION.ADMIN)
+ if node.status == NODE_STATUS.ALLOCATED:
+ raise PermissionDenied()
+ return node
+
+ def get_next_url(self):
+ return reverse('node-list')
+
+ def name_object(self, obj):
+ """See `HelpfulDeleteView`."""
+ return "Node %s" % obj.system_id
+
+
+def get_longpoll_context():
+ if messaging is not None and django_settings.LONGPOLL_PATH is not None:
+ try:
+ return {
+ 'longpoll_queue': messaging.getQueue().name,
+ 'LONGPOLL_PATH': django_settings.LONGPOLL_PATH,
+ }
+ except NoRabbit as e:
+ getLogger('maasserver').warn(
+ "Could not connect to RabbitMQ: %s", e)
+ return {}
+ else:
+ return {}
+
+
+class NodeListView(ListView):
+
+ context_object_name = "node_list"
+
+ def get_queryset(self):
+ # Return node list sorted, newest first.
+ return Node.objects.get_nodes(
+ user=self.request.user,
+ perm=NODE_PERMISSION.VIEW).order_by('-id')
+
+ def get_context_data(self, **kwargs):
+ context = super(NodeListView, self).get_context_data(**kwargs)
+ context.update(get_longpoll_context())
+ return context
+
+
+class NodesCreateView(CreateView):
+
+ model = Node
+
+ def get_success_url(self):
+ return reverse('index')
+
+
+class SSHKeyCreateView(CreateView):
+
+ form_class = SSHKeyForm
+ template_name = 'maasserver/prefs_add_sshkey.html'
+
+ def get_form_kwargs(self):
+ kwargs = super(SSHKeyCreateView, self).get_form_kwargs()
+ kwargs['user'] = self.request.user
+ return kwargs
+
+ def form_valid(self, form):
+ messages.info(self.request, "SSH key added.")
+ return super(SSHKeyCreateView, self).form_valid(form)
+
+ def get_success_url(self):
+ return reverse('prefs')
+
+
+class SSHKeyDeleteView(HelpfulDeleteView):
+
+ template_name = 'maasserver/prefs_confirm_delete_sshkey.html'
+ context_object_name = 'key'
+ model = SSHKey
+
+ def get_object(self):
+ keyid = self.kwargs.get('keyid', None)
+ key = get_object_or_404(SSHKey, id=keyid)
+ if key.user != self.request.user:
+ raise PermissionDenied("Can't delete this key. It's not yours.")
+ return key
+
+ def get_next_url(self):
+ return reverse('prefs')
+
+
+def process_form(request, form_class, redirect_url, prefix,
+ success_message=None, form_kwargs=None):
+ """Utility method to process subforms (i.e. forms with a prefix).
+
+ :param request: The request which contains the data to be validated.
+ :type request: django.http.HttpRequest
+ :param form_class: The form class used to perform the validation.
+ :type form_class: django.forms.Form
+ :param redirect_url: The url where the user should be redirected if the
+ form validates successfully.
+ :type redirect_url: basestring
+ :param prefix: The prefix of the form.
+ :type prefix: basestring
+ :param success_message: An optional message that will be displayed if the
+ form validates successfully.
+ :type success_message: basestring
+ :param form_kwargs: An optional dict that will passed to the form creation
+ method.
+ :type form_kwargs: dict or None
+ :return: A tuple of the validated form and a response (the response will
+ not be None only if the form has been validated correctly).
+ :rtype: tuple
+
+ """
+ if form_kwargs is None:
+ form_kwargs = {}
+ if '%s_submit' % prefix in request.POST:
+ form = form_class(
+ data=request.POST, prefix=prefix, **form_kwargs)
+ if form.is_valid():
+ if success_message is not None:
+ messages.info(request, success_message)
+ form.save()
+ return form, HttpResponseRedirect(redirect_url)
+ else:
+ form = form_class(prefix=prefix, **form_kwargs)
+ return form, None
+
+
+def userprefsview(request):
+ user = request.user
+ # Process the profile update form.
+ profile_form, response = process_form(
+ request, ProfileForm, reverse('prefs'), 'profile', "Profile updated.",
+ {'instance': user})
+ if response is not None:
+ return response
+
+ # Process the password change form.
+ password_form, response = process_form(
+ request, PasswordChangeForm, reverse('prefs'), 'password',
+ "Password updated.", {'user': user})
+ if response is not None:
+ return response
+
+ return render_to_response(
+ 'maasserver/prefs.html',
+ {
+ 'profile_form': profile_form,
+ 'password_form': password_form,
+ },
+ context_instance=RequestContext(request))
+
+
+class AccountsView(DetailView):
+ """Read-only view of user's account information."""
+
+ 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):
+ """Add-user view."""
+
+ form_class = NewUserCreationForm
+
+ template_name = 'maasserver/user_add.html'
+
+ context_object_name = 'new_user'
+
+ def get_success_url(self):
+ return reverse('settings')
+
+ def form_valid(self, form):
+ messages.info(self.request, "User added.")
+ return super(AccountsAdd, self).form_valid(form)
+
+
+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 as e:
+ messages.info(request, unicode(e))
+ return HttpResponseRedirect(self.get_next_url())
+
+
+class AccountsEdit(TemplateView, ModelFormMixin,
+ SingleObjectTemplateResponseMixin):
+
+ model = User
+ template_name = 'maasserver/user_edit.html'
+
+ def get_object(self):
+ username = self.kwargs.get('username', None)
+ return get_object_or_404(User, username=username)
+
+ def respond(self, request, profile_form, password_form):
+ """Generate a response."""
+ return self.render_to_response({
+ 'profile_form': profile_form,
+ 'password_form': password_form,
+ })
+
+ def get(self, request, *args, **kwargs):
+ """Called by `TemplateView`: handle a GET request."""
+ self.object = user = self.get_object()
+ profile_form = EditUserForm(instance=user, prefix='profile')
+ password_form = AdminPasswordChangeForm(user=user, prefix='password')
+ return self.respond(request, profile_form, password_form)
+
+ def post(self, request, *args, **kwargs):
+ """Called by `TemplateView`: handle a POST request."""
+ self.object = user = self.get_object()
+ next_page = reverse('settings')
+
+ # Process the profile-editing form, if that's what was submitted.
+ profile_form, response = process_form(
+ request, EditUserForm, next_page, 'profile', "Profile updated.",
+ {'instance': user})
+ if response is not None:
+ return response
+
+ # Process the password change form, if that's what was submitted.
+ password_form, response = process_form(
+ request, AdminPasswordChangeForm, next_page, 'password',
+ "Password updated.", {'user': user})
+ if response is not None:
+ return response
+
+ return self.respond(request, profile_form, password_form)
+
+
+def settings(request):
+ user_list = UserProfile.objects.all_users().order_by('username')
+ # Process the MAAS & network form.
+ maas_and_network_form, response = process_form(
+ request, MAASAndNetworkForm, reverse('settings'), 'maas_and_network',
+ "Configuration updated.")
+ if response is not None:
+ return response
+
+ # Process the Commissioning form.
+ commissioning_form, response = process_form(
+ request, CommissioningForm, reverse('settings'), 'commissioning',
+ "Configuration updated.")
+ if response is not None:
+ return response
+
+ # Process the Ubuntu form.
+ ubuntu_form, response = process_form(
+ request, UbuntuForm, reverse('settings'), 'ubuntu',
+ "Configuration updated.")
+ if response is not None:
+ return response
+
+ return render_to_response(
+ 'maasserver/settings.html',
+ {
+ 'user_list': user_list,
+ 'maas_and_network_form': maas_and_network_form,
+ 'commissioning_form': commissioning_form,
+ 'ubuntu_form': ubuntu_form,
+ },
+ context_instance=RequestContext(request))
+
+
+def settings_add_archive(request):
+ if request.method == 'POST':
+ form = AddArchiveForm(request.POST)
+ if form.is_valid():
+ form.save()
+ messages.info(request, "Archive added.")
+ return HttpResponseRedirect(reverse('settings'))
+ else:
+ form = AddArchiveForm()
+
+ return render_to_response(
+ 'maasserver/settings_add_archive.html',
+ {'form': form},
+ context_instance=RequestContext(request))
+
+
+def get_yui_location():
+ if django_settings.STATIC_ROOT:
+ return os.path.join(
+ django_settings.STATIC_ROOT, 'jslibs', 'yui')
+ else:
+ return os.path.join(
+ os.path.dirname(os.path.dirname(__file__)), 'static',
+ 'jslibs', 'yui')
+
+
+def combo_view(request):
+ """Handle a request for combining a set of files."""
+ fnames = parse_qs(request.META.get("QUERY_STRING", ""))
+ YUI_LOCATION = get_yui_location()
+
+ if fnames:
+ if fnames[0].endswith('.js'):
+ content_type = 'text/javascript; charset=UTF-8'
+ elif fnames[0].endswith('.css'):
+ content_type = 'text/css'
+ else:
+ return HttpResponseBadRequest("Invalid file type requested.")
+ content = b"".join(
+ combine_files(
+ fnames, YUI_LOCATION, resource_prefix='/',
+ rewrite_urls=True))
+
+ return HttpResponse(
+ content_type=content_type, status=200, content=content)
+
+ return HttpResponseNotFound()
=== added file 'src/maasserver/views/account.py'
--- src/maasserver/views/account.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/views/account.py 2012-04-23 09:15:24 +0000
@@ -0,0 +1,38 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Account views."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = [
+ "login",
+ "logout",
+ ]
+
+from maasserver.models import UserProfile
+from django.contrib.auth.views import (
+ login as dj_login,
+ logout as dj_logout,
+ )
+from django.contrib import messages
+from django.conf import settings as django_settings
+from django.core.urlresolvers import reverse
+
+
+def login(request):
+ extra_context = {
+ 'no_users': UserProfile.objects.all_users().count() == 0,
+ 'create_command': django_settings.MAAS_CLI,
+ }
+ return dj_login(request, extra_context=extra_context)
+
+
+def logout(request):
+ messages.info(request, "You have been logged out.")
+ return dj_logout(request, next_page=reverse('login'))
Follow ups