← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/manage-mirrors into lp:maas

 

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

Commit message:
Add page to manage (delete or add) archives.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/manage-mirrors/+merge/133867

This branch changes the "Add custom archives" page into a "Manage custom archives pages": instead of simply featuring a form to add new archives, the page now also contains the list of the built-in and the custom archives along with a link to delete custom archives.

= Notes =

Screenshot: http://people.canonical.com/~rvb/settings-manage-archives.png

This was pre-imp'ed with Jeroen and we decided it would be good to forbid the deletion of the default (built-in) archives.

In case you wonder, the deleted test in src/maasserver/tests/test_views_settings.py has simply been moved to src/maasserver/tests/test_views_settings_manage_archives.py.
-- 
https://code.launchpad.net/~rvb/maas/manage-mirrors/+merge/133867
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/manage-mirrors into lp:maas.
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py	2012-11-09 17:09:07 +0000
+++ src/maasserver/forms.py	2012-11-12 08:54:21 +0000
@@ -691,15 +691,24 @@
 class AddArchiveForm(ConfigForm):
     archive_name = HostnameFormField(label="Archive name")
 
+    def clean_archive_name(self):
+        """Check that the archive does not already exist."""
+        archive_name = self.cleaned_data['archive_name']
+        archive_choice = [archive_name, archive_name]
+        archive_choices = Config.objects.get_config('archive_choices')
+        if archive_choice in archive_choices:
+            raise forms.ValidationError("Archive already exists.")
+        return archive_name
+
     def save(self):
         """Save the archive name in the Config table.
 
         This implementation of `save` does not support the `commit` argument.
         """
         archive_name = self.cleaned_data.get('archive_name')
-        archives = Config.objects.get_config('archive_choices')
-        archives.append([archive_name, archive_name])
-        Config.objects.set_config('archive_choices', archives)
+        archive_choices = Config.objects.get_config('archive_choices')
+        archive_choices.append([archive_name, archive_name])
+        Config.objects.set_config('archive_choices', archive_choices)
 
 
 class NodeGroupInterfaceForm(ModelForm):

=== modified file 'src/maasserver/models/config.py'
--- src/maasserver/models/config.py	2012-11-09 09:11:51 +0000
+++ src/maasserver/models/config.py	2012-11-12 08:54:21 +0000
@@ -34,6 +34,23 @@
 from provisioningserver.enum import POWER_TYPE
 
 
+# Built-in archives that can't be removed.
+builtin_archive_choices = [
+    [
+        'http://archive.ubuntu.com/ubuntu',
+        'http://archive.ubuntu.com/ubuntu',
+    ],
+    [
+        'http://ports.ubuntu.com/ubuntu-ports',
+        'http://ports.ubuntu.com/ubuntu-ports',
+    ],
+    [
+        'https://maas.ubuntu.com/images',
+        'https://maas.ubuntu.com/images',
+    ],
+]
+
+
 def get_default_config():
     return {
         ## settings default values.
@@ -45,22 +62,7 @@
         'fallback_master_archive': False,
         'keep_mirror_list_uptodate': False,
         'fetch_new_releases': False,
-        'archive_choices': (
-            [
-                [
-                    'http://archive.ubuntu.com/ubuntu',
-                    'http://archive.ubuntu.com/ubuntu',
-                ],
-                [
-                    'http://ports.ubuntu.com/ubuntu-ports',
-                    'http://ports.ubuntu.com/ubuntu-ports',
-                ],
-                [
-                    'https://maas.ubuntu.com/images',
-                    'https://maas.ubuntu.com/images',
-                ],
-            ]
-        ),
+        'archive_choices': builtin_archive_choices,
         'main_archive': 'http://archive.ubuntu.com/ubuntu',
         'ports_archive': 'http://ports.ubuntu.com/ubuntu-ports',
         'cloud_images_archive': 'https://maas.ubuntu.com/images',

=== modified file 'src/maasserver/templates/maasserver/settings.html'
--- src/maasserver/templates/maasserver/settings.html	2012-11-09 09:11:51 +0000
+++ src/maasserver/templates/maasserver/settings.html	2012-11-12 08:54:21 +0000
@@ -108,10 +108,10 @@
           {% include "maasserver/form_field.html" %}
         {% endwith %}
         <li>
-          <a href="{% url "settings-add-archive" %}">
+          <a href="{% url "settings-manage-archives" %}">
             <img src="{{ STATIC_URL }}img/inline_add.png"
                  alt="Add" class="icon" />
-            Add custom archive
+            Manage custom archives
           </a>
         </li>
         </ul>

=== added file 'src/maasserver/templates/maasserver/settings_archive_delete.html'
--- src/maasserver/templates/maasserver/settings_archive_delete.html	1970-01-01 00:00:00 +0000
+++ src/maasserver/templates/maasserver/settings_archive_delete.html	2012-11-12 08:54:21 +0000
@@ -0,0 +1,23 @@
+{% extends "maasserver/base.html" %}
+
+{% block nav-active-settings %}active{% endblock %}
+{% block title %}Delete Archive{% endblock %}
+{% block page-title %}Delete Archive{% endblock %}
+
+{% block content %}
+  <div class="block auto-width">
+    <h2>
+      Are you sure you want to delete the archive "{{ archive }}"?
+    </h2>
+    <p>This action is permanent and can not be undone.</p>
+    <p>
+    <form action="{% url 'settings-delete-archive'%}?archive={{archive|urlencode:""}}"
+          id='delete-archive'
+          method="post">{% csrf_token %}
+      <input type="submit" value="Delete archive" class="right" />
+      <a href="{% url 'settings-manage-archives' %}">Cancel</a>
+    </form>
+    </p>
+  </div>
+{% endblock %}
+

=== renamed file 'src/maasserver/templates/maasserver/settings_add_archive.html' => 'src/maasserver/templates/maasserver/settings_manage_archives.html'
--- src/maasserver/templates/maasserver/settings_add_archive.html	2012-06-28 12:05:09 +0000
+++ src/maasserver/templates/maasserver/settings_manage_archives.html	2012-11-12 08:54:21 +0000
@@ -1,10 +1,37 @@
 {% extends "maasserver/base.html" %}
 
 {% block nav-active-settings %}active{% endblock %}
-{% block title %}Add archive{% endblock %}
-{% block page-title %}Add archive{% endblock %}
+{% block title %}Manage archives{% endblock %}
+{% block page-title %}Manage archives{% endblock %}
 
 {% block content %}
+  <div class="block size7 first">
+  <h2>Manage archives</h2>
+    <h3>Built-in archives</h2>
+    <table class="list" id="builtin-archive-list">
+    {% for archive, _ in builtin_archive_choices %}
+      <tr class="{% cycle 'even' 'odd' %}">
+        <td class="archive">{{ archive }}</td>
+      </tr>
+    {% endfor %}
+    </table>
+    <h3>Custom archives</h3>
+    <table class="list" id="custom-archive-list">
+    {% for archive, _ in custom_archive_choices %}
+      <tr class="{% cycle 'even' 'odd' %}">
+        <td class="archive">{{ archive }}</td>
+        <td>
+        <a title="Delete archive {{ archive }}"
+           href="{% url 'settings-delete-archive'%}?archive={{archive|urlencode:""}}">
+           <img src="{{ STATIC_URL }}img/delete.png" alt="delete" />
+        </a>
+        </td>
+      </tr>
+    {% empty %}
+      No custom archive.
+    {% endfor %}
+    </table>
+  <h2>Add custom archive</h2>
   <form action="." method="post" class="block auto-width">
     {% csrf_token %}
     <ul>
@@ -15,4 +42,5 @@
     <input type="submit" class="right" value="Add archive" />
     <a class="link-button" href="{% url 'settings' %}">Cancel</a>
   </form>
+  </div>
 {% endblock %}

=== modified file 'src/maasserver/tests/test_forms.py'
--- src/maasserver/tests/test_forms.py	2012-11-09 17:07:11 +0000
+++ src/maasserver/tests/test_forms.py	2012-11-12 08:54:21 +0000
@@ -30,6 +30,7 @@
     NODEGROUPINTERFACE_MANAGEMENT,
     )
 from maasserver.forms import (
+    AddArchiveForm,
     AdminNodeForm,
     AdminNodeWithMACAddressesForm,
     ConfigForm,
@@ -992,3 +993,14 @@
         self.assertTrue(form.is_valid())
         form.save()
         self.assertEqual(data['name'], reload_object(nodegroup).name)
+
+
+class TestAddArchiveForm(TestCase):
+
+    def test_refuses_duplicate_archives(self):
+        archive_choices = Config.objects.get_config('archive_choices')
+        existing_archive = archive_choices[0][0]
+        form = AddArchiveForm(data={'archive_name': existing_archive})
+        self.assertFalse(form.is_valid())
+        self.assertEqual(
+            "Archive already exists.", form._errors['archive_name'][0])

=== modified file 'src/maasserver/tests/test_views_settings.py'
--- src/maasserver/tests/test_views_settings.py	2012-11-09 09:11:51 +0000
+++ src/maasserver/tests/test_views_settings.py	2012-11-12 08:54:21 +0000
@@ -171,19 +171,6 @@
                 Config.objects.get_config('default_distro_series'),
             ))
 
-    def test_settings_add_archive_POST(self):
-        choices = Config.objects.get_config('archive_choices')
-        response = self.client.post(
-            '/settings/archives/add/',
-            data={'archive_name': 'http://my.hostname.com'}
-        )
-        new_choices = Config.objects.get_config('archive_choices')
-
-        self.assertEqual(httplib.FOUND, response.status_code, response.content)
-        self.assertItemsEqual(
-            choices + [['http://my.hostname.com', 'http://my.hostname.com']],
-            new_choices)
-
     def test_settings_kernelopts_POST(self):
         new_kernel_opts = "--new='arg' --flag=1 other"
         response = self.client.post(

=== added file 'src/maasserver/tests/test_views_settings_manage_archives.py'
--- src/maasserver/tests/test_views_settings_manage_archives.py	1970-01-01 00:00:00 +0000
+++ src/maasserver/tests/test_views_settings_manage_archives.py	2012-11-12 08:54:21 +0000
@@ -0,0 +1,143 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test maasserver settings views."""
+
+from __future__ import (
+    absolute_import,
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = []
+
+import httplib
+from urllib import urlencode
+
+from django.core.urlresolvers import reverse
+from lxml.html import fromstring
+from maasserver.models import Config
+from maasserver.models.config import builtin_archive_choices
+from maasserver.testing import extract_redirect
+from maasserver.testing.testcase import AdminLoggedInTestCase
+
+
+def setup_custom_archives():
+    custom_archives = [
+        'http://my.hostname.com/ubuntu',
+        'https://example.com/custom/archive'
+        ]
+    archive_choices = Config.objects.get_config('archive_choices')
+    for custom_archive in custom_archives:
+        archive_choices.append([custom_archive, custom_archive])
+    Config.objects.set_config('archive_choices', archive_choices)
+    return custom_archives
+
+
+class SettingsManageArchiveTest(AdminLoggedInTestCase):
+
+    def test_page_displays_built_in_archives(self):
+        archives = [choice[0] for choice in builtin_archive_choices]
+        response = self.client.get(reverse('settings-manage-archives'))
+        document = fromstring(response.content)
+        archive_list = [
+            arch_row.text_content()
+            for arch_row in document.xpath(
+                "//table[@id='builtin-archive-list']//td[@class='archive']")]
+        self.assertItemsEqual(archives, archive_list)
+
+    def test_page_displays_custom_archives(self):
+        custom_archives = setup_custom_archives()
+        response = self.client.get(reverse('settings-manage-archives'))
+        document = fromstring(response.content)
+        archive_list = [
+            arch_row.text_content()
+            for arch_row in document.xpath(
+                "//table[@id='custom-archive-list']//td[@class='archive']")]
+        self.assertItemsEqual(custom_archives, archive_list)
+
+    def test_page_displays_links_to_delete_custom_archives(self):
+        custom_archives = setup_custom_archives()
+        response = self.client.get(reverse('settings-manage-archives'))
+        document = fromstring(response.content)
+        link_list = [
+            link for link in document.xpath(
+                "//table[@id='custom-archive-list']//td/a/@href")]
+        delete_links = [
+            '%s?%s' % (
+                reverse('settings-delete-archive'),
+                urlencode({'archive': archive}))
+            for archive in custom_archives
+            ]
+        self.assertItemsEqual(delete_links, link_list)
+
+    def test_page_adds_archive_POST(self):
+        choices = Config.objects.get_config('archive_choices')
+        response = self.client.post(
+            reverse('settings-manage-archives'),
+            data={'archive_name': 'http://my.hostname.com'}
+        )
+        new_choices = Config.objects.get_config('archive_choices')
+
+        self.assertEqual(httplib.FOUND, response.status_code, response.content)
+        self.assertItemsEqual(
+            choices + [['http://my.hostname.com', 'http://my.hostname.com']],
+            new_choices)
+
+
+class SettingsDeleteArchiveTest(AdminLoggedInTestCase):
+
+    def test_page_delete_archive_POST(self):
+        custom_archives = setup_custom_archives()
+        archive_choices = Config.objects.get_config('archive_choices')
+        archive_to_delete = custom_archives.pop()
+        delete_link = '%s?%s' % (
+            reverse('settings-delete-archive'),
+            urlencode({'archive': archive_to_delete}))
+        response = self.client.post(delete_link)
+        self.assertEqual(httplib.FOUND, response.status_code, response.content)
+        archive_choices.remove([archive_to_delete, archive_to_delete]),
+        self.assertItemsEqual(
+            archive_choices,
+            Config.objects.get_config('archive_choices'))
+
+    def test_page_posts_to_self(self):
+        custom_archives = setup_custom_archives()
+        archive_to_delete = custom_archives.pop()
+        delete_link = '%s?%s' % (
+            reverse('settings-delete-archive'),
+            urlencode({'archive': archive_to_delete}))
+        response = self.client.get(delete_link)
+        document = fromstring(response.content)
+        form_post_links = document.xpath(
+            "//form[@id='delete-archive']/@action")
+        self.assertEqual(
+            (httplib.OK, [delete_link]),
+            (response.status_code, form_post_links))
+
+    def test_page_errors_when_deleting_unknown_archive(self):
+        delete_link = '%s?%s' % (
+            reverse('settings-delete-archive'),
+            urlencode({'archive': 'http://unknown.archive'}))
+        response = self.client.post(delete_link)
+        response = self.client.get(extract_redirect(response))
+        self.assertEqual(
+            ["Unknown archive: 'http://unknown.archive'."],
+            [message.message for message in response.context['messages']])
+
+    def test_page_errors_when_deleting_builtin_archive(self):
+        delete_link = '%s?%s' % (
+            reverse('settings-delete-archive'),
+            urlencode({'archive': builtin_archive_choices[0][0]}))
+        response = self.client.post(delete_link)
+        response = self.client.get(extract_redirect(response))
+        self.assertEqual(
+            (
+                ["Cannot remove a built-in archive."],
+                builtin_archive_choices,
+            ),
+            (
+                [message.message for message in response.context['messages']],
+                Config.objects.get_config('archive_choices'),
+            ))

=== modified file 'src/maasserver/urls.py'
--- src/maasserver/urls.py	2012-10-15 09:32:06 +0000
+++ src/maasserver/urls.py	2012-11-12 08:54:21 +0000
@@ -46,7 +46,8 @@
     AccountsEdit,
     AccountsView,
     settings,
-    settings_add_archive,
+    settings_delete_archive,
+    settings_manage_archives,
     )
 from maasserver.views.settings_clusters import (
     ClusterDelete,
@@ -158,8 +159,11 @@
     # /XXX
     adminurl(r'^settings/$', settings, name='settings'),
     adminurl(
-        r'^settings/archives/add/$', settings_add_archive,
-        name='settings-add-archive'),
+        r'^settings/archives/manage/$', settings_manage_archives,
+        name='settings-manage-archives'),
+    adminurl(
+        r'^settings/archives/delete/$',
+        settings_delete_archive, name='settings-delete-archive'),
     adminurl(r'^accounts/add/$', AccountsAdd.as_view(), name='accounts-add'),
     adminurl(
         r'^accounts/(?P<username>\w+)/edit/$', AccountsEdit.as_view(),

=== modified file 'src/maasserver/views/settings.py'
--- src/maasserver/views/settings.py	2012-11-08 10:40:48 +0000
+++ src/maasserver/views/settings.py	2012-11-12 08:54:21 +0000
@@ -16,7 +16,8 @@
     "AccountsEdit",
     "AccountsView",
     "settings",
-    "settings_add_archive",
+    "settings_manage_archives",
+    "settings_delete_archive",
     ]
 
 from django.contrib import messages
@@ -43,15 +44,17 @@
     AddArchiveForm,
     CommissioningForm,
     EditUserForm,
+    GlobalKernelOptsForm,
     MAASAndNetworkForm,
     NewUserCreationForm,
     UbuntuForm,
-    GlobalKernelOptsForm,
     )
 from maasserver.models import (
+    Config,
     NodeGroup,
     UserProfile,
     )
+from maasserver.models.config import builtin_archive_choices
 from maasserver.views import process_form
 
 
@@ -230,17 +233,55 @@
         context_instance=RequestContext(request))
 
 
-def settings_add_archive(request):
+def settings_manage_archives(request):
+    archive_choices = Config.objects.get_config('archive_choices')
+    custom_archive_choices = [
+        archive_choice for archive_choice in archive_choices
+        if archive_choice not in builtin_archive_choices]
     if request.method == 'POST':
         form = AddArchiveForm(request.POST)
         if form.is_valid():
             form.save()
             messages.info(request, "Archive added.")
-            return HttpResponseRedirect(reverse('settings'))
+            return HttpResponseRedirect(reverse('settings-manage-archives'))
     else:
         form = AddArchiveForm()
-
-    return render_to_response(
-        'maasserver/settings_add_archive.html',
-        {'form': form},
+    context = {
+        'form': form,
+        'custom_archive_choices': custom_archive_choices,
+        'builtin_archive_choices': builtin_archive_choices,
+        }
+    return render_to_response(
+        'maasserver/settings_manage_archives.html',
+        context,
+        context_instance=RequestContext(request))
+
+
+def settings_delete_archive(request):
+    archive_choices = Config.objects.get_config('archive_choices')
+    archive = request.GET.get('archive')
+    archive_choice = [archive, archive]
+
+    # Check that the selected archive is not a built-in archive.
+    if archive_choice in builtin_archive_choices:
+        messages.info(request, "Cannot remove a built-in archive.")
+        return HttpResponseRedirect(reverse('settings-manage-archives'))
+
+    # Check that the selected archive is in the list of archives.
+    if archive_choice not in archive_choices:
+        messages.info(request, "Unknown archive: '%s'." % archive)
+        return HttpResponseRedirect(reverse('settings-manage-archives'))
+
+    if request.method == 'POST':
+        archive_choices.remove([archive, archive])
+        Config.objects.set_config('archive_choices', archive_choices)
+        messages.info(request, "Archive removed.")
+        return HttpResponseRedirect(reverse('settings-manage-archives'))
+
+    context = {
+        'archive': archive,
+        }
+    return render_to_response(
+        'maasserver/settings_archive_delete.html',
+        context,
         context_instance=RequestContext(request))