Merge lp:~blake-rouse/maas/node-list-columns into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 2639
Proposed branch: lp:~blake-rouse/maas/node-list-columns
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 340 lines (+149/-38)
7 files modified
src/maasserver/models/macaddress.py (+1/-0)
src/maasserver/models/node.py (+14/-14)
src/maasserver/models/tests/test_node.py (+15/-6)
src/maasserver/templates/maasserver/nodes_listing.html (+44/-10)
src/maasserver/views/nodes.py (+59/-6)
src/maasserver/views/tags.py (+6/-2)
src/maasserver/views/tests/test_nodes.py (+10/-0)
To merge this branch: bzr merge lp:~blake-rouse/maas/node-list-columns
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+229033@code.launchpad.net

Commit message

New design of the nodes listing page.

Description of the change

New design of the nodes listing page.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Looks good, but some attention is needed before landing.

review: Approve
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Added comment, fixing others.

Revision history for this message
Gavin Panella (allenap) :
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (18.1 KiB)

The attempt to merge lp:~blake-rouse/maas/node-list-columns into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Hit http://security.ubuntu.com trusty-security Release.gpg
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Hit http://security.ubuntu.com trusty-security Release
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release
Hit http://security.ubuntu.com trusty-security/main Sources
Hit http://security.ubuntu.com trusty-security/universe Sources
Hit http://security.ubuntu.com trusty-security/main amd64 Packages
Hit http://security.ubuntu.com trusty-security/universe amd64 Packages
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Ign http://security.ubuntu.com trusty-security/main Translation-en_US
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Ign http://security.ubuntu.com trusty-security/universe Translation-en_US
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en_US
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make postgresql python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-netaddr python-netifaces python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-twisted py...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/models/macaddress.py'
2--- src/maasserver/models/macaddress.py 2014-07-02 10:41:48 +0000
3+++ src/maasserver/models/macaddress.py 2014-07-31 14:27:53 +0000
4@@ -69,6 +69,7 @@
5 class Meta(DefaultMeta):
6 verbose_name = "MAC address"
7 verbose_name_plural = "MAC addresses"
8+ ordering = ('created', )
9
10 def __unicode__(self):
11 address = self.mac_address
12
13=== modified file 'src/maasserver/models/node.py'
14--- src/maasserver/models/node.py 2014-07-31 13:43:31 +0000
15+++ src/maasserver/models/node.py 2014-07-31 14:27:53 +0000
16@@ -837,20 +837,20 @@
17 self.clean_status()
18
19 def display_status(self):
20- """Return status text as displayed to the user.
21-
22- The UI representation is taken from NODE_STATUS_CHOICES_DICT and may
23- interpolate the variable "owner" to reflect the username of the node's
24- current owner, if any.
25- """
26- status_text = NODE_STATUS_CHOICES_DICT[self.status]
27- if self.status == NODE_STATUS.ALLOCATED:
28- # The User is represented as its username in interpolation.
29- # Don't just say self.owner.username here, or there will be
30- # trouble with unowned nodes!
31- return "%s to %s" % (status_text, self.owner)
32- else:
33- return status_text
34+ """Return status text as displayed to the user."""
35+ return NODE_STATUS_CHOICES_DICT[self.status]
36+
37+ def display_memory(self):
38+ """Return memory in GB."""
39+ if self.memory < 1024:
40+ return '%.1f' % (self.memory / 1024.0)
41+ return '%d' % (self.memory / 1024)
42+
43+ def display_storage(self):
44+ """Return storage in GB."""
45+ if self.storage < 1024:
46+ return '%.1f' % (self.storage / 1024.0)
47+ return '%d' % (self.storage / 1024)
48
49 def add_mac_address(self, mac_address):
50 """Add a new MAC address to this `Node`.
51
52=== modified file 'src/maasserver/models/tests/test_node.py'
53--- src/maasserver/models/tests/test_node.py 2014-07-31 11:39:10 +0000
54+++ src/maasserver/models/tests/test_node.py 2014-07-31 14:27:53 +0000
55@@ -243,12 +243,21 @@
56 NODE_STATUS_CHOICES_DICT[node.status],
57 node.display_status())
58
59- def test_display_status_for_allocated_node_shows_owner(self):
60- node = factory.make_node(
61- owner=factory.make_user(), status=NODE_STATUS.ALLOCATED)
62- self.assertEqual(
63- "Allocated to %s" % node.owner.username,
64- node.display_status())
65+ def test_display_memory_returns_decimal_less_than_1024(self):
66+ node = factory.make_node(memory=512)
67+ self.assertEqual('0.5', node.display_memory())
68+
69+ def test_display_memory_returns_value_divided_by_1024(self):
70+ node = factory.make_node(memory=2048)
71+ self.assertEqual('2', node.display_memory())
72+
73+ def test_display_storage_returns_decimal_less_than_1024(self):
74+ node = factory.make_node(storage=512)
75+ self.assertEqual('0.5', node.display_storage())
76+
77+ def test_display_storage_returns_value_divided_by_1024(self):
78+ node = factory.make_node(storage=2048)
79+ self.assertEqual('2', node.display_storage())
80
81 def test_add_node_with_token(self):
82 user = factory.make_user()
83
84=== modified file 'src/maasserver/templates/maasserver/nodes_listing.html'
85--- src/maasserver/templates/maasserver/nodes_listing.html 2014-01-07 09:49:25 +0000
86+++ src/maasserver/templates/maasserver/nodes_listing.html 2014-07-31 14:27:53 +0000
87@@ -10,23 +10,48 @@
88 class="{{ sort_classes.hostname }}">
89 <acronym title="Fully Qualified Domain Name">FQDN</acronym>
90 </a></th>
91- <th>
92- <acronym
93- title="Media Access Control addresses">MAC</acronym>
94- </th>
95+ <th></th>
96 <th>
97 <a href="{{ sort_links.status }}"
98 class="{{ sort_classes.status }}">Status</a>
99 </th>
100 <th>
101+ <a href="{{ sort_links.owner }}"
102+ class="{{ sort_classes.owner }}">Owner</a>
103+ </th>
104+ <th>
105+ <a href="{{ sort_links.cpu_count }}"
106+ class="{{ sort_classes.cpu_count }}">Cores</a>
107+ </th>
108+ <th>
109+ <a href="{{ sort_links.memory }}"
110+ class="{{ sort_classes.memory }}">RAM (GB)</a>
111+ </th>
112+ <th>
113+ <a href="{{ sort_links.storage }}"
114+ class="{{ sort_classes.storage }}">Disk (GB)</a>
115+ </th>
116+ <th>
117+ <a href="{{ sort_links.primary_mac }}"
118+ class="{{ sort_classes.primary_mac }}">
119+ <acronym
120+ title="Media Access Control addresses">MAC</acronym>
121+ </a>
122+ </th>
123+ <th>
124 <a href="{{ sort_links.zone }}"
125 class="{{ sort_classes.zone }}">Zone</a>
126 </th>
127 {% else %}
128 <th><acronym title="Fully Qualified Domain Name">FQDN</acronym></th>
129+ <th></th>
130+ <th>Status</th>
131+ <th>Owner</th>
132+ <th>Cores</th>
133+ <th>RAM (GB)</th>
134+ <th>Disk (GB)</th>
135 <th><acronym
136 title="Media Access Control addresses">MAC</acronym></th>
137- <th>Status</th>
138 <th>Zone</th>
139 {% endif %}
140 </tr>
141@@ -43,14 +68,23 @@
142 <a href="{% url 'node-view' node.system_id %}">
143 {{ node.fqdn }}
144 </a>
145- {% if node.power_type == '' %} <img src="{{ STATIC_URL }}img/warning.png" title="No power type defined"/> {% endif %}
146 </td>
147+ <td>{% if node.power_type == '' %} <img src="{{ STATIC_URL }}img/warning.png" title="No power type defined"/> {% endif %}</td>
148+ <td>{{ node.display_status }}</td>
149+ <th>{{ node.owner|default_if_none:"" }}</th>
150+ <th>{{ node.cpu_count }}</th>
151+ <th>{{ node.display_memory }}</th>
152+ <th>{{ node.display_storage }}</th>
153 <td>
154- {% for macaddress in node.macaddress_set.all reversed %}
155- {{ macaddress }}{% if not forloop.last %},{% endif %}
156- {% endfor %}
157+ <span title="{{node.primary_mac_vendor}}">{{node.primary_mac}}</span>
158+ {% if node.extra_macs %}
159+ <span title="
160+ {% for mac in node.extra_macs %}
161+ {{mac}}
162+ {% endfor %}
163+ ">(+{{ node.extra_macs|length }})</span>
164+ {% endif %}
165 </td>
166- <td>{{ node.display_status }}</td>
167 <td class="zone-column">
168 <a href="{% url 'zone-view' node.zone.name %}">{{ node.zone }}</a>
169 </td>
170
171=== modified file 'src/maasserver/views/nodes.py'
172--- src/maasserver/views/nodes.py 2014-06-09 19:58:06 +0000
173+++ src/maasserver/views/nodes.py 2014-07-31 14:27:53 +0000
174@@ -25,6 +25,11 @@
175 ]
176
177 from cgi import escape
178+from operator import attrgetter
179+from netaddr import (
180+ EUI,
181+ NotRegisteredError,
182+ )
183 from textwrap import dedent
184 from urllib import urlencode
185
186@@ -192,11 +197,44 @@
187 return mark_safe("[\n%s\n]" % ',\n'.join(names))
188
189
190+def get_vendor_for_mac(mac):
191+ """Return vendor for MAC."""
192+ data = EUI(mac)
193+ try:
194+ return data.oui.registration().org
195+ except NotRegisteredError:
196+ return 'Unknown Vendor'
197+
198+
199+def configure_macs(nodes):
200+ """Configures the each node in the query to have an "macs" attribute,
201+ that contains a list of macs, sorted by created.
202+
203+ The list is structed to contain the MAC and its vendor.
204+ """
205+ for node in nodes:
206+ macs = node.macaddress_set.all()
207+ macs = sorted(macs, key=lambda mac: mac.created)
208+ macs = ['%s' % mac.mac_address for mac in macs]
209+ if len(macs) == 0:
210+ node.primary_mac = None
211+ node.primary_mac_vendor = None
212+ node.extra_macs = []
213+ else:
214+ node.primary_mac = macs[0]
215+ node.primary_mac_vendor = get_vendor_for_mac(node.primary_mac)
216+ node.extra_macs = macs[1:]
217+ return nodes
218+
219+
220 class NodeListView(PaginatedListView, FormMixin, ProcessFormView):
221
222 context_object_name = "node_list"
223 form_class = BulkNodeActionForm
224- sort_fields = ('hostname', 'status', 'zone')
225+ sort_fields = (
226+ 'hostname', 'status', 'owner', 'cpu_count',
227+ 'memory', 'storage', 'zone')
228+ late_sort_fields = ('primary_mac', )
229
230 def populate_modifiers(self, request):
231 self.query = request.GET.get("query")
232@@ -284,7 +322,6 @@
233 if self.sort_dir == 'desc':
234 custom_order = '-%s' % custom_order
235 order_by = (custom_order, )
236-
237 return order_by + ('-created', )
238
239 def _constrain_nodes(self, nodes_query):
240@@ -315,7 +352,8 @@
241 nodes = nodes.order_by(*self._compose_sort_order())
242 if self.query:
243 nodes = self._constrain_nodes(nodes)
244- return prefetch_nodes_listing(nodes)
245+ nodes = prefetch_nodes_listing(nodes)
246+ return configure_macs(nodes)
247
248 def _prepare_sort_links(self):
249 """Returns 2 dicts, with sort fields as keys and
250@@ -323,13 +361,14 @@
251 """
252
253 # Build relative URLs for the links, just with the params
254- links = {field: '?' for field in self.sort_fields}
255- classes = {field: 'sort-none' for field in self.sort_fields}
256+ fields = self.sort_fields + self.late_sort_fields
257+ links = {field: '?' for field in fields}
258+ classes = {field: 'sort-none' for field in fields}
259
260 params = self.request.GET.copy()
261 reverse_dir = 'asc' if self.sort_dir == 'desc' else 'desc'
262
263- for field in self.sort_fields:
264+ for field in fields:
265 params['sort'] = field
266 if field == self.sort_by:
267 params['dir'] = reverse_dir
268@@ -341,8 +380,22 @@
269
270 return links, classes
271
272+ def late_sort(self, context):
273+ """Sorts the node_list with sorting arguments that require
274+ late sorting.
275+ """
276+ node_list = context['node_list']
277+ reverse = (self.sort_dir == 'desc')
278+ if self.sort_by in self.late_sort_fields:
279+ node_list = sorted(
280+ node_list, key=attrgetter(self.sort_by),
281+ reverse=reverse)
282+ context['node_list'] = node_list
283+ return context
284+
285 def get_context_data(self, **kwargs):
286 context = super(NodeListView, self).get_context_data(**kwargs)
287+ context = self.late_sort(context)
288 context.update(get_longpoll_context())
289 form_class = self.get_form_class()
290 form = self.get_form(form_class)
291
292=== modified file 'src/maasserver/views/tags.py'
293--- src/maasserver/views/tags.py 2013-10-07 09:12:40 +0000
294+++ src/maasserver/views/tags.py 2014-07-31 14:27:53 +0000
295@@ -22,7 +22,10 @@
296 Tag,
297 )
298 from maasserver.views import PaginatedListView
299-from maasserver.views.nodes import prefetch_nodes_listing
300+from maasserver.views.nodes import (
301+ prefetch_nodes_listing,
302+ configure_macs,
303+ )
304
305
306 class TagView(PaginatedListView):
307@@ -43,7 +46,8 @@
308 user=self.request.user, perm=NODE_PERMISSION.VIEW,
309 from_nodes=self.tag.node_set.all())
310 nodes = nodes.order_by('-created')
311- return prefetch_nodes_listing(nodes)
312+ nodes = prefetch_nodes_listing(nodes)
313+ return configure_macs(nodes)
314
315 def get_context_data(self, **kwargs):
316 context = super(TagView, self).get_context_data(**kwargs)
317
318=== modified file 'src/maasserver/views/tests/test_nodes.py'
319--- src/maasserver/views/tests/test_nodes.py 2014-07-18 15:44:55 +0000
320+++ src/maasserver/views/tests/test_nodes.py 2014-07-31 14:27:53 +0000
321@@ -151,9 +151,19 @@
322 response = self.client.get(reverse('node-list'))
323 sort_hostname = '?sort=hostname&dir=asc'
324 sort_status = '?sort=status&dir=asc'
325+ sort_owner = '?sort=owner&dir=asc'
326+ sort_cpu_count = '?sort=cpu_count&dir=asc'
327+ sort_memory = '?sort=memory&dir=asc'
328+ sort_storage = '?sort=storage&dir=asc'
329+ sort_primary_mac = '?sort=primary_mac&dir=asc'
330 sort_zone = '?sort=zone&dir=asc'
331 self.assertIn(sort_hostname, get_content_links(response))
332 self.assertIn(sort_status, get_content_links(response))
333+ self.assertIn(sort_owner, get_content_links(response))
334+ self.assertIn(sort_cpu_count, get_content_links(response))
335+ self.assertIn(sort_memory, get_content_links(response))
336+ self.assertIn(sort_storage, get_content_links(response))
337+ self.assertIn(sort_primary_mac, get_content_links(response))
338 self.assertIn(sort_zone, get_content_links(response))
339
340 def test_node_list_ignores_unknown_sort_param(self):