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
=== modified file 'src/maasserver/static/css/components/table_list.css'
--- src/maasserver/static/css/components/table_list.css 2012-04-05 06:23:59 +0000
+++ src/maasserver/static/css/components/table_list.css 2012-12-03 07:15:29 +0000
@@ -30,6 +30,19 @@
30 border-color: #dd4814;30 border-color: #dd4814;
31 cursor: pointer;31 cursor: pointer;
32 }32 }
33.sort-none,
34.sort-asc,
35.sort-desc {
36 background-repeat: no-repeat;
37 background-position: right center;
38 padding-right: 16px;
39 }
40.sort-asc {
41 background-image: url('../?img/sort_ascending.png');
42 }
43.sort-desc {
44 background-image: url('../?img/sort_descending.png');
45 }
33/* ul list */46/* ul list */
34ul.list {47ul.list {
35 list-style: none;48 list-style: none;
3649
=== added file 'src/maasserver/static/img/sort_ascending.png'
37Binary 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 differ50Binary 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
=== added file 'src/maasserver/static/img/sort_descending.png'
38Binary 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 differ51Binary 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
=== modified file 'src/maasserver/templates/maasserver/node_list.html'
--- src/maasserver/templates/maasserver/node_list.html 2012-10-17 05:20:13 +0000
+++ src/maasserver/templates/maasserver/node_list.html 2012-12-03 07:15:29 +0000
@@ -38,7 +38,7 @@
38 {% if input_query_error %}38 {% if input_query_error %}
39 <p class="form-errors">{{input_query_error}}</p>39 <p class="form-errors">{{input_query_error}}</p>
40 {% endif %}40 {% endif %}
41 {% include "maasserver/nodes_listing.html" %}41 {% include "maasserver/nodes_listing.html" with sorting="true" %}
42 {% include "maasserver/pagination.html" %}42 {% include "maasserver/pagination.html" %}
43 <a id="addnode" href="#" class="button right space-top">+ Add node</a>43 <a id="addnode" href="#" class="button right space-top">+ Add node</a>
44 <div class="clear"></div>44 <div class="clear"></div>
4545
=== modified file 'src/maasserver/templates/maasserver/nodes_listing.html'
--- src/maasserver/templates/maasserver/nodes_listing.html 2012-11-16 13:30:09 +0000
+++ src/maasserver/templates/maasserver/nodes_listing.html 2012-12-03 07:15:29 +0000
@@ -2,9 +2,25 @@
2 <table class="list">2 <table class="list">
3 <thead>3 <thead>
4 <tr>4 <tr>
5 {% if sorting == "true" %}
6 <th><a href="{{ sort_links.hostname }}"
7 class="{{ sort_classes.hostname }}">
8 <acronym title="Fully Qualified Domain Name">FQDN</acronym>
9 </a></th>
10 <th>
11 <acronym
12 title="Media Access Control addresses">MAC</acronym>
13 </th>
14 <th>
15 <a href="{{ sort_links.status }}"
16 class="{{ sort_classes.status }}">Status</a>
17 </th>
18 {% else %}
5 <th><acronym title="Fully Qualified Domain Name">FQDN</acronym></th>19 <th><acronym title="Fully Qualified Domain Name">FQDN</acronym></th>
6 <th><acronym20 <th><acronym
7 title="Media Access Control addresses">MAC</acronym></th>21 title="Media Access Control addresses">MAC</acronym></th>
22 <th>Status</th>
23 {% endif %}
8 </tr>24 </tr>
9 </thead>25 </thead>
10 {% for node in node_list %}26 {% for node in node_list %}
1127
=== modified file 'src/maasserver/templates/maasserver/tag_view.html'
--- src/maasserver/templates/maasserver/tag_view.html 2012-11-07 11:32:16 +0000
+++ src/maasserver/templates/maasserver/tag_view.html 2012-12-03 07:15:29 +0000
@@ -34,7 +34,7 @@
34 </ul>34 </ul>
35 <div id="nodes" class="block first pad-top">35 <div id="nodes" class="block first pad-top">
36 <h2 class="block first line-top pad-top">{{ paginator.count }} Node{{ paginator.count|pluralize }}</h2>36 <h2 class="block first line-top pad-top">{{ paginator.count }} Node{{ paginator.count|pluralize }}</h2>
37 {% include "maasserver/nodes_listing.html" %}37 {% include "maasserver/nodes_listing.html" with sorting="false" %}
38 {% include "maasserver/pagination.html" %}38 {% include "maasserver/pagination.html" %}
39 </div>39 </div>
40{% endblock %}40{% endblock %}
4141
=== modified file 'src/maasserver/tests/test_views_nodes.py'
--- src/maasserver/tests/test_views_nodes.py 2012-11-29 13:22:20 +0000
+++ src/maasserver/tests/test_views_nodes.py 2012-12-03 07:15:29 +0000
@@ -13,7 +13,12 @@
13__all__ = []13__all__ = []
1414
15import httplib15import httplib
16from operator import attrgetter
16from unittest import skip17from unittest import skip
18from urlparse import (
19 parse_qsl,
20 urlparse,
21 )
1722
18from django.conf import settings23from django.conf import settings
19from django.core.urlresolvers import reverse24from django.core.urlresolvers import reverse
@@ -81,6 +86,15 @@
81 enlist_preseed_link = reverse('enlist-preseed-view')86 enlist_preseed_link = reverse('enlist-preseed-view')
82 self.assertIn(enlist_preseed_link, get_content_links(response))87 self.assertIn(enlist_preseed_link, get_content_links(response))
8388
89 def test_node_list_contains_column_sort_links(self):
90 # Just create a node to have something in the list
91 factory.make_node()
92 response = self.client.get(reverse('node-list'))
93 sort_hostname = '?sort=hostname&dir=asc'
94 sort_status = '?sort=status&dir=asc'
95 self.assertIn(sort_hostname, get_content_links(response))
96 self.assertIn(sort_status, get_content_links(response))
97
84 def test_node_list_lists_nodes_from_different_nodegroups(self):98 def test_node_list_lists_nodes_from_different_nodegroups(self):
85 # Bug 1084443.99 # Bug 1084443.
86 nodegroup1 = factory.make_node_group()100 nodegroup1 = factory.make_node_group()
@@ -91,6 +105,99 @@
91 response = self.client.get(reverse('node-list'))105 response = self.client.get(reverse('node-list'))
92 self.assertEqual(httplib.OK, response.status_code)106 self.assertEqual(httplib.OK, response.status_code)
93107
108 def test_node_list_sorts_by_hostname(self):
109 names = ['zero', 'one', 'five']
110 nodes = [factory.make_node(hostname=n) for n in names]
111
112 # First check the ascending sort order
113 sorted_nodes = sorted(nodes, key=attrgetter('hostname'))
114 response = self.client.get(
115 reverse('node-list'), {
116 'sort': 'hostname',
117 'dir': 'asc'})
118 node_links = [
119 reverse('node-view', args=[node.system_id])
120 for node in sorted_nodes]
121 self.assertEqual(
122 node_links,
123 [link for link in get_content_links(response)
124 if link.startswith('/nodes/node')])
125
126 # Now check the reverse order
127 node_links = list(reversed(node_links))
128 response = self.client.get(
129 reverse('node-list'), {
130 'sort': 'hostname',
131 'dir': 'desc'})
132 self.assertEqual(
133 node_links,
134 [link for link in get_content_links(response)
135 if link.startswith('/nodes/node')])
136
137 def test_node_list_sorts_by_status(self):
138 statuses = {
139 NODE_STATUS.READY,
140 NODE_STATUS.DECLARED,
141 NODE_STATUS.FAILED_TESTS,
142 }
143 nodes = [factory.make_node(status=s) for s in statuses]
144
145 # First check the ascending sort order
146 sorted_nodes = sorted(nodes, key=attrgetter('status'))
147 response = self.client.get(
148 reverse('node-list'), {
149 'sort': 'status',
150 'dir': 'asc'})
151 node_links = [
152 reverse('node-view', args=[node.system_id])
153 for node in sorted_nodes]
154 self.assertEqual(
155 node_links,
156 [link for link in get_content_links(response)
157 if link.startswith('/nodes/node')])
158
159 # Now check the reverse order
160 node_links = list(reversed(node_links))
161 response = self.client.get(
162 reverse('node-list'), {
163 'sort': 'status',
164 'dir': 'desc'})
165 self.assertEqual(
166 node_links,
167 [link for link in get_content_links(response)
168 if link.startswith('/nodes/node')])
169
170 def test_node_list_sort_preserves_other_params(self):
171 # Set a very small page size to save creating lots of nodes
172 page_size = 2
173 self.patch(nodes_views.NodeListView, 'paginate_by', page_size)
174
175 nodes = []
176 tag = factory.make_tag('shiny')
177 for name in ('bbb', 'ccc', 'ddd', 'aaa'):
178 node = factory.make_node(hostname=name)
179 node.tags = [tag]
180 nodes.append(node)
181
182 params = {
183 'sort': 'hostname',
184 'dir': 'asc',
185 'page': '1',
186 'query': 'maas-tags=shiny'}
187 response = self.client.get(reverse('node-list'), params)
188 document = fromstring(response.content)
189 header_links = document.xpath("//div[@id='nodes']/table//th/a/@href")
190 fields = iter(('hostname', 'status'))
191 field_dirs = iter(('desc', 'asc'))
192 for link in header_links:
193 self.assertThat(
194 parse_qsl(urlparse(link).query),
195 ContainsAll([
196 ('page', '1'),
197 ('query', 'maas-tags=shiny'),
198 ('sort', next(fields)),
199 ('dir', next(field_dirs))]))
200
94 def test_node_list_displays_fqdn_dns_not_managed(self):201 def test_node_list_displays_fqdn_dns_not_managed(self):
95 nodes = [factory.make_node() for i in range(3)]202 nodes = [factory.make_node() for i in range(3)]
96 response = self.client.get(reverse('node-list'))203 response = self.client.get(reverse('node-list'))
@@ -448,7 +555,8 @@
448 {"query": "maas-tags=shiny cpu=2"})555 {"query": "maas-tags=shiny cpu=2"})
449 node2_link = reverse('node-view', args=[node2.system_id])556 node2_link = reverse('node-view', args=[node2.system_id])
450 document = fromstring(response.content)557 document = fromstring(response.content)
451 node_links = document.xpath("//div[@id='nodes']/table//a/@href")558 node_links = document.xpath(
559 "//div[@id='nodes']/table//td/a/@href")
452 self.assertEqual([node2_link], node_links)560 self.assertEqual([node2_link], node_links)
453561
454 def test_node_list_paginates(self):562 def test_node_list_paginates(self):
@@ -461,7 +569,7 @@
461 # Order node links with newest first as the view is expected to569 # Order node links with newest first as the view is expected to
462 node_links = [reverse('node-view', args=[node.system_id])570 node_links = [reverse('node-view', args=[node.system_id])
463 for node in reversed(nodes)]571 for node in reversed(nodes)]
464 expr_node_links = XPath("//div[@id='nodes']/table//a/@href")572 expr_node_links = XPath("//div[@id='nodes']/table//td/a/@href")
465 expr_page_anchors = XPath("//div[@class='pagination']//a")573 expr_page_anchors = XPath("//div[@class='pagination']//a")
466 # Fetch first page, should link newest two nodes and page 2574 # Fetch first page, should link newest two nodes and page 2
467 response = self.client.get(reverse('node-list'))575 response = self.client.get(reverse('node-list'))
@@ -473,7 +581,8 @@
473 # Fetch second page, should link next nodes and adjacent pages581 # Fetch second page, should link next nodes and adjacent pages
474 response = self.client.get(reverse('node-list'), {"page": 2})582 response = self.client.get(reverse('node-list'), {"page": 2})
475 page2 = fromstring(response.content)583 page2 = fromstring(response.content)
476 self.assertEqual(node_links[page_size:page_size * 2],584 self.assertEqual(
585 node_links[page_size:page_size * 2],
477 expr_node_links(page2))586 expr_node_links(page2))
478 self.assertEqual([("first", "."), ("previous", "."),587 self.assertEqual([("first", "."), ("previous", "."),
479 ("next", "?page=3"), ("last", "?page=3")],588 ("next", "?page=3"), ("last", "?page=3")],
@@ -501,8 +610,9 @@
501 {"query": "maas-tags=odd", "page": 3})610 {"query": "maas-tags=odd", "page": 3})
502 document = fromstring(response.content)611 document = fromstring(response.content)
503 self.assertIn("5 matching nodes", document.xpath("string(//h1)"))612 self.assertIn("5 matching nodes", document.xpath("string(//h1)"))
504 self.assertEqual([last_node_link],613 self.assertEqual(
505 document.xpath("//div[@id='nodes']/table//a/@href"))614 [last_node_link],
615 document.xpath("//div[@id='nodes']/table//td/a/@href"))
506 self.assertEqual([("first", "?query=maas-tags%3Dodd"),616 self.assertEqual([("first", "?query=maas-tags%3Dodd"),
507 ("previous", "?query=maas-tags%3Dodd&page=2")],617 ("previous", "?query=maas-tags%3Dodd&page=2")],
508 [(a.text.lower(), a.get("href"))618 [(a.text.lower(), a.get("href"))
509619
=== modified file 'src/maasserver/views/nodes.py'
--- src/maasserver/views/nodes.py 2012-11-29 13:22:20 +0000
+++ src/maasserver/views/nodes.py 2012-12-03 07:15:29 +0000
@@ -110,13 +110,26 @@
110 def get(self, request, *args, **kwargs):110 def get(self, request, *args, **kwargs):
111 self.query = request.GET.get("query")111 self.query = request.GET.get("query")
112 self.query_error = None112 self.query_error = None
113 self.sort_by = request.GET.get("sort")
114 self.sort_dir = request.GET.get("dir")
115
113 return super(NodeListView, self).get(request, *args, **kwargs)116 return super(NodeListView, self).get(request, *args, **kwargs)
114117
115 def get_queryset(self):118 def get_queryset(self):
116 # Return node list sorted, newest first.119 # Default sort - newest first, unless sorting params are
120 # present. In addition, to ensure order consistency, when
121 # sorting by non-unique fields (like status), we always
122 # sort by the unique creation date as well
123 if self.sort_by is not None:
124 prefix = '-' if self.sort_dir == 'desc' else ''
125 order_by = (prefix + self.sort_by, '-created')
126 else:
127 order_by = ('-created', )
128
129 # Return the sorted node list
117 nodes = Node.objects.get_nodes(130 nodes = Node.objects.get_nodes(
118 user=self.request.user, prefetch_mac=True,131 user=self.request.user, prefetch_mac=True,
119 perm=NODE_PERMISSION.VIEW,).order_by('-created')132 perm=NODE_PERMISSION.VIEW,).order_by(*order_by)
120 if self.query:133 if self.query:
121 try:134 try:
122 return constrain_nodes(nodes, _parse_constraints(self.query))135 return constrain_nodes(nodes, _parse_constraints(self.query))
@@ -127,11 +140,39 @@
127 nodes = nodes.prefetch_related('nodegroup__nodegroupinterface_set')140 nodes = nodes.prefetch_related('nodegroup__nodegroupinterface_set')
128 return nodes141 return nodes
129142
143 def _prepare_sort_links(self):
144 """Returns 2 dicts, with sort fields as keys and
145 links and CSS classes for the that field.
146 """
147
148 fields = ('hostname', 'status')
149 # Build relative URLs for the links, just with the params
150 links = {field: '?' for field in fields}
151 classes = {field: 'sort-none' for field in fields}
152
153 params = self.request.GET.copy()
154 reverse_dir = 'asc' if self.sort_dir == 'desc' else 'desc'
155
156 for field in fields:
157 params['sort'] = field
158 if field == self.sort_by:
159 params['dir'] = reverse_dir
160 classes[field] = 'sort-%s' % self.sort_dir
161 else:
162 params['dir'] = 'asc'
163
164 links[field] += params.urlencode()
165
166 return links, classes
167
130 def get_context_data(self, **kwargs):168 def get_context_data(self, **kwargs):
131 context = super(NodeListView, self).get_context_data(**kwargs)169 context = super(NodeListView, self).get_context_data(**kwargs)
132 context.update(get_longpoll_context())170 context.update(get_longpoll_context())
133 context["input_query"] = self.query171 context["input_query"] = self.query
134 context["input_query_error"] = self.query_error172 context["input_query_error"] = self.query_error
173 links, classes = self._prepare_sort_links()
174 context["sort_links"] = links
175 context["sort_classes"] = classes
135 return context176 return context
136177
137178

Subscribers

People subscribed via source and target branches

to status/vote changes: