Merge lp:~julian-edwards/maas/backport-r1294-1299 into lp:maas/1.2

Proposed by Julian Edwards
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 1324
Proposed branch: lp:~julian-edwards/maas/backport-r1294-1299
Merge into: lp:maas/1.2
Diff against target: 328 lines (+189/-9)
6 files modified
src/maasserver/static/css/components/table_list.css (+13/-0)
src/maasserver/templates/maasserver/node_list.html (+1/-1)
src/maasserver/templates/maasserver/nodes_listing.html (+16/-0)
src/maasserver/templates/maasserver/tag_view.html (+1/-1)
src/maasserver/tests/test_views_nodes.py (+115/-5)
src/maasserver/views/nodes.py (+43/-2)
To merge this branch: bzr merge lp:~julian-edwards/maas/backport-r1294-1299
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+137493@code.launchpad.net

Commit message

Fix bug #994887: Nodes list view now supports sorting by hostname(mac) or status columns. (backport r1294 and r1299 from trunk)

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/static/css/components/table_list.css'
2--- src/maasserver/static/css/components/table_list.css 2012-04-05 06:23:59 +0000
3+++ src/maasserver/static/css/components/table_list.css 2012-12-03 07:15:29 +0000
4@@ -30,6 +30,19 @@
5 border-color: #dd4814;
6 cursor: pointer;
7 }
8+.sort-none,
9+.sort-asc,
10+.sort-desc {
11+ background-repeat: no-repeat;
12+ background-position: right center;
13+ padding-right: 16px;
14+ }
15+.sort-asc {
16+ background-image: url('../?img/sort_ascending.png');
17+ }
18+.sort-desc {
19+ background-image: url('../?img/sort_descending.png');
20+ }
21 /* ul list */
22 ul.list {
23 list-style: none;
24
25=== added file 'src/maasserver/static/img/sort_ascending.png'
26Binary files src/maasserver/static/img/sort_ascending.png 1970-01-01 00:00:00 +0000 and src/maasserver/static/img/sort_ascending.png 2012-12-03 07:15:29 +0000 differ
27=== added file 'src/maasserver/static/img/sort_descending.png'
28Binary files src/maasserver/static/img/sort_descending.png 1970-01-01 00:00:00 +0000 and src/maasserver/static/img/sort_descending.png 2012-12-03 07:15:29 +0000 differ
29=== modified file 'src/maasserver/templates/maasserver/node_list.html'
30--- src/maasserver/templates/maasserver/node_list.html 2012-10-17 05:20:13 +0000
31+++ src/maasserver/templates/maasserver/node_list.html 2012-12-03 07:15:29 +0000
32@@ -38,7 +38,7 @@
33 {% if input_query_error %}
34 <p class="form-errors">{{input_query_error}}</p>
35 {% endif %}
36- {% include "maasserver/nodes_listing.html" %}
37+ {% include "maasserver/nodes_listing.html" with sorting="true" %}
38 {% include "maasserver/pagination.html" %}
39 <a id="addnode" href="#" class="button right space-top">+ Add node</a>
40 <div class="clear"></div>
41
42=== modified file 'src/maasserver/templates/maasserver/nodes_listing.html'
43--- src/maasserver/templates/maasserver/nodes_listing.html 2012-11-16 13:30:09 +0000
44+++ src/maasserver/templates/maasserver/nodes_listing.html 2012-12-03 07:15:29 +0000
45@@ -2,9 +2,25 @@
46 <table class="list">
47 <thead>
48 <tr>
49+ {% if sorting == "true" %}
50+ <th><a href="{{ sort_links.hostname }}"
51+ class="{{ sort_classes.hostname }}">
52+ <acronym title="Fully Qualified Domain Name">FQDN</acronym>
53+ </a></th>
54+ <th>
55+ <acronym
56+ title="Media Access Control addresses">MAC</acronym>
57+ </th>
58+ <th>
59+ <a href="{{ sort_links.status }}"
60+ class="{{ sort_classes.status }}">Status</a>
61+ </th>
62+ {% else %}
63 <th><acronym title="Fully Qualified Domain Name">FQDN</acronym></th>
64 <th><acronym
65 title="Media Access Control addresses">MAC</acronym></th>
66+ <th>Status</th>
67+ {% endif %}
68 </tr>
69 </thead>
70 {% for node in node_list %}
71
72=== modified file 'src/maasserver/templates/maasserver/tag_view.html'
73--- src/maasserver/templates/maasserver/tag_view.html 2012-11-07 11:32:16 +0000
74+++ src/maasserver/templates/maasserver/tag_view.html 2012-12-03 07:15:29 +0000
75@@ -34,7 +34,7 @@
76 </ul>
77 <div id="nodes" class="block first pad-top">
78 <h2 class="block first line-top pad-top">{{ paginator.count }} Node{{ paginator.count|pluralize }}</h2>
79- {% include "maasserver/nodes_listing.html" %}
80+ {% include "maasserver/nodes_listing.html" with sorting="false" %}
81 {% include "maasserver/pagination.html" %}
82 </div>
83 {% endblock %}
84
85=== modified file 'src/maasserver/tests/test_views_nodes.py'
86--- src/maasserver/tests/test_views_nodes.py 2012-11-29 13:22:20 +0000
87+++ src/maasserver/tests/test_views_nodes.py 2012-12-03 07:15:29 +0000
88@@ -13,7 +13,12 @@
89 __all__ = []
90
91 import httplib
92+from operator import attrgetter
93 from unittest import skip
94+from urlparse import (
95+ parse_qsl,
96+ urlparse,
97+ )
98
99 from django.conf import settings
100 from django.core.urlresolvers import reverse
101@@ -81,6 +86,15 @@
102 enlist_preseed_link = reverse('enlist-preseed-view')
103 self.assertIn(enlist_preseed_link, get_content_links(response))
104
105+ def test_node_list_contains_column_sort_links(self):
106+ # Just create a node to have something in the list
107+ factory.make_node()
108+ response = self.client.get(reverse('node-list'))
109+ sort_hostname = '?sort=hostname&dir=asc'
110+ sort_status = '?sort=status&dir=asc'
111+ self.assertIn(sort_hostname, get_content_links(response))
112+ self.assertIn(sort_status, get_content_links(response))
113+
114 def test_node_list_lists_nodes_from_different_nodegroups(self):
115 # Bug 1084443.
116 nodegroup1 = factory.make_node_group()
117@@ -91,6 +105,99 @@
118 response = self.client.get(reverse('node-list'))
119 self.assertEqual(httplib.OK, response.status_code)
120
121+ def test_node_list_sorts_by_hostname(self):
122+ names = ['zero', 'one', 'five']
123+ nodes = [factory.make_node(hostname=n) for n in names]
124+
125+ # First check the ascending sort order
126+ sorted_nodes = sorted(nodes, key=attrgetter('hostname'))
127+ response = self.client.get(
128+ reverse('node-list'), {
129+ 'sort': 'hostname',
130+ 'dir': 'asc'})
131+ node_links = [
132+ reverse('node-view', args=[node.system_id])
133+ for node in sorted_nodes]
134+ self.assertEqual(
135+ node_links,
136+ [link for link in get_content_links(response)
137+ if link.startswith('/nodes/node')])
138+
139+ # Now check the reverse order
140+ node_links = list(reversed(node_links))
141+ response = self.client.get(
142+ reverse('node-list'), {
143+ 'sort': 'hostname',
144+ 'dir': 'desc'})
145+ self.assertEqual(
146+ node_links,
147+ [link for link in get_content_links(response)
148+ if link.startswith('/nodes/node')])
149+
150+ def test_node_list_sorts_by_status(self):
151+ statuses = {
152+ NODE_STATUS.READY,
153+ NODE_STATUS.DECLARED,
154+ NODE_STATUS.FAILED_TESTS,
155+ }
156+ nodes = [factory.make_node(status=s) for s in statuses]
157+
158+ # First check the ascending sort order
159+ sorted_nodes = sorted(nodes, key=attrgetter('status'))
160+ response = self.client.get(
161+ reverse('node-list'), {
162+ 'sort': 'status',
163+ 'dir': 'asc'})
164+ node_links = [
165+ reverse('node-view', args=[node.system_id])
166+ for node in sorted_nodes]
167+ self.assertEqual(
168+ node_links,
169+ [link for link in get_content_links(response)
170+ if link.startswith('/nodes/node')])
171+
172+ # Now check the reverse order
173+ node_links = list(reversed(node_links))
174+ response = self.client.get(
175+ reverse('node-list'), {
176+ 'sort': 'status',
177+ 'dir': 'desc'})
178+ self.assertEqual(
179+ node_links,
180+ [link for link in get_content_links(response)
181+ if link.startswith('/nodes/node')])
182+
183+ def test_node_list_sort_preserves_other_params(self):
184+ # Set a very small page size to save creating lots of nodes
185+ page_size = 2
186+ self.patch(nodes_views.NodeListView, 'paginate_by', page_size)
187+
188+ nodes = []
189+ tag = factory.make_tag('shiny')
190+ for name in ('bbb', 'ccc', 'ddd', 'aaa'):
191+ node = factory.make_node(hostname=name)
192+ node.tags = [tag]
193+ nodes.append(node)
194+
195+ params = {
196+ 'sort': 'hostname',
197+ 'dir': 'asc',
198+ 'page': '1',
199+ 'query': 'maas-tags=shiny'}
200+ response = self.client.get(reverse('node-list'), params)
201+ document = fromstring(response.content)
202+ header_links = document.xpath("//div[@id='nodes']/table//th/a/@href")
203+ fields = iter(('hostname', 'status'))
204+ field_dirs = iter(('desc', 'asc'))
205+ for link in header_links:
206+ self.assertThat(
207+ parse_qsl(urlparse(link).query),
208+ ContainsAll([
209+ ('page', '1'),
210+ ('query', 'maas-tags=shiny'),
211+ ('sort', next(fields)),
212+ ('dir', next(field_dirs))]))
213+
214 def test_node_list_displays_fqdn_dns_not_managed(self):
215 nodes = [factory.make_node() for i in range(3)]
216 response = self.client.get(reverse('node-list'))
217@@ -448,7 +555,8 @@
218 {"query": "maas-tags=shiny cpu=2"})
219 node2_link = reverse('node-view', args=[node2.system_id])
220 document = fromstring(response.content)
221- node_links = document.xpath("//div[@id='nodes']/table//a/@href")
222+ node_links = document.xpath(
223+ "//div[@id='nodes']/table//td/a/@href")
224 self.assertEqual([node2_link], node_links)
225
226 def test_node_list_paginates(self):
227@@ -461,7 +569,7 @@
228 # Order node links with newest first as the view is expected to
229 node_links = [reverse('node-view', args=[node.system_id])
230 for node in reversed(nodes)]
231- expr_node_links = XPath("//div[@id='nodes']/table//a/@href")
232+ expr_node_links = XPath("//div[@id='nodes']/table//td/a/@href")
233 expr_page_anchors = XPath("//div[@class='pagination']//a")
234 # Fetch first page, should link newest two nodes and page 2
235 response = self.client.get(reverse('node-list'))
236@@ -473,7 +581,8 @@
237 # Fetch second page, should link next nodes and adjacent pages
238 response = self.client.get(reverse('node-list'), {"page": 2})
239 page2 = fromstring(response.content)
240- self.assertEqual(node_links[page_size:page_size * 2],
241+ self.assertEqual(
242+ node_links[page_size:page_size * 2],
243 expr_node_links(page2))
244 self.assertEqual([("first", "."), ("previous", "."),
245 ("next", "?page=3"), ("last", "?page=3")],
246@@ -501,8 +610,9 @@
247 {"query": "maas-tags=odd", "page": 3})
248 document = fromstring(response.content)
249 self.assertIn("5 matching nodes", document.xpath("string(//h1)"))
250- self.assertEqual([last_node_link],
251- document.xpath("//div[@id='nodes']/table//a/@href"))
252+ self.assertEqual(
253+ [last_node_link],
254+ document.xpath("//div[@id='nodes']/table//td/a/@href"))
255 self.assertEqual([("first", "?query=maas-tags%3Dodd"),
256 ("previous", "?query=maas-tags%3Dodd&page=2")],
257 [(a.text.lower(), a.get("href"))
258
259=== modified file 'src/maasserver/views/nodes.py'
260--- src/maasserver/views/nodes.py 2012-11-29 13:22:20 +0000
261+++ src/maasserver/views/nodes.py 2012-12-03 07:15:29 +0000
262@@ -110,13 +110,26 @@
263 def get(self, request, *args, **kwargs):
264 self.query = request.GET.get("query")
265 self.query_error = None
266+ self.sort_by = request.GET.get("sort")
267+ self.sort_dir = request.GET.get("dir")
268+
269 return super(NodeListView, self).get(request, *args, **kwargs)
270
271 def get_queryset(self):
272- # Return node list sorted, newest first.
273+ # Default sort - newest first, unless sorting params are
274+ # present. In addition, to ensure order consistency, when
275+ # sorting by non-unique fields (like status), we always
276+ # sort by the unique creation date as well
277+ if self.sort_by is not None:
278+ prefix = '-' if self.sort_dir == 'desc' else ''
279+ order_by = (prefix + self.sort_by, '-created')
280+ else:
281+ order_by = ('-created', )
282+
283+ # Return the sorted node list
284 nodes = Node.objects.get_nodes(
285 user=self.request.user, prefetch_mac=True,
286- perm=NODE_PERMISSION.VIEW,).order_by('-created')
287+ perm=NODE_PERMISSION.VIEW,).order_by(*order_by)
288 if self.query:
289 try:
290 return constrain_nodes(nodes, _parse_constraints(self.query))
291@@ -127,11 +140,39 @@
292 nodes = nodes.prefetch_related('nodegroup__nodegroupinterface_set')
293 return nodes
294
295+ def _prepare_sort_links(self):
296+ """Returns 2 dicts, with sort fields as keys and
297+ links and CSS classes for the that field.
298+ """
299+
300+ fields = ('hostname', 'status')
301+ # Build relative URLs for the links, just with the params
302+ links = {field: '?' for field in fields}
303+ classes = {field: 'sort-none' for field in fields}
304+
305+ params = self.request.GET.copy()
306+ reverse_dir = 'asc' if self.sort_dir == 'desc' else 'desc'
307+
308+ for field in fields:
309+ params['sort'] = field
310+ if field == self.sort_by:
311+ params['dir'] = reverse_dir
312+ classes[field] = 'sort-%s' % self.sort_dir
313+ else:
314+ params['dir'] = 'asc'
315+
316+ links[field] += params.urlencode()
317+
318+ return links, classes
319+
320 def get_context_data(self, **kwargs):
321 context = super(NodeListView, self).get_context_data(**kwargs)
322 context.update(get_longpoll_context())
323 context["input_query"] = self.query
324 context["input_query_error"] = self.query_error
325+ links, classes = self._prepare_sort_links()
326+ context["sort_links"] = links
327+ context["sort_classes"] = classes
328 return context
329
330

Subscribers

People subscribed via source and target branches

to status/vote changes: